From 6573d4d9fb097795185f3b56f36802ec02c37891 Mon Sep 17 00:00:00 2001 From: MickaelK Date: Sat, 16 Dec 2023 03:26:53 +1100 Subject: [PATCH] feature (viewerpage): form app --- public/assets/boot/ctrl_boot_frontoffice.js | 3 + public/assets/components/fab.css | 16 +++ public/assets/components/fab.js | 16 +++ public/assets/helpers/loader.js | 4 +- .../pages/viewerpage/application_audio.js | 9 +- .../pages/viewerpage/application_form.js | 133 +++++++++++++----- .../pages/viewerpage/application_pdf.js | 2 - public/assets/pages/viewerpage/common.js | 15 ++ public/assets/pages/viewerpage/common_fab.js | 6 + 9 files changed, 159 insertions(+), 45 deletions(-) create mode 100644 public/assets/components/fab.css create mode 100644 public/assets/components/fab.js create mode 100644 public/assets/pages/viewerpage/common_fab.js diff --git a/public/assets/boot/ctrl_boot_frontoffice.js b/public/assets/boot/ctrl_boot_frontoffice.js index b107eaec..0dddfca7 100644 --- a/public/assets/boot/ctrl_boot_frontoffice.js +++ b/public/assets/boot/ctrl_boot_frontoffice.js @@ -1,8 +1,10 @@ import rxjs, { ajax } from "../lib/rx.js"; import { loadJS } from "../helpers/loader.js"; // import { setup_cache } from "../helpers/cache.js"; +import { init as setup_loader } from "../helpers/loader.js"; import { report } from "../helpers/log.js"; + export default async function main() { try { await Promise.all([ // procedure with no outside dependencies @@ -12,6 +14,7 @@ export default async function main() { setup_device(), // setup_sw(), // TODO setup_blue_death_screen(), + setup_loader(), setup_history(), ]); // await Config.refresh() diff --git a/public/assets/components/fab.css b/public/assets/components/fab.css new file mode 100644 index 00000000..1bcc46f6 --- /dev/null +++ b/public/assets/components/fab.css @@ -0,0 +1,16 @@ +.component_fab { + position: fixed; + bottom: 20px; + right: 20px; + z-index: 2; +} +.component_fab .content { + height: 25px; + width: 25px; + padding: 13px; + border-radius: 50%; + background: var(--dark); + box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px; + z-index: 1000; + cursor: pointer; +} diff --git a/public/assets/components/fab.js b/public/assets/components/fab.js new file mode 100644 index 00000000..f9e4f14c --- /dev/null +++ b/public/assets/components/fab.js @@ -0,0 +1,16 @@ +import { loadCSS } from "../helpers/loader.js"; + +export default class ComponentFab extends window.HTMLButtonElement { + constructor() { + super(); + this.innerHTML = `
`; + this.classList.add("component_fab"); + } + + async render($icon) { + await loadCSS(import.meta.url, "./fab.css"); + this.querySelector(".content").replaceChildren($icon); + } +} + +customElements.define("component-fab", ComponentFab, { extends: "button" }); diff --git a/public/assets/helpers/loader.js b/public/assets/helpers/loader.js index f8657b20..a327af04 100644 --- a/public/assets/helpers/loader.js +++ b/public/assets/helpers/loader.js @@ -4,7 +4,7 @@ let version = null; export async function loadJS(baseURL, path, opts = {}) { const $script = document.createElement("script"); - const link = new URL(path, baseURL); + const link = new URL(path, baseURL) + "?version=" + version; $script.setAttribute("src", link.toString()); for (let key in opts) { $script.setAttribute(key, opts[key]); @@ -20,7 +20,7 @@ export async function loadJS(baseURL, path, opts = {}) { export async function loadCSS(baseURL, path) { const $style = document.createElement("link"); - const link = new URL(path, baseURL); + const link = new URL(path, baseURL) + "?version=" + version; $style.setAttribute("href", link.toString()); $style.setAttribute("rel", "stylesheet"); if (document.head.querySelector(`[href="${link.toString()}"]`)) return Promise.resolve(); diff --git a/public/assets/pages/viewerpage/application_audio.js b/public/assets/pages/viewerpage/application_audio.js index 21c39f2b..f98a752a 100644 --- a/public/assets/pages/viewerpage/application_audio.js +++ b/public/assets/pages/viewerpage/application_audio.js @@ -1,14 +1,13 @@ import { createElement } from "../../lib/skeleton/index.js"; -import rxjs, { effect } from "../lib/rx.js"; +import { onDestroy } from "../../lib/skeleton/lifecycle.js"; +import rxjs, { effect } from "../../lib/rx.js"; import WaveSurfer from "../../lib/vendor/wavesurfer.js"; export default function(render) { const $page = createElement(`
-
- AUDIO -
+
AUDIO
`); render($page); @@ -24,5 +23,5 @@ export default function(render) { height: 200, barWidth: 1, }); - // onDestroy(() => wavesurfer.destroy()); + onDestroy(() => wavesurfer.destroy()); } diff --git a/public/assets/pages/viewerpage/application_form.js b/public/assets/pages/viewerpage/application_form.js index 7ab3c0ba..3cb1b987 100644 --- a/public/assets/pages/viewerpage/application_form.js +++ b/public/assets/pages/viewerpage/application_form.js @@ -1,64 +1,125 @@ import { createElement } from "../../lib/skeleton/index.js"; -import rxjs, { effect, applyMutation } from "../../lib/rx.js"; +import rxjs, { effect, applyMutation, onClick } from "../../lib/rx.js"; +import { animate, slideXIn, opacityOut } from "../../lib/animate.js"; import { qs, qsa } from "../../lib/dom.js"; import { loadCSS } from "../../helpers/loader.js"; import { createForm, mutateForm } from "../../lib/form.js"; import { formTmpl } from "../../components/form.js"; -import ajax from "../../lib/ajax.js"; import ctrlError from "../ctrl_error.js"; -import { transition, getDownloadUrl } from "./common.js"; +import { transition, getFile$, saveFile$ } from "./common.js"; +import { $ICON } from "./common_fab.js"; +import "../../components/icon.js"; import "../../components/menubar.js"; +import "../../components/fab.js"; export default function(render) { const $page = createElement(`
-
-
+
+
`); render($page); transition(qs($page, ".formviewer_container")); - const spec$ = ajax(getDownloadUrl()).pipe( - rxjs.map(({ response }) => JSON.parse(response)), - rxjs.share(), - ); - const setup$ = spec$.pipe( - rxjs.mergeMap((formSpec) => createForm(formSpec, formTmpl({ - renderLeaf: ({ label = "", format = (s) => s, description = "", required = false }) => { - const mandatory = required ? `*` : ""; - return createElement(` - - `); - }, - }))), - applyMutation(qs($page, "form"), "replaceChildren"), - rxjs.catchError(ctrlError()), - rxjs.share(), - ); + const file$ = new rxjs.ReplaySubject(1); + const $fab = qs($page, `[is="component-fab"]`); + const formState = () => [...new FormData(qs($page, "form"))].reduce((acc, el) => { + acc[el[0]] = el[1]; + return acc; + }, {}); - effect(setup$); - effect(setup$.pipe( - rxjs.mergeMap(() => qsa($page, "form [name]")), - rxjs.mergeMap(($el) => rxjs.fromEvent($el, "input")), - rxjs.tap(() => console.log("SETUP SAVE BUTTON")), + // feature1: setup the dom + effect(getFile$().pipe( + rxjs.map((content) => JSON.parse(content)), + rxjs.mergeMap((formSpec) => rxjs.from(createForm(formSpec, formTmpl({ + renderLeaf: ({ label, format, description, required }) => createElement(` + + `), + }))).pipe( + applyMutation(qs($page, "form"), "replaceChildren"), + rxjs.mapTo(formSpec), + )), + rxjs.tap((formSpec) => file$.next(formObjToJSON(formSpec))), + rxjs.catchError(ctrlError()), + )); + + // feature2: display/hide save button + effect(rxjs.merge( + file$.asObservable(), + file$.pipe( + rxjs.first(), + rxjs.mergeMap((formSpec) => rxjs.from(qsa($page, "form [name]")).pipe( + rxjs.mergeMap(($el) => rxjs.fromEvent($el, "input")), + rxjs.mapTo(formSpec), + )), + ), + ).pipe( + rxjs.map((originalState) => { + const smod = (key, value) => value ? value : undefined; + return JSON.stringify(originalState, smod) !== JSON.stringify(formState(), smod); + }), + rxjs.mergeMap(async (isSaveButtonVisible) => { + if (isSaveButtonVisible && $fab.classList.contains("hidden")) { + $fab.render($ICON.SAVING); + $fab.classList.remove("hidden"); + await animate($fab, { time: 100, keyframes: slideXIn(40) }); + } else if (!isSaveButtonVisible && !$fab.classList.contains("hidden")) { + await animate($fab, { time: 100, keyframes: opacityOut() }); + $fab.classList.add("hidden"); + } + }), + rxjs.catchError(ctrlError()), + )); + + // feature3: submit the form + effect(onClick($fab).pipe( + rxjs.tap(() => { + $fab.render($ICON.LOADING) + $fab.disabled = true; + }), + rxjs.mergeMap(($fab) => rxjs.of(JSON.stringify(formState())).pipe( + saveFile$(), + rxjs.mergeMap(() => getFile$().pipe(rxjs.tap((formSpec) => file$.next(JSON.parse(formSpec))))), + )), + rxjs.tap(() => { + $fab.render($ICON.SAVING); + $fab.removeAttribute("disabled"); + }), + rxjs.catchError(ctrlError()), )); } export function init() { return loadCSS(import.meta.url, "./application_form.css"); } + +const formObjToJSON = (o, level = 0) => { + const obj = Object.assign({}, o); + Object.keys(obj).forEach((key) => { + const t = obj[key]; + if ("label" in t && "type" in t && "default" in t && "value" in t) { + obj[key] = obj[key].value; + } else { + obj[key] = formObjToJSON(obj[key], level + 1); + } + }); + return obj; +}; diff --git a/public/assets/pages/viewerpage/application_pdf.js b/public/assets/pages/viewerpage/application_pdf.js index 20a77b05..829a60c9 100644 --- a/public/assets/pages/viewerpage/application_pdf.js +++ b/public/assets/pages/viewerpage/application_pdf.js @@ -1,6 +1,5 @@ import { createElement } from "../../lib/skeleton/index.js"; import rxjs, { effect, onLoad } from "../../lib/rx.js"; -import { animate, opacityIn } from "../../lib/animate.js"; import { qs } from "../../lib/dom.js"; import { createLoader } from "../../components/loader.js"; import { loadCSS, loadJS } from "../../helpers/loader.js"; @@ -82,7 +81,6 @@ async function ctrlPDFJs(render) { } createBr(); }), - rxjs.tap(() => transition($container)), rxjs.catchError(ctrlError()), )); } diff --git a/public/assets/pages/viewerpage/common.js b/public/assets/pages/viewerpage/common.js index 8da1f3ec..4d5f1d2c 100644 --- a/public/assets/pages/viewerpage/common.js +++ b/public/assets/pages/viewerpage/common.js @@ -1,10 +1,25 @@ import { transition as transitionLib, slideYIn } from "../../lib/animate.js"; import { basename } from "../../lib/path.js"; +import rxjs from "../../lib/rx.js"; +import ajax from "../../lib/ajax.js"; export function transition($node) { return transitionLib($node, { timeEnter: 150, enter: slideYIn(2) }); } +export function getFile$() { + return ajax(getDownloadUrl()).pipe( + rxjs.map(({ response }) => response), + ); +} + +export function saveFile$() { + return rxjs.pipe( + rxjs.delay(2000), + rxjs.tap((content) => console.log("SAVED", content)), + ); +} + export function getFilename() { return basename(getCurrentPath()) || "untitled.dat"; } diff --git a/public/assets/pages/viewerpage/common_fab.js b/public/assets/pages/viewerpage/common_fab.js new file mode 100644 index 00000000..9e874a56 --- /dev/null +++ b/public/assets/pages/viewerpage/common_fab.js @@ -0,0 +1,6 @@ +import { createElement } from "../../lib/skeleton/index.js"; + +export const $ICON = { + SAVING: createElement(`save`), + LOADING: createElement(``), +};