lightview 1.4.8-b → 1.6.2-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 && remote.put) {
255
+ remote.handleRemote({variable,config:remote.config,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,53 +315,48 @@ const {observe} = (() => {
291
315
  nodes.push(root, ...getNodes(root.shadowRoot))
292
316
  } else {
293
317
  for (const node of root.childNodes) {
294
- if(node.tagName==="SCRIPT") continue;
318
+ if (node.tagName === "SCRIPT") continue;
295
319
  if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.includes("${")) {
296
320
  node.template ||= node.nodeValue;
297
321
  nodes.push(node);
298
322
  } else if (node.nodeType === Node.ELEMENT_NODE) {
299
- let skip;
300
- if (node.getAttribute("type") === "radio") nodes.push(node);
323
+ let skip, pushed;
301
324
  [...node.attributes].forEach((attr) => {
302
325
  if (attr.value.includes("${")) {
303
326
  attr.template ||= attr.value;
327
+ pushed = true;
304
328
  nodes.push(node);
305
329
  } else if (attr.name.includes(":") || attr.name.startsWith("l-")) {
306
330
  skip = attr.name.includes("l-for:");
331
+ pushed = true;
307
332
  nodes.push(node)
308
333
  }
309
334
  })
335
+ if (!pushed && node.getAttribute("type") === "radio") nodes.push(node);
310
336
  if (!skip && !node.shadowRoot) nodes.push(...getNodes(node));
311
337
  }
312
338
  }
313
339
  }
314
340
  return nodes;
315
341
  }
316
- const resolveNodeOrText = (node, component,safe) => {
317
- const type = typeof(node),
342
+ const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
343
+ const resolveNodeOrText = (node, component, safe) => {
344
+ const type = typeof (node),
318
345
  template = type === "string" ? node.trim() : node.template;
319
346
  if (template) {
320
347
  try {
321
348
  let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
322
- value = node.nodeType===Node.TEXT_NODE || !safe ? value : Lightview.escapeHTML(value);
323
- if(type==="string") return value;
324
- node.nodeValue = value=="null" || value=="undefined" ? "" : value;
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;
325
352
  } catch (e) {
326
- console.warn(e);
353
+ //console.warn(e);
327
354
  if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
355
+ return undefined;
328
356
  }
329
357
  }
330
358
  return node?.nodeValue;
331
359
  }
332
- const render = (hasTemplate, render) => {
333
- let observer;
334
- if (hasTemplate) {
335
- if (observer) observer.cancel();
336
- observer = observe(render)
337
- } else {
338
- render();
339
- }
340
- }
341
360
  const inputTypeToType = (inputType) => {
342
361
  if (!inputType) return "any"
343
362
  if (["text", "tel", "email", "url", "search", "radio", "color", "password"].includes(inputType)) return "string";
@@ -353,26 +372,28 @@ const {observe} = (() => {
353
372
  })
354
373
  }
355
374
  const bound = new WeakSet();
356
- const bindInput = (input, variableName, component,value) => {
357
- if(bound.has(input)) return;
375
+ const bindInput = (input, variableName, component, value) => {
376
+ if (bound.has(input)) return;
358
377
  bound.add(input);
359
378
  const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
360
- type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype),
361
- deflt = input.getAttribute("default");
379
+ type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype);
362
380
  value ||= input.getAttribute("value");
363
381
  let variable = component.vars[variableName] || {type};
364
382
  if (type !== variable.type) {
365
383
  if (variable.type === "any" || variable.type === "unknown") variable.type = type;
366
384
  else throw new TypeError(`Attempt to bind <input name="${variableName}" type="${type}"> to variable ${variableName}:${variable.type}`)
367
385
  }
368
- component.variables({[variableName]: type});
369
- if(inputtype!=="radio") component.setValue(variableName,value);
386
+ component.variables({[variableName]: type},{reactive:true});
387
+ if(inputtype!=="radio") {
388
+ if(value.includes("${")) input.attributes.value.value = "";
389
+ else component.setVariableValue(variableName, coerce(value,type));
390
+ }
370
391
  let eventname = "change";
371
392
  if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
372
393
  eventname = "input";
373
394
  }
374
395
  const listener = (event) => {
375
- if(event) event.stopImmediatePropagation();
396
+ if (event) event.stopImmediatePropagation();
376
397
  let value = input.value;
377
398
  if (inputtype === "checkbox") {
378
399
  value = input.checked
@@ -380,7 +401,7 @@ const {observe} = (() => {
380
401
  if (input.hasAttribute("multiple")) {
381
402
  const varvalue = component.varsProxy[variableName];
382
403
  value = [...input.querySelectorAll("option")]
383
- .filter((option) => option.selected || resolveNodeOrText(option.attributes.value||option.innerText,component)===value)
404
+ .filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value) //todo make sync comopat
384
405
  .map((option) => option.getAttribute("value") || option.innerText);
385
406
  }
386
407
  }
@@ -396,11 +417,6 @@ const {observe} = (() => {
396
417
  }
397
418
  }
398
419
  let reserved = {
399
- any: {value: "any",constant: true},
400
- boolean: {value: "boolean", constant: true},
401
- string: {value: "string", constant: true},
402
- number: {value: "number", constant: true},
403
- object: {value: "object", constant: true},
404
420
  observed: {value: true, constant: true},
405
421
  reactive: {value: true, constant: true},
406
422
  shared: {value: true, constant: true},
@@ -411,7 +427,9 @@ const {observe} = (() => {
411
427
  const instances = new Set(),
412
428
  dom = domElementNode.tagName === "TEMPLATE"
413
429
  ? domElementNode.content.cloneNode(true)
414
- : domElementNode.cloneNode(true);
430
+ : domElementNode.cloneNode(true),
431
+ observedAttributes = [];
432
+ observedAttributes.add = function(name) { observedAttributes.includes(name) || observedAttributes.push(name); }
415
433
  if (domElementNode.tagName === "TEMPLATE") domElementNode = domElementNode.cloneNode(true);
416
434
  return class CustomElement extends HTMLElement {
417
435
  static get instances() {
@@ -421,10 +439,18 @@ const {observe} = (() => {
421
439
  constructor() {
422
440
  super();
423
441
  instances.add(this);
424
- observer ||= createObserver(this, framed);
425
442
  const currentComponent = this,
426
443
  shadow = this.attachShadow({mode: "open"}),
427
444
  eventlisteners = {};
445
+ // needs to be local to the instance
446
+ Object.defineProperty(this,"changeListener",{value:
447
+ function({variableName, value}) {
448
+ if (currentComponent.changeListener.targets.has(variableName)) {
449
+ value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
450
+ if (value == null) removeComponentAttribute(currentComponent, variableName);
451
+ else setComponentAttribute(currentComponent, variableName, value);
452
+ }
453
+ }});
428
454
  this.vars = {
429
455
  ...reserved,
430
456
  addEventListener: {
@@ -439,9 +465,10 @@ const {observe} = (() => {
439
465
  constant: true
440
466
  },
441
467
  postEvent: {
442
- value: (eventName, event) => {
468
+ value: (eventName, event = {}) => {
443
469
  //event = {...event}
444
470
  event.type = eventName;
471
+ event.target = currentComponent;
445
472
  eventlisteners[eventName]?.forEach((f) => f(event));
446
473
  },
447
474
  type: "function",
@@ -451,6 +478,8 @@ const {observe} = (() => {
451
478
  };
452
479
  this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
453
480
  this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
481
+ this.changeListener.targets = new Set();
482
+ this.varsProxy.addEventListener("change", this.changeListener);
454
483
  if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported: true});
455
484
  ["getElementById", "querySelector", "querySelectorAll"]
456
485
  .forEach((fname) => {
@@ -481,138 +510,148 @@ const {observe} = (() => {
481
510
  shadow = ctx.shadowRoot;
482
511
  for (const attr of this.defaultAttributes) this.hasAttribute(attr.name) || this.setAttribute(attr.name, attr.value);
483
512
  const scripts = shadow.querySelectorAll("script"),
484
- promises = [];
485
- for (const script of scripts) {
513
+ promises = [],
514
+ scriptpromises = [];
515
+ for (const script of [...scripts]) {
486
516
  if (script.attributes.src?.value?.includes("/lightview.js")) continue;
487
- if (script.className !== "lightview" && !((script.getAttribute("type") || "").includes("lightview/"))) continue;
488
- const scriptid = Math.random() + "",
489
- currentScript = document.createElement("script");
517
+ const currentScript = document.createElement("script");
518
+ if (script.className !== "lightview" && !((script.attributes.type?.value || "").includes("lightview/"))) {
519
+ for (const attr of script.attributes) currentScript.setAttribute(attr.name,attr.value);
520
+ scriptpromises.push(new Promise((resolve) => {
521
+ currentScript.onload = () => resolve();
522
+ }))
523
+ shadow.appendChild(currentScript);
524
+ currentScript.remove();
525
+ continue;
526
+ };
527
+ const scriptid = Math.random() + "";
490
528
  for (const attr of script.attributes) {
491
529
  currentScript.setAttribute(attr.name, attr.name === "type" ? attr.value.replace("lightview/", "") : attr.value);
492
530
  }
493
531
  currentScript.classList.remove("lightview");
494
532
  const text = script.innerHTML.replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1").replaceAll(/\r?\n/g, "");
495
- currentScript.innerHTML = `Function('if(window["${scriptid}"]?.ctx) { with(window["${scriptid}"].ctx) { ${text}; } window["${scriptid}"](); }')(); `;
533
+ currentScript.innerHTML = `Object.getPrototypeOf(async function(){}).constructor('if(window["${scriptid}"]?.ctx) { const ctx = window["${scriptid}"].ctx; { with(ctx) { ${text}; } } }')().then(() => window["${scriptid}"]()); `;
496
534
  let resolver;
497
- promises.push(new Promise((resolve) => resolver = resolve));
498
535
  window[scriptid] = () => {
499
536
  delete window[scriptid];
500
537
  currentScript.remove();
501
538
  resolver();
502
539
  }
503
540
  window[scriptid].ctx = ctx.varsProxy;
504
- ctx.appendChild(currentScript);
541
+ promises.push(new Promise((resolve) => {
542
+ resolver = resolve;
543
+ // wait for all regular scripts to load first
544
+ Promise.all(scriptpromises).then(() => shadow.appendChild(currentScript));
545
+ }));
505
546
  }
506
547
  Promise.all(promises).then(() => {
507
548
  const nodes = getNodes(ctx);
508
549
  nodes.forEach((node) => {
509
550
  if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
510
- render(!!node.template, () => resolveNodeOrText(node, this))
551
+ observe(() => resolveNodeOrText(node, this));
511
552
  } else if (node.nodeType === Node.ELEMENT_NODE) {
512
553
  // resolve the value before all else;
513
- const attr = node.attributes.value;
514
- if (attr && attr.template) {
515
- render(!!attr.template, () => {
516
- const value = resolveNodeOrText(attr, this),
554
+ const attr = node.attributes.value,
555
+ template = attr?.template;
556
+ if (attr && template) {
557
+ let value = resolveNodeOrText(attr, this),
517
558
  eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
518
- if(node.attributes.value) {
519
- const template = attr.template;
520
- if(/\$\{[a-zA-z_]+\}/g.test(template)) {
521
- const name = template.substring(2,template.length-1);
522
- bindInput(node,name,this,value);
523
- }
524
- }
525
- if (eltype === "checkbox") {
526
- if (coerce(value, "boolean") === true) {
527
- node.setAttribute("checked", "");
528
- node.checked = true;
529
- } else {
530
- node.removeAttribute("checked");
531
- node.checked = false;
532
- }
533
- const vname = resolveNodeOrText(node.attributes.name, ctx);
534
- if (vname) ctx.setValue(vname, node.checked, {coerceTo: "boolean"});
535
- }
536
- if (node.tagName === "SELECT") {
537
- let values = [value];
538
- if (node.hasAttribute("multiple")) values = coerce(value, Array);
539
- [...node.querySelectorAll("option")].forEach((option) => {
540
- if (option.hasAttribute("value")) {
541
- if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
542
- option.setAttribute("selected", "");
543
- option.selected = true;
544
- }
545
- } else if (values.includes(resolveNodeOrText(option.innerText,ctx))) {
546
- option.setAttribute("selected", "");
547
- option.selected = true;
559
+ const template = attr.template;
560
+ if (template) {
561
+ if (/\$\{[a-zA-z_]+\}/g.test(template)) {
562
+ const name = template.substring(2, template.length - 1);
563
+ if(!this.vars[name] || this.vars[name].reactive) {
564
+ bindInput(node, name, this, value);
548
565
  }
549
- })
550
- }
551
- });
552
- }
553
- [...node.attributes].forEach((attr) => {
554
- if (attr.name === "value" && attr.template) return;
555
- const {name, value} = attr;
556
- if (name === "type") {
557
- if (value === "radio") {
558
- const name = resolveNodeOrText(node.attributes.name, ctx);
559
- for (const vname of this.getVariableNames()) {
560
- if (vname === name) {
561
- render(true, () => {
562
- const name = resolveNodeOrText(node.attributes.name, ctx),
563
- varvalue = Function("context", "with(context) { return `${" + name + "}` }")(ctx.varsProxy);
564
- if (varvalue == resolveNodeOrText(node.attributes.value, ctx)) {
566
+ }
567
+ observe(() => {
568
+ const value = resolveNodeOrText(template, ctx);
569
+ if(value!==undefined) {
570
+ if (eltype === "checkbox") {
571
+ if (coerce(value, "boolean") === true) {
565
572
  node.setAttribute("checked", "");
566
573
  node.checked = true;
567
574
  } else {
568
575
  node.removeAttribute("checked");
569
576
  node.checked = false;
570
577
  }
571
- });
572
- bindInput(node, name, ctx);
573
- break;
578
+ } else if (node.tagName === "SELECT") {
579
+ let values = [value];
580
+ if (node.hasAttribute("multiple")) values = coerce(value, Array);
581
+ [...node.querySelectorAll("option")].forEach(async (option) => {
582
+ if (option.hasAttribute("value")) {
583
+ if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
584
+ option.setAttribute("selected", "");
585
+ option.selected = true;
586
+ }
587
+ } else if (values.includes(resolveNodeOrText(option.innerText, ctx))) {
588
+ option.setAttribute("selected", "");
589
+ option.selected = true;
590
+ }
591
+ })
592
+ } else if (eltype!=="radio") {
593
+ attr.value = value;
594
+ }
574
595
  }
575
- }
596
+ });
576
597
  }
598
+ }
599
+ [...node.attributes].forEach(async (attr) => {
600
+ if (attr.name === "value" && attr.template) return;
601
+ const {name, value} = attr,
602
+ vname = node.attributes.name?.value;
603
+ if (name === "type" && value=="radio" && vname) {
604
+ bindInput(node, vname, this);
605
+ observe(() => {
606
+ const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
607
+ if (node.attributes.value.value == varvalue) {
608
+ node.setAttribute("checked", "");
609
+ node.checked = true;
610
+ } else {
611
+ node.removeAttribute("checked");
612
+ node.checked = false;
613
+ }
614
+ });
577
615
  }
578
616
 
579
617
  const [type, ...params] = name.split(":");
580
618
  if (type === "") { // name is :something
581
- render(!!attr.template, () => {
582
- const value = attr.value,
583
- elvalue = resolveNodeOrText(node.attributes.value, ctx),
584
- eltype = resolveNodeOrText(node.attributes.type, ctx),
585
- elname = resolveNodeOrText(node.attributes.name, ctx);
619
+ observe(() => {
620
+ const value = attr.value;
586
621
  if (params[0]) {
587
622
  if (value === "true") node.setAttribute(params[0], "")
588
623
  else node.removeAttribute(params[0]);
589
- } else if (eltype === "checkbox" || node.tagName === "OPTION") {
590
- if (value === "true") node.setAttribute("checked", "")
591
- else node.removeAttribute("checked");
624
+ } else {
625
+ const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx) : null,
626
+ eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
627
+ if (eltype === "checkbox" || node.tagName === "OPTION") {
628
+ if (elvalue === "true") node.setAttribute("checked", "")
629
+ else node.removeAttribute("checked");
630
+ }
592
631
  }
593
632
  })
594
633
  } else if (type === "l-on") {
595
634
  let listener;
596
- render(!!attr.template, () => {
635
+ observe(() => {
597
636
  const value = resolveNodeOrText(attr, this);
598
637
  if (listener) node.removeEventListener(params[0], listener);
599
638
  listener = this[value] || window[value] || Function(value);
600
639
  addListener(node, params[0], listener);
601
640
  })
602
641
  } else if (type === "l-if") {
603
- render(!!attr.template, () => {
642
+ observe(() => {
604
643
  const value = resolveNodeOrText(attr, this);
605
644
  node.style.setProperty("display", value === "true" ? "revert" : "none");
606
645
  })
607
646
  } else if (type === "l-for") {
608
647
  node.template ||= node.innerHTML;
609
- render(!!attr.template, () => {
648
+ observe(() => {
610
649
  const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
611
650
  value = resolveNodeOrText(attr, this),
612
651
  coerced = coerce(value, what === "each" ? Array : "object"),
613
652
  target = what === "each" ? coerced : Object[what](coerced),
614
- html = target.reduce((html, item, i, target) => {
615
- return html += Function("vars","context", "with(vars) { with(context) { return `" + node.template + "` }}")(
653
+ html = target.reduce( (html, item, i, target) => {
654
+ return html += Function("vars", "context", "with(vars) { with(context) { return `" + node.template + "` }}")(
616
655
  ctx.varsProxy,
617
656
  {
618
657
  [vname]: item,
@@ -633,61 +672,53 @@ const {observe} = (() => {
633
672
  else node.appendChild(parsed.body.firstChild);
634
673
  }
635
674
  })
636
- } else if (attr.template) {
637
- render(!!attr.template, () => resolveNodeOrText(attr, this));
675
+ } else if(attr.template) {
676
+ observe(() => {
677
+ resolveNodeOrText(attr, this);
678
+ })
638
679
  }
639
680
  })
640
681
  }
641
682
  })
642
683
  shadow.normalize();
643
- observer.observe(ctx, {attributeOldValue: true});
644
- if (ctx.hasOwnProperty("connectedCallback")) ctx.connectedCallback();
684
+ observer ||= createObserver(ctx, framed);
685
+ observer.observe(ctx, {attributeOldValue: true, subtree:true, characterData:true, characterDataOldValue:true});
686
+ ctx.vars.postEvent.value("connected");
645
687
  })
646
688
  }
647
-
648
- adopted(value) {
649
- this.adoptedCallback = value;
650
- //Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
689
+ adoptedCallback(callback) {
690
+ this.vars.postEvent.value("adopted");
651
691
  }
652
-
653
- connected(callback) {
654
- this.connectedCallback = callback;
655
- //Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
692
+ disconnectedCallback() {
693
+ this.vars.postEvent.value("disconnected");
656
694
  }
657
-
658
- attributeChanged(callback) {
659
- this.attributeChangedCallback = callback;
660
- //Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
695
+ get observedAttributes() {
696
+ return CustomElement.observedAttributes;
661
697
  }
662
-
663
- disconnected(callback) {
664
- Object.defineProperty(this, "disconnectedCallback", {
665
- configurable: true,
666
- writable: true,
667
- value: () => {
668
- value();
669
- super.disconnectedCallback(callback);
670
- }
671
- });
698
+ static get observedAttributes() {
699
+ return observedAttributes;
672
700
  }
673
701
 
674
702
  getVariableNames() {
675
- return Object.keys(this.vars).filter((name) => {
676
- return !(name in reserved) && !["self", "addEventListener", "postEvent"].includes(name)
677
- })
703
+ return Object.keys(this.vars)
704
+ .filter(name => !(name in reserved) && !["self", "addEventListener", "postEvent"].includes(name))
705
+ }
706
+
707
+ getVariable(name) {
708
+ return this.vars[name] ? {...this.vars[name]} : undefined;
678
709
  }
679
710
 
680
- setValue(variableName, value, {coerceTo = typeof (value)} = {}) {
711
+ setVariableValue(variableName, value, {coerceTo = typeof (value)} = {}) {
681
712
  if (!this.isConnected) {
682
713
  instances.delete(this);
683
714
  return false;
684
715
  }
685
716
  let {type} = this.vars[variableName] || {};
686
717
  if (type) {
687
- value = coerce(value, type);
688
718
  if (this.varsProxy[variableName] !== value) {
689
719
  const variable = this.vars[variableName];
690
720
  if (variable.shared) {
721
+ value = type.validate ? type.validate(value,variable) : coerce(value,coerceTo);
691
722
  const event = new VariableEvent({
692
723
  variableName: variableName,
693
724
  value: value,
@@ -706,20 +737,29 @@ const {observe} = (() => {
706
737
  return false;
707
738
  }
708
739
 
709
- getValue(variableName) {
740
+ getVariableValue(variableName) {
710
741
  return this.vars[variableName]?.value;
711
742
  }
712
743
 
713
- variables(variables, {observed, reactive, shared, exported, imported} = {}) { // options = {observed,reactive,shared,exported,imported}
744
+ variables(variables, {observed, reactive, shared, exported, imported, remote, constant,set} = {}) { // options = {observed,reactive,shared,exported,imported}
714
745
  const addEventListener = this.varsProxy.addEventListener;
715
746
  if (variables !== undefined) {
716
747
  Object.entries(variables)
717
748
  .forEach(([key, type]) => {
718
749
  const variable = this.vars[key] ||= {name: key, type};
750
+ if(set!==undefined && constant!==undefined) throw new TypeError(`${key} has the constant value ${constant} and can't be set to ${set}`);
751
+ variable.value = set;
752
+ if(constant!==undefined) {
753
+ variable.constant = true;
754
+ variable.value = constant;
755
+ }
719
756
  if (observed || imported) {
720
757
  variable.value = this.hasAttribute(key) ? coerce(this.getAttribute(key), variable.type) : variable.value;
721
- variable.observed = observed;
722
758
  variable.imported = imported;
759
+ if(variable.observed) {
760
+ variable.observed = observed;
761
+ this.observedAttributes.add(key);
762
+ }
723
763
  }
724
764
  if (reactive) {
725
765
  variable.reactive = true;
@@ -728,21 +768,21 @@ const {observe} = (() => {
728
768
  if (shared) {
729
769
  variable.shared = true;
730
770
  addEventListener("change", ({variableName, value}) => {
731
- if (this.vars[variableName]?.shared) {
732
- this.siblings.forEach((instance) => instance.setValue(variableName, value))
733
- }
771
+ if (this.vars[variableName]?.shared) this.siblings.forEach((instance) => instance.setVariableValue(variableName, value))
734
772
  })
735
773
  }
736
774
  if (exported) {
737
775
  variable.exported = true;
738
776
  // in case the export goes up to an iframe
739
777
  if (variable.value != null) setComponentAttribute(this, key, variable.value);
740
- addEventListener("change", ({variableName, value}) => {
741
- value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
742
- if (value == null) removeComponentAttribute(this, variableName);
743
- else setComponentAttribute(this, variableName, value);
744
- })
778
+ this.changeListener.targets.add(key);
779
+ }
780
+ if (remote) {
781
+ if(typeof(remote)==="function") remote = remote(`./${key}`);
782
+ variable.remote = remote;
783
+ remote.handleRemote({variable, config:remote.config, reactive,component:this});
745
784
  }
785
+ if(type.validate) type.validate(variable.value,variable);
746
786
  });
747
787
  }
748
788
  return Object.entries(this.vars)
@@ -751,44 +791,23 @@ const {observe} = (() => {
751
791
  return result;
752
792
  }, {});
753
793
  }
754
-
755
- constants(variables) {
756
- if (variables !== undefined) {
757
- Object.entries(variables)
758
- .forEach(([key, value]) => {
759
- const type = typeof (value) === "function" ? value : typeof (value),
760
- variable = this.vars[key];
761
- if (variable !== undefined) throw new TypeError(`${variable.constant ? "const" : "let"} ${key}:${variable.type} already declared.`);
762
- if (value === undefined) throw new TypeError(`const ${key}:undefined must be initialized.`);
763
- this.vars[key] = {type, value, constant: true};
764
- })
765
- }
766
- return Object.entries(this.vars)
767
- .reduce((result, [key, variable]) => {
768
- if (variable.constant) result[key] = {...variable};
769
- return result;
770
- }, {});
771
- }
772
794
  }
773
795
  }
774
- const createComponent = (name, node, {framed,observer} = {}) => {
796
+
797
+ const createComponent = (name, node, {framed, observer} = {}) => {
775
798
  let ctor = customElements.get(name);
776
799
  if (ctor) {
777
- if (framed && !ctor.lightviewFramed) {
778
- ctor.lightviewFramed = true;
779
- } else {
780
- console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
781
- }
800
+ if (framed && !ctor.lightviewFramed) ctor.lightviewFramed = true;
801
+ else console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
782
802
  return ctor;
783
803
  }
784
804
  ctor = createClass(node, {observer, framed});
785
805
  customElements.define(name, ctor);
786
- Lightview.customElements.set(name,ctor);
806
+ Lightview.customElements.set(name, ctor);
787
807
  return ctor;
788
808
  }
789
809
  Lightview.customElements = new Map();
790
810
  Lightview.createComponent = createComponent;
791
- //Object.defineProperty(Lightview, "createComponent", {writable: true, configurable: true, value: createComponent})
792
811
  const importLink = async (link, observer) => {
793
812
  const url = (new URL(link.getAttribute("href"), window.location.href)),
794
813
  as = link.getAttribute("as") || getNameFromPath(url.pathname);
@@ -800,12 +819,12 @@ const {observe} = (() => {
800
819
  dom = parser.parseFromString(html, "text/html"),
801
820
  unhide = !!dom.head.querySelector('meta[name="l-unhide"]'),
802
821
  links = dom.head.querySelectorAll('link[href$=".html"][rel=module]');
803
- for(const childlink of links) {
822
+ for (const childlink of links) {
804
823
  const href = childlink.getAttribute("href"),
805
- childurl = new URL(href,url.href);
806
- childlink.setAttribute("href",childurl.href);
807
- if(link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin",link.getAttribute("crossorigin"))
808
- await importLink(childlink,observer);
824
+ childurl = new URL(href, url.href);
825
+ childlink.setAttribute("href", childurl.href);
826
+ if (link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin", link.getAttribute("crossorigin"))
827
+ await importLink(childlink, observer);
809
828
  }
810
829
  if (unhide) dom.body.removeAttribute("hidden");
811
830
  createComponent(as, dom.body, {observer});
@@ -815,7 +834,7 @@ const {observe} = (() => {
815
834
  const importLinks = async () => {
816
835
  const observer = createObserver(document.body);
817
836
  for (const link of [...document.querySelectorAll(`link[href$=".html"][rel=module]`)]) {
818
- await importLink(link,observer);
837
+ await importLink(link, observer);
819
838
  }
820
839
  }
821
840
 
@@ -883,21 +902,19 @@ const {observe} = (() => {
883
902
  postMessage({type: "setAttribute", argsList: ["height", height + 20]});
884
903
  }
885
904
  resize();
886
- onresize(document.body, () => {
887
- resize();
888
- });
905
+ onresize(document.body, () => resize());
889
906
  return
890
907
  }
891
908
  if (type === "setAttribute") {
892
909
  const [name, value] = [...argsList],
893
910
  variable = document.body.vars[name];
894
- if (variable && variable.imported) document.body.setValue(name, value);
911
+ if (variable && variable.imported) document.body.setVariableValue(name, value);
895
912
  return;
896
913
  }
897
914
  if (type === "removeAttribute") {
898
915
  const [name] = argsList[0],
899
916
  variable = document.body.vars[name];
900
- if (variable && variable.imported) document.body.setValue(name, undefined);
917
+ if (variable && variable.imported) document.body.setVariableValue(name, undefined);
901
918
 
902
919
  }
903
920
  });
@@ -935,9 +952,7 @@ const {observe} = (() => {
935
952
  }
936
953
  if (type === "setAttribute") {
937
954
  const [name, value] = [...argsList];
938
- if (iframe.getAttribute(name) !== value + "") {
939
- iframe.setAttribute(name, value);
940
- }
955
+ if (iframe.getAttribute(name) !== value + "") iframe.setAttribute(name, value);
941
956
  return;
942
957
  }
943
958
  if (type === "removeAttribute") {
@@ -985,7 +1000,7 @@ const {observe} = (() => {
985
1000
  addListener(document, "DOMContentLoaded", (event) => loader(callback));
986
1001
  }
987
1002
  Lightview.whenFramed = whenFramed;
988
- //Object.defineProperty(Lightview, "whenFramed", {configurable: true, writable: true, value: whenFramed});
1003
+
989
1004
  if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) {
990
1005
  // loads for unframed content
991
1006
  // CodePen mucks with window.parent