UNPKG

26.9 kBJavaScriptView Raw
1function systemReducer(state, action) {
2 switch (action.type) {
3 case "SET": {
4 const { name, value } = action.payload;
5 return {
6 ...state,
7 [name]: value,
8 }
9 }
10 case "MERGE": {
11 return {
12 ...state,
13 ...action.payload,
14 }
15 }
16 }
17}
18
19/*
20
21Notes
22
23 - any call to dispatch will immediately invoke the reducer
24 - any call to dispatch within middleware is also immediately processed
25 - SET and MERGE are not interceptable by middleware
26 - middleware functions are assumed to call next synchronously
27 - the render function (subscriber) is invoked once for every call to dispatch
28 - the render function is debounced
29
30*/
31
32function configure(
33 { update = {}, middleware = [], derivations = {}, initialState = {} },
34 node
35) {
36 let subscribers = [];
37 let state;
38 let updatedCallback = () => {};
39
40 function updateState(o) {
41 state = { ...o };
42 for (let k in derivations) {
43 state[k] = derivations[k](o);
44 }
45 }
46
47 updateState(initialState);
48
49 function getState() {
50 return { ...state }
51 }
52
53 function subscribe(fn) {
54 subscribers.push(fn);
55 }
56
57 function flush() {
58 subscribers.forEach((fn) => fn());
59 subscribers = [];
60 }
61
62 function onUpdate(fn) {
63 updatedCallback = fn;
64 }
65
66 function dispatch(action) {
67 const { type } = action;
68 if (type === "SET" || type === "MERGE") {
69 updateState(systemReducer(getState(), action));
70 } else {
71 if (middleware.length) {
72 let mw = middleware.slice();
73
74 let next = (action) => {
75 if (mw.length) {
76 let x = mw.shift();
77
78 if (action.type in x) {
79 x[action.type](action, next, {
80 getState,
81 dispatch,
82 afterNextRender: subscribe,
83 });
84 } else {
85 next(action);
86 }
87 } else if (action.type in update) {
88 updateState(update[action.type](getState(), action));
89 }
90 };
91
92 let x = mw.shift();
93
94 if (type in x) {
95 x[type](action, next, {
96 getState,
97 dispatch,
98 afterNextRender: subscribe,
99 });
100 } else {
101 next(action);
102 }
103 } else if (type in update) {
104 updateState(update[type](getState(), action));
105 }
106 }
107 updatedCallback();
108 }
109
110 for (let actionName in update) {
111 if (actionName.startsWith("$")) {
112 node.addEventListener(actionName, ({ detail }) => dispatch(detail));
113 }
114 }
115
116 return {
117 dispatch,
118 getState,
119 onUpdate,
120 flush,
121 }
122}
123
124const TEXT = 1;
125const ATTRIBUTE = 2;
126const INPUT = 3;
127const EVENT = 4;
128const REPEAT = 5;
129
130const updateFormControl = (node, value) => {
131 if (node.nodeName === "SELECT") {
132 Array.from(node.querySelectorAll("option")).forEach((option) => {
133 option.selected = value.includes(option.value);
134 });
135 return
136 }
137
138 let checked;
139
140 switch (node.getAttribute("type")) {
141 case "checkbox":
142 checked = value;
143 if (node.checked === checked) break
144 if (checked) {
145 node.setAttribute("checked", "");
146 } else {
147 node.removeAttribute("checked");
148 }
149 break
150 case "radio":
151 checked = value === node.getAttribute("value");
152 if (node.checked === checked) break
153 node.checked = checked;
154 if (checked) {
155 node.setAttribute("checked", "");
156 } else {
157 node.removeAttribute("checked");
158 }
159 break
160 default:
161 if (node.value === value) break
162 node.setAttribute("value", (node.value = value || ""));
163 break
164 }
165};
166
167const last = (v = []) => v[v.length - 1];
168
169const isWhitespace = (node) => {
170 return node.nodeType === node.TEXT_NODE && node.nodeValue.trim() === ""
171};
172
173const walk = (node, callback, deep = true) => {
174 if (!node) return
175 if (!isWhitespace(node)) {
176 let v = callback(node);
177 if (v === false) return
178 if (v?.nodeName) return walk(v, callback, deep)
179 }
180 if (deep) walk(node.firstChild, callback, deep);
181 walk(node.nextSibling, callback, deep);
182};
183
184const transformBrackets = (str = "") => {
185 let parts = str.split(/(\[[^\]]+\])/).filter((v) => v);
186 return parts.reduce((a, part) => {
187 let v = part.charAt(0) === "[" ? "." + part.replace(/\./g, ":") : part;
188 return a + v
189 }, "")
190};
191
192const getTarget = (path, target) => {
193 let parts = transformBrackets(path)
194 .split(".")
195 .map((k) => {
196 if (k.charAt(0) === "[") {
197 let p = k.slice(1, -1).replace(/:/g, ".");
198 return getValueAtPath(p, target)
199 } else {
200 return k
201 }
202 });
203
204 let t =
205 parts.slice(0, -1).reduce((o, k) => {
206 return o && o[k]
207 }, target) || target;
208 return [t, last(parts)]
209};
210
211const getValueAtPath = (path, target) => {
212 let [a, b] = getTarget(path, target);
213 let v = a?.[b];
214 if (typeof v === "function") return v.bind(a)
215 return v
216};
217
218const fragmentFromTemplate = (v) => {
219 if (typeof v === "string") {
220 let tpl = document.createElement("template");
221 tpl.innerHTML = v.trim();
222 return tpl.content
223 }
224 if (v.nodeName === "TEMPLATE") return v.cloneNode(true).content
225 if (v.nodeName === "defs") return v.firstElementChild.cloneNode(true)
226};
227
228const debounce = (fn) => {
229 let wait = false;
230 let invoke = false;
231 return () => {
232 if (wait) {
233 invoke = true;
234 } else {
235 wait = true;
236 fn();
237 requestAnimationFrame(() => {
238 if (invoke) fn();
239 wait = false;
240 });
241 }
242 }
243};
244
245const isPrimitive = (v) => v === null || typeof v !== "object";
246
247const typeOf = (v) =>
248 Object.prototype.toString.call(v).match(/\s(.+[^\]])/)[1];
249
250const pascalToKebab = (string) =>
251 string.replace(/[\w]([A-Z])/g, function (m) {
252 return m[0] + "-" + m[1].toLowerCase()
253 });
254
255const kebabToPascal = (string) =>
256 string.replace(/[\w]-([\w])/g, function (m) {
257 return m[0] + m[2].toUpperCase()
258 });
259
260const applyAttribute = (node, name, value) => {
261 name = pascalToKebab(name);
262
263 if (typeof value === "boolean") {
264 if (name.startsWith("aria-")) {
265 value = "" + value;
266 } else if (value) {
267 value = "";
268 }
269 }
270
271 if (typeof value === "string" || typeof value === "number") {
272 node.setAttribute(name, value);
273 } else {
274 node.removeAttribute(name);
275 }
276};
277
278const attributeToProp = (k, v) => {
279 let name = kebabToPascal(k);
280 if (v === "") v = true;
281 if (k.startsWith("aria-")) {
282 if (v === "true") v = true;
283 if (v === "false") v = false;
284 }
285 return {
286 name,
287 value: v,
288 }
289};
290
291function parseEach(node) {
292 let each = node.getAttribute("each");
293 let m = each?.match(/(.+)\s+in\s+(.+)/);
294 if (!m) {
295 if (!each) return m
296 return {
297 path: each.trim(),
298 key: node.getAttribute("key"),
299 }
300 }
301 let [_, left, right] = m;
302 let parts = left.match(/\(([^\)]+)\)/);
303 let [a, b] = (parts ? parts[1].split(",") : [left]).map((v) => v.trim());
304
305 return {
306 path: right.trim(),
307 identifier: b ? b : a,
308 index: b ? a : b,
309 key: node.getAttribute("key"),
310 }
311}
312
313const getBlockSize = (template) => {
314 let i = 0;
315 walk(template.content?.firstChild || template.firstChild, () => i++, false);
316 return i
317};
318
319const nextNonWhitespaceSibling = (node) => {
320 return isWhitespace(node.nextSibling)
321 ? nextNonWhitespaceSibling(node.nextSibling)
322 : node.nextSibling
323};
324
325const getBlockFragments = (template, numBlocks) => {
326 let blockSize = getBlockSize(template);
327
328 let r = [];
329 if (numBlocks) {
330 while (numBlocks--) {
331 let f = document.createDocumentFragment();
332 let n = blockSize;
333 while (n--) {
334 f.appendChild(nextNonWhitespaceSibling(template));
335 }
336 r.push(f);
337 }
338 }
339 return r
340};
341
342const getBlocks = (template) => {
343 let numBlocks = template.getAttribute("length");
344 let blockSize = getBlockSize(template);
345 let r = [];
346 let node = template;
347 if (numBlocks) {
348 while (numBlocks--) {
349 let f = [];
350 let n = blockSize;
351 while (n--) {
352 node = nextNonWhitespaceSibling(node);
353 f.push(node);
354 }
355 r.push(f);
356 }
357 }
358 return r
359};
360
361const compareKeyedLists = (key, a = [], b = []) => {
362 let delta = b.map(([k, item]) =>
363 !key ? (k in a ? k : -1) : a.findIndex(([_, v]) => v[key] === item[key])
364 );
365 if (a.length !== b.length || !delta.every((a, b) => a === b)) return delta
366};
367
368function lastChild(v) {
369 return (v.nodeType === v.DOCUMENT_FRAGMENT_NODE && v.lastChild) || v
370}
371
372const updateList = (template, delta, entries, createListItem) => {
373 let n = template.getAttribute("length") || 0;
374 let blocks = getBlockFragments(template, n);
375 let t = template;
376
377 delta.forEach((i, newIndex) => {
378 let frag =
379 i === -1
380 ? createListItem(entries[newIndex][1], entries[newIndex][0])
381 : blocks[i];
382 let x = lastChild(frag);
383 t.after(frag);
384 t = x;
385 });
386
387 template.setAttribute("length", delta.length);
388};
389
390const VALUE = 1;
391const KEY = 2;
392const FUNCTION = 3;
393
394const hasMustache = (v) => v.match(/({{[^{}]+}})/);
395
396const getParts = (value) =>
397 value
398 .trim()
399 .split(/({{[^{}]+}})/)
400 .filter((v) => v)
401 .map((value) => {
402 let match = value.match(/{{([^{}]+)}}/);
403
404 if (!match)
405 return {
406 type: VALUE,
407 value,
408 negated: false,
409 }
410
411 value = match[1].trim();
412 let negated = value.charAt(0) === "!";
413 if (negated) value = value.slice(1);
414
415 return {
416 type: KEY,
417 value,
418 negated,
419 }
420 });
421
422const getValueFromParts = (target, parts) => {
423 return parts.reduce((a, part) => {
424 let { type, value, negated } = part;
425
426 let v;
427
428 if (type === VALUE) v = value;
429 if (type === KEY) {
430 v = getValueAtPath(value, target);
431 }
432 if (type === FUNCTION) {
433 let args = part.args.map((value) => getValueAtPath(value, target));
434
435 v = getValueAtPath(part.method, target)?.(...args);
436 }
437
438 if (negated) v = !v;
439
440 return a || a === 0 ? a + v : v
441 }, "")
442};
443
444const pascalToKebab$1 = (string) =>
445 string.replace(/[\w]([A-Z])/g, function (m) {
446 return m[0] + "-" + m[1].toLowerCase()
447 });
448
449const kebabToPascal$1 = (string) =>
450 string.replace(/[\w]-([\w])/g, function (m) {
451 return m[0] + m[2].toUpperCase()
452 });
453
454const parseStyles = (value) => {
455 let type = typeof value;
456
457 if (type === "string")
458 return value.split(";").reduce((o, value) => {
459 const [k, v] = value.split(":").map((v) => v.trim());
460 if (k) o[k] = v;
461 return o
462 }, {})
463
464 if (type === "object") return value
465
466 return {}
467};
468
469const joinStyles = (value) =>
470 Object.entries(value)
471 .map(([k, v]) => `${k}: ${v};`)
472 .join(" ");
473
474const convertStyles = (o) =>
475 Object.keys(o).reduce((a, k) => {
476 a[pascalToKebab$1(k)] = o[k];
477 return a
478 }, {});
479
480const applyAttribute$1 = (node, name, value) => {
481 if (name === "style") {
482 value = joinStyles(
483 convertStyles({
484 ...parseStyles(node.getAttribute("style")),
485 ...parseStyles(value),
486 })
487 );
488 } else if (name === "class") {
489 switch (typeOf(value)) {
490 case "Array":
491 value = value.join(" ");
492 break
493 case "Object":
494 value = Object.keys(value)
495 .reduce((a, k) => {
496 if (value[k]) a.push(k);
497 return a
498 }, [])
499 .join(" ");
500 break
501 }
502 } else if (!isPrimitive(value)) {
503 return (node[kebabToPascal$1(name)] = value)
504 }
505
506 name = pascalToKebab$1(name);
507
508 if (typeof value === "boolean") {
509 if (name.startsWith("aria-")) {
510 value = "" + value;
511 } else if (value) {
512 value = "";
513 }
514 }
515
516 let current = node.getAttribute(name);
517
518 if (value === current) return
519
520 if (typeof value === "string" || typeof value === "number") {
521 node.setAttribute(name, value);
522 } else {
523 node.removeAttribute(name);
524 }
525};
526
527const handler = ({ path, identifier, key, index, i, k }) => ({
528 get(target, property) {
529 let x = getValueAtPath(path, target);
530
531 // x === the collection
532
533 if (property === identifier) {
534 for (let n in x) {
535 let v = x[n];
536 if (key) {
537 if (v[key] === k) return v
538 } else {
539 if (n == i) return v
540 }
541 }
542 }
543
544 if (property === index) {
545 for (let n in x) {
546 let v = x[n];
547 if (key) {
548 if (v[key] === k) return n
549 } else {
550 if (n == i) return n
551 }
552 }
553 }
554
555 let t = key ? x.find((v) => v[key] === k) : x[i];
556 if (t?.hasOwnProperty?.(property)) return t[property]
557
558 return Reflect.get(...arguments)
559 },
560 set(target, property, value) {
561 let x = getValueAtPath(path, target);
562 let t = key ? x.find((v) => v[key] === k) : x[i];
563 if (t && !isPrimitive(t)) {
564 t[property] = value;
565 return true
566 }
567
568 return Reflect.set(...arguments)
569 },
570});
571
572const createContext = (v = []) => {
573 let context = v;
574 return {
575 get: () => context,
576 push: (v) => context.push(v),
577 wrap: (state) => {
578 return context.reduce(
579 (target, ctx) => new Proxy(target, handler(ctx)),
580 state
581 )
582 },
583 }
584};
585
586const HYDRATE_ATTR = "mosaic-hydrate";
587
588const render = (
589 target,
590 { getState, dispatch },
591 template,
592 updatedCallback,
593 beforeMountCallback
594) => {
595 let observer = () => {
596 let subscribers = new Set();
597 return {
598 publish: (cb) => {
599 for (let fn of subscribers) {
600 fn();
601 }
602 cb?.();
603 },
604 subscribe(fn) {
605 subscribers.add(fn);
606 },
607 }
608 };
609
610 const createSubscription = {
611 [TEXT]: ({ value, node, context }, { getState }) => {
612 return {
613 handler: () => {
614 let state = context ? context.wrap(getState()) : getState();
615 let a = node.textContent;
616 let b = getValueFromParts(state, getParts(value));
617 if (a !== b) node.textContent = b;
618 },
619 }
620 },
621 [ATTRIBUTE]: ({ value, node, name, context }, { getState }) => {
622 return {
623 handler: () => {
624 let state = context ? context.wrap(getState()) : getState();
625 let b = getValueFromParts(state, getParts(value));
626
627 applyAttribute$1(node, name, b);
628
629 if (node.nodeName === "OPTION") {
630 let path = node.parentNode.getAttribute("name");
631 let selected = getValueAtPath(path, state);
632 node.selected = selected === b;
633 }
634 },
635 }
636 },
637 [INPUT]: ({ node, path, context }, { getState, dispatch }) => {
638 node.addEventListener("input", () => {
639 let value =
640 node.getAttribute("type") === "checkbox" ? node.checked : node.value;
641
642 if (value.trim?.().length && !isNaN(value)) value = +value;
643
644 if (context) {
645 let state = context.wrap(getState());
646 state[path] = value;
647 dispatch({
648 type: "MERGE",
649 payload: state,
650 });
651 } else {
652 dispatch({
653 type: "SET",
654 payload: {
655 name: path,
656 value,
657 context,
658 },
659 });
660 }
661 });
662
663 return {
664 handler: () => {
665 let state = context ? context.wrap(getState()) : getState();
666 updateFormControl(node, getValueAtPath(path, state));
667 },
668 }
669 },
670 [EVENT]: (
671 { node, eventType, actionType, context },
672 { dispatch, getState }
673 ) => {
674 /*
675
676 NB that context is only passed for local actions not prefixed with "$"
677
678 */
679
680 node.addEventListener(eventType, (event) => {
681 let isGlobal = actionType.startsWith("$");
682
683 let action = {
684 type: actionType,
685 event,
686 };
687
688 if (!isGlobal)
689 action.context = context ? context.wrap(getState()) : getState();
690
691 dispatch(action);
692
693 if (isGlobal) {
694 node.dispatchEvent(
695 new CustomEvent(actionType, {
696 detail: action,
697 bubbles: true,
698 })
699 );
700 }
701 });
702 return {
703 handler: () => {},
704 }
705 },
706 [REPEAT]: (
707 {
708 node,
709 context,
710 map,
711 path,
712 identifier,
713 index,
714 key,
715 blockIndex,
716 hydrate,
717 pickupNode,
718 },
719 { getState }
720 ) => {
721 let oldValue;
722 node.$t = blockIndex - 1;
723
724 const initialiseBlock = (rootNode, i, k, exitNode) => {
725 walk(
726 rootNode,
727 multi(
728 (node) => {
729 if (node === exitNode) return false
730 },
731 bindAll(
732 map,
733 hydrate,
734 createContext(
735 (context?.get() || []).concat({
736 path,
737 identifier,
738 key,
739 index,
740 i,
741 k,
742 })
743 )
744 ),
745 (child) => (child.$t = blockIndex)
746 )
747 );
748 };
749
750 function firstChild(v) {
751 return (v.nodeType === v.DOCUMENT_FRAGMENT_NODE && v.firstChild) || v
752 }
753
754 const createListItem = (datum, i) => {
755 let k = datum[key];
756 let frag = fragmentFromTemplate(node);
757 initialiseBlock(firstChild(frag), i, k);
758 return frag
759 };
760
761 if (hydrate) {
762 let x = getValueAtPath(path, getState());
763 let blocks = getBlocks(node);
764
765 blocks.forEach((block, i) => {
766 let datum = x[i];
767 let k = datum?.[key];
768 initialiseBlock(block[0], i, k, last(block).nextSibling);
769 });
770
771 pickupNode = last(last(blocks)).nextSibling;
772 }
773
774 return {
775 handler: () => {
776 let state = context ? context.wrap(getState()) : getState();
777
778 const newValue = Object.entries(getValueAtPath(path, state) || []);
779 const delta = compareKeyedLists(key, oldValue, newValue);
780
781 if (delta) {
782 updateList(node, delta, newValue, createListItem);
783 }
784 oldValue = newValue.slice(0);
785 },
786 pickupNode,
787 }
788 },
789 };
790
791 const mediator = () => {
792 const o = observer();
793 return {
794 bind(v) {
795 let s = createSubscription[v.type](v, { getState, dispatch });
796 o.subscribe(s.handler);
797 return s
798 },
799 // scheduleUpdate: debounce((state, cb) => {
800 // return o.publish(state, cb)
801 // }),
802 update(cb) {
803 return o.publish(cb)
804 },
805 }
806 };
807
808 const { bind, update } = mediator();
809
810 let blockCount = 0;
811
812 const parse = (frag) => {
813 let index = 0;
814 let map = {};
815
816 walk(frag, (node) => {
817 let x = [];
818 let pickupNode;
819 switch (node.nodeType) {
820 case node.TEXT_NODE: {
821 let value = node.textContent;
822 if (hasMustache(value)) {
823 x.push({
824 type: TEXT,
825 value,
826 });
827 }
828 break
829 }
830 case node.ELEMENT_NODE: {
831 let each = parseEach(node);
832
833 if (each) {
834 let ns = node.namespaceURI;
835 let m;
836
837 if (ns.endsWith("/svg")) {
838 node.removeAttribute("each");
839 let tpl = document.createElementNS(ns, "defs");
840 tpl.innerHTML = node.outerHTML;
841 node.parentNode.replaceChild(tpl, node);
842 node = tpl;
843 m = parse(node.firstChild);
844 } else {
845 if (node.nodeName !== "TEMPLATE") {
846 node.removeAttribute("each");
847 let tpl = document.createElement("template");
848
849 tpl.innerHTML = node.outerHTML;
850 node.parentNode.replaceChild(tpl, node);
851 node = tpl;
852 }
853 m = parse(node.content.firstChild);
854 }
855
856 pickupNode = node.nextSibling;
857
858 x.push({
859 type: REPEAT,
860 map: m,
861 blockIndex: blockCount++,
862 ...each,
863 pickupNode,
864 });
865
866 break
867 }
868
869 let attrs = node.attributes;
870 let i = attrs.length;
871 while (i--) {
872 let { name, value } = attrs[i];
873
874 if (
875 name === ":name" &&
876 value &&
877 (node.nodeName === "INPUT" ||
878 node.nodeName === "SELECT" ||
879 node.nodeName === "TEXTAREA")
880 ) {
881 x.push({
882 type: INPUT,
883 path: value,
884 });
885
886 node.removeAttribute(name);
887 node.setAttribute("name", value);
888 } else if (name.startsWith(":on")) {
889 node.removeAttribute(name);
890 let eventType = name.split(":on")[1];
891 x.push({
892 type: EVENT,
893 eventType,
894 actionType: value,
895 });
896 } else if (name.startsWith(":")) {
897 let prop = name.slice(1);
898
899 let v = value || prop;
900
901 if (!v.includes("{{")) v = `{{${v}}}`;
902
903 x.push({
904 type: ATTRIBUTE,
905 name: prop,
906 value: v,
907 });
908 node.removeAttribute(name);
909 }
910 }
911 }
912 }
913 if (x.length) map[index] = x;
914 index++;
915 return pickupNode
916 });
917
918 return map
919 };
920
921 const multi =
922 (...fns) =>
923 (...args) => {
924 for (let fn of fns) {
925 let v = fn(...args);
926 if (v === false) return false
927 }
928 };
929
930 const bindAll = (bMap, hydrate = 0, context) => {
931 let index = 0;
932 return (node) => {
933 let k = index;
934 let p;
935 if (k in bMap) {
936 bMap[k].forEach((v) => {
937 let x = bind({
938 ...v,
939 node,
940 context,
941 hydrate,
942 });
943 p = x.pickupNode;
944 });
945 node.$i = index;
946 }
947 index++;
948 return p
949 }
950 };
951
952 let frag = fragmentFromTemplate(template);
953 let map = parse(frag);
954 let hydrate = target.hasAttribute?.(HYDRATE_ATTR);
955 if (hydrate) {
956 walk(target, bindAll(map, 1));
957 } else {
958 walk(frag, bindAll(map));
959 beforeMountCallback?.(frag);
960 target.prepend(frag);
961 update();
962 target.setAttribute?.(HYDRATE_ATTR, 1);
963 }
964
965 return debounce(() => update(updatedCallback))
966};
967
968const childNodes = (node) => {
969 let frag = document.createDocumentFragment();
970 while (node.firstChild) {
971 frag.appendChild(node.firstChild);
972 }
973 return frag
974};
975
976const mergeSlots = (targetNode, sourceNode) => {
977 let namedSlots = sourceNode.querySelectorAll("slot[name]");
978
979 namedSlots.forEach((slot) => {
980 let name = slot.attributes.name.value;
981 let node = targetNode.querySelector(`[slot="${name}"]`);
982 if (!node) {
983 slot.parentNode.replaceChild(childNodes(slot), slot);
984 return
985 }
986 node.removeAttribute("slot");
987 slot.parentNode.replaceChild(node, slot);
988 });
989
990 let defaultSlot = sourceNode.querySelector("slot:not([name])");
991
992 if (defaultSlot) {
993 defaultSlot.parentNode.replaceChild(
994 childNodes(targetNode.innerHTML.trim() ? targetNode : defaultSlot),
995 defaultSlot
996 );
997 }
998};
999
1000function getDataScript(node) {
1001 return node.querySelector(`script[type="application/mosaic"]`)
1002}
1003
1004function createDataScript(node) {
1005 let ds = document.createElement("script");
1006 ds.setAttribute("type", "application/mosaic");
1007 node.append(ds);
1008 return ds
1009}
1010
1011function serialise(node, state) {
1012 let ds = getDataScript(node) || createDataScript(node);
1013
1014 ds.innerText = JSON.stringify(state);
1015}
1016
1017function deserialise(node) {
1018 return JSON.parse(getDataScript(node)?.innerText || "{}")
1019}
1020
1021const define = (name, factory, template) =>
1022 customElements.define(
1023 name,
1024 class extends HTMLElement {
1025 async connectedCallback() {
1026 if (!this.initialised) {
1027 let config = factory(this);
1028
1029 if (config instanceof Promise) config = await config;
1030
1031 let {
1032 update,
1033 middleware,
1034 derivations,
1035 subscribe,
1036 shadow,
1037 initialState = {},
1038 } = config;
1039
1040 this.connectedCallback = config.connectedCallback;
1041 this.disconnectedCallback = config.disconnectedCallback;
1042
1043 const { dispatch, getState, onUpdate, flush } = configure(
1044 config,
1045 this
1046 );
1047
1048 dispatch({
1049 type: "MERGE",
1050 payload: deserialise(this),
1051 });
1052
1053 initialState = getState();
1054
1055 let observedProps = Object.keys(initialState).filter(
1056 (v) => v.charAt(0) === "$"
1057 );
1058
1059 let observedAttributes = observedProps
1060 .map((v) => v.slice(1))
1061 .map(pascalToKebab);
1062
1063 let sa = this.setAttribute;
1064 this.setAttribute = (k, v) => {
1065 if (observedAttributes.includes(k)) {
1066 let { name, value } = attributeToProp(k, v);
1067 dispatch({
1068 type: "SET",
1069 payload: { name: "$" + name, value },
1070 });
1071 }
1072 sa.apply(this, [k, v]);
1073 };
1074
1075 observedAttributes.forEach((name) => {
1076 let property = attributeToProp(name).name;
1077
1078 let value;
1079
1080 if (this.hasAttribute(name)) {
1081 value = this.getAttribute(name);
1082 } else {
1083 value = this[property] || initialState["$" + property];
1084 }
1085
1086 Object.defineProperty(this, property, {
1087 get() {
1088 return getState()["$" + property]
1089 },
1090 set(v) {
1091 dispatch({
1092 type: "SET",
1093 payload: { name: "$" + property, value: v },
1094 });
1095 if (isPrimitive(v)) {
1096 applyAttribute(this, property, v);
1097 }
1098 },
1099 });
1100
1101 this[property] = value;
1102 });
1103
1104 let beforeMountCallback;
1105
1106 if (shadow) {
1107 this.attachShadow({
1108 mode: shadow,
1109 });
1110 } else {
1111 beforeMountCallback = (frag) => mergeSlots(this, frag);
1112 }
1113
1114 onUpdate(
1115 render(
1116 this.shadowRoot || this,
1117 { getState, dispatch },
1118 template,
1119 () => {
1120 serialise(this, getState());
1121
1122 observedProps.forEach((k) => {
1123 let v = getState()[k];
1124 if (isPrimitive(v)) applyAttribute(this, k.slice(1), v);
1125 });
1126 subscribe?.(getState());
1127 flush();
1128 },
1129 beforeMountCallback
1130 )
1131 );
1132 this.initialised = true;
1133 }
1134 this.connectedCallback?.();
1135 }
1136 disconnectedCallback() {
1137 this.disconnectedCallback?.();
1138 }
1139 }
1140 );
1141
1142export { define };