lightview 1.4.7-b → 1.5.1-b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lightview.js CHANGED
@@ -31,19 +31,21 @@ const {observe} = (() => {
31
31
  const parser = new DOMParser();
32
32
 
33
33
  const templateSanitizer = (string) => {
34
- return string.replace(/function\s+/g,"")
35
- .replace(/function\(/g,"")
36
- .replace(/=\s*>/g,"")
37
- .replace(/(while|do|for|alert)\s*\(/g,"")
38
- .replace(/console\.[a-zA-Z$]+\s*\(/g,"");
34
+ return string.replace(/function\s+/g, "")
35
+ .replace(/function\(/g, "")
36
+ .replace(/=\s*>/g, "")
37
+ .replace(/(while|do|for|alert)\s*\(/g, "")
38
+ .replace(/console\.[a-zA-Z$]+\s*\(/g, "");
39
39
  }
40
40
  Lightview.sanitizeTemplate = templateSanitizer;
41
41
 
42
42
  const escaper = document.createElement('textarea');
43
+
43
44
  function escapeHTML(html) {
44
45
  escaper.textContent = html;
45
46
  return escaper.innerHTML;
46
47
  }
48
+
47
49
  Lightview.escapeHTML = escapeHTML;
48
50
 
49
51
  const addListener = (node, eventName, callback) => {
@@ -68,7 +70,8 @@ const {observe} = (() => {
68
70
  return "l-" + name;
69
71
  }
70
72
  const observe = (f, thisArg, argsList = []) => {
71
- function observer(...args) {
73
+ const observer = (...args) => {
74
+ if(observer.cancelled) return;
72
75
  CURRENTOBSERVER = observer;
73
76
  try {
74
77
  f.call(thisArg || this, ...argsList, ...args);
@@ -77,19 +80,18 @@ const {observe} = (() => {
77
80
  }
78
81
  CURRENTOBSERVER = null;
79
82
  }
80
-
81
83
  observer.cancel = () => observer.cancelled = true;
82
84
  observer();
83
85
  return observer;
84
86
  }
85
87
  const coerce = (value, toType) => {
86
- if (value + "" === "null" || value + "" === "undefined") return value;
88
+ if (value + "" === "null" || value + "" === "undefined" || toType==="any") return value;
87
89
  const type = typeof (value);
88
90
  if (type === toType) return value;
89
91
  if (toType === "number") return parseFloat(value + "");
90
92
  if (toType === "boolean") {
91
93
  if (["on", "checked", "selected"].includes(value)) return true;
92
- if(value==null || value==="") return false;
94
+ if (value == null || value === "") return false;
93
95
  try {
94
96
  const parsed = JSON.parse(value + "");
95
97
  if (typeof (parsed) === "boolean") return parsed;
@@ -101,7 +103,7 @@ const {observe} = (() => {
101
103
  if (toType === "string") return value + "";
102
104
  const isfunction = typeof (toType) === "function";
103
105
  if ((toType === "object" || isfunction)) {
104
- if (type === "object") {
106
+ if (type === "object" && isfunction) {
105
107
  if (value instanceof toType) return value;
106
108
  }
107
109
  if (type === "string") {
@@ -112,10 +114,10 @@ const {observe} = (() => {
112
114
  if (instance instanceof Array) {
113
115
  let parsed = tryParse(value.startsWith("[") ? value : `[${value}]`);
114
116
  if (!Array.isArray(parsed)) {
115
- if(value.includes(",")) parsed = value.split(",");
117
+ if (value.includes(",")) parsed = value.split(",");
116
118
  else {
117
119
  parsed = tryParse(`["${value}"]`);
118
- if(!Array.isArray(parsed) || parsed[0]!==value && parsed.length!==1) parsed = null;
120
+ if (!Array.isArray(parsed) || parsed[0] !== value && parsed.length !== 1) parsed = null;
119
121
  }
120
122
  }
121
123
  if (!Array.isArray(parsed)) {
@@ -144,14 +146,16 @@ const {observe} = (() => {
144
146
  }
145
147
  throw new TypeError(`Unable to coerce ${value} to ${toType}`)
146
148
  }
147
- const Reactor = (value) => {
148
- if (value && typeof (value) === "object") {
149
- if (value.__isReactor__) return value;
149
+ const Reactor = (data) => {
150
+ if (data && typeof (data) === "object") {
151
+ if (data.__isReactor__) return data;
150
152
  const childReactors = [],
151
153
  dependents = {},
152
- proxy = new Proxy(value, {
154
+ proxy = new Proxy(data, {
153
155
  get(target, property) {
154
156
  if (property === "__isReactor__") return true;
157
+ if(property=== "__dependents__") return dependents;
158
+ if(property=== "__reactorProxyTarget__") return data;
155
159
  if (target instanceof Array) {
156
160
  if (property === "toJSON") return function toJSON() {
157
161
  return [...target];
@@ -160,15 +164,19 @@ const {observe} = (() => {
160
164
  return JSON.stringify([...target]);
161
165
  }
162
166
  }
167
+ if(target instanceof Date) {
168
+ return Reflect.get(target,property);
169
+ }
163
170
  let value = target[property];
164
171
  const type = typeof (value);
165
172
  if (CURRENTOBSERVER && typeof (property) !== "symbol" && type !== "function") {
166
173
  const observers = dependents[property] ||= new Set();
167
174
  observers.add(CURRENTOBSERVER)
168
175
  }
176
+ if(value===undefined) return;
169
177
  if (childReactors.includes(value) || (value && type !== "object") || typeof (property) === "symbol") {
170
- // Dated must be bound to work with proxies
171
- if (type === "function" && [Date].includes(value)) value = value.bind(target)
178
+ // Dates and Promises must be bound to work with proxies
179
+ if (type === "function" && ([Date].includes(value) || property==="then")) value = value.bind(target)
172
180
  return value;
173
181
  }
174
182
  if (value && type === "object") {
@@ -178,8 +186,14 @@ const {observe} = (() => {
178
186
  target[property] = value;
179
187
  return value;
180
188
  },
181
- set(target, property, value) {
189
+ async set(target, property, value) {
190
+ if(target instanceof Promise) {
191
+ console.warn(`Setting ${property} = ${value} on a Promise in Reactor`);
192
+ }
182
193
  const type = typeof (value);
194
+ if(value && type==="object" && value instanceof Promise) {
195
+ value = await value;
196
+ }
183
197
  if (target[property] !== value) {
184
198
  if (value && type === "object") {
185
199
  value = Reactor(value);
@@ -197,7 +211,7 @@ const {observe} = (() => {
197
211
  });
198
212
  return proxy;
199
213
  }
200
- return value;
214
+ return data;
201
215
  }
202
216
 
203
217
  class VariableEvent {
@@ -209,6 +223,9 @@ const {observe} = (() => {
209
223
  const createVarsProxy = (vars, component, constructor) => {
210
224
  return new Proxy(vars, {
211
225
  get(target, property) {
226
+ if(target instanceof Date) {
227
+ return Reflect.get(target,property);
228
+ }
212
229
  let {value} = target[property] || {};
213
230
  if (typeof (value) === "function") return value.bind(target);
214
231
  return value;
@@ -221,16 +238,22 @@ const {observe} = (() => {
221
238
  if (event.defaultPrevented) delete target[property].value;
222
239
  return true;
223
240
  }
224
- const {type, value, shared, exported, constant, reactive} = target[property];
241
+ const variable = target[property],
242
+ {type, value, shared, exported, constant, reactive, remote} = variable;
225
243
  if (constant) throw new TypeError(`${property}:${type} is a constant`);
244
+ newValue = type.validate ? type.validate(newValue,target[property]) : coerce(newValue,type);
226
245
  const newtype = typeof (newValue),
227
246
  typetype = typeof (type);
228
- if (newValue == null || type === "any" || newtype === type || (typetype === "function" && newValue && newtype === "object" && newValue instanceof type)) {
247
+ if (newValue == null || type === "any" || (newtype === type && typetype==="string") || (typetype === "function" && (newValue && newtype === "object" && newValue instanceof type) || variable.validityState?.valid)) {
229
248
  if (value !== newValue) {
230
249
  event.oldValue = value;
231
250
  target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
232
251
  target.postEvent.value("change", event);
233
- if (event.defaultPrevented) target[property].value = value;
252
+ if (event.defaultPrevented) {
253
+ target[property].value = value;
254
+ } else if(remote) {
255
+ handleRemote({variable,remote,reactive},true);
256
+ }
234
257
  }
235
258
  return true;
236
259
  }
@@ -245,22 +268,21 @@ const {observe} = (() => {
245
268
  });
246
269
  }
247
270
  const createObserver = (domNode, framed) => {
248
- const observer = new MutationObserver((mutations) => {
271
+ const mobserver = new MutationObserver((mutations) => {
249
272
  mutations.forEach((mutation) => {
273
+ const target = mutation.target;
250
274
  if (mutation.type === "attributes") {
251
275
  //if (framed) debugger;
252
276
  const name = mutation.attributeName,
253
- target = mutation.target,
254
277
  value = target.getAttribute(name);
255
278
  if (framed && name === "message" && target instanceof IFrameElement) {
256
- if (value) console.log("message", value);
279
+ //if (value) console.log("message", value);
257
280
  target.removeAttribute(name);
258
281
  target.dispatchEvent(new CustomEvent("message", {detail: JSON.parse(value)}))
259
282
  }
260
283
  if (target.observedAttributes && target.observedAttributes.includes(name)) {
261
284
  if (value !== mutation.oldValue) {
262
- target.setValue(name, value);
263
- if (target.attributeChangedCallback) target.attributeChangedCallback(name, value, mutation.oldValue);
285
+ target.setVariableValue(name, value);
264
286
  }
265
287
  }
266
288
  } else if (mutation.type === "childList") {
@@ -270,11 +292,13 @@ const {observe} = (() => {
270
292
  for (const target of mutation.addedNodes) {
271
293
  if (target.connectedCallback) target.connectedCallback();
272
294
  }
295
+ } else if(mutation.type === "characterData") {
296
+ if(target.characterDataMutationCallback) target.characterDataMutationCallback(target,mutation.oldValue,target.textContent);
273
297
  }
274
298
  });
275
299
  });
276
- observer.observe(domNode, {subtree: true, childList: true});
277
- return observer;
300
+ mobserver.observe(domNode, {subtree: true, childList: true});
301
+ return mobserver;
278
302
  }
279
303
  const querySelectorAll = (node, selector) => {
280
304
  const nodes = [...node.querySelectorAll(selector)],
@@ -291,41 +315,49 @@ const {observe} = (() => {
291
315
  nodes.push(root, ...getNodes(root.shadowRoot))
292
316
  } else {
293
317
  for (const node of root.childNodes) {
318
+ if (node.tagName === "SCRIPT") continue;
294
319
  if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.includes("${")) {
295
320
  node.template ||= node.nodeValue;
296
321
  nodes.push(node);
297
322
  } else if (node.nodeType === Node.ELEMENT_NODE) {
298
- let skip;
299
- if (node.getAttribute("type") === "radio") nodes.push(node);
323
+ let skip, pushed;
300
324
  [...node.attributes].forEach((attr) => {
301
325
  if (attr.value.includes("${")) {
302
326
  attr.template ||= attr.value;
327
+ pushed = true;
303
328
  nodes.push(node);
304
329
  } else if (attr.name.includes(":") || attr.name.startsWith("l-")) {
305
330
  skip = attr.name.includes("l-for:");
331
+ pushed = true;
306
332
  nodes.push(node)
307
333
  }
308
334
  })
335
+ if (!pushed && node.getAttribute("type") === "radio") nodes.push(node);
309
336
  if (!skip && !node.shadowRoot) nodes.push(...getNodes(node));
310
337
  }
311
338
  }
312
339
  }
313
340
  return nodes;
314
341
  }
315
- const resolveNode = (node, component,safe) => {
316
- if (node?.template) {
342
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
343
+ const resolveNodeOrText = (node, component, safe) => {
344
+ const type = typeof (node),
345
+ template = type === "string" ? node.trim() : node.template;
346
+ if (template) {
317
347
  try {
318
- let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(node?.template) + "` }")(component.varsProxy);
319
- value = node.nodeType===Node.TEXT_NODE || !safe ? value : Lightview.escapeHTML(value);
320
- node.nodeValue = value=="null" || value=="undefined" ? "" : value;
348
+ let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
349
+ value = node.nodeType === Node.TEXT_NODE || !safe ? value : Lightview.escapeHTML(value);
350
+ if (type === "string") return value==="undefined" ? undefined : value;
351
+ node.nodeValue = value == "null" || value == "undefined" ? "" : value;
321
352
  } catch (e) {
322
- console.warn(e);
353
+ //console.warn(e);
323
354
  if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
355
+ return undefined;
324
356
  }
325
357
  }
326
358
  return node?.nodeValue;
327
359
  }
328
- const render = (hasTemplate, render) => {
360
+ const render = (hasTemplate, render) => {
329
361
  let observer;
330
362
  if (hasTemplate) {
331
363
  if (observer) observer.cancel();
@@ -348,37 +380,43 @@ const {observe} = (() => {
348
380
  addListener(node, "click", anchorHandler);
349
381
  })
350
382
  }
351
- const bindInput = (input, name, component,value) => {
383
+ const bound = new WeakSet();
384
+ const bindInput = (input, variableName, component, value) => {
385
+ if (bound.has(input)) return;
386
+ bound.add(input);
352
387
  const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
353
- type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype),
354
- deflt = input.getAttribute("default");
388
+ type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype);
355
389
  value ||= input.getAttribute("value");
356
- let variable = component.vars[name] || {type};
390
+ let variable = component.vars[variableName] || {type};
357
391
  if (type !== variable.type) {
358
392
  if (variable.type === "any" || variable.type === "unknown") variable.type = type;
359
- else throw new TypeError(`Attempt to bind <input name="${name}" type="${type}"> to variable ${name}:${variable.type}`)
393
+ else throw new TypeError(`Attempt to bind <input name="${variableName}" type="${type}"> to variable ${variableName}:${variable.type}`)
394
+ }
395
+ component.variables({[variableName]: type},{reactive:true});
396
+ if(inputtype!=="radio") {
397
+ if(value.includes("${")) input.attributes.value.value = "";
398
+ else component.setVariableValue(variableName, coerce(value,type));
360
399
  }
361
- component.variables({[name]: type});
362
- component.setValue(name,value);
363
400
  let eventname = "change";
364
401
  if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
365
402
  eventname = "input";
366
403
  }
367
- addListener(input, eventname, (event) => {
368
- event.stopImmediatePropagation();
369
- const target = event.target;
370
- let value = target.value;
404
+ const listener = (event) => {
405
+ if (event) event.stopImmediatePropagation();
406
+ let value = input.value;
371
407
  if (inputtype === "checkbox") {
372
408
  value = input.checked
373
- } else if (target.tagName === "SELECT") {
374
- if (target.hasAttribute("multiple")) {
375
- value = [...target.querySelectorAll("option")]
376
- .filter((option) => option.selected || resolveNode(option.attributes.value, component) == value || option.innerText == value)
409
+ } else if (input.tagName === "SELECT") {
410
+ if (input.hasAttribute("multiple")) {
411
+ const varvalue = component.varsProxy[variableName];
412
+ value = [...input.querySelectorAll("option")]
413
+ .filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value) //todo make sync comopat
377
414
  .map((option) => option.getAttribute("value") || option.innerText);
378
415
  }
379
416
  }
380
- component.varsProxy[name] = coerce(value, type);
381
- })
417
+ component.varsProxy[variableName] = coerce(value, type);
418
+ };
419
+ addListener(input, eventname, listener);
382
420
  }
383
421
  const tryParse = (value) => {
384
422
  try {
@@ -388,22 +426,20 @@ const {observe} = (() => {
388
426
  }
389
427
  }
390
428
  let reserved = {
391
- any: {value: "any",constant: true},
392
- boolean: {value: "boolean", constant: true},
393
- string: {value: "string", constant: true},
394
- number: {value: "number", constant: true},
395
- object: {value: "object", constant: true},
396
429
  observed: {value: true, constant: true},
397
430
  reactive: {value: true, constant: true},
398
431
  shared: {value: true, constant: true},
399
432
  exported: {value: true, constant: true},
400
- imported: {value: true, constant: true}
433
+ imported: {value: true, constant: true},
434
+ remote: {}
401
435
  };
402
436
  const createClass = (domElementNode, {observer, framed}) => {
403
437
  const instances = new Set(),
404
438
  dom = domElementNode.tagName === "TEMPLATE"
405
439
  ? domElementNode.content.cloneNode(true)
406
- : domElementNode.cloneNode(true);
440
+ : domElementNode.cloneNode(true),
441
+ observedAttributes = [];
442
+ observedAttributes.add = function(name) { observedAttributes.includes(name) || observedAttributes.push(name); }
407
443
  if (domElementNode.tagName === "TEMPLATE") domElementNode = domElementNode.cloneNode(true);
408
444
  return class CustomElement extends HTMLElement {
409
445
  static get instances() {
@@ -413,10 +449,18 @@ const {observe} = (() => {
413
449
  constructor() {
414
450
  super();
415
451
  instances.add(this);
416
- observer ||= createObserver(this, framed);
417
452
  const currentComponent = this,
418
453
  shadow = this.attachShadow({mode: "open"}),
419
454
  eventlisteners = {};
455
+ // needs to be local to the instance
456
+ Object.defineProperty(this,"changeListener",{value:
457
+ function({variableName, value}) {
458
+ if (currentComponent.changeListener.targets.has(variableName)) {
459
+ value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
460
+ if (value == null) removeComponentAttribute(currentComponent, variableName);
461
+ else setComponentAttribute(currentComponent, variableName, value);
462
+ }
463
+ }});
420
464
  this.vars = {
421
465
  ...reserved,
422
466
  addEventListener: {
@@ -431,9 +475,10 @@ const {observe} = (() => {
431
475
  constant: true
432
476
  },
433
477
  postEvent: {
434
- value: (eventName, event) => {
478
+ value: (eventName, event={}) => {
435
479
  //event = {...event}
436
480
  event.type = eventName;
481
+ event.target = currentComponent;
437
482
  eventlisteners[eventName]?.forEach((f) => f(event));
438
483
  },
439
484
  type: "function",
@@ -443,6 +488,8 @@ const {observe} = (() => {
443
488
  };
444
489
  this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
445
490
  this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
491
+ this.changeListener.targets = new Set();
492
+ this.varsProxy.addEventListener("change", this.changeListener);
446
493
  if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported: true});
447
494
  ["getElementById", "querySelector", "querySelectorAll"]
448
495
  .forEach((fname) => {
@@ -473,138 +520,149 @@ const {observe} = (() => {
473
520
  shadow = ctx.shadowRoot;
474
521
  for (const attr of this.defaultAttributes) this.hasAttribute(attr.name) || this.setAttribute(attr.name, attr.value);
475
522
  const scripts = shadow.querySelectorAll("script"),
476
- promises = [];
477
- for (const script of scripts) {
523
+ promises = [],
524
+ scriptpromises = [];
525
+ for (const script of [...scripts]) {
478
526
  if (script.attributes.src?.value?.includes("/lightview.js")) continue;
479
- if (script.className !== "lightview" && !((script.getAttribute("type") || "").includes("lightview/"))) continue;
480
- const scriptid = Math.random() + "",
481
- currentScript = document.createElement("script");
527
+ const currentScript = document.createElement("script");
528
+ if (script.className !== "lightview" && !((script.attributes.type?.value || "").includes("lightview/"))) {
529
+ for (const attr of script.attributes) currentScript.setAttribute(attr.name,attr.value);
530
+ scriptpromises.push(new Promise((resolve) => {
531
+ currentScript.onload = () => resolve();
532
+ }))
533
+ shadow.appendChild(currentScript);
534
+ currentScript.remove();
535
+ continue;
536
+ };
537
+ const scriptid = Math.random() + "";
482
538
  for (const attr of script.attributes) {
483
539
  currentScript.setAttribute(attr.name, attr.name === "type" ? attr.value.replace("lightview/", "") : attr.value);
484
540
  }
485
541
  currentScript.classList.remove("lightview");
486
542
  const text = script.innerHTML.replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1").replaceAll(/\r?\n/g, "");
487
- currentScript.innerHTML = `Function('if(window["${scriptid}"]?.ctx) { with(window["${scriptid}"].ctx) { ${text}; } window["${scriptid}"](); }')(); `;
543
+ currentScript.innerHTML = `Object.getPrototypeOf(async function(){}).constructor('if(window["${scriptid}"]?.ctx) { const ctx = window["${scriptid}"].ctx; { with(ctx) { ${text}; } } }')().then(() => window["${scriptid}"]()); `;
488
544
  let resolver;
489
- promises.push(new Promise((resolve) => resolver = resolve));
490
545
  window[scriptid] = () => {
491
546
  delete window[scriptid];
492
547
  currentScript.remove();
493
548
  resolver();
494
549
  }
495
550
  window[scriptid].ctx = ctx.varsProxy;
496
- ctx.appendChild(currentScript);
551
+ promises.push(new Promise((resolve) => {
552
+ resolver = resolve;
553
+ // wait for all regular scripts to load first
554
+ Promise.all(scriptpromises).then(() => shadow.appendChild(currentScript));
555
+ }));
497
556
  }
498
557
  Promise.all(promises).then(() => {
499
558
  const nodes = getNodes(ctx);
500
559
  nodes.forEach((node) => {
501
560
  if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
502
- render(!!node.template, () => resolveNode(node, this))
561
+ render(!!node.template, () => resolveNodeOrText(node, this))
503
562
  } else if (node.nodeType === Node.ELEMENT_NODE) {
504
563
  // resolve the value before all else;
505
- const attr = node.attributes.value;
506
- if (attr && attr.template) {
507
- render(!!attr.template, () => {
508
- const value = resolveNode(attr, this),
509
- eltype = resolveNode(node.attributes.type, ctx);
510
- if(node.attributes.value) {
511
- const template = attr.template;
512
- if(/\$\{[a-zA-z_]+\}/g.test(template)) {
513
- const name = template.substring(2,template.length-1);
514
- bindInput(node,name,this,value);
515
- }
516
- }
517
- if (eltype === "checkbox") {
518
- if (coerce(value, "boolean") === true) {
519
- node.setAttribute("checked", "");
520
- node.checked = true;
521
- } else {
522
- node.removeAttribute("checked");
523
- node.checked = false;
524
- }
525
- const vname = resolveNode(node.attributes.name, ctx);
526
- if (vname) ctx.setValue(vname, node.checked, {coerceTo: "boolean"});
527
- }
528
- if (node.tagName === "SELECT") {
529
- let values = [value];
530
- if (node.hasAttribute("multiple")) values = coerce(value, Array);
531
- [...node.querySelectorAll("option")].forEach((option) => {
532
- if (option.hasAttribute("value")) {
533
- if (values.includes(resolveNode(option.attributes.value, ctx))) {
534
- option.setAttribute("selected", "");
535
- option.selected = true;
536
- }
537
- } else if (option.innerText.trim() === value) {
538
- option.setAttribute("selected", "");
539
- option.selected = true;
564
+ const attr = node.attributes.value,
565
+ template = attr?.template;
566
+ if (attr && template) {
567
+ //render(!!template, () => {
568
+ let value = resolveNodeOrText(attr, this),
569
+ eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
570
+ const template = attr.template;
571
+ if (template) {
572
+ if (/\$\{[a-zA-z_]+\}/g.test(template)) {
573
+ const name = template.substring(2, template.length - 1);
574
+ if(!this.vars[name] || this.vars[name].reactive) {
575
+ bindInput(node, name, this, value);
540
576
  }
541
- })
542
- }
543
- });
544
- }
545
- [...node.attributes].forEach((attr) => {
546
- if (attr.name === "value" && attr.template) return;
547
- const {name, value} = attr;
548
- if (name === "type") {
549
- if (value === "radio") {
550
- const name = resolveNode(node.attributes.name, ctx);
551
- for (const vname of this.getVariableNames()) {
552
- if (vname === name) {
553
- render(true, () => {
554
- const name = resolveNode(node.attributes.name, ctx),
555
- varvalue = Function("context", "with(context) { return `${" + name + "}` }")(ctx.varsProxy);
556
- if (varvalue == resolveNode(node.attributes.value, ctx)) {
577
+ }
578
+ observe(() => {
579
+ const value = resolveNodeOrText(template, ctx);
580
+ if(value!==undefined) {
581
+ if (eltype === "checkbox") {
582
+ if (coerce(value, "boolean") === true) {
557
583
  node.setAttribute("checked", "");
558
584
  node.checked = true;
559
585
  } else {
560
586
  node.removeAttribute("checked");
561
587
  node.checked = false;
562
588
  }
563
- });
564
- bindInput(node, name, ctx);
565
- break;
589
+ } else if (node.tagName === "SELECT") {
590
+ let values = [value];
591
+ if (node.hasAttribute("multiple")) values = coerce(value, Array);
592
+ [...node.querySelectorAll("option")].forEach(async (option) => {
593
+ if (option.hasAttribute("value")) {
594
+ if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
595
+ option.setAttribute("selected", "");
596
+ option.selected = true;
597
+ }
598
+ } else if (values.includes(resolveNodeOrText(option.innerText, ctx))) {
599
+ option.setAttribute("selected", "");
600
+ option.selected = true;
601
+ }
602
+ })
603
+ } else if (eltype!=="radio") {
604
+ attr.value = value;
605
+ }
566
606
  }
567
- }
607
+ });
568
608
  }
609
+ }
610
+ [...node.attributes].forEach(async (attr) => {
611
+ if (attr.name === "value" && attr.template) return;
612
+ const {name, value} = attr,
613
+ vname = node.attributes.name?.value;
614
+ if (name === "type" && value=="radio" && vname) {
615
+ bindInput(node, vname, this);
616
+ render(true, () => {
617
+ const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
618
+ if (node.attributes.value.value == varvalue) {
619
+ node.setAttribute("checked", "");
620
+ node.checked = true;
621
+ } else {
622
+ node.removeAttribute("checked");
623
+ node.checked = false;
624
+ }
625
+ });
569
626
  }
570
627
 
571
628
  const [type, ...params] = name.split(":");
572
629
  if (type === "") { // name is :something
573
- render(!!attr.template, () => {
574
- const value = attr.value,
575
- elvalue = resolveNode(node.attributes.value, ctx),
576
- eltype = resolveNode(node.attributes.type, ctx),
577
- elname = resolveNode(node.attributes.name, ctx);
630
+ render(!!attr.template, () => {
631
+ const value = attr.value;
578
632
  if (params[0]) {
579
633
  if (value === "true") node.setAttribute(params[0], "")
580
634
  else node.removeAttribute(params[0]);
581
- } else if (eltype === "checkbox" || node.tagName === "OPTION") {
582
- if (value === "true") node.setAttribute("checked", "")
583
- else node.removeAttribute("checked");
635
+ } else {
636
+ const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx) : null,
637
+ eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
638
+ if (eltype === "checkbox" || node.tagName === "OPTION") {
639
+ if (elvalue === "true") node.setAttribute("checked", "")
640
+ else node.removeAttribute("checked");
641
+ }
584
642
  }
585
643
  })
586
644
  } else if (type === "l-on") {
587
645
  let listener;
588
- render(!!attr.template, () => {
589
- const value = resolveNode(attr, this);
646
+ render(!!attr.template, () => {
647
+ const value = resolveNodeOrText(attr, this);
590
648
  if (listener) node.removeEventListener(params[0], listener);
591
649
  listener = this[value] || window[value] || Function(value);
592
650
  addListener(node, params[0], listener);
593
651
  })
594
652
  } else if (type === "l-if") {
595
- render(!!attr.template, () => {
596
- const value = resolveNode(attr, this);
653
+ render(!!attr.template, () => {
654
+ const value = resolveNodeOrText(attr, this);
597
655
  node.style.setProperty("display", value === "true" ? "revert" : "none");
598
656
  })
599
657
  } else if (type === "l-for") {
600
658
  node.template ||= node.innerHTML;
601
- render(!!attr.template, () => {
659
+ render(!!attr.template, () => {
602
660
  const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
603
- value = resolveNode(attr, this),
661
+ value = resolveNodeOrText(attr, this),
604
662
  coerced = coerce(value, what === "each" ? Array : "object"),
605
663
  target = what === "each" ? coerced : Object[what](coerced),
606
- html = target.reduce((html, item, i, target) => {
607
- return html += Function("vars","context", "with(vars) { with(context) { return `" + node.template + "` }}")(
664
+ html = target.reduce( (html, item, i, target) => {
665
+ return html += Function("vars", "context", "with(vars) { with(context) { return `" + node.template + "` }}")(
608
666
  ctx.varsProxy,
609
667
  {
610
668
  [vname]: item,
@@ -625,42 +683,33 @@ const {observe} = (() => {
625
683
  else node.appendChild(parsed.body.firstChild);
626
684
  }
627
685
  })
628
- } else if (attr.template) {
629
- render(!!attr.template, () => resolveNode(attr, this));
686
+ } else {
687
+ render(!!attr.template, () => {
688
+ resolveNodeOrText(attr, this);
689
+ })
630
690
  }
631
691
  })
632
692
  }
633
693
  })
634
694
  shadow.normalize();
635
- observer.observe(ctx, {attributeOldValue: true});
636
- if (ctx.hasOwnProperty("connectedCallback")) ctx.connectedCallback();
695
+ observer ||= createObserver(ctx, framed);
696
+ observer.observe(ctx, {attributeOldValue: true, subtree:true, characterData:true, characterDataOldValue:true});
697
+ ctx.vars.postEvent.value("connected");
637
698
  })
638
699
  }
639
-
640
- adopted(value) {
641
- this.adoptedCallback = value;
642
- //Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
700
+ adoptedCallback(callback) {
701
+ this.vars.postEvent.value("adopted");
702
+ super.adoptedCallback();
643
703
  }
644
-
645
- connected(callback) {
646
- this.connectedCallback = callback;
647
- //Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
704
+ disconnectedCallback() {
705
+ this.vars.postEvent.value("disconnected");
706
+ super.disconnectedCallback();
648
707
  }
649
-
650
- attributeChanged(callback) {
651
- this.attributeChangedCallback = callback;
652
- //Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
708
+ get observedAttributes() {
709
+ return CustomElement.observedAttributes;
653
710
  }
654
-
655
- disconnected(callback) {
656
- Object.defineProperty(this, "disconnectedCallback", {
657
- configurable: true,
658
- writable: true,
659
- value: () => {
660
- value();
661
- super.disconnectedCallback(callback);
662
- }
663
- });
711
+ static get observedAttributes() {
712
+ return observedAttributes;
664
713
  }
665
714
 
666
715
  getVariableNames() {
@@ -669,17 +718,17 @@ const {observe} = (() => {
669
718
  })
670
719
  }
671
720
 
672
- setValue(variableName, value, {coerceTo = typeof (value)} = {}) {
721
+ setVariableValue(variableName, value, {coerceTo = typeof (value)} = {}) {
673
722
  if (!this.isConnected) {
674
723
  instances.delete(this);
675
724
  return false;
676
725
  }
677
726
  let {type} = this.vars[variableName] || {};
678
727
  if (type) {
679
- value = coerce(value, type);
680
728
  if (this.varsProxy[variableName] !== value) {
681
729
  const variable = this.vars[variableName];
682
730
  if (variable.shared) {
731
+ value = type.validate ? type.validate(value,variable) : coerce(value,coerceTo);
683
732
  const event = new VariableEvent({
684
733
  variableName: variableName,
685
734
  value: value,
@@ -698,11 +747,11 @@ const {observe} = (() => {
698
747
  return false;
699
748
  }
700
749
 
701
- getValue(variableName) {
750
+ getVariableValue(variableName) {
702
751
  return this.vars[variableName]?.value;
703
752
  }
704
753
 
705
- variables(variables, {observed, reactive, shared, exported, imported} = {}) { // options = {observed,reactive,shared,exported,imported}
754
+ variables(variables, {observed, reactive, shared, exported, imported, remote} = {}) { // options = {observed,reactive,shared,exported,imported}
706
755
  const addEventListener = this.varsProxy.addEventListener;
707
756
  if (variables !== undefined) {
708
757
  Object.entries(variables)
@@ -712,6 +761,7 @@ const {observe} = (() => {
712
761
  variable.value = this.hasAttribute(key) ? coerce(this.getAttribute(key), variable.type) : variable.value;
713
762
  variable.observed = observed;
714
763
  variable.imported = imported;
764
+ if(variable.observed) this.observedAttributes.add(key);
715
765
  }
716
766
  if (reactive) {
717
767
  variable.reactive = true;
@@ -720,21 +770,20 @@ const {observe} = (() => {
720
770
  if (shared) {
721
771
  variable.shared = true;
722
772
  addEventListener("change", ({variableName, value}) => {
723
- if (this.vars[variableName]?.shared) {
724
- this.siblings.forEach((instance) => instance.setValue(variableName, value))
725
- }
773
+ if (this.vars[variableName]?.shared) this.siblings.forEach((instance) => instance.setVariableValue(variableName, value))
726
774
  })
727
775
  }
728
776
  if (exported) {
729
777
  variable.exported = true;
730
778
  // in case the export goes up to an iframe
731
779
  if (variable.value != null) setComponentAttribute(this, key, variable.value);
732
- addEventListener("change", ({variableName, value}) => {
733
- value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
734
- if (value == null) removeComponentAttribute(this, variableName);
735
- else setComponentAttribute(this, variableName, value);
736
- })
780
+ this.changeListener.targets.add(key);
781
+ }
782
+ if (remote) {
783
+ variable.remote = remote;
784
+ handleRemote({variable, remote, reactive});
737
785
  }
786
+ if(type.validate) type.validate(undefined,variable);
738
787
  });
739
788
  }
740
789
  return Object.entries(this.vars)
@@ -763,24 +812,143 @@ const {observe} = (() => {
763
812
  }
764
813
  }
765
814
  }
766
- const createComponent = (name, node, {framed,observer} = {}) => {
815
+
816
+ const remoteProxy = ({json, variable,remote, reactive}) => {
817
+ const type = typeof (remote);
818
+ return new Proxy(json, {
819
+ get(target,property) {
820
+ if(property==="__remoteProxytarget__") return json;
821
+ return target[property];
822
+ },
823
+ async set(target, property, value) {
824
+ if(value && typeof(value)==="object" && value instanceof Promise) value = await value;
825
+ const oldValue = target[property];
826
+ if (oldValue !== value) {
827
+ let remotevalue;
828
+ if (type === "string") {
829
+ const href = new URL(remote,window.location.href).href;
830
+ remotevalue = patch({target,property,value,oldValue},href,variable);
831
+ } else if(remote && type==="object") {
832
+ let href;
833
+ if(remote.path) href = new URL(remote.path,window.location.href).href;
834
+ if(!remote.patch) {
835
+ if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
836
+ remote.patch = patch;
837
+ }
838
+ remotevalue = remote.patch({target,property,value,oldValue},href,variable);
839
+ }
840
+ if(remotevalue) {
841
+ await remotevalue.then((newjson) => {
842
+ if (newjson && typeof (newjson) === "object" && reactive) {
843
+ const target = variable.value?.__reactorProxyTarget__ ? json : variable.value;
844
+ Object.entries(newjson).forEach(([key,newValue]) => {
845
+ if(target[key]!==newValue) target[key] = newValue;
846
+ })
847
+ Object.keys(target).forEach((key) => {
848
+ if(!(key in newjson)) delete target[key];
849
+ });
850
+ if(variable.value?.__reactorProxyTarget__) {
851
+ const dependents = variable.value.__dependents__,
852
+ observers = dependents[property] || [];
853
+ [...observers].forEach((f) => {
854
+ if (f.cancelled) dependents[property].delete(f);
855
+ else f();
856
+ })
857
+ }
858
+ } else {
859
+ variable.value = json;
860
+ }
861
+ })
862
+ }
863
+ }
864
+ return true;
865
+ }
866
+ })
867
+ }
868
+
869
+ const patch = ({target,property,value,oldValue},href,variable) => {
870
+ return fetch(href, {
871
+ method: "PATCH",
872
+ body: JSON.stringify({property,value,oldValue}),
873
+ headers: {
874
+ "Content-Type": "application/json"
875
+ }
876
+ }).then((response) => {
877
+ if (response.status < 400) return response.json();
878
+ })
879
+ }
880
+
881
+ const get = (href,variable) => {
882
+ return fetch(href)
883
+ .then((response) => {
884
+ if (response.status < 400) return response.json();
885
+ })
886
+ }
887
+
888
+ const put = (href,variable) => {
889
+ return fetch(href, {
890
+ method: "PUT",
891
+ body: JSON.stringify(variable.value),
892
+ headers: {
893
+ "Content-Type": "application/json"
894
+ }
895
+ }).then((response) => {
896
+ if (response.status === 200) return response.json();
897
+ })
898
+ }
899
+
900
+ const handleRemote = async ({variable, remote, reactive},doput) => {
901
+ const type = typeof (remote);
902
+ let value;
903
+ if (type === "string") {
904
+ const href = new URL(remote,window.location.href).href;
905
+ value = (doput
906
+ ? put(href,variable)
907
+ : get(href,variable));
908
+ if(variable.value===undefined) variable.value = value;
909
+ } else if (remote && type === "object") {
910
+ let href;
911
+ if(remote.path) href = new URL(remote.path,window.location.href).href;
912
+ if(!remote.get || !remote.put) {
913
+ if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
914
+ if(!remote.get) remote.get = get;
915
+ if(!remote.put) remote.put = put;
916
+ }
917
+ value = (doput
918
+ ? remote.put(href,variable)
919
+ : remote.get(href,variable));
920
+ if(remote.ttl && !doput && !remote.intervalId) {
921
+ remote.intervalId = setInterval(async () => {
922
+ await handleRemote({variable, remote, reactive});
923
+ })
924
+ }
925
+ if(variable.value===undefined) variable.value = value;
926
+ }
927
+ if(value) {
928
+ variable.value = await value.then((json) => {
929
+ if (json && typeof (json) === "object" && reactive) {
930
+ return remoteProxy({json, variable,remote, reactive})
931
+ } else {
932
+ return json;
933
+ }
934
+ })
935
+ }
936
+ }
937
+
938
+ const createComponent = (name, node, {framed, observer} = {}) => {
767
939
  let ctor = customElements.get(name);
768
940
  if (ctor) {
769
- if (framed && !ctor.lightviewFramed) {
770
- ctor.lightviewFramed = true;
771
- } else {
772
- console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
773
- }
941
+ if (framed && !ctor.lightviewFramed) ctor.lightviewFramed = true;
942
+ else console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
774
943
  return ctor;
775
944
  }
776
945
  ctor = createClass(node, {observer, framed});
777
946
  customElements.define(name, ctor);
778
- Lightview.customElements.set(name,ctor);
947
+ Lightview.customElements.set(name, ctor);
779
948
  return ctor;
780
949
  }
781
950
  Lightview.customElements = new Map();
782
951
  Lightview.createComponent = createComponent;
783
- //Object.defineProperty(Lightview, "createComponent", {writable: true, configurable: true, value: createComponent})
784
952
  const importLink = async (link, observer) => {
785
953
  const url = (new URL(link.getAttribute("href"), window.location.href)),
786
954
  as = link.getAttribute("as") || getNameFromPath(url.pathname);
@@ -792,12 +960,12 @@ const {observe} = (() => {
792
960
  dom = parser.parseFromString(html, "text/html"),
793
961
  unhide = !!dom.head.querySelector('meta[name="l-unhide"]'),
794
962
  links = dom.head.querySelectorAll('link[href$=".html"][rel=module]');
795
- for(const childlink of links) {
963
+ for (const childlink of links) {
796
964
  const href = childlink.getAttribute("href"),
797
- childurl = new URL(href,url.href);
798
- childlink.setAttribute("href",childurl.href);
799
- if(link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin",link.getAttribute("crossorigin"))
800
- await importLink(childlink,observer);
965
+ childurl = new URL(href, url.href);
966
+ childlink.setAttribute("href", childurl.href);
967
+ if (link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin", link.getAttribute("crossorigin"))
968
+ await importLink(childlink, observer);
801
969
  }
802
970
  if (unhide) dom.body.removeAttribute("hidden");
803
971
  createComponent(as, dom.body, {observer});
@@ -807,7 +975,7 @@ const {observe} = (() => {
807
975
  const importLinks = async () => {
808
976
  const observer = createObserver(document.body);
809
977
  for (const link of [...document.querySelectorAll(`link[href$=".html"][rel=module]`)]) {
810
- await importLink(link,observer);
978
+ await importLink(link, observer);
811
979
  }
812
980
  }
813
981
 
@@ -875,21 +1043,19 @@ const {observe} = (() => {
875
1043
  postMessage({type: "setAttribute", argsList: ["height", height + 20]});
876
1044
  }
877
1045
  resize();
878
- onresize(document.body, () => {
879
- resize();
880
- });
1046
+ onresize(document.body, () => resize());
881
1047
  return
882
1048
  }
883
1049
  if (type === "setAttribute") {
884
1050
  const [name, value] = [...argsList],
885
1051
  variable = document.body.vars[name];
886
- if (variable && variable.imported) document.body.setValue(name, value);
1052
+ if (variable && variable.imported) document.body.setVariableValue(name, value);
887
1053
  return;
888
1054
  }
889
1055
  if (type === "removeAttribute") {
890
1056
  const [name] = argsList[0],
891
1057
  variable = document.body.vars[name];
892
- if (variable && variable.imported) document.body.setValue(name, undefined);
1058
+ if (variable && variable.imported) document.body.setVariableValue(name, undefined);
893
1059
 
894
1060
  }
895
1061
  });
@@ -927,9 +1093,7 @@ const {observe} = (() => {
927
1093
  }
928
1094
  if (type === "setAttribute") {
929
1095
  const [name, value] = [...argsList];
930
- if (iframe.getAttribute(name) !== value + "") {
931
- iframe.setAttribute(name, value);
932
- }
1096
+ if (iframe.getAttribute(name) !== value + "") iframe.setAttribute(name, value);
933
1097
  return;
934
1098
  }
935
1099
  if (type === "removeAttribute") {
@@ -977,7 +1141,7 @@ const {observe} = (() => {
977
1141
  addListener(document, "DOMContentLoaded", (event) => loader(callback));
978
1142
  }
979
1143
  Lightview.whenFramed = whenFramed;
980
- //Object.defineProperty(Lightview, "whenFramed", {configurable: true, writable: true, value: whenFramed});
1144
+
981
1145
  if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) {
982
1146
  // loads for unframed content
983
1147
  // CodePen mucks with window.parent
@@ -985,4 +1149,4 @@ const {observe} = (() => {
985
1149
  }
986
1150
 
987
1151
  return {observe}
988
- })();
1152
+ })();