1 | import { configure } from "./update.js"
|
2 | import { render } from "./render.js"
|
3 | import { mergeSlots } from "./mergeSlots.js"
|
4 | import {
|
5 | applyAttribute,
|
6 | attributeToProp,
|
7 | isPrimitive,
|
8 | pascalToKebab,
|
9 | } from "./helpers.js"
|
10 |
|
11 | function getDataScript(node) {
|
12 | return node.querySelector(`script[type="application/mosaic"]`)
|
13 | }
|
14 |
|
15 | function createDataScript(node) {
|
16 | let ds = document.createElement("script")
|
17 | ds.setAttribute("type", "application/mosaic")
|
18 | node.append(ds)
|
19 | return ds
|
20 | }
|
21 |
|
22 | function serialise(node, state) {
|
23 | let ds = getDataScript(node) || createDataScript(node)
|
24 |
|
25 | ds.innerText = JSON.stringify(state)
|
26 | }
|
27 |
|
28 | function deserialise(node) {
|
29 | return JSON.parse(getDataScript(node)?.innerText || "{}")
|
30 | }
|
31 |
|
32 | let count = 0
|
33 |
|
34 | function nextId() {
|
35 | return count++
|
36 | }
|
37 |
|
38 | export const define = (name, factory, template) =>
|
39 | customElements.define(
|
40 | name,
|
41 | class extends HTMLElement {
|
42 | async connectedCallback() {
|
43 | if (!this.initialised) {
|
44 | let config = factory(this)
|
45 | const slice = `name.${nextId()}`
|
46 |
|
47 | if (config instanceof Promise) config = await config
|
48 |
|
49 | let {
|
50 | update,
|
51 | middleware,
|
52 | derivations,
|
53 | subscribe,
|
54 | shadow,
|
55 | initialState = {},
|
56 | } = config
|
57 |
|
58 | this.connectedCallback = config.connectedCallback
|
59 | this.disconnectedCallback = config.disconnectedCallback
|
60 |
|
61 | const { dispatch, getState, onUpdate, flush } = configure(
|
62 | config,
|
63 | this
|
64 | )
|
65 |
|
66 | dispatch({
|
67 | type: "MERGE",
|
68 | payload: deserialise(this),
|
69 | })
|
70 |
|
71 | initialState = getState()
|
72 |
|
73 | let observedProps = Object.keys(initialState).filter(
|
74 | (v) => v.charAt(0) === "$"
|
75 | )
|
76 |
|
77 | let observedAttributes = observedProps
|
78 | .map((v) => v.slice(1))
|
79 | .map(pascalToKebab)
|
80 |
|
81 | let sa = this.setAttribute
|
82 | this.setAttribute = (k, v) => {
|
83 | if (observedAttributes.includes(k)) {
|
84 | let { name, value } = attributeToProp(k, v)
|
85 | dispatch({
|
86 | type: "SET",
|
87 | payload: { name: "$" + name, value },
|
88 | })
|
89 | }
|
90 | sa.apply(this, [k, v])
|
91 | }
|
92 |
|
93 | observedAttributes.forEach((name) => {
|
94 | let property = attributeToProp(name).name
|
95 |
|
96 | let value
|
97 |
|
98 | if (this.hasAttribute(name)) {
|
99 | value = this.getAttribute(name)
|
100 | } else {
|
101 | value = this[property] || initialState["$" + property]
|
102 | }
|
103 |
|
104 | Object.defineProperty(this, property, {
|
105 | get() {
|
106 | return getState()["$" + property]
|
107 | },
|
108 | set(v) {
|
109 | dispatch({
|
110 | type: "SET",
|
111 | payload: { name: "$" + property, value: v },
|
112 | })
|
113 | if (isPrimitive(v)) {
|
114 | applyAttribute(this, property, v)
|
115 | }
|
116 | },
|
117 | })
|
118 |
|
119 | this[property] = value
|
120 | })
|
121 |
|
122 | let beforeMountCallback
|
123 |
|
124 | if (shadow) {
|
125 | this.attachShadow({
|
126 | mode: shadow,
|
127 | })
|
128 | } else {
|
129 | beforeMountCallback = (frag) => mergeSlots(this, frag)
|
130 | }
|
131 |
|
132 | onUpdate(
|
133 | render(
|
134 | this.shadowRoot || this,
|
135 | { getState, dispatch },
|
136 | template,
|
137 | () => {
|
138 | serialise(this, getState())
|
139 |
|
140 | observedProps.forEach((k) => {
|
141 | let v = getState()[k]
|
142 | if (isPrimitive(v)) applyAttribute(this, k.slice(1), v)
|
143 | })
|
144 | subscribe?.(getState())
|
145 | flush()
|
146 | },
|
147 | beforeMountCallback
|
148 | )
|
149 | )
|
150 | this.initialised = true
|
151 | }
|
152 | this.connectedCallback?.()
|
153 | }
|
154 | disconnectedCallback() {
|
155 | this.disconnectedCallback?.()
|
156 | }
|
157 | }
|
158 | )
|