From df914edcb5522670309ceb8dfd0195dc70fb81d4 Mon Sep 17 00:00:00 2001 From: Hamcha Date: Sun, 21 Jun 2020 19:16:01 +0200 Subject: [PATCH] Add IDB-backed caching --- .eslintrc.js | 1 + LICENSE | 23 ++ README.md | 2 +- package.json | 1 + src/TabManager.ts | 55 ++++- src/cache.ts | 110 ++++++++++ src/userscript.ts | 550 +++++++++++++++++++++++----------------------- src/utils.ts | 6 +- style/main.scss | 1 + tsconfig.json | 1 + yarn.lock | 5 + 11 files changed, 470 insertions(+), 285 deletions(-) create mode 100644 LICENSE create mode 100644 src/cache.ts diff --git a/.eslintrc.js b/.eslintrc.js index 2f90cce..5112eb8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { "no-param-reassign": "off", "no-alert": "off", "no-console": "off", + "func-names": "off", "@typescript-eslint/ban-ts-comment": [ "error", { diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8790cc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +This code is license under ISC, full copy below: + + Copyright 2020, Alessandro Gatti + + Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +This code uses idb-keyval, licensed under Apache 2: + + Copyright 2016, Jake Archibald + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index 37d78e2..a7c256a 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,4 @@ ## Development -`yarn && yarn dev` +`yarn dev` diff --git a/package.json b/package.json index a89522a..a4015ee 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "eslint-config-prettier": "^6.11.0", "eslint-plugin-import": "2.21.2", "eslint-plugin-prettier": "^3.1.4", + "idb-keyval": "^3.2.0", "parcel-bundler": "^1.12.4", "parcel-plugin-sw-cache": "^0.3.1", "prettier": "^2.0.5", diff --git a/src/TabManager.ts b/src/TabManager.ts index 9053f1c..7f956fa 100644 --- a/src/TabManager.ts +++ b/src/TabManager.ts @@ -1,7 +1,9 @@ // @ts-expect-error: Asset imports are handled by parcel import speen from "~/assets/images/speen.svg"; import { getPageHTML } from "./wiki"; -import userscript from "./userscript"; +import { processHTML, bindFunctions, CURRENT_VERSION } from "./userscript"; +import cache from "./cache"; +import { nextAnimationFrame } from "./utils"; // @ts-expect-error: Parcel image import import unknown from "~/assets/images/tab-icons/unknown.svg"; @@ -22,21 +24,52 @@ function initWaiting(elem: HTMLElement) { } async function loadPage(page: string, elem: HTMLElement) { + let html: string | null = null; + const key = `page:${page}`; + + // Check cache for pre-processed page + try { + const cachedPage = await cache.get(key); + if (cachedPage) { + if (cachedPage.version === CURRENT_VERSION) { + console.log(`${page}: found cached entry`); + html = cachedPage.value; + } else { + console.log(`${page}: found outdated cache entry`); + } + } + } catch (e) { + console.log(`${page}: failed to retrieve cache entry:`, e); + } + // Fetch page content - console.log(`${page}: fetching`); - let html = await getPageHTML(page); + if (!html) { + console.log(`${page}: fetching`); + html = await getPageHTML(page); - // Convert relative links to absolute - html = html.replace(/"\/wiki/gi, '"//tgstation13.org/wiki'); + // Convert relative links to absolute (and proxied) + html = html.replace(/"\/wiki/gi, '"//tgproxy.ovo.ovh/wiki'); - // Set as HTML content and run HTML manipulations on it - requestAnimationFrame(() => { + await nextAnimationFrame(); + + // Set as HTML content and run HTML manipulations on it elem.innerHTML = html; + console.log(`${page}: processing`); - userscript(elem, page); - console.log(`${page}: userscript applied`); - elem.classList.remove("waiting"); - }); + processHTML(elem, page); + + // Save result to cache + cache.set(key, elem.innerHTML, CURRENT_VERSION).then(() => { + console.log(`${page}: saved to cache`); + }); + } else { + // Set cached content as HTML + elem.innerHTML = html; + } + + bindFunctions(elem, page); + console.log(`${page}: userscript applied`); + elem.classList.remove("waiting"); } type TabElements = { diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..416e55c --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,110 @@ +/* eslint-disable no-shadow */ + +interface CacheEntry { + version: string; + value: T; +} + +export class Store { + readonly dbp: Promise; + + constructor(dbName = "tg-cache", readonly storeName = "keyval") { + this.dbp = new Promise((resolve, reject) => { + const openreq = indexedDB.open(dbName, 1); + openreq.onerror = () => reject(openreq.error); + openreq.onsuccess = () => resolve(openreq.result); + + // First time setup: create an empty object store + openreq.onupgradeneeded = () => { + openreq.result.createObjectStore(storeName); + }; + }); + } + + withIDBStore( + type: IDBTransactionMode, + callback: (store: IDBObjectStore) => void + ): Promise { + return this.dbp.then( + (db) => + new Promise((resolve, reject) => { + const transaction = db.transaction(this.storeName, type); + transaction.oncomplete = () => resolve(); + transaction.onabort = () => reject(transaction.error); + transaction.onerror = () => reject(transaction.error); + callback(transaction.objectStore(this.storeName)); + }) + ); + } +} + +let defaultStore: Store; + +function getDefaultStore() { + if (!defaultStore) defaultStore = new Store(); + return defaultStore; +} + +export function get( + key: IDBValidKey, + store = getDefaultStore() +): Promise> { + let req: IDBRequest; + return store + .withIDBStore("readonly", (store) => { + req = store.get(key); + }) + .then(() => req.result); +} + +export function set( + key: IDBValidKey, + value: Type, + version: string, + store = getDefaultStore() +): Promise { + return store.withIDBStore("readwrite", (store) => { + store.put({ version, value }, key); + }); +} + +export function del( + key: IDBValidKey, + store = getDefaultStore() +): Promise { + return store.withIDBStore("readwrite", (store) => { + store.delete(key); + }); +} + +export function clear(store = getDefaultStore()): Promise { + return store.withIDBStore("readwrite", (store) => { + store.clear(); + }); +} + +export function keys(store = getDefaultStore()): Promise { + const dbkeys: IDBValidKey[] = []; + + return store + .withIDBStore("readonly", (store) => { + // This would be store.getAllKeys(), but it isn't supported by Edge or Safari. + // And openKeyCursor isn't supported by Safari. + (store.openKeyCursor || store.openCursor).call( + store + ).onsuccess = function () { + if (!this.result) return; + dbkeys.push(this.result.key); + this.result.continue(); + }; + }) + .then(() => dbkeys); +} + +export default { + get, + set, + keys, + del, + clear, +}; diff --git a/src/userscript.ts b/src/userscript.ts index 6c24238..125f853 100644 --- a/src/userscript.ts +++ b/src/userscript.ts @@ -2,7 +2,145 @@ import { darken, ColorFmt, lighten } from "./darkmode"; import searchBox from "./search"; import { findParent } from "./utils"; -export default function userscript(root: HTMLElement, docname: string): void { +// This is used for cache busting when userscript changes significantly. +// Only change it when you made changes to the processHTML part! +export const CURRENT_VERSION = "7930f961710641f022ef0cd3ad3277ffd4eab7eb"; + +function chemistryFixups(root: HTMLElement) { + // Fix inconsistencies with

on random parts + // Ideally I'd like a

or something on every part, wrapping it completely, but for now let's just kill 'em + root + .querySelectorAll( + "table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext" + ) + .forEach((td) => { + const tmp = td.cloneNode() as HTMLElement; + // The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop. + // Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index. + Array.from(td.childNodes).forEach((el) => { + if (el instanceof HTMLParagraphElement) { + tmp.append(...el.childNodes); + } else { + tmp.append(el); + } + }); + td.parentNode.replaceChild(tmp, td); + }); + + // Enrich "x part" with checkboxes and parts + Array.from(root.querySelectorAll("td")) + .filter((el) => el.textContent.indexOf(" part") >= 0) + .forEach((el) => { + el.innerHTML = el.innerHTML.replace( + /((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi, + (match, ...m) => + `${m[2].replace( + /()/gi, + '$1' + )}` + ); + }); + + // Wrap every recipe with extra metadata + root.querySelectorAll(".bgus_part").forEach((el) => { + if ("parts" in el.parentElement.dataset) { + el.parentElement.dataset.parts = ( + parseInt(el.parentElement.dataset.parts, 10) + + parseInt(el.dataset.amount, 10) + ).toString(); + } else { + el.parentElement.dataset.parts = el.dataset.amount; + } + }); + + // Remove "Removed medicines" section + const remTable = root.querySelector( + "#Non-craftable_Medicines + h4 + p + table" + ); + remTable.parentElement.removeChild(remTable); + + // Restructure recipes to work in a narrow window + root + .querySelectorAll("div[data-name] .wikitable.sortable tr") + .forEach((row) => { + const sectionEl = findParent( + row, + (sel) => "name" in sel.dataset && sel.dataset.name !== "" + ); + const section = sectionEl.dataset.name; + if (row.querySelector("td") === null) { + // Remove unused rows if found + const headers = row.querySelectorAll("th"); + headers.forEach((th, i) => { + if (i < 2) { + th.classList.add("table-head"); + return; + } + th.parentElement.removeChild(th); + }); + return; + } + const rows = Array.from(row.querySelectorAll("td")).slice(1); + let treatment = null; + let desc = null; + let metabolism = null; + let overdose = null; + let addiction = null; + // Handle special cases + switch (section) { + case "Components": + case "Virology Recipes": + [desc] = rows; + break; + case "Narcotics": + [desc, metabolism, overdose, addiction] = rows; + break; + case "Explosive Strength": + case "Other Reagents": + case "Mutation Toxins": + [desc, metabolism] = rows; + break; + default: + // All fields + [treatment, desc, metabolism, overdose, addiction] = rows; + } + const title = row.querySelector("th"); + let content = `

`; + if (treatment) { + content += `

${treatment.innerHTML}

`; + } + if (metabolism) { + content += `

${metabolism.innerHTML}

`; + } + if (addiction && addiction.innerHTML.trim() !== "N/A") { + content += `

${addiction.innerHTML}

`; + } + if (overdose && overdose.innerHTML.trim() !== "N/A") { + content += `

${overdose.innerHTML}

`; + } + if (desc) { + content += `

${desc.innerHTML}

`; + } + title.classList.add("reagent-ext"); + title.innerHTML = content; + if (desc) desc.parentElement.removeChild(desc); + if (treatment) treatment.parentElement.removeChild(treatment); + if (metabolism) metabolism.parentElement.removeChild(metabolism); + if (overdose) overdose.parentElement.removeChild(overdose); + if (addiction) addiction.parentElement.removeChild(addiction); + }); + + // Enable page-specific CSS rules + root.classList.add("bchem"); +} + +export function processHTML(root: HTMLElement, docname: string): void { // Add header const header = document.createElement("h1"); header.className = "pageheader"; @@ -98,281 +236,149 @@ export default function userscript(root: HTMLElement, docname: string): void { } }); - // Tell user that better chemistry is loading - const postbody = root; - const statusMessage = document.createElement("div"); - statusMessage.innerHTML = ` - - -
- Hang on... Better guides is loading. -
`; - postbody.insertBefore(statusMessage, postbody.firstChild); - - function betterChemistry() { - // Fix inconsistencies with

on random parts - // Ideally I'd like a

or something on every part, wrapping it completely, but for now let's just kill 'em - document - .querySelectorAll( - "table.wikitable > tbody > tr:not(:first-child) > td:nth-child(2), .tooltiptext" - ) - .forEach((td) => { - const tmp = td.cloneNode() as HTMLElement; - // The cast to Array is necessary because, while childNodes's NodeList technically has a forEach method, it's a live list and operations mess with its lenght in the middle of the loop. - // Nodes can only have one parent so append removes them from the original NodeList and shifts the following one back into the wrong index. - Array.from(td.childNodes).forEach((el) => { - if (el instanceof HTMLParagraphElement) { - tmp.append(...el.childNodes); - } else { - tmp.append(el); - } - }); - td.parentNode.replaceChild(tmp, td); - }); - - // Enrich "x part" with checkboxes and parts - Array.from(document.querySelectorAll("td")) - .filter((el) => el.textContent.indexOf(" part") >= 0) - .forEach((el) => { - el.innerHTML = el.innerHTML.replace( - /((\d+)\s+(?:parts?|units?))(.*?(?:<\/a>|\n|$))/gi, - (match, ...m) => - `${m[2].replace( - /()/gi, - '$1' - )}` - ); - }); - // Add event to autofill child checkboxes - root - .querySelectorAll(".bgus_part_tooltip > .bgus_checkbox") - .forEach((box: HTMLInputElement) => { - const tooltip = box.parentElement.nextElementSibling; - box.addEventListener("click", () => { - tooltip - .querySelectorAll(".bgus_checkbox") - .forEach((el: HTMLInputElement) => { - el.checked = box.checked; - }); - }); - }); - - // Add event to collapse subsections - root.querySelectorAll(".bgus_nested_element").forEach((twistie) => { - twistie.addEventListener("click", () => { - twistie.classList.toggle("bgus_collapsed"); - }); - }); - - // Wrap every recipe with extra metadata - root.querySelectorAll(".bgus_part").forEach((el) => { - if ("parts" in el.parentElement.dataset) { - el.parentElement.dataset.parts = ( - parseInt(el.parentElement.dataset.parts, 10) + - parseInt(el.dataset.amount, 10) - ).toString(); - } else { - el.parentElement.dataset.parts = el.dataset.amount; - } - }); - - const setPartSize = (labels, ml) => { - labels.forEach((el) => { - const part = el.parentElement.dataset.amount; - const total = el.parentElement.parentElement.dataset.parts; - const amt = Math.ceil(ml * (part / total)); - el.innerHTML = `${amt} ml`; - // Lookup tooltips - let next = el.parentElement.nextElementSibling; - while (next) { - if (next.classList.contains("tooltip")) { - const sublabels = []; - next.querySelector(".tooltiptext").childNodes.forEach((ch) => { - if (ch.classList && ch.classList.contains("bgus_part")) { - sublabels.push(ch.querySelector(".bgus_part_label")); - } - }); - setPartSize(sublabels, amt); - } - if (next.classList.contains("bgus_part")) { - // Done searching - break; - } - next = next.nextElementSibling; - } - }); - }; - - root.classList.add("bchem"); - // Init fuzzy search with elements - const el = Array.from( - root.querySelectorAll( - "table.wikitable > tbody > tr:not(:first-child) > th" - ) - ); - const name = el.map((elem) => { - let partial = ""; - elem.childNodes.forEach((t) => { - if (t instanceof Text) { - partial += t.textContent; - } - }); - return partial.trim(); - }); - const box = searchBox( - el, - name.map((e, i) => ({ id: i, str: e })) - ); - document.body.appendChild(box); - - // Remove "Removed medicines" section - const remTable = root.querySelector( - "#Non-craftable_Medicines + h4 + p + table" - ); - remTable.parentElement.removeChild(remTable); - - root - .querySelectorAll("div[data-name] .wikitable.sortable tr") - .forEach((row) => { - const sectionEl = findParent( - row, - (sel) => "name" in sel.dataset && sel.dataset.name !== "" - ); - const section = sectionEl.dataset.name; - if (row.querySelector("td") === null) { - // Remove unused rows if found - const headers = row.querySelectorAll("th"); - headers.forEach((th, i) => { - if (i < 2) { - th.classList.add("table-head"); - return; - } - th.parentElement.removeChild(th); - }); - return; - } - const rows = Array.from(row.querySelectorAll("td")).slice(1); - let treatment = null; - let desc = null; - let metabolism = null; - let overdose = null; - let addiction = null; - // Handle special cases - switch (section) { - case "Components": - case "Virology Recipes": - [desc] = rows; - break; - case "Narcotics": - [desc, metabolism, overdose, addiction] = rows; - break; - case "Explosive Strength": - case "Other Reagents": - case "Mutation Toxins": - [desc, metabolism] = rows; - break; - default: - // All fields - [treatment, desc, metabolism, overdose, addiction] = rows; - } - const title = row.querySelector("th"); - let content = `

`; - if (treatment) { - content += `

${treatment.innerHTML}

`; - } - if (metabolism) { - content += `

${metabolism.innerHTML}

`; - } - if (addiction && addiction.innerHTML.trim() !== "N/A") { - content += `

${addiction.innerHTML}

`; - } - if (overdose && overdose.innerHTML.trim() !== "N/A") { - content += `

${overdose.innerHTML}

`; - } - if (desc) { - content += `

${desc.innerHTML}

`; - } - title.classList.add("reagent-ext"); - title.innerHTML = content; - if (desc) desc.parentElement.removeChild(desc); - if (treatment) treatment.parentElement.removeChild(treatment); - if (metabolism) metabolism.parentElement.removeChild(metabolism); - if (overdose) overdose.parentElement.removeChild(overdose); - if (addiction) addiction.parentElement.removeChild(addiction); - }); - - document.body.addEventListener("keydown", (ev) => { - if (ev.shiftKey) { - switch (ev.keyCode) { - // SHIFT+C = Toggle checkboxes - case 67: { - root.classList.toggle("bgus_cbox"); - root - .querySelectorAll(".bgus_checkbox:checked") - .forEach((sel: HTMLInputElement) => { - sel.checked = false; - }); - break; - } - - // SHIFT+B = Set whole size (beaker?) for parts/units - case 66: { - const size = parseInt( - prompt("Write target ml (0 to reset)", "90"), - 10 - ); - if (Number.isNaN(size) || size <= 0) { - // Reset to parts/unit - root - .querySelectorAll(".bgus_part_label") - .forEach((sel: HTMLElement) => { - sel.innerHTML = sel.dataset.src; - }); - return; - } - setPartSize( - root.querySelectorAll("td > .bgus_part > .bgus_part_label"), - +size - ); - break; - } - - default: - // Do nothing - } - } - }); - } - - function betterGeneric() { - const el = Array.from( - root.querySelectorAll("div.mw-headline-cont[id][data-name]") - ); - const name = el.map((elem: HTMLDivElement) => elem.dataset.name.trim()); - - // Init fuzzy search with headlines - const box = searchBox( - el, - name.map((e, i) => ({ id: i, str: e })), - { alignment: "start" } - ); - root.appendChild(box); - } - switch (docname) { case "Guide_to_chemistry": - betterChemistry(); + chemistryFixups(root); break; default: - betterGeneric(); + } +} + +function chemistryScript(root: HTMLElement) { + // Add event to autofill child checkboxes + root + .querySelectorAll(".bgus_part_tooltip > .bgus_checkbox") + .forEach((box: HTMLInputElement) => { + const tooltip = box.parentElement.nextElementSibling; + box.addEventListener("click", () => { + tooltip + .querySelectorAll(".bgus_checkbox") + .forEach((el: HTMLInputElement) => { + el.checked = box.checked; + }); + }); + }); + + // Add event to collapse subsections + root.querySelectorAll(".bgus_nested_element").forEach((twistie) => { + twistie.addEventListener("click", () => { + twistie.classList.toggle("bgus_collapsed"); + }); + }); + + const setPartSize = (labels, ml) => { + labels.forEach((el) => { + const part = el.parentElement.dataset.amount; + const total = el.parentElement.parentElement.dataset.parts; + const amt = Math.ceil(ml * (part / total)); + el.innerHTML = `${amt} ml`; + // Lookup tooltips + let next = el.parentElement.nextElementSibling; + while (next) { + if (next.classList.contains("tooltip")) { + const sublabels = []; + next.querySelector(".tooltiptext").childNodes.forEach((ch) => { + if (ch.classList && ch.classList.contains("bgus_part")) { + sublabels.push(ch.querySelector(".bgus_part_label")); + } + }); + setPartSize(sublabels, amt); + } + if (next.classList.contains("bgus_part")) { + // Done searching + break; + } + next = next.nextElementSibling; + } + }); + }; + + // Init fuzzy search with elements + const el = Array.from( + root.querySelectorAll( + "table.wikitable > tbody > tr:not(:first-child) > th" + ) + ); + const name = el.map((elem) => + elem.querySelector(".reagent-header").textContent.trim().replace("▮", "") + ); + const box = searchBox( + el, + name.map((e, i) => ({ id: i, str: e })) + ); + document.body.appendChild(box); + + document.body.addEventListener("keydown", (ev) => { + if (ev.shiftKey) { + switch (ev.keyCode) { + // SHIFT+C = Toggle checkboxes + case 67: { + root.classList.toggle("bgus_cbox"); + root + .querySelectorAll(".bgus_checkbox:checked") + .forEach((sel: HTMLInputElement) => { + sel.checked = false; + }); + break; + } + + // SHIFT+B = Set whole size (beaker?) for parts/units + case 66: { + const size = parseInt( + prompt("Write target ml (0 to reset)", "90"), + 10 + ); + if (Number.isNaN(size) || size <= 0) { + // Reset to parts/unit + root + .querySelectorAll(".bgus_part_label") + .forEach((sel: HTMLElement) => { + sel.innerHTML = sel.dataset.src; + }); + return; + } + setPartSize( + root.querySelectorAll("td > .bgus_part > .bgus_part_label"), + +size + ); + break; + } + + default: + // Do nothing + } + } + }); +} + +function genericScript(root: HTMLElement) { + const el = Array.from( + root.querySelectorAll("div.mw-headline-cont[id][data-name]") + ); + const name = el.map((elem: HTMLDivElement) => elem.dataset.name.trim()); + + // Init fuzzy search with headlines + const box = searchBox( + el, + name.map((e, i) => ({ id: i, str: e })), + { alignment: "start" } + ); + root.appendChild(box); +} + +export function bindFunctions(root: HTMLElement, docname: string): void { + switch (docname) { + case "Guide_to_chemistry": + chemistryScript(root); + break; + default: + genericScript(root); break; } - // Everything is loaded, remove loading bar - statusMessage.innerHTML = ""; } + +export default { + CURRENT_VERSION, + processHTML, + bindFunctions, +}; diff --git a/src/utils.ts b/src/utils.ts index c51c129..ddc0367 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,4 +12,8 @@ export function findParent( return parent; } -export default { findParent }; +export function nextAnimationFrame(): Promise { + return new Promise((resolve) => requestAnimationFrame(() => resolve())); +} + +export default { findParent, nextAnimationFrame }; diff --git a/style/main.scss b/style/main.scss index 4344f9a..d02595e 100644 --- a/style/main.scss +++ b/style/main.scss @@ -66,6 +66,7 @@ body { overflow: hidden; .page { //visibility: hidden; + will-change: display; display: none; overflow-y: scroll; grid-row: 1; diff --git a/tsconfig.json b/tsconfig.json index 70ae44c..6e1fd7e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "ES2020", "baseUrl": ".", + "allowSyntheticDefaultImports": true, "paths": { "~*": ["./src/*"] } diff --git a/yarn.lock b/yarn.lock index b0463ca..ced071e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3230,6 +3230,11 @@ icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0: resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0= +idb-keyval@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-3.2.0.tgz#cbbf354deb5684b6cdc84376294fc05932845bd6" + integrity sha512-slx8Q6oywCCSfKgPgL0sEsXtPVnSbTLWpyiDcu6msHOyKOLari1TD1qocXVCft80umnkk3/Qqh3lwoFt8T/BPQ== + ieee754@^1.1.4: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"