feature (viewerpage): form app

This commit is contained in:
MickaelK
2023-12-16 03:26:53 +11:00
parent 7494da3e57
commit 6573d4d9fb
9 changed files with 159 additions and 45 deletions
@@ -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()
+16
View File
@@ -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;
}
+16
View File
@@ -0,0 +1,16 @@
import { loadCSS } from "../helpers/loader.js";
export default class ComponentFab extends window.HTMLButtonElement {
constructor() {
super();
this.innerHTML = `<div class="content"></div>`;
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" });
+2 -2
View File
@@ -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();
@@ -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(`
<div class="component_formviewer">
<component-menubar></component-menubar>
<div id="waveform">
AUDIO
</div>
<div id="waveform">AUDIO</div>
</div>
`);
render($page);
@@ -24,5 +23,5 @@ export default function(render) {
height: 200,
barWidth: 1,
});
// onDestroy(() => wavesurfer.destroy());
onDestroy(() => wavesurfer.destroy());
}
@@ -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(`
<div class="component_formviewer">
<component-menubar></component-menubar>
<div class="formviewer_container">
<form class="sticky box">
</form>
<form class="sticky box"></form>
</div>
<button is="component-fab"></button>
</div>
`);
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 ? `<span class="mandatory">*</span>` : "";
return createElement(`
<label class="no-select">
<div>
<span> ${format(label)} ${mandatory} </span>
<div data-bind="children"></div>
</div>
<div>
<span class="nothing"></span>
<div class="description">${description}</div>
</div>
</label>
`);
},
}))),
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(`
<label class="no-select">
<div>
<span>
${format(label)}
` + (required === true ? `<span class="mandatory">*</span>`: "") +`
</span>
<div data-bind="children"></div>
</div>
<div>
<span class="nothing"></span>
<div class="description">${description || ""}</div>
</div>
</label>
`),
}))).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;
};
@@ -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()),
));
}
+15
View File
@@ -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";
}
@@ -0,0 +1,6 @@
import { createElement } from "../../lib/skeleton/index.js";
export const $ICON = {
SAVING: createElement(`<img class="component_icon" draggable="false" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MzguNTMzIDQzOC41MzMiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDQzOC41MzMgNDM4LjUzMzsiPgogIDxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik00MzIuODIzLDEyMS4wNDljLTMuODA2LTkuMTMyLTguMzc3LTE2LjM2Ny0xMy43MDktMjEuNjk1bC03OS45NDEtNzkuOTQyYy01LjMyNS01LjMyNS0xMi41Ni05Ljg5NS0yMS42OTYtMTMuNzA0ICAgQzMwOC4zNDYsMS45MDMsMjk5Ljk2OSwwLDI5Mi4zNTcsMEgyNy40MDlDMTkuNzk4LDAsMTMuMzI1LDIuNjYzLDcuOTk1LDcuOTkzYy01LjMzLDUuMzI3LTcuOTkyLDExLjc5OS03Ljk5MiwxOS40MTR2MzgzLjcxOSAgIGMwLDcuNjE3LDIuNjYyLDE0LjA4OSw3Ljk5MiwxOS40MTdjNS4zMyw1LjMyNSwxMS44MDMsNy45OTEsMTkuNDE0LDcuOTkxaDM4My43MThjNy42MTgsMCwxNC4wODktMi42NjYsMTkuNDE3LTcuOTkxICAgYzUuMzI1LTUuMzI4LDcuOTg3LTExLjgsNy45ODctMTkuNDE3VjE0Ni4xNzhDNDM4LjUzMSwxMzguNTYyLDQzNi42MjksMTMwLjE4OCw0MzIuODIzLDEyMS4wNDl6IE0xODIuNzI1LDQ1LjY3NyAgIGMwLTIuNDc0LDAuOTA1LTQuNjExLDIuNzE0LTYuNDIzYzEuODA3LTEuODA0LDMuOTQ5LTIuNzA4LDYuNDIzLTIuNzA4aDU0LjgxOWMyLjQ2OCwwLDQuNjA5LDAuOTAyLDYuNDE3LDIuNzA4ICAgYzEuODEzLDEuODEyLDIuNzE3LDMuOTQ5LDIuNzE3LDYuNDIzdjkxLjM2MmMwLDIuNDc4LTAuOTEsNC42MTgtMi43MTcsNi40MjdjLTEuODA4LDEuODAzLTMuOTQ5LDIuNzA4LTYuNDE3LDIuNzA4aC01NC44MTkgICBjLTIuNDc0LDAtNC42MTctMC45MDItNi40MjMtMi43MDhjLTEuODA5LTEuODEyLTIuNzE0LTMuOTQ5LTIuNzE0LTYuNDI3VjQ1LjY3N3ogTTMyOC45MDYsNDAxLjk5MUgxMDkuNjMzVjI5Mi4zNTVoMjE5LjI3MyAgIFY0MDEuOTkxeiBNNDAyLDQwMS45OTFoLTM2LjU1MmgtMC4wMDdWMjgzLjIxOGMwLTcuNjE3LTIuNjYzLTE0LjA4NS03Ljk5MS0xOS40MTdjLTUuMzI4LTUuMzI4LTExLjgtNy45OTQtMTkuNDEtNy45OTRIMTAwLjQ5OCAgIGMtNy42MTQsMC0xNC4wODcsMi42NjYtMTkuNDE3LDcuOTk0Yy01LjMyNyw1LjMyOC03Ljk5MiwxMS44LTcuOTkyLDE5LjQxN3YxMTguNzczSDM2LjU0NFYzNi41NDJoMzYuNTQ0djExOC43NzEgICBjMCw3LjYxNSwyLjY2MiwxNC4wODQsNy45OTIsMTkuNDE0YzUuMzMsNS4zMjcsMTEuODAzLDcuOTkzLDE5LjQxNyw3Ljk5M2gxNjQuNDU2YzcuNjEsMCwxNC4wODktMi42NjYsMTkuNDEtNy45OTMgICBjNS4zMjUtNS4zMjcsNy45OTQtMTEuNzk5LDcuOTk0LTE5LjQxNFYzNi41NDJjMi44NTQsMCw2LjU2MywwLjk1LDExLjEzNiwyLjg1M2M0LjU3MiwxLjkwMiw3LjgwNiwzLjgwNSw5LjcwOSw1LjcwOGw4MC4yMzIsODAuMjMgICBjMS45MDIsMS45MDMsMy44MDYsNS4xOSw1LjcwOCw5Ljg1MWMxLjkwOSw0LjY2NSwyLjg1Nyw4LjMzLDIuODU3LDEwLjk5NFY0MDEuOTkxeiIvPgo8L3N2Zz4K" alt="save" style="height: 100%; width: 100%;">`),
LOADING: createElement(`<component-icon name="loading"></component-icon>`),
};