aboutsummaryrefslogtreecommitdiff
/**
 * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
 *
 * Make spreadsheets on docs.google.com browsable without nonfree js.
 *
 * Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * As additional permission under GNU GPL version 3 section 7, you
 * may distribute forms of that code without the copy of the GNU
 * GPL normally required by section 4, provided you include this
 * license notice and, in case of non-source distribution, a URL
 * through which recipients can access the Corresponding Source.
 * If you modify file(s) with this exception, you may extend this
 * exception to your version of the file(s), but you are not
 * obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * As a special exception to the GPL, any HTML file which merely
 * makes function calls to this code, and for that purpose
 * includes it by reference shall be deemed a separate work for
 * copyright law purposes. If you modify this code, you may extend
 * this exception to your version of the code, but you are not
 * obligated to do so. If you do not wish to do so, delete this
 * exception statement from your version.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
 * license. Although I request that you do not make use of this code in a
 * proprietary program, I am not going to enforce this in court.
 */

/* Use with https://docs.google.com/spreadsheets/d/** */

"use strict";

const current_params = new URLSearchParams(window.location.search);
const work_url = new URL(window.location.href);
let reload_needed = false;

/*
 * URLs ending with "/pubhtml/sheet?<something>" allow displaying a single sheet
 * from a document. If we're on one of those URLs, reload to a corresponding URL
 * ending with "/pub".
 */
if (work_url.pathname.endsWith("/pubhtml/sheet")) {
    reload_needed = true;
    work_url.pathname = work_url.pathname.replace(/pubhtml\/sheet$/, "pub");
}

/*
 * The "widget=true" parameter in the URL may cause preview to be
 * unavailable. If this is the case, reload without it.
 */
const widget_setting = current_params.get("widget");
if (widget_setting !== null && widget_setting.toLowerCase() == "true") {
    if (work_url.pathname.endsWith("/pub") ||
	work_url.pathname.endsWith("/pubhtml"))
	reload_needed = true;
}

/*
 * When reloading, get rid or parameters other than "gid" - we don't use them
 * anyway.
 */
if (reload_needed) {
    const gid = current_params.get("gid");
    if (gid === null)
	work_url.search = "";
    else
	work_url.search = new URLSearchParams({gid});

    window.location.href = work_url;
}

function initial_gid() {
    let gid = null;

    const param_strings_to_try = [
	window.location.hash.substring(1),
	window.location.search.substring(1)
    ];

    for (const params in param_strings_to_try)
	gid = gid || new URLSearchParams(params).get("gid");

    return gid;
}

/* For URLs where path ends with "/edit". */
function PageHandler_Edit() {
}

PageHandler_Edit.prototype.get_sheets_count = function() {
    /* Extract sheet data from a script that sets the `bootstrapData' variable. */
    let data = null;
    for (const script of document.scripts) {
	const match = /bootstrapData = ({([^;]|[^}];)+})/
	      .exec(script.textContent);
	if (!match)
	    continue;
	data = JSON.parse(match[1]);
    }

    let count = 0;

    for (const entry of data.changes.topsnapshot) {
	if (Array.isArray(entry) && entry[0] === 21350203)
	    count++;
    }

    return count;
}

PageHandler_Edit.prototype.make_preview = function() {
    /* Make sure the view is scrollable. */
    document.body.style.width    = "100vw";
    document.body.style.height   = "100vh";
    document.body.style.overflow = "scroll";

    let container = document.querySelectorAll(".waffle")[0];
    let main_gid = null;

    while (container) {
	container = container.parentElement;
	console.log(container);
	if (container === document.body || !container)
	    break;

	const match = /([0-9]+)-grid-container/.exec(container.id);
	if (match)
	    main_gid = match[1];

	container.removeAttribute("style");
	container.style.width = "-moz-fit-content";
	container.style.width = "fit-content";
    }

    /*
     * Hide editor bottom bar - it doesn't work anyway. Also hide no Javascript
     * warning (if any).
     */
    for (const selector of [
	"#grid-bottom-bar",
	".jfk-butterBar-warning",
	"noscript"
    ]) {
	for (const element of document.querySelectorAll(selector))
	    element.style.display = "none";
    }

    /* Hide non-working tool bars and widgets above the sheet grid. */
    const editor_container = document.getElementById("docs-editor-container");
    for (const element of editor_container.parentElement.children) {
	if (element !== editor_container)
	    element.style.display = "none";
    }

    /* Add information about additional sheets that are not visible. */
    try {
	const subsheets_count = this.get_sheets_count();
	if (subsheets_count > 1) {
	    const notice = document.createElement("aside");

	    notice.style.display         = "block";
	    notice.style.backgroundColor = "white";
	    notice.style.margin          = "5px";
	    notice.style.fontStyle       = "italic";

	    notice.innerText = `This document contains ${subsheets_count - 1} additional subsheet(s). Download to view.`;

	    editor_container.prepend(notice);
	}
    } catch(e) {
	console.error(e);
    }
}

PageHandler_Edit.prototype.get_doc_title = function() {
    const title_span = document.getElementById("docs-title-input-label-inner");
    return title_span ? title_span.innerText : "<unknown document>";
}

PageHandler_Edit.prototype.get_download_link = function() {
    const link = new URL(window.location.href);
    link.pathname = link.pathname.replace(/edit$/, "export");
    link.search = new URLSearchParams({format: "ods"});

    return "" + link;
}

/* For URLs where path ends with "/pub", "/htmlview" and "/pubhtml". */
function PageHandler_PubHtmlviewPubhtml() {
}

PageHandler_PubHtmlviewPubhtml.prototype.make_preview = function() {
    /* Remove the "Updated automatically every 5 minutes" text. */
    for (const elem of [...document.querySelectorAll("#footer>.dash+a~*")])
	elem.remove();

    /*
     * If there is more than one sheet, make sheets switchable through button
     * clicks.
     */
    const views = [...document.querySelectorAll("#sheets-viewport>[id]")];

    function get_button(sheet_view) {
	return document.getElementById(`sheet-button-${sheet_view.id}`);
    }

    function show_view(sheet_view) {
	for (const processed_view of views) {
	    const button = get_button(processed_view);

	    processed_view.style.display = processed_view === sheet_view ?
		"initial": "none";

	    if (button !== null)
		button.style.textDecoration = processed_view === sheet_view ?
		"underline" : "";
	}
    }

    for (const sheet_view of views) {
	const button = get_button(sheet_view);
	if (button !== null)
	    button.addEventListener("click", () => show_view(sheet_view));
    }

    /* Make one of the sheets visible from the beginning. */
    if (views.length > 0) {
	let initial_view = views[0];

	const gid = "" + initial_gid();

	for (const sheet_view of views) {
	    if (sheet_view.id === gid)
		initial_view = sheet_view;
	}

	show_view(initial_view);
    }

    /* Hide doc title (we'll be replacing it with our own element). */
    const doc_title_elem = document.getElementById("doc-title");
    if (doc_title_elem !== null)
	doc_title_elem.style.display = "none";
}

PageHandler_PubHtmlviewPubhtml.prototype.get_doc_title = function() {
    const title_span = document.querySelector("#doc-title .name");
    return title_span ? title_span.innerText : "<unknown document>";
}

PageHandler_PubHtmlviewPubhtml.prototype.get_download_link = function() {
    if (!window.location.pathname.endsWith("/pub"))
	return null;

    const link = new URL(window.location.href);
    link.search = new URLSearchParams({output: "ods"});

    return "" + link;
}

function main() {
    let page_handler = null;

    if (window.location.pathname.endsWith("/edit"))
	page_handler = new PageHandler_Edit();
    else if (window.location.pathname.endsWith("/pub") ||
	     window.location.pathname.endsWith("/htmlview") ||
	     window.location.pathname.endsWith("/pubhtml"))
	page_handler = new PageHandler_PubHtmlviewPubhtml();

    if (page_handler === null) {
	console.error("Unknown type of docs page.");
    } else {
	try {
	    page_handler.make_preview();
	} catch(e) {
	    console.error(e);
	}

	/* Add our own title&download bar */
	const title         = page_handler.get_doc_title();
	const download_link = page_handler.get_download_link();

	const doc_heading     = document.createElement("h3");
	const title_text      = document.createElement("span");
	const download_button = document.createElement("a");

	title_text.style.margin = "0 20px";
	title_text.textContent  = title;
	doc_heading.append(title_text);

	download_button.style.borderRadius    = "5px";
	download_button.style.padding         = "10px";
	download_button.style.color           = "#333";
	download_button.style.backgroundColor = "lightgreen";
	download_button.style.textDecoration  = "none";
	download_button.style.boxShadow       = "-4px 8px 8px #888";
	download_button.style.display         = "inline-block";
	download_button.textContent           = "download";

	if (download_link === null) {
	    download_button.style.backgroundColor = "lightgray";
	    download_button.style.userSelect      = "none";
	    download_button.style.color           = "#606060";
	    download_button.textContent           = "download unavailable";
	} else {
	    download_button.href                  = download_link;
	}

	doc_heading.append(download_button);

	doc_heading.style.padding = "0 20px; color: #555";
	document.body.prepend(doc_heading);
    }
}

if (!reload_needed)
    main();