feature (admin): modal component

This commit is contained in:
Mickael Kerjean 2023-07-25 23:32:30 +10:00
parent a301c28d96
commit c83f57f8b2
6 changed files with 223 additions and 17 deletions

14
public/Makefile Normal file
View file

@ -0,0 +1,14 @@
all:
find . -type f -name '*.html' | xargs brotli -f -k
find . -type f -name '*.html' | xargs gzip -f -k
find . -type f -name '*.js' | xargs brotli -f -k
find . -type f -name '*.js' | xargs gzip -f -k --best
find . -type f -name '*.css' | xargs brotli -f -k
find . -type f -name '*.css' | xargs gzip -f -k
clean:
find . -name '*.gz' -exec rm {} \;
find . -name '*.br' -exec rm {} \;
serve:
go run server.go

View file

@ -0,0 +1,56 @@
.component_modal{
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: #f2f3f5f0;
z-index: 1000;
}
.component_modal > div{
box-shadow: 1px 2px 20px rgba(0, 0, 0, 0.1);
background: white;
width: 80%;
max-width: 310px;
padding: 20px 20px 0 20px;
border-radius: 2px;
}
.dark-mode .component_modal{
background: var(--bg-color);
}
.component_popup .popup--content {
font-size: 1.1em;
margin: 0;
}
.component_popup .popup--content p {
margin: 0;
}
.component_popup .popup--content .modal-error-message {
font-size: 15px;
}
.component_popup .buttons {
margin: 15px -20px 0 -20px;
display: flex;
}
.component_popup .buttons > div {
display: flex;
width: 100%;
}
.component_popup .buttons [type="submit"] {
border-radius: 10px 0 0;
}
.component_popup .buttons > button {
width: 50%;
margin-left: auto;
}
.component_popup .buttons button {
text-transform: uppercase;
}
.component_popup .modal-error-message {
color: var(--error);
}
.component_popup .center {
text-align: center;
}

View file

@ -1,13 +1,114 @@
import { createElement } from "../common/skeleton/index.js";
import { createElement } from "../lib/skeleton/index.js";
import rxjs, { applyMutation } from "../lib/rxjs/index.js";
import { animate } from "../lib/animate/index.js";
import { qs } from "../lib/dom/index.js";
export function prompt(label, okFn, errFn) {
const $node = createElement(`
<dialog open>
<p>Greetings, one and all!</p>
<form method="dialog">
<button>OK</button>
</form>
</dialog>`);
document.body.appendChild($node);
okFn("OK")
import CSSLoader from "../../helpers/css.js";
// http://127.0.0.1:8000/admin/setup
const _observables = [];
const effect = (obs) => _observables.push(obs.subscribe());
const free = () => {
for (let i=0; i<_observables.length; i++) {
_observables[i].unsubscribe();
}
_observables = [];
}
class Modal extends HTMLElement {
constructor() {
super();
}
trigger($node, opts = {}) {
const { onQuit, leftButton, rightButton } = opts;
const $modal = createElement(`
<div class="component_modal" id="modal-box">
<style>${css}</style>
<div>
<div class="component_popup">
<div class="popup--content">
<div class="modal-message" data-bind="body"><!-- MODAL BODY --></div>
</div>
<div class="buttons">
<button type="submit" class="emphasis">OK</button>
</div>
</div>
</div>
</div>`);
this.replaceChildren($modal);
// feature: setup the modal body
effect(rxjs.of([$node]).pipe(
applyxMutation(qs($modal, `[data-bind="body"]`), "appendChild"),
));
// feature: closing the modal
effect(rxjs.merge(
rxjs.fromEvent($modal, "click").pipe(
rxjs.filter((e) => e.target.getAttribute("id") === "modal-box")
),
rxjs.fromEvent(window, "keydown").pipe(
rxjs.filter((e) => e.keyCode === 27),
),
).pipe(
rxjs.tap(() => typeof onQuit === "function" && onQuit()),
rxjs.tap(() => animate(qs($modal, "div > div"), {
time: 200,
keyframes: [
{ opacity: 1, transform: "translateY(0)" },
{ opacity: 0, transform: "translateY(20px)" },
]
})),
rxjs.delay(100),
rxjs.tap(() => animate($modal, {
time: 200,
keyframes: [ { opacity: 1 }, { opacity: 0 } ]
})),
rxjs.mapTo([]), applyMutation($modal, "remove"),
rxjs.tap(free),
));
// feature: animate opening
effect(rxjs.of(["opacity", "0"]).pipe(
applyMutation(qs($modal, "div > div"), "style", "setProperty"),
rxjs.tap(() => animate($modal, {
time: 250,
keyframes: [
{ opacity: 0 },
{ opacity: 1 },
],
})),
rxjs.delay(50),
rxjs.tap(() => animate(qs($modal, "div > div"), {
time: 200,
keyframes: [
{ opacity: 0, transform: "translateY(10px)" },
{ opacity: 1, transform: "translateY(0)" },
],
})),
));
// feature: center horizontally
effect(rxjs.fromEvent(window, "resize").pipe(
rxjs.startWith(null),
rxjs.distinct(() => document.body.offsetHeight),
rxjs.map(() => {
let size = 300;
const $box = document.querySelector("#modal-box > div");
if ($box) size = $box.offsetHeight;
size = Math.round((document.body.offsetHeight - size) / 2);
if (size < 0) return 0;
if (size > 250) return 250;
return size;
}),
rxjs.map((size) => ["margin", `${size}px auto 0 auto`]),
applyMutation(qs(this, ".component_modal > div"), "style", "setProperty"),
));
}
}
customElements.define("component-modal", Modal);
const css = await CSSLoader(import.meta, "modal.css");

13
public/helpers/modal.js Normal file
View file

@ -0,0 +1,13 @@
// prompt, alert, confirm, modal, popup?
class ModalManager {
constructor() {
this.$dom = document.body.querySelector("component-modal");
if (!this.$dom) throw new Error("dom not set");
}
alert($node, opts) {
this.$dom.trigger($node, opts);
}
}
export default new ModalManager();

View file

@ -32,6 +32,10 @@
loader: `<data-loader></data-loader>`
});
</script>
<script type="module" src="/components/modal.js"></script>
<component-modal></component-modal>
<noscript>
<div>
<h2>Error: Javascript is off</h2>

View file

@ -5,13 +5,15 @@ import { ApplicationError } from "../../lib/error/index.js";
import { transition, animate } from "../../lib/animate/index.js";
import CSSLoader from "../../helpers/css.js";
import modal from "../../helpers/modal.js";
import ctrlError from "../ctrl_error.js";
import WithShell from "./decorator_sidemenu.js";
import { zoomIn, slideXOut, slideXIn, cssHideMenu } from "./animate.js";
import "../../components/icon.js";
const stepper$ = new rxjs.BehaviorSubject(1);
const stepper$ = new rxjs.BehaviorSubject(2);
export default function(render) {
const $page = createElement(`
@ -23,7 +25,6 @@ export default function(render) {
render($page);
effect(stepper$.pipe(
dbg("CHANGE"),
rxjs.map((step) => {
switch(step) {
case 1: return WithShell(componentStep1);
@ -94,7 +95,6 @@ function componentStep2(render) {
// feature: navigate previous step
effect(rxjs.fromEvent(qs($page, `[data-bind="previous"]`), "click").pipe(
dbg("click"),
rxjs.tap(() => stepper$.next(1)),
));
@ -104,11 +104,29 @@ function componentStep2(render) {
rxjs.tap(() => animate(qs($page, "h4"), { time: 200, keyframes: slideXIn(30) })),
rxjs.delay(200),
rxjs.mapTo([]), applyMutation(qs($page, "style"), "remove"),
dbg("")
));
// feature: opt in for telemetry
onDestroy(() => console.log("opt in for telemetry"));
// feature: telemetry popup
onDestroy(() => {
const $node = createElement(`
<div>
<p style="text-align: justify;">Help making this software better by sending crash reports and anonymous usage statistics</p>
<form style="font-size: 0.9em; margin-top: 10px;">
<label>
<div class="component_checkbox">
<input type="checkbox">
<span class="indicator"></span>
</div>I accept but the data is not to be share with any third party
</label>
</form>
</div>
`);
return new Promise((done) => {
modal.alert($node, {
onQuit: done,
});
});
});
}
const animateOut = ($el) => {