/** * 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();