From 75688d8658233c0b6c1b7e7c73d1f06efe1c6453 Mon Sep 17 00:00:00 2001 From: Benjamin Aster Date: Wed, 24 Jan 2024 21:53:42 +0100 Subject: [PATCH] 0.1.25 --- app.webmanifest | 1 - css/global.css | 46 ++++---- css/main.css | 289 +++++++++++++++++++++++++++------------------ html/editor.c.html | 21 +--- html/files.c.html | 230 +++++++++++++++++++----------------- html/header.c.html | 49 ++------ index.html | 81 +++++++------ js/app.js | 145 ++++++++++++++--------- js/files.js | 139 +++++++++++----------- js/global.d.ts | 2 + js/main.js | 11 +- jsconfig.json | 16 +++ service-worker.js | 11 -- 13 files changed, 556 insertions(+), 485 deletions(-) create mode 100644 jsconfig.json delete mode 100644 service-worker.js diff --git a/app.webmanifest b/app.webmanifest index 0df6a89..9b83903 100644 --- a/app.webmanifest +++ b/app.webmanifest @@ -1,6 +1,5 @@ { "name": "PAMM", - "short_name": "PAMM", "description": "Pretty Awesome Math Markup – A user-friendly LaTeX alternative with a live editor.", "start_url": "./", "scope": "./", diff --git a/css/global.css b/css/global.css index 9122b6e..cf2036a 100644 --- a/css/global.css +++ b/css/global.css @@ -55,6 +55,7 @@ h1, h2, h3, h4, h5, h6, strong, b { button, input, textarea, select, option { all: unset; + outline: revert; } label, button, summary, select, option { @@ -89,7 +90,8 @@ dialog { inset: 0; margin: auto; border: none; - padding: 1rem; + padding: 0; + color: inherit; } ::view-transition-image-pair(*), ::view-transition-old(*), ::view-transition-new(*) { @@ -101,31 +103,27 @@ dialog { color: var(--background); } -@media (hover: none) { - ::-webkit-scrollbar { - display: none; +@media (hover) { + ::-webkit-scrollbar, ::-webkit-scrollbar-corner { + inline-size: .8rem; + block-size: .8rem; + background: none; } -} -::-webkit-scrollbar, ::-webkit-scrollbar-corner { - inline-size: .8rem; - block-size: .8rem; - background: none; -} - -::-webkit-scrollbar-button:start:increment, ::-webkit-scrollbar-button:end:decrement { - display: none; -} + ::-webkit-scrollbar-button:start:increment, ::-webkit-scrollbar-button:end:decrement { + display: none; + } -::-webkit-scrollbar-thumb, ::-webkit-scrollbar-button { - background-color: var(--scrollbar-color); - inline-size: .8rem; - block-size: .8rem; - border: .2rem solid transparent; - border-radius: .4rem; - background-clip: padding-box; -} + ::-webkit-scrollbar-thumb, ::-webkit-scrollbar-button { + inline-size: .8rem; + block-size: .8rem; + background-color: var(--scrollbar-color); + border: .2rem solid transparent; + border-radius: .4rem; + background-clip: padding-box; + } -::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-button:hover { - background-color: var(--scrollbar-hover-color); + ::-webkit-scrollbar-thumb:hover, ::-webkit-scrollbar-button:hover { + background-color: var(--scrollbar-hover-color); + } } diff --git a/css/main.css b/css/main.css index 268c8f0..8b3e87b 100644 --- a/css/main.css +++ b/css/main.css @@ -5,7 +5,7 @@ @import url("./global.css") layer(global); @layer important { - :root.loading * { + :root.no-transitions * { transition: none; animation: none; } @@ -13,6 +13,33 @@ [hidden] { display: none; } + + @media print { + :root { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + + body { + display: block; + block-size: unset; + overflow: unset; + } + + main { + display: block; + overflow: unset; + } + + c-header, dialog { + display: none; + } + + @page { + size: A4 portrait; + margin: 0; + } + } } @layer main { @@ -73,112 +100,88 @@ --scrollbar-color: oklch(var(--accent-lightness) var(--accent-chroma) var(--accent-hue)); --scrollbar-hover-color: oklch(calc(var(--accent-lightness) + 10%) var(--accent-chroma) var(--accent-hue)); - scrollbar-color: var(--scrollbar-color) transparent; font-family: system-ui, sans-serif; - background-color: var(--background); + background-color: var(--gray-1); color: var(--color); overflow-wrap: break-word; + line-height: 1.5; + -webkit-text-size-adjust: none; + text-size-adjust: none; -webkit-tap-highlight-color: transparent; + overscroll-behavior: none; + + @media (hover) { + @supports not selector(::-webkit-scrollbar) { + scrollbar-color: var(--scrollbar-color) transparent; + } + } + + @media not (hover) { + scrollbar-color: var(--transparent-gray-1) transparent; + } } body { box-sizing: border-box; margin: 0; block-size: 100dvb; - /* overflow-y: hidden; */ - /* display: grid; - grid-template: auto 1fr / 1fr; - grid-template-areas: - "titlebar" - "main"; */ display: flex; flex-direction: column; - /* overflow: hidden; */ - /* overflow: clip; */ - } - - header { - grid-area: titlebar; + overscroll-behavior: none; } main { flex-grow: 1; flex-basis: 0; - grid-area: main; display: flex; flex-direction: column; - /* overflow: clip; */ overflow: hidden; - /* overflow: auto; */ - /* overflow: clip; */ box-sizing: border-box; - contain: layout; - /* contain: paint; */ - /* contain: size layout style paint; */ - /* contain: layout paint style inline-size; */ - /* contain: size; */ - /* background-clip: border-box; */ - /* overflow-clip-margin: border-box; */ - - /* isolation: isolate; */ + background-color: var(--background); } :root:is([data-transition=toggling-view]) main { view-transition-name: main-content; } - /* Arabic (ar), Hebrew (iw), Pashto (ps), Persian (fa), Sindhi (sd), Urdu (ur), Uyghur (ug), Yiddish (yi) */ - :root:is(.translated-rtl, :lang(ar), :lang(iw), :lang(ps), :lang(fa), :lang(sd), :lang(ur), :lang(ug), :lang(yi)) { + :root.translated-rtl { direction: rtl; - --titlebar-area-inline-start: calc(100dvi - env(titlebar-area-x, 0px) - env(titlebar-area-width, 0px)); --titlebar-area-inline-end: env(titlebar-area-x, 0px); } - @media not all and (display-mode: browser) { - body { - overscroll-behavior-block: none; - } - } - - @media print { - :root { - -webkit-print-color-adjust: exact; - print-color-adjust: exact; - } + dialog { + transition: opacity, display, translate, scale; + transition-duration: .1s; + transition-behavior: allow-discrete; + transition-timing-function: ease-out; - body { - display: block; - block-size: unset; - overflow: unset; - } - - main { - display: block; - overflow: unset; + @starting-style { + opacity: 0%; + scale: 50%; } - c-header { + &:not(:modal) { display: none; - } - - @page { - size: A4 portrait; - margin: 0; + opacity: 0%; + scale: 50%; } } - + dialog.messagebox { - display: flex; - flex-direction: column; - gap: .6rem; - padding: .8rem 1rem; border-radius: .5rem; background-color: var(--gray-2); box-shadow: 0 0 4rem var(--shadow-color); max-inline-size: calc(min(100% - 6rem, 40rem)); + & > form { + display: flex; + flex-direction: column; + gap: .6rem; + padding: .8rem 1rem; + } + & .message { white-space: pre-wrap; } @@ -240,17 +243,72 @@ } dialog.export { - &[open] { + inline-size: min(100% - 6rem, 50rem); + border-radius: .8rem; + background-color: var(--transparent-gray-2); + --backdrop-filter: blur(2rem); + -webkit-backdrop-filter: var(--backdrop-filter); + backdrop-filter: var(--backdrop-filter); + box-shadow: 0 0 4rem var(--shadow-color); + + & > form { display: flex; flex-direction: column; gap: .6rem; - inline-size: min(100% - 6rem, 50rem); - border-radius: .8rem; - background-color: var(--transparent-gray-2); - --backdrop-filter: blur(2rem); - -webkit-backdrop-filter: var(--backdrop-filter); - backdrop-filter: var(--backdrop-filter); - box-shadow: 0 0 4rem var(--shadow-color); + padding: 1rem; + container: ul / inline-size; + + & > ul { + display: flex; + + & > li { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0; + position: relative; + transition: background-color .1s; + display: flex; + flex-direction: inherit; + align-items: stretch; + + &:not(:first-of-type)::before { + content: ""; + background-color: var(--transparent-gray-3); + flex-basis: 1px; + transition: inherit; + } + + &:hover { + background-color: var(--transparent-gray-2); + border-radius: .5rem; + + :is(&, & + li)::before { + background-color: transparent; + } + } + + & button { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; + block-size: 100%; + padding: .6rem; + inline-size: 100%; + block-size: 100%; + box-sizing: border-box; + --icon-size: min(3rem, 80%); + + &::before { + opacity: 80%; + } + } + } + + @container ul (inline-size < 30rem) { + flex-direction: column; + } + } } &::backdrop { @@ -262,51 +320,6 @@ font-size: 1.2rem; } - & > ul { - display: flex; - - & > li { - flex-grow: 1; - flex-shrink: 1; - flex-basis: 0; - position: relative; - transition: background-color .1s, border-color .1s; - - &:not(:first-of-type) { - border-inline-start: 1px solid var(--transparent-gray-3); - } - - &:hover { - background-color: var(--transparent-gray-2); - border-radius: .5rem; - border-inline-start-color: transparent; - - & + li { - border-inline-start-color: transparent; - } - } - - & button { - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; - block-size: 100%; - padding: .6rem; - inline-size: 100%; - block-size: 100%; - box-sizing: border-box; - /* --icon-size: min(6rem, 80%); */ - --icon-size: min(3rem, 80%); - - &::before { - opacity: 80%; - } - } - } - - } - & button.close { display: grid; @@ -386,7 +399,30 @@ inline-size: 100%; block-size: 100%; } - + } + + &[data-transition=theme-change] { + &::view-transition-group(*) { + animation-duration: .4s; + animation-timing-function: ease-in; + } + + &::view-transition-old(root) { + animation: none; + } + + &::view-transition-new(root) { + animation-name: circular-portal + } + } + } + + @keyframes circular-portal { + from { + clip-path: circle(0px at calc(var(--mouse-x) * 1px) calc(var(--mouse-y) * 1px)); + } + to { + clip-path: circle(hypot(100dvb, 100dvi) at calc(var(--mouse-x) * 1px) calc(var(--mouse-y) * 1px)); } } @@ -414,6 +450,25 @@ @property --blur-radius { syntax: ""; - initial-value: 0rem; inherits: true; + initial-value: 0px; +} + +@keyframes collapse { + 0% { + padding-block-start: calc(var(--titlebar-area-block-start) + var(--titlebar-area-block-size)); + padding-inline: 0; + } + 40% { + padding-block-start: calc(var(--titlebar-area-block-start) + var(--titlebar-area-block-size)); + } + 60% { + inline-size: 100dvi; + padding-inline-start: var(--titlebar-area-inline-start); + padding-inline-end: var(--titlebar-area-inline-end); + } + 100% { + padding-block-start: calc(var(--titlebar-area-block-start) + var(--padding)); + min-block-size: var(--titlebar-area-block-size); + } } diff --git a/html/editor.c.html b/html/editor.c.html index 6e4a2bb..22bb2ad 100644 --- a/html/editor.c.html +++ b/html/editor.c.html @@ -1,10 +1,10 @@
- +
-
+
@@ -64,7 +64,6 @@ background-color: var(--background); overflow: hidden; contain: layout; - /* view-transition-name: text-input; */ & textarea { padding: .8rem 1rem; @@ -130,13 +129,9 @@ padding: 1rem; padding-block-start: 0; background-color: var(--background); - /* overflow-y: auto; */ overflow: auto; - overflow-wrap: break-word; font-size: 1.1rem; - line-height: 1.5rem; contain: layout; - /* view-transition-name: html-output; */ & .section { content-visibility: auto; @@ -153,22 +148,10 @@ } :root[data-engine=blink] & .html-output :is(msqrt > *, mroot > :first-child) { - /* translate: 0 calc(3 * 1em / 16); */ padding-block-start: calc(3 * 1em / 16); padding-inline: calc(2 * 1em / 16); } - :root[data-engine=blink] & .html-output mfrac > :last-child { - /* translate: 0 calc(1 * 1em / 16); */ - /* padding-block-start: calc(1 * 1em / 16) */ - /* translate: 0 .2em; */ - /* translate: 0 2px; */ - } - - & math { - /* margin-block: .2em; */ - } - & math[display=block] { margin-block: .5em; } diff --git a/html/files.c.html b/html/files.c.html index 3b1bf55..c58e52c 100644 --- a/html/files.c.html +++ b/html/files.c.html @@ -67,21 +67,23 @@

New file on disk

-
- -

Recently opened files

-
- -
    - -
+
+
+ +

Recently opened files

+
+ +
    + +
+
diff --git a/index.html b/index.html index 319b53a..5d7d080 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + @@ -13,9 +13,8 @@ PAMM - - + @@ -51,56 +50,60 @@ - + diff --git a/js/app.js b/js/app.js index 36b47f9..8913a9c 100644 --- a/js/app.js +++ b/js/app.js @@ -1,7 +1,4 @@ -/// -/// - export const $ = /** @template {string} K */ (/** @type {K} */ selector, /** @type {HTMLElement | Document | DocumentFragment} */ root = document) => ( root.querySelector(selector) ); @@ -20,6 +17,8 @@ export const encodeFile = (/** @type {{ text: string, data?: Record return `version 1\n-----\n${text}\n-----\n${JSON.stringify(data, null, "\t")}` }; +export const parseHTML = Range.prototype.createContextualFragment.bind(new Range()); + export const decodeFile = (/** @type {{ fileContent: string }} */ { fileContent }) => { const { data: dataString, text } = fileContent.match(/^version 1\n-----\n(?.*)\n-----\n(?{(?:(?!\n-----\n).)*})$/s)?.groups ?? {}; const data = (() => { @@ -32,6 +31,31 @@ export const decodeFile = (/** @type {{ fileContent: string }} */ { fileContent return { text, data }; } +const executeOnTransitionEnd = async (/** @type {Element} */ element, /** @type {Function} */ callback) => { + await Promise.allSettled( + element.getAnimations().filter((animation) => animation instanceof CSSTransition).map(({ finished }) => finished) + ); + callback(); + + // const onTransitionEnd = () => { + // element.removeEventListener("transitionend", onTransitionEnd); + // element.removeEventListener("transitioncancel", onTransitionEnd); + // callback(); + // }; + // if (element.getAnimations().some((animation) => animation instanceof CSSTransition)) { + // element.addEventListener("transitionend", onTransitionEnd); + // element.addEventListener("transitioncancel", onTransitionEnd); + // } else onTransitionEnd(); +}; + +export const removeAfterTransition = (/** @type {HTMLElement} */ element) => { + executeOnTransitionEnd(element, () => element.remove()); +}; + +export const _expose = (/** @type {Record} */ object) => { + for (const [key, value] of Object.entries(object)) self[key] = value; +}; + export const database = await new class { #database; constructor() { @@ -220,7 +244,7 @@ export const database = await new class { { - const introductionFileText = decodeFile({ fileContent: await (await window.fetch("./assets/introduction.pamm")).text() }).text; + const introductionFileText = decodeFile({ fileContent: await (await window.fetch(import.meta.resolve("../assets/introduction.pamm"))).text() }).text; await database.put({ store: "files", data: { ...await database.get({ store: "files", key: "b-introduction" }), content: introductionFileText } }); } @@ -229,7 +253,7 @@ export const database = await new class { if (navigator.userAgentData?.brands?.find(({ brand }) => brand === "Chromium")) engine = "blink"; else if (navigator.userAgent.match(/\bFirefox\//)) engine = "gecko"; else if (navigator.userAgent.match(/\bChrome\//)) engine = "blink"; - else if (navigator.userAgent.match(/\bVersion\//)) engine = "webkit"; + else if (navigator.userAgent.match(/\bAppleWebKit\//)) engine = "webkit"; else engine = "unknown"; document.documentElement.dataset.engine = engine; } @@ -260,11 +284,13 @@ export const alert = async (/** @type {{ message: string, userGestureCallback?: $(".message", dialog).textContent = message; document.body.append(dialog); dialog.showModal(); - await new Promise((resolve) => $("button.ok", dialog).addEventListener("click", async () => { + const { promise, resolve } = Promise.withResolvers(); + dialog.addEventListener("close", async () => { await userGestureCallback?.(); resolve(); - }, { once: true })); - dialog.remove(); + }); + await promise; + removeAfterTransition(dialog); }; export const confirm = async (/** @type {{ message: string, userGestureCallback?: Function }} */ { message, userGestureCallback }) => { @@ -272,48 +298,56 @@ export const confirm = async (/** @type {{ message: string, userGestureCallback? $(".message", dialog).textContent = message; document.body.append(dialog); dialog.showModal(); - const accepted = await new Promise((resolve) => { - $("button.ok", dialog).addEventListener("click", async () => { - await userGestureCallback?.(); - resolve(true); - }, { once: true }); - $("button.cancel", dialog).addEventListener("click", () => resolve(false), { once: true }); + const { promise, resolve } = Promise.withResolvers(); + dialog.addEventListener("close", async () => { + await userGestureCallback?.(); + resolve(); }); - dialog.remove(); - return { accepted }; + await promise; + removeAfterTransition(dialog); + return { accepted: dialog.returnValue === "ok" }; }; +_expose({ alert }) + export const prompt = async (/** @type {{ message: string, defaultValue?: string }} */ { message, defaultValue = "" }) => { const dialog = $("dialog.prompt", messageboxesTemplate).cloneNode(true); $(".message", dialog).textContent = message; $("input.input", dialog).value = defaultValue; document.body.append(dialog); dialog.showModal(); - const accepted = await new Promise((resolve) => { - $(".input", dialog).addEventListener("keydown", ({ key }) => { if (key === "Enter") resolve(true) }); - $("button.ok", dialog).addEventListener("click", () => resolve(true), { once: true }); - $("button.cancel", dialog).addEventListener("click", () => resolve(false), { once: true }); - }); - const value = accepted ? $("input.input", dialog).value : undefined; - dialog.remove(); + const { promise, resolve } = Promise.withResolvers(); + dialog.addEventListener("close", resolve); + await promise; + const accepted = dialog.returnValue === "ok"; + const value = accepted ? /** @type {string} */ (new FormData($("form", dialog)).get("input")) : undefined; + removeAfterTransition(dialog); return { accepted, value }; + + + // const accepted = await new Promise((resolve) => { + // $(".input", dialog).addEventListener("keydown", (event) => { + // if (event.key === "Enter") { + // event.preventDefault(); + // resolve(true); + // } + // }); + // $("button.ok", dialog).addEventListener("click", () => resolve(true), { once: true }); + // $("button.cancel", dialog).addEventListener("click", () => resolve(false), { once: true }); + // }); }; export const setTitle = (/** @type {string} */ title) => { - const titleArray = [ - title, - " – ", - appMeta.shortName, - ]; + const titleArray = [title, appMeta.shortName]; if (window.matchMedia("(display-mode: standalone), (display-mode: window-controls-overlay)").matches) titleArray.reverse(); - document.title = titleArray.join(""); + document.title = titleArray.join(" – "); }; const useTransitions = Boolean(document.startViewTransition && !window.matchMedia("(prefers-reduced-motion: reduce)").matches); // const useTransitions = false; -export const transition = async (/** @type {() => Promise} */ callback, /** @type {{ name?: string, resolveWhenFinished?: boolean }} */ { name, resolveWhenFinished = false } = {}) => { - if (!useTransitions || document.documentElement.classList.contains("loading")) { +export const transition = async (/** @type {() => Promise} */ callback, /** @type {{ name: string, resolveWhenFinished?: boolean }} */ { name, resolveWhenFinished = false }) => { + if (!useTransitions || document.documentElement.classList.contains("no-transitions")) { await callback(); return; } @@ -371,15 +405,12 @@ export const appMeta = { mimeType: "text/pretty-awesome-math-markup", }; -navigator.serviceWorker?.register("./service-worker.js", { scope: "./", updateViaCache: "all" }); - - { - const customElements = [ - "editor", - "header", - "files", - ]; + const customElements = { + editor: import.meta.resolve("../html/editor.c.html"), + header: import.meta.resolve("../html/header.c.html"), + files: import.meta.resolve("../html/files.c.html"), + }; // const tempDocument = document.implementation.createHTMLDocument(); @@ -403,11 +434,9 @@ navigator.serviceWorker?.register("./service-worker.js", { scope: "./", updateVi // } // }; - await Promise.all(customElements.map(async (name) => { - const html = await (await window.fetch(`./html/${name}.c.html`)).text(); - const content = (/** @type {HTMLTemplateElement} */ ( - new DOMParser().parseFromString(``, "text/html").head.firstElementChild - )).content; + await Promise.all(Object.entries(customElements).map(async ([name, path]) => { + const html = await (await window.fetch(path)).text(); + const content = parseHTML(html); const styleElement = content.querySelector("style"); const css = `c-${name} { ${styleElement.textContent} }`; @@ -450,7 +479,7 @@ export const elements = { get foldersUL() { return $("c-files ul.folders") }, get filesUL() { return $("c-files ul.files") }, get breadcrumbUL() { return $("c-files nav.breadcrumb ul") }, - get fileNameInput() { return $("c-header input.file-name") }, + get fileNameInput() { return $("c-header input[name=file-name]") }, get toggleThemeButton() { return ($("c-header button[data-action=toggle-theme]")) }, get toggleLayoutButton() { return ($("c-header button[data-action=toggle-editor-layout]")) }, files: document.querySelector("c-files"), @@ -459,16 +488,12 @@ export const elements = { get htmlOutput() { return $("section.html-output", this.editor) }, }; -// @ts-ignore -window.elements = elements; - if (navigator.windowControlsOverlay) { document.documentElement.classList.add("no-wco-animation"); const mediaMatch = window.matchMedia("(display-mode: window-controls-overlay)"); - const onGeometryChange = (/** @type {{ matches: boolean }} */ { matches }) => { + mediaMatch.addEventListener("change", () => { document.documentElement.classList.remove("no-wco-animation"); - }; - mediaMatch.addEventListener("change", onGeometryChange, { once: true }); + }, { once: true }); } { @@ -493,17 +518,25 @@ if (navigator.windowControlsOverlay) { const mediaMatch = window.matchMedia("(prefers-color-scheme: light)"); const themeInStorage = storage.get("color-theme") ?? "os-default"; let currentTheme = ((themeInStorage === "os-default" && mediaMatch.matches) || themeInStorage === "light") ? "light" : "dark"; - - const updateTheme = () => { - document.documentElement.classList.toggle("light-theme", currentTheme === "light"); - const themeColor = window.getComputedStyle(document.querySelector("c-header")).backgroundColor.trim(); + let mouseX = 0; + let mouseY = 0; + + const updateTheme = async () => { + document.documentElement.style.setProperty("--mouse-x", mouseX || button.offsetLeft); + document.documentElement.style.setProperty("--mouse-y", mouseY || button.offsetTop); + await transition(async () => { + document.documentElement.classList.toggle("light-theme", currentTheme === "light"); + }, { name: "theme-change", resolveWhenFinished: false }); + const themeColor = window.getComputedStyle(document.documentElement).backgroundColor.trim(); document.querySelector("meta[name=theme-color]").content = themeColor; }; updateTheme(); - button.addEventListener("click", async () => { + button.addEventListener("click", async (event) => { currentTheme = currentTheme === "dark" ? "light" : "dark"; storage.set("color-theme", ((currentTheme === "light") === mediaMatch.matches) ? "os-default" : currentTheme); + mouseX = event.clientX; + mouseY = event.clientY; updateTheme(); }); diff --git a/js/files.js b/js/files.js index ec1dab9..5d4f12a 100644 --- a/js/files.js +++ b/js/files.js @@ -1,8 +1,5 @@ -/// -/// - -import { $, $$, database, fileSystemAccessSupported, isApple, alert, confirm, prompt, setTitle, transition, elements, appMeta, encodeFile, decodeFile, storage } from "./app.js"; +import { $, $$, database, fileSystemAccessSupported, isApple, alert, confirm, prompt, setTitle, transition, elements, appMeta, encodeFile, decodeFile, storage, removeAfterTransition } from "./app.js"; import { startRendering } from "./main.js"; import parseDocument from "./parse/document/parseDocument.js"; import renderDocument from "./render/document/renderDocument.js"; @@ -85,6 +82,15 @@ const renderFile = async (/** @type {{ storageType: FileStorageType, id?: string if (!fileContent) return {}; const { data, text } = decodeFile({ fileContent }); elements.textInput.value = text; + if (window.FileSystemObserver) { + const observer = new FileSystemObserver(async (records) => { + let fileContent = await (await fileHandle.getFile()).text(); + const { data, text } = decodeFile({ fileContent }); + elements.textInput.value = text; + startRendering(); + }); + observer.observe(fileHandle); + } startRendering(); elements.textInput.focus(); window.requestAnimationFrame(async () => (await 0, elements.textInput.scrollTo({ top: 0 }))); @@ -470,61 +476,65 @@ const fileUtils = new class { renderFileArguments, } = {}) { const dialog = /** @type {HTMLDialogElement} */ ($("template#export-dialog").content.firstElementChild.cloneNode(true)); - $("button.close", dialog).addEventListener("click", () => dialog.remove()); - $("[data-action=download]", dialog).addEventListener("click", async () => { - dialog.remove(); - this.downloadFile({ name, content }); - }); - $("[data-action=print]", dialog).addEventListener("click", async () => { - if (renderFileArguments) { - await renderFile(renderFileArguments); - - window.addEventListener("afterprint", async () => { - await 0; // https://crbug.com/1316315 - await toggleView({ filesView: true }); - await displayFolder({ id: currentFolder.id }); - }, { once: true }); - } - dialog.remove(); - this.printFile(); - }); - $("[data-action=export-html]", dialog).addEventListener("click", async () => { - dialog.remove(); - const tempDiv = document.createElement("div"); - const tree = parseDocument(content ?? elements.textInput.value.normalize()) - for (const item of tree) { - tempDiv.append(renderDocument([item])); - tempDiv.append(document.createTextNode("\n")); - } - const anchor = document.createElement("a"); - const html = [ - ``, - ``, - ``, - ``, - ``, - ``, - Object.assign(document.createElement("title"), { textContent: name ?? currentFile.name }).outerHTML, - ``, - ``, - ``, - ``, - tempDiv.innerHTML, - ``, - ``, - ].join("\n"); - anchor.href = URL.createObjectURL(new Blob([html], { type: appMeta.mimeType })); - anchor.download = (name ?? currentFile.name) + ".html"; - anchor.click(); - URL.revokeObjectURL(anchor.href); - }); document.body.append(dialog); dialog.showModal(); + dialog.addEventListener("close", async () => { + removeAfterTransition(dialog); + switch (dialog.returnValue) { + case ("download"): { + this.downloadFile({ name, content }); + break; + } + case ("print"): { + if (renderFileArguments) { + await renderFile(renderFileArguments); + + window.addEventListener("afterprint", async () => { + await 0; // https://crbug.com/1316315 + await toggleView({ filesView: true }); + await displayFolder({ id: currentFolder.id }); + }, { once: true }); + } + this.printFile(); + break; + } + case ("export-html"): { + const tempDiv = document.createElement("div"); + const tree = parseDocument(content ?? elements.textInput.value.normalize()) + for (const item of tree) { + tempDiv.append(renderDocument([item])); + tempDiv.append(document.createTextNode("\n")); + } + const anchor = document.createElement("a"); + const html = [ + ``, + ``, + ``, + ``, + ``, + ``, + Object.assign(document.createElement("title"), { textContent: name ?? currentFile.name }).outerHTML, + ``, + ``, + ``, + ``, + tempDiv.innerHTML, + ``, + ``, + ].join("\n"); + anchor.href = URL.createObjectURL(new Blob([html], { type: appMeta.mimeType })); + anchor.download = (name ?? currentFile.name) + ".html"; + anchor.click(); + URL.revokeObjectURL(anchor.href); + break; + } + } + }); }; }; @@ -539,12 +549,7 @@ const fileUtils = new class { elements.recentlyOpenedButton.addEventListener("click", async () => { const dialog = elements.recentlyOpenedDialog; - const closeDialog = () => { - dialog.close(); - for (const element of $$(":scope > ul > li", dialog)) { - element.remove(); - } - }; + // const closeDialog = () => dialog.close(); const recentlyOpenedFiles = await Promise.all((await database.get({ store: "key-value", key: "recently-opened" })).value.map(async ({ id, storageType }) => { const store = { "indexeddb": "files", @@ -558,7 +563,7 @@ const fileUtils = new class { })); const UL = $("ul", dialog); const listItem = $(":scope > template", UL).content; - $("button.close", dialog).addEventListener("click", closeDialog, { once: true }); + for (const element of $$(":scope > form > ul > li", dialog)) element.remove(); for (const { id, storageType, name, fileHandle } of recentlyOpenedFiles) { const clone = listItem.cloneNode(true); $("[data-storagetype]", clone).dataset.storagetype = storageType; @@ -567,7 +572,7 @@ const fileUtils = new class { for (const [selectorString, changeURL] of /** @type {const} */ ([["a.link", false], ["a.permalink", true]])) { if (storageType === "indexeddb") { $(selectorString, clone).addEventListener("click", (event) => { - closeDialog(); + dialog.close(); itemClickHandler({ id, type: "file", storageType, changeURL })(event); }); } else if (storageType === "file-system") { @@ -577,7 +582,7 @@ const fileUtils = new class { if (await fileHandle.queryPermission({ mode: "read" }) !== "granted") { if (await fileHandle.requestPermission({ mode: "readwrite" }) !== "granted") return; } - closeDialog(); + dialog.close(); await itemClickHandler({ id, type: "file", storageType, fileHandle, changeURL })(); }); } else throw new Error(`Unknown storage type (${storageType})`); @@ -696,7 +701,7 @@ window.addEventListener("beforeunload", (event) => { } window.setTimeout(() => { - document.documentElement.classList.remove("loading"); + document.documentElement.classList.remove("no-transitions"); }, 500); })(); } diff --git a/js/global.d.ts b/js/global.d.ts index cf51611..eccd692 100644 --- a/js/global.d.ts +++ b/js/global.d.ts @@ -6,3 +6,5 @@ type ItemType = "folder" | "file"; type EditorLayout = "aside" | "stacked"; declare function log(data: T, trace?: boolean): T; + +declare var FileSystemObserver: any; diff --git a/js/main.js b/js/main.js index 214c68d..20f9d11 100644 --- a/js/main.js +++ b/js/main.js @@ -1,5 +1,12 @@ -/// +{ + // Promise.withResolvers() polyfill until browser support is better + Promise.withResolvers ??= function () { + let resolve, reject; + let promise = new Promise((res, rej) => (resolve = res, reject = rej)); + return { promise, resolve, reject }; + }; +} import { elements, $$, storage, transition } from "./app.js"; import parseDocument from "./parse/document/parseDocument.js"; @@ -9,7 +16,7 @@ import { keyDown } from "./files.js"; export const { startRendering } = (() => { let previousSectionsArray = []; - const handleInput = function (/** @type {InputEvent} */ { data } = /** @type {any} */ ({})) { + const handleInput = function (/** @type {Partial} */ { data } = ({})) { const { value, selectionStart, selectionEnd } = this; document.documentElement.classList.add("file-dirty"); diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..868f126 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "types": [ + "better-typescript", + // "../better-ts", + "./js/global.d.ts", + ], + }, +} \ No newline at end of file diff --git a/service-worker.js b/service-worker.js deleted file mode 100644 index 9aab716..0000000 --- a/service-worker.js +++ /dev/null @@ -1,11 +0,0 @@ - -/// -/// -/// -/// -/// - -self.addEventListener("fetch", (event) => { - event.respondWith; -}); -