UNPKG

9.93 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var constants = require('./constants.js');
6var formControl = require('./formControl.js');
7var helpers = require('./helpers.js');
8var list = require('./list.js');
9var token = require('./token.js');
10var attribute = require('./attribute.js');
11var context = require('./context.js');
12
13const HYDRATE_ATTR = "mosaic-hydrate";
14
15const render = (
16 target,
17 { getState, dispatch },
18 template,
19 updatedCallback,
20 beforeMountCallback
21) => {
22 let observer = () => {
23 let subscribers = new Set();
24 return {
25 publish: (cb) => {
26 for (let fn of subscribers) {
27 fn();
28 }
29 cb?.();
30 },
31 subscribe(fn) {
32 subscribers.add(fn);
33 },
34 }
35 };
36
37 const createSubscription = {
38 [constants.TEXT]: ({ value, node, context }, { getState }) => {
39 return {
40 handler: () => {
41 let state = context ? context.wrap(getState()) : getState();
42 let a = node.textContent;
43 let b = token.getValueFromParts(state, token.getParts(value));
44 if (a !== b) node.textContent = b;
45 },
46 }
47 },
48 [constants.ATTRIBUTE]: ({ value, node, name, context }, { getState }) => {
49 return {
50 handler: () => {
51 let state = context ? context.wrap(getState()) : getState();
52 let b = token.getValueFromParts(state, token.getParts(value));
53
54 attribute.applyAttribute(node, name, b);
55
56 if (node.nodeName === "OPTION") {
57 let path = node.parentNode.getAttribute("name");
58 let selected = helpers.getValueAtPath(path, state);
59 node.selected = selected === b;
60 }
61 },
62 }
63 },
64 [constants.INPUT]: ({ node, path, context }, { getState, dispatch }) => {
65 node.addEventListener("input", () => {
66 let value =
67 node.getAttribute("type") === "checkbox" ? node.checked : node.value;
68
69 if (value.trim?.().length && !isNaN(value)) value = +value;
70
71 if (context) {
72 let state = context.wrap(getState());
73 state[path] = value;
74 dispatch({
75 type: "MERGE",
76 payload: state,
77 });
78 } else {
79 dispatch({
80 type: "SET",
81 payload: {
82 name: path,
83 value,
84 context,
85 },
86 });
87 }
88 });
89
90 return {
91 handler: () => {
92 let state = context ? context.wrap(getState()) : getState();
93 formControl.updateFormControl(node, helpers.getValueAtPath(path, state));
94 },
95 }
96 },
97 [constants.EVENT]: (
98 { node, eventType, actionType, context },
99 { dispatch, getState }
100 ) => {
101 /*
102
103 NB that context is only passed for local actions not prefixed with "$"
104
105 */
106
107 node.addEventListener(eventType, (event) => {
108 let isGlobal = actionType.startsWith("$");
109
110 let action = {
111 type: actionType,
112 event,
113 };
114
115 if (!isGlobal)
116 action.context = context ? context.wrap(getState()) : getState();
117
118 dispatch(action);
119
120 if (isGlobal) {
121 node.dispatchEvent(
122 new CustomEvent(actionType, {
123 detail: action,
124 bubbles: true,
125 })
126 );
127 }
128 });
129 return {
130 handler: () => {},
131 }
132 },
133 [constants.REPEAT]: (
134 {
135 node,
136 context: context$1,
137 map,
138 path,
139 identifier,
140 index,
141 key,
142 blockIndex,
143 hydrate,
144 pickupNode,
145 },
146 { getState }
147 ) => {
148 let oldValue;
149 node.$t = blockIndex - 1;
150
151 const initialiseBlock = (rootNode, i, k, exitNode) => {
152 helpers.walk(
153 rootNode,
154 multi(
155 (node) => {
156 if (node === exitNode) return false
157 },
158 bindAll(
159 map,
160 hydrate,
161 context.createContext(
162 (context$1?.get() || []).concat({
163 path,
164 identifier,
165 key,
166 index,
167 i,
168 k,
169 })
170 )
171 ),
172 (child) => (child.$t = blockIndex)
173 )
174 );
175 };
176
177 function firstChild(v) {
178 return (v.nodeType === v.DOCUMENT_FRAGMENT_NODE && v.firstChild) || v
179 }
180
181 const createListItem = (datum, i) => {
182 let k = datum[key];
183 let frag = helpers.fragmentFromTemplate(node);
184 initialiseBlock(firstChild(frag), i, k);
185 return frag
186 };
187
188 if (hydrate) {
189 let x = helpers.getValueAtPath(path, getState());
190 let blocks = list.getBlocks(node);
191
192 blocks.forEach((block, i) => {
193 let datum = x[i];
194 let k = datum?.[key];
195 initialiseBlock(block[0], i, k, helpers.last(block).nextSibling);
196 });
197
198 pickupNode = helpers.last(helpers.last(blocks)).nextSibling;
199 }
200
201 return {
202 handler: () => {
203 let state = context$1 ? context$1.wrap(getState()) : getState();
204
205 const newValue = Object.entries(helpers.getValueAtPath(path, state) || []);
206 const delta = list.compareKeyedLists(key, oldValue, newValue);
207
208 if (delta) {
209 list.updateList(node, delta, newValue, createListItem);
210 }
211 oldValue = newValue.slice(0);
212 },
213 pickupNode,
214 }
215 },
216 };
217
218 const mediator = () => {
219 const o = observer();
220 return {
221 bind(v) {
222 let s = createSubscription[v.type](v, { getState, dispatch });
223 o.subscribe(s.handler);
224 return s
225 },
226 // scheduleUpdate: debounce((state, cb) => {
227 // return o.publish(state, cb)
228 // }),
229 update(cb) {
230 return o.publish(cb)
231 },
232 }
233 };
234
235 const { bind, update } = mediator();
236
237 let blockCount = 0;
238
239 const parse = (frag) => {
240 let index = 0;
241 let map = {};
242
243 helpers.walk(frag, (node) => {
244 let x = [];
245 let pickupNode;
246 switch (node.nodeType) {
247 case node.TEXT_NODE: {
248 let value = node.textContent;
249 if (token.hasMustache(value)) {
250 x.push({
251 type: constants.TEXT,
252 value,
253 });
254 }
255 break
256 }
257 case node.ELEMENT_NODE: {
258 let each = list.parseEach(node);
259
260 if (each) {
261 let ns = node.namespaceURI;
262 let m;
263
264 if (ns.endsWith("/svg")) {
265 node.removeAttribute("each");
266 let tpl = document.createElementNS(ns, "defs");
267 tpl.innerHTML = node.outerHTML;
268 node.parentNode.replaceChild(tpl, node);
269 node = tpl;
270 m = parse(node.firstChild);
271 } else {
272 if (node.nodeName !== "TEMPLATE") {
273 node.removeAttribute("each");
274 let tpl = document.createElement("template");
275
276 tpl.innerHTML = node.outerHTML;
277 node.parentNode.replaceChild(tpl, node);
278 node = tpl;
279 }
280 m = parse(node.content.firstChild);
281 }
282
283 pickupNode = node.nextSibling;
284
285 x.push({
286 type: constants.REPEAT,
287 map: m,
288 blockIndex: blockCount++,
289 ...each,
290 pickupNode,
291 });
292
293 break
294 }
295
296 let attrs = node.attributes;
297 let i = attrs.length;
298 while (i--) {
299 let { name, value } = attrs[i];
300
301 if (
302 name === ":name" &&
303 value &&
304 (node.nodeName === "INPUT" ||
305 node.nodeName === "SELECT" ||
306 node.nodeName === "TEXTAREA")
307 ) {
308 x.push({
309 type: constants.INPUT,
310 path: value,
311 });
312
313 node.removeAttribute(name);
314 node.setAttribute("name", value);
315 } else if (name.startsWith(":on")) {
316 node.removeAttribute(name);
317 let eventType = name.split(":on")[1];
318 x.push({
319 type: constants.EVENT,
320 eventType,
321 actionType: value,
322 });
323 } else if (name.startsWith(":")) {
324 let prop = name.slice(1);
325
326 let v = value || prop;
327
328 if (!v.includes("{{")) v = `{{${v}}}`;
329
330 x.push({
331 type: constants.ATTRIBUTE,
332 name: prop,
333 value: v,
334 });
335 node.removeAttribute(name);
336 }
337 }
338 }
339 }
340 if (x.length) map[index] = x;
341 index++;
342 return pickupNode
343 });
344
345 return map
346 };
347
348 const multi =
349 (...fns) =>
350 (...args) => {
351 for (let fn of fns) {
352 let v = fn(...args);
353 if (v === false) return false
354 }
355 };
356
357 const bindAll = (bMap, hydrate = 0, context) => {
358 let index = 0;
359 return (node) => {
360 let k = index;
361 let p;
362 if (k in bMap) {
363 bMap[k].forEach((v) => {
364 let x = bind({
365 ...v,
366 node,
367 context,
368 hydrate,
369 });
370 p = x.pickupNode;
371 });
372 node.$i = index;
373 }
374 index++;
375 return p
376 }
377 };
378
379 let frag = helpers.fragmentFromTemplate(template);
380 let map = parse(frag);
381 let hydrate = target.hasAttribute?.(HYDRATE_ATTR);
382 if (hydrate) {
383 helpers.walk(target, bindAll(map, 1));
384 } else {
385 helpers.walk(frag, bindAll(map));
386 beforeMountCallback?.(frag);
387 target.prepend(frag);
388 update();
389 target.setAttribute?.(HYDRATE_ATTR, 1);
390 }
391
392 return helpers.debounce(() => update(updatedCallback))
393};
394
395exports.render = render;