lightview 1.6.5-b → 1.7.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
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  MIT License
3
3
 
4
- Copyright (c) 2020 Simon Y. Blackwell - Lightview Small, simple, powerful UI creation ...
4
+ Copyright (c) 2022 AnyWhichWay, LLC - Lightview Small, simple, powerful UI creation ...
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
@@ -23,6 +23,10 @@ SOFTWARE.
23
23
  */
24
24
 
25
25
  // <script src="https://000686818.codepen.website/lightview.js?as=x-body"></script>
26
+ /*
27
+ self.variables({name:"string"})
28
+ imported(x) => exported(x) => reactive(x) => remote(x,{path:".
29
+ */
26
30
 
27
31
  const Lightview = {};
28
32
 
@@ -40,17 +44,33 @@ const {observe} = (() => {
40
44
  Lightview.sanitizeTemplate = templateSanitizer;
41
45
 
42
46
  const escaper = document.createElement('textarea');
43
-
44
47
  function escapeHTML(html) {
45
48
  escaper.textContent = html;
46
49
  return escaper.innerHTML;
47
50
  }
48
-
49
51
  Lightview.escapeHTML = escapeHTML;
50
52
 
51
- const addListener = (node, eventName, callback) => {
52
- node.addEventListener(eventName, callback); // just used to make code footprint smaller
53
+ const isArrowFunction = (f) => typeof(f)==="function" && (f+"").match(/\(*.*\)*\s*=>/g);
54
+
55
+ const getTemplateVariableName = (template) => {
56
+ if(template && /^\$\{[a-zA-z_.]*\}$/g.test(template)) return template.substring(2, template.length - 1);
57
+ }
58
+
59
+ const walk = (target,path,depth=path.length-1,create) => {
60
+ for(let i=0;i<=depth;i++) {
61
+ target = (target[path[i]]==null && create ? target[path[i]] = (typeof(create)==="function" ? Object.create(create.prototype) : {}) : target[path[i]]);
62
+ if(target===undefined) return;
63
+ }
64
+ return target;
53
65
  }
66
+
67
+ const addListener = (node, eventName, callback, self) => {
68
+ node.addEventListener(eventName, (event) => {
69
+ if(self) event.self = self;
70
+ callback(event);
71
+ });
72
+ }
73
+
54
74
  const anchorHandler = async (event) => {
55
75
  event.preventDefault();
56
76
  const target = event.target;
@@ -103,9 +123,7 @@ const {observe} = (() => {
103
123
  if (toType === "string") return value + "";
104
124
  const isfunction = typeof (toType) === "function";
105
125
  if ((toType === "object" || isfunction)) {
106
- if (type === "object" && isfunction) {
107
- if (value instanceof toType) return value;
108
- }
126
+ if (type === "object" && isfunction && value instanceof toType) return value;
109
127
  if (type === "string") {
110
128
  value = value.trim();
111
129
  try {
@@ -157,16 +175,10 @@ const {observe} = (() => {
157
175
  if(property=== "__dependents__") return dependents;
158
176
  if(property=== "__reactorProxyTarget__") return data;
159
177
  if (target instanceof Array) {
160
- if (property === "toJSON") return function toJSON() {
161
- return [...target];
162
- }
163
- if (property === "toString") return function toString() {
164
- return JSON.stringify([...target]);
165
- }
166
- }
167
- if(target instanceof Date) {
168
- return Reflect.get(target,property);
178
+ if (property === "toJSON") return function toJSON() { return [...target]; }
179
+ if (property === "toString") return function toString() { return JSON.stringify([...target]); }
169
180
  }
181
+ if(target instanceof Date) return Reflect.get(target,property);
170
182
  let value = target[property];
171
183
  const type = typeof (value);
172
184
  if (CURRENTOBSERVER && typeof (property) !== "symbol" && type !== "function") {
@@ -191,9 +203,7 @@ const {observe} = (() => {
191
203
  console.warn(`Setting ${property} = ${value} on a Promise in Reactor`);
192
204
  }
193
205
  const type = typeof (value);
194
- if(value && type==="object" && value instanceof Promise) {
195
- value = await value;
196
- }
206
+ if(value && type==="object" && value instanceof Promise) value = await value;
197
207
  if (target[property] !== value) {
198
208
  if (value && type === "object") {
199
209
  value = Reactor(value);
@@ -223,10 +233,9 @@ const {observe} = (() => {
223
233
  const createVarsProxy = (vars, component, constructor) => {
224
234
  return new Proxy(vars, {
225
235
  get(target, property) {
226
- if(target instanceof Date) {
227
- return Reflect.get(target,property);
228
- }
229
- let {value} = target[property] || {};
236
+ if(target instanceof Date) return Reflect.get(target,property);
237
+ let {value,get} = target[property] || {};
238
+ if(get) return target[property].value = get.call(target[property]);
230
239
  if (typeof (value) === "function") return value.bind(target);
231
240
  return value;
232
241
  },
@@ -239,7 +248,8 @@ const {observe} = (() => {
239
248
  return true;
240
249
  }
241
250
  const variable = target[property],
242
- {type, value, shared, exported, constant, reactive, remote} = variable;
251
+ {value, shared, exported, constant, reactive, remote} = variable;
252
+ let type = variable.type;
243
253
  if (constant) throw new TypeError(`${property}:${type} is a constant`);
244
254
  if(newValue!=null || type.required) newValue = type.validate ? type.validate(newValue,target[property]) : coerce(newValue,type);
245
255
  const newtype = typeof (newValue),
@@ -252,11 +262,9 @@ const {observe} = (() => {
252
262
  event.oldValue = value;
253
263
  target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
254
264
  target.postEvent.value("change", event);
255
- if (event.defaultPrevented) {
256
- target[property].value = value;
257
- } else if(remote && remote.put) {
258
- remote.handleRemote({variable,config:remote.config,reactive},true);
259
- }
265
+ if (event.defaultPrevented) target[property].value = value;
266
+ else if(remote && (variable.reactive || remote.put)) remote.handleRemote({variable,config:remote.config},true);
267
+ else if(variable.set) variable.set(newValue);
260
268
  }
261
269
  return true;
262
270
  }
@@ -283,10 +291,8 @@ const {observe} = (() => {
283
291
  target.removeAttribute(name);
284
292
  target.dispatchEvent(new CustomEvent("message", {detail: JSON.parse(value)}))
285
293
  }
286
- if (target.observedAttributes && target.observedAttributes.includes(name)) {
287
- if (value !== mutation.oldValue) {
288
- target.setVariableValue(name, value);
289
- }
294
+ if (target.observedAttributes && target.observedAttributes.includes(name) && value !== mutation.oldValue) {
295
+ target.setVariableValue(name, value);
290
296
  }
291
297
  } else if (mutation.type === "childList") {
292
298
  for (const target of mutation.removedNodes) {
@@ -313,45 +319,54 @@ const {observe} = (() => {
313
319
  return nodes;
314
320
  }
315
321
  const getNodes = (root) => {
316
- const nodes = [];
322
+ const nodes = new Set();
317
323
  if (root.shadowRoot) {
318
- nodes.push(root, ...getNodes(root.shadowRoot))
324
+ nodes.add(root);
325
+ getNodes(root.shadowRoot).forEach((node) => nodes.add(node))
319
326
  } else {
320
327
  for (const node of root.childNodes) {
321
328
  if (node.tagName === "SCRIPT") continue;
322
329
  if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.includes("${")) {
323
330
  node.template ||= node.nodeValue;
324
- nodes.push(node);
331
+ nodes.add(node);
325
332
  } else if (node.nodeType === Node.ELEMENT_NODE) {
326
- let skip, pushed;
333
+ let skip;
327
334
  [...node.attributes].forEach((attr) => {
328
335
  if (attr.value.includes("${")) {
329
336
  attr.template ||= attr.value;
330
- pushed = true;
331
- nodes.push(node);
337
+ nodes.add(node);
332
338
  } else if (attr.name.includes(":") || attr.name.startsWith("l-")) {
333
339
  skip = attr.name.includes("l-for:");
334
- pushed = true;
335
- nodes.push(node)
340
+ nodes.add(node)
336
341
  }
337
342
  })
338
- if (!pushed && node.getAttribute("type") === "radio") nodes.push(node);
339
- if (!skip && !node.shadowRoot) nodes.push(...getNodes(node));
343
+ if (node.getAttribute("type") === "radio") nodes.add(node);
344
+ if (!skip && !node.shadowRoot) getNodes(node).forEach((node) => nodes.add(node));
340
345
  }
341
346
  }
342
347
  }
343
348
  return nodes;
344
349
  }
345
350
 
346
- const resolveNodeOrText = (node, component, safe) => {
351
+ const resolveNodeOrText = (node, component, safe,extras=node.extras||{}) => {
347
352
  const type = typeof (node),
348
353
  template = type === "string" ? node.trim() : node.template;
349
354
  if (template) {
355
+ const name = getTemplateVariableName(template);
350
356
  try {
351
- let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
352
- value = node.nodeType === Node.TEXT_NODE || !safe ? value : Lightview.escapeHTML(value);
357
+ let value = (name
358
+ ? component[name] || walk(extras,name.split(".")) || walk(component.varsProxy,name.split("."))
359
+ : Function("context", "extras", "with(context) { with(extras) { return `" + (safe ? template : Lightview.sanitizeTemplate(template)) + "` } }")(component.varsProxy,extras));
360
+ //let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
361
+ if(typeof(value)==="function") return value;
362
+ value = (name || node.nodeType === Node.TEXT_NODE || safe ? value : Lightview.escapeHTML(value));
353
363
  if (type === "string") return value==="undefined" ? undefined : value;
354
- node.nodeValue = value == "null" || value == "undefined" ? "" : value;
364
+ if(name) {
365
+ node.nodeValue = value==null ? "" : typeof(value)==="string" ? value : JSON.stringify(value);
366
+ } else {
367
+ node.nodeValue = value == "null" || value == "undefined" ? "" : value;
368
+ }
369
+ return value;
355
370
  } catch (e) {
356
371
  //console.warn(e);
357
372
  if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
@@ -375,24 +390,36 @@ const {observe} = (() => {
375
390
  })
376
391
  }
377
392
  const bound = new WeakSet();
378
- const bindInput = (input, variableName, component, value) => {
393
+ const bindInput = (input, variableName, component, value, object) => {
379
394
  if (bound.has(input)) return;
380
395
  bound.add(input);
381
396
  const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
382
- type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype);
383
- value ||= input.getAttribute("value");
384
- let variable = component.vars[variableName] || {type};
385
- if (type !== variable.type) {
386
- if (variable.type === "any" || variable.type === "unknown") variable.type = type;
387
- else throw new TypeError(`Attempt to bind <input name="${variableName}" type="${type}"> to variable ${variableName}:${variable.type}`)
388
- }
389
- component.variables({[variableName]: type},{reactive:true});
390
- if(inputtype!=="radio") {
391
- if(value.includes("${")) input.attributes.value.value = "";
392
- else component.setVariableValue(variableName, coerce(value,type));
397
+ nameparts = variableName.split(".");
398
+ let type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype);
399
+ const variable = walk(component.vars,nameparts) || {type};
400
+ if(type==="any") type = variable.type;
401
+ if(value==null) value = input.getAttribute("value");
402
+ if(object && nameparts.length>1) {
403
+ const [root,...path] = nameparts;
404
+ object = walk(object,path,path.length-2,true);
405
+ const key = path[path.length-1];
406
+ object[key] = coerce(value,type);
407
+ } else {
408
+ if (type !== variable.type) {
409
+ if (variable.type === "any" || variable.type === "unknown") variable.type = type;
410
+ else throw new TypeError(`Attempt to bind <input name="${variableName}" type="${type}"> to variable ${variableName}:${variable.type}`)
411
+ }
412
+ const existing = component.vars[variableName];
413
+ if(!existing || existing.type!==type || !existing.reactive) component.variables({[variableName]: type},{reactive});
414
+ if(inputtype!=="radio") {
415
+ if(typeof(value)==="string" && value.includes("${")) input.attributes.value.value = "";
416
+ else component.setVariableValue(variableName, coerce(value,type));
417
+ }
393
418
  }
394
419
  let eventname = "change";
395
- if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
420
+ if(input.tagName==="FORM") {
421
+ eventname = "submit"
422
+ } else if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
396
423
  eventname = "input";
397
424
  }
398
425
  const listener = (event) => {
@@ -400,17 +427,21 @@ const {observe} = (() => {
400
427
  let value = input.value;
401
428
  if (inputtype === "checkbox") {
402
429
  value = input.checked
403
- } else if (input.tagName === "SELECT") {
404
- if (input.hasAttribute("multiple")) {
405
- const varvalue = component.varsProxy[variableName];
406
- value = [...input.querySelectorAll("option")]
407
- .filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value) //todo make sync comopat
408
- .map((option) => option.getAttribute("value") || option.innerText);
409
- }
430
+ } else if (input.tagName === "SELECT" && input.hasAttribute("multiple")) {
431
+ value = [...input.querySelectorAll("option")]
432
+ .filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value)
433
+ .map((option) => option.getAttribute("value") || option.innerText);
434
+ }
435
+ if(object) {
436
+ const [root,...path] = nameparts;
437
+ object = walk(object,nameparts,path.length-2,true);
438
+ } else {
439
+ object = walk(component.varsProxy,nameparts,nameparts.length-2,true);
410
440
  }
411
- component.varsProxy[variableName] = coerce(value, type);
441
+ const key = nameparts[nameparts.length-1];
442
+ object[key] = coerce(value,type);
412
443
  };
413
- addListener(input, eventname, listener);
444
+ addListener(input, eventname, listener,component);
414
445
  }
415
446
  const tryParse = (value) => {
416
447
  try {
@@ -419,14 +450,89 @@ const {observe} = (() => {
419
450
  return value;
420
451
  }
421
452
  }
453
+ const observed = () => {
454
+ return {
455
+ init({variable, component}) {
456
+ const name = variable.name;
457
+ variable.value = component.hasAttribute(name) ? coerce(component.getAttribute(name), variable.type) : variable.value;
458
+ variable.observed = true;
459
+ component.observedAttributes.add(variable.name);
460
+ }
461
+ }
462
+ }
463
+ const reactive = () => {
464
+ return {
465
+ init({variable, component}) {
466
+ variable.reactive = true;
467
+ component.vars[variable.name] = Reactor(variable);
468
+ }
469
+ }
470
+ }
471
+ const shared = () => {
472
+ return {
473
+ init({variable, component}) {
474
+ variable.shared = true;
475
+ /*addEventListener("change", ({variableName, value}) => {
476
+ if (variableName===variable.name && component.vars[variableName]?.shared) component.siblings.forEach((instance) => instance.setVariableValue(variableName, value))
477
+ })*/
478
+ variable.set = function(newValue) {
479
+ if(component.vars[this.name]?.shared) component.siblings.forEach((instance) => instance.setVariableValue(this.name, newValue));
480
+ }
481
+ }
482
+ }
483
+ }
484
+ const exported = () => {
485
+ return {
486
+ init({variable, component}) {
487
+ const name = variable.name;
488
+ variable.exported = true;
489
+ variable.set = (newValue) => {
490
+ if(variable.exported) {
491
+ if(newValue==null) {
492
+ removeComponentAttribute(component, name);
493
+ } else {
494
+ newValue = typeof (newValue) === "string" ? newValue : JSON.stringify(newValue);
495
+ setComponentAttribute(component, name, newValue);
496
+ }
497
+ }
498
+ }
499
+ variable.set(variable.value);
500
+ }
501
+ }
502
+ }
503
+ const imported = () => {
504
+ return {
505
+ init({variable, component}) {
506
+ const name = variable.name;
507
+ variable.imported = true;
508
+ variable.value = component.hasAttribute(name) ? coerce(component.getAttribute(name), variable.type) : variable.value;
509
+ }
510
+ }
511
+ }
512
+
422
513
  let reserved = {
423
- observed: {value: true, constant: true},
424
- reactive: {value: true, constant: true},
425
- shared: {value: true, constant: true},
426
- exported: {value: true, constant: true},
427
- imported: {value: true, constant: true}
514
+ observed: {
515
+ constant: true,
516
+ value: observed
517
+ },
518
+ reactive: {
519
+ constant: true,
520
+ value: reactive
521
+ },
522
+ shared: {
523
+ constant: true,
524
+ value: shared
525
+ },
526
+ exported: {
527
+ constant: true,
528
+ value: exported
529
+ },
530
+ imported: {
531
+ constant: true,
532
+ value: imported
533
+ }
428
534
  };
429
- const createClass = (domElementNode, {observer, framed}) => {
535
+ const createClass = (domElementNode, {observer, framed, href}) => {
430
536
  const instances = new Set(),
431
537
  dom = domElementNode.tagName === "TEMPLATE"
432
538
  ? domElementNode.content.cloneNode(true)
@@ -438,24 +544,19 @@ const {observe} = (() => {
438
544
  static get instances() {
439
545
  return instances;
440
546
  }
441
-
442
547
  constructor() {
443
548
  super();
444
549
  instances.add(this);
445
550
  const currentComponent = this,
446
551
  shadow = this.attachShadow({mode: "open"}),
447
552
  eventlisteners = {};
448
- // needs to be local to the instance
449
- Object.defineProperty(this,"changeListener",{value:
450
- function({variableName, value}) {
451
- if (currentComponent.changeListener.targets.has(variableName)) {
452
- value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
453
- if (value == null) removeComponentAttribute(currentComponent, variableName);
454
- else setComponentAttribute(currentComponent, variableName, value);
455
- }
456
- }});
457
553
  this.vars = {
458
554
  ...reserved,
555
+ observe: {
556
+ value: (...args) => observe(...args),
557
+ type: "function",
558
+ constant: true
559
+ },
459
560
  addEventListener: {
460
561
  value: (eventName, listener) => {
461
562
  const listeners = eventlisteners[eventName] ||= new Set();
@@ -481,9 +582,7 @@ const {observe} = (() => {
481
582
  };
482
583
  this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
483
584
  this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
484
- this.changeListener.targets = new Set();
485
- this.varsProxy.addEventListener("change", this.changeListener);
486
- if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported: true});
585
+ if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported});
487
586
  ["getElementById", "querySelector", "querySelectorAll"]
488
587
  .forEach((fname) => {
489
588
  Object.defineProperty(this, fname, {
@@ -514,15 +613,22 @@ const {observe} = (() => {
514
613
  for (const attr of this.defaultAttributes) this.hasAttribute(attr.name) || this.setAttribute(attr.name, attr.value);
515
614
  const scripts = shadow.querySelectorAll("script"),
516
615
  promises = [];
517
- // scriptpromises = [];
518
616
  for (const script of [...scripts]) {
519
617
  if (script.attributes.src?.value?.includes("/lightview.js")) continue;
618
+ const text = script.innerHTML
619
+ .replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1") // remove comments;
620
+ .replaceAll(/\r?\n/g, "") // remove \n
621
+ .replaceAll(/import\s*\((\s*["'][\.\/].*["'])\)/g,`import(new URL($1,"${href ? href : window.location.href}").href)`) // handle relative paths
622
+ .replaceAll(/'(([^'\\]|\\.)*)'/g,"\\'$1\\'"); // handle quotes
520
623
  const currentScript = document.createElement("script");
521
624
  if (script.className !== "lightview" && !((script.attributes.type?.value || "").includes("lightview/"))) {
522
625
  for (const attr of script.attributes) currentScript.setAttribute(attr.name,attr.value);
626
+ currentScript.innerHTML = text;
523
627
  shadow.appendChild(currentScript);
524
628
  await new Promise((resolve) => {
629
+ const timeout = setTimeout(() => resolve(),500);
525
630
  currentScript.onload = () => {
631
+ clearTimeout(timeout);
526
632
  currentScript.remove();
527
633
  resolve();
528
634
  }
@@ -534,12 +640,12 @@ const {observe} = (() => {
534
640
  currentScript.setAttribute(attr.name, attr.name === "type" ? attr.value.replace("lightview/", "") : attr.value);
535
641
  }
536
642
  currentScript.classList.remove("lightview");
537
- const text = script.innerHTML.replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1").replaceAll(/\r?\n/g, "");
538
643
  currentScript.innerHTML = `Object.getPrototypeOf(async function(){}).constructor('if(window["${scriptid}"]?.ctx) { const ctx = window["${scriptid}"].ctx; { with(ctx) { ${text}; } } }')().then(() => window["${scriptid}"]()); `;
539
644
  await new Promise((resolve) => {
540
645
  window[scriptid] = () => {
541
646
  delete window[scriptid];
542
647
  currentScript.remove();
648
+ script.remove();
543
649
  resolve();
544
650
  }
545
651
  window[scriptid].ctx = ctx.varsProxy;
@@ -547,155 +653,196 @@ const {observe} = (() => {
547
653
  })
548
654
  }
549
655
  // Promise.all(promises).then(() => {
550
- const nodes = getNodes(ctx);
551
- nodes.forEach((node) => {
552
- if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
553
- observe(() => resolveNodeOrText(node, this));
554
- } else if (node.nodeType === Node.ELEMENT_NODE) {
555
- // resolve the value before all else;
556
- const attr = node.attributes.value,
557
- template = attr?.template;
558
- if (attr && template) {
559
- let value = resolveNodeOrText(attr, this),
560
- eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
561
- const template = attr.template;
562
- if (template) {
563
- if (/\$\{[a-zA-z_]+\}/g.test(template)) {
564
- const name = template.substring(2, template.length - 1);
565
- if(!this.vars[name] || this.vars[name].reactive) {
566
- bindInput(node, name, this, value);
656
+ const nodes = getNodes(ctx),
657
+ processNodes = (nodes,object) => {
658
+ nodes.forEach((node) => {
659
+ if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
660
+ observe(() => resolveNodeOrText(node, this,true,node.extras));
661
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
662
+ // resolve the value before all else;
663
+ const attr = node.attributes.value,
664
+ template = attr?.template;
665
+ if (attr && template) {
666
+ //let value = resolveNodeOrText(attr, this),
667
+ // ;
668
+ const eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx, false,node.extras) : null,
669
+ template = attr.template;
670
+ if (template) {
671
+ const name = getTemplateVariableName(template);
672
+ if (name) {
673
+ const nameparts = name.split(".");
674
+ if(node.extras && node.extras[nameparts[0]]) {
675
+ object = node.extras[nameparts[0]];
676
+ }
677
+ if(!this.vars[nameparts[0]] || this.vars[nameparts[0]].reactive || object) {
678
+ bindInput(node, name, this, resolveNodeOrText(attr, this,false,node.extras), object);
679
+ }
680
+ }
681
+ observe(() => {
682
+ const value = resolveNodeOrText(template, ctx,false,node.extras);
683
+ if(value!==undefined) {
684
+ if (eltype === "checkbox") {
685
+ if (coerce(value, "boolean") === true) {
686
+ node.setAttribute("checked", "");
687
+ node.checked = true;
688
+ } else {
689
+ node.removeAttribute("checked");
690
+ node.checked = false;
691
+ }
692
+ } else if (node.tagName === "SELECT") {
693
+ let values = [value];
694
+ if (node.hasAttribute("multiple")) values = coerce(value, Array);
695
+ [...node.querySelectorAll("option")].forEach(async (option) => {
696
+ if (option.hasAttribute("value")) {
697
+ if (values.includes(resolveNodeOrText(option.attributes.value, ctx,false,node.extras))) {
698
+ option.setAttribute("selected", "");
699
+ option.selected = true;
700
+ }
701
+ } else if (values.includes(resolveNodeOrText(option.innerText, ctx,false,node.extras))) {
702
+ option.setAttribute("selected", "");
703
+ option.selected = true;
704
+ }
705
+ })
706
+ } else if (eltype!=="radio") {
707
+ attr.value = value;
708
+ }
709
+ }
710
+ });
567
711
  }
568
712
  }
569
- observe(() => {
570
- const value = resolveNodeOrText(template, ctx);
571
- if(value!==undefined) {
572
- if (eltype === "checkbox") {
573
- if (coerce(value, "boolean") === true) {
713
+ [...node.attributes].forEach(async (attr) => {
714
+ if (attr.name === "value" && attr.template) return;
715
+ const {name, value} = attr,
716
+ vname = node.attributes.name?.value;
717
+ if (name === "type" && value=="radio" && vname) {
718
+ bindInput(node, vname, this, undefined, object);
719
+ observe(() => {
720
+ const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
721
+ if (node.attributes.value.value == varvalue) {
574
722
  node.setAttribute("checked", "");
575
723
  node.checked = true;
576
724
  } else {
577
725
  node.removeAttribute("checked");
578
726
  node.checked = false;
579
727
  }
580
- } else if (node.tagName === "SELECT") {
581
- let values = [value];
582
- if (node.hasAttribute("multiple")) values = coerce(value, Array);
583
- [...node.querySelectorAll("option")].forEach(async (option) => {
584
- if (option.hasAttribute("value")) {
585
- if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
586
- option.setAttribute("selected", "");
587
- option.selected = true;
588
- }
589
- } else if (values.includes(resolveNodeOrText(option.innerText, ctx))) {
590
- option.setAttribute("selected", "");
591
- option.selected = true;
592
- }
593
- })
594
- } else if (eltype!=="radio") {
595
- attr.value = value;
596
- }
597
- }
598
- });
599
- }
600
- }
601
- [...node.attributes].forEach(async (attr) => {
602
- if (attr.name === "value" && attr.template) return;
603
- const {name, value} = attr,
604
- vname = node.attributes.name?.value;
605
- if (name === "type" && value=="radio" && vname) {
606
- bindInput(node, vname, this);
607
- observe(() => {
608
- const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
609
- if (node.attributes.value.value == varvalue) {
610
- node.setAttribute("checked", "");
611
- node.checked = true;
612
- } else {
613
- node.removeAttribute("checked");
614
- node.checked = false;
728
+ });
615
729
  }
616
- });
617
- }
618
730
 
619
- const [type, ...params] = name.split(":");
620
- if (type === "") { // name is :something
621
- observe(() => {
622
- const value = attr.value;
623
- if (params[0]) {
624
- if (value === "true") node.setAttribute(params[0], "")
625
- else node.removeAttribute(params[0]);
626
- } else {
627
- const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx) : null,
628
- eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
629
- if (eltype === "checkbox" || node.tagName === "OPTION") {
630
- if (elvalue === "true") node.setAttribute("checked", "")
631
- else node.removeAttribute("checked");
632
- }
633
- }
634
- })
635
- } else if (type === "l-on") {
636
- let listener;
637
- observe(() => {
638
- const value = resolveNodeOrText(attr, this);
639
- if (listener) node.removeEventListener(params[0], listener);
640
- listener = this[value] || window[value] || Function(value);
641
- addListener(node, params[0], listener);
642
- })
643
- } else if (type === "l-if") {
644
- observe(() => {
645
- const value = resolveNodeOrText(attr, this);
646
- node.style.setProperty("display", value === "true" ? "revert" : "none");
647
- })
648
- } else if (type === "l-for") {
649
- node.template ||= node.innerHTML;
650
- observe(() => {
651
- const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
652
- value = resolveNodeOrText(attr, this),
653
- coerced = coerce(value, what === "each" ? Array : "object"),
654
- target = what === "each" ? coerced : Object[what](coerced),
655
- html = target.reduce( (html, item, i, target) => {
656
- return html += Function("vars", "context", "with(vars) { with(context) { return `" + node.template + "` }}")(
657
- ctx.varsProxy,
658
- {
659
- [vname]: item,
660
- [index]: i,
661
- [array]: target
662
- })
663
- }, ""),
664
- parsed = parser.parseFromString(html, "text/html");
665
- if (!window.lightviewDebug) {
666
- if (after) {
667
- node.style.setProperty("display", "none")
668
- } else {
669
- while (node.lastElementChild) node.lastElementChild.remove();
670
- }
671
- }
672
- while (parsed.body.firstChild) {
673
- if (after) node.parentElement.insertBefore(parsed.body.firstChild, node);
674
- else node.appendChild(parsed.body.firstChild);
731
+ const [type, ...params] = name.split(":");
732
+ if (type === "") { // name is :something
733
+ observe(() => {
734
+ const value = attr.value;
735
+ if (params[0]) {
736
+ if (value === "true") node.setAttribute(params[0], "")
737
+ else node.removeAttribute(params[0]);
738
+ } else {
739
+ const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx,false,node.extras) : null,
740
+ eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx,false,node.extras) : null;
741
+ if (eltype === "checkbox" || node.tagName === "OPTION") {
742
+ if (elvalue === true) node.setAttribute("checked", "")
743
+ else node.removeAttribute("checked");
744
+ }
745
+ }
746
+ })
747
+ } else if (type === "l-on") {
748
+ let listener;
749
+ observe(() => {
750
+ const value = resolveNodeOrText(attr, this,true,node.extras);
751
+ if (listener) node.removeEventListener(params[0], listener);
752
+ listener = null;
753
+ if(typeof(value)==="function") {
754
+ listener = value;
755
+ } else {
756
+ try {
757
+ listener = Function("return " + value)();
758
+ } catch(e) {
759
+
760
+ }
761
+ }
762
+ if(listener) addListener(node, params[0], listener,ctx);
763
+ })
764
+ } else if (type === "l-if") {
765
+ observe(() => {
766
+ const value = resolveNodeOrText(attr, this,true,node.extras);
767
+ node.style.setProperty("display", value == true ? "revert" : "none");
768
+ })
769
+ } else if (type === "l-for") {
770
+ node.template ||= node.innerHTML;
771
+ node.clone ||= node.cloneNode(true);
772
+ observe(() => {
773
+ const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
774
+ value = resolveNodeOrText(attr, this,false,node.extras),
775
+ coerced = coerce(value, what === "each" ? Array : "object"),
776
+ target = what === "each" ? coerced : Object[what](coerced),
777
+ children = target.reduce((children,item,i,target) => {
778
+ const clone = node.clone.cloneNode(true),
779
+ extras = node.extras = {
780
+ [vname]: item,
781
+ [index]: i,
782
+ [array]: target
783
+ },
784
+ nodes = [...getNodes(clone)].map((node) => {
785
+ node.extras = extras;
786
+ return node;
787
+ });
788
+ processNodes(nodes);
789
+ children.push(...clone.childNodes);
790
+ return children;
791
+ },[]);
792
+ if (!window.lightviewDebug) {
793
+ if (after) {
794
+ node.style.setProperty("display", "none")
795
+ } else {
796
+ while (node.lastElementChild) node.lastElementChild.remove();
797
+ }
798
+ }
799
+ //const nodes = getNodes(parsed.body);
800
+ children.forEach((child) => {
801
+ //while (parsed.body.firstChild) {
802
+ //const child = parsed.body.firstChild;
803
+ if (after) node.parentElement.insertBefore(child, node);
804
+ else node.appendChild(child);
805
+ })
806
+ //processNodes(nodes);
807
+ })
808
+ } else if(attr.template) {
809
+ observe(() => {
810
+ resolveNodeOrText(attr, this,false,node.extras);
811
+ })
675
812
  }
676
813
  })
677
- } else if(attr.template) {
678
- observe(() => {
679
- resolveNodeOrText(attr, this);
680
- })
681
814
  }
682
815
  })
816
+ };
817
+ nodes.forEach((node) => {
818
+ if(node.tagName==="FORM") {
819
+ const value = node.getAttribute("value"),
820
+ name = getTemplateVariableName(value);
821
+ if(name) {
822
+ const childnodes = [...nodes].filter((childnode) => node!==childnode && node.contains(childnode));
823
+ childnodes.forEach((node) => nodes.delete(node));
824
+ const variable = ctx.vars[name] ||= {type: "object", reactive:true, value: Reactor({})};
825
+ if(variable.type !== "object" || !variable.reactive || !variable.value || typeof(variable.value)!=="object") {
826
+ throw new TypeError(`Can't bind form ${node.getAttribute("id")} to non-object variable ${name}`);
827
+ }
828
+ processNodes(childnodes,variable.value);
829
+ }
683
830
  }
684
831
  })
832
+ processNodes(nodes);
685
833
  shadow.normalize();
686
834
  observer ||= createObserver(ctx, framed);
687
835
  observer.observe(ctx, {attributeOldValue: true, subtree:true, characterData:true, characterDataOldValue:true});
836
+ if(this.hasAttribute("l-unhide")) this.removeAttribute("hidden");
688
837
  //ctx.vars.postEvent.value("connected");
689
838
  this.dispatchEvent(new Event("connected"));
690
839
  // })
691
840
  }
692
841
  adoptedCallback(callback) {
693
842
  this.dispatchEvent(new Event("adopted"));
694
- //this.vars.postEvent.value("adopted");
695
843
  }
696
844
  disconnectedCallback() {
697
845
  this.dispatchEvent(new Event("disconnected"));
698
- //this.vars.postEvent.value("disconnected");
699
846
  }
700
847
  get observedAttributes() {
701
848
  return CustomElement.observedAttributes;
@@ -706,7 +853,7 @@ const {observe} = (() => {
706
853
 
707
854
  getVariableNames() {
708
855
  return Object.keys(this.vars)
709
- .filter(name => !(name in reserved) && !["self", "addEventListener", "postEvent"].includes(name))
856
+ .filter(name => !(name in reserved) && !["self", "addEventListener", "postEvent","observe"].includes(name))
710
857
  }
711
858
 
712
859
  getVariable(name) {
@@ -746,11 +893,13 @@ const {observe} = (() => {
746
893
  return this.vars[variableName]?.value;
747
894
  }
748
895
 
749
- variables(variables, {observed, reactive, shared, exported, imported, remote, constant,set} = {}) { // options = {observed,reactive,shared,exported,imported}
750
- const addEventListener = this.varsProxy.addEventListener;
896
+ variables(variables, {remote, constant,set,...rest} = {}) { // options = {observed,reactive,shared,exported,imported}
897
+ const options = {remote, constant,...rest},
898
+ addEventListener = this.varsProxy.addEventListener;
751
899
  if (variables !== undefined) {
752
900
  Object.entries(variables)
753
901
  .forEach(([key, type]) => {
902
+ if(isArrowFunction(type)) type = type();
754
903
  const variable = this.vars[key] ||= {name: key, type};
755
904
  if(set!==undefined && constant!==undefined) throw new TypeError(`${key} has the constant value ${constant} and can't be set to ${set}`);
756
905
  variable.value = set;
@@ -758,35 +907,19 @@ const {observe} = (() => {
758
907
  variable.constant = true;
759
908
  variable.value = constant;
760
909
  }
761
- if (observed || imported) {
762
- variable.value = this.hasAttribute(key) ? coerce(this.getAttribute(key), variable.type) : variable.value;
763
- variable.imported = imported;
764
- if(variable.observed) {
765
- variable.observed = observed;
766
- this.observedAttributes.add(key);
767
- }
768
- }
769
- if (reactive) {
770
- variable.reactive = true;
771
- this.vars[key] = Reactor(variable);
772
- }
773
- if (shared) {
774
- variable.shared = true;
775
- addEventListener("change", ({variableName, value}) => {
776
- if (this.vars[variableName]?.shared) this.siblings.forEach((instance) => instance.setVariableValue(variableName, value))
777
- })
778
- }
779
- if (exported) {
780
- variable.exported = true;
781
- // in case the export goes up to an iframe
782
- if (variable.value != null) setComponentAttribute(this, key, variable.value);
783
- this.changeListener.targets.add(key);
784
- }
785
910
  if (remote) {
786
911
  if(typeof(remote)==="function") remote = remote(`./${key}`);
787
912
  variable.remote = remote;
788
- remote.handleRemote({variable, config:remote.config, reactive,component:this});
913
+ remote.handleRemote({variable, config:remote.config,component:this});
789
914
  }
915
+ // todo: handle custom functional types, remote should actually be handled this way
916
+ Object.entries(rest).forEach(([type,f]) => {
917
+ const functionalType = variable[type] = typeof(f)==="function" ? f() : f;
918
+ if(functionalType.init) functionalType.init({variable,options,component:this});
919
+ if((rest.get!==undefined || rest.set!==undefined) && constant!==undefined) throw new TypeError(`${key} has the constant value ${constant} and can't have a getter or setter`);
920
+ variable.set != functionalType.set;
921
+ variable.get != functionalType.get;
922
+ });
790
923
  if(type.validate && variable.value!==undefined) type.validate(variable.value,variable);
791
924
  });
792
925
  }
@@ -799,14 +932,14 @@ const {observe} = (() => {
799
932
  }
800
933
  }
801
934
 
802
- const createComponent = (name, node, {framed, observer} = {}) => {
935
+ const createComponent = (name, node, {framed, observer, href} = {}) => {
803
936
  let ctor = customElements.get(name);
804
937
  if (ctor) {
805
938
  if (framed && !ctor.lightviewFramed) ctor.lightviewFramed = true;
806
939
  else console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
807
940
  return ctor;
808
941
  }
809
- ctor = createClass(node, {observer, framed});
942
+ ctor = createClass(node, {observer, framed, href});
810
943
  customElements.define(name, ctor);
811
944
  Lightview.customElements.set(name, ctor);
812
945
  return ctor;
@@ -832,7 +965,7 @@ const {observe} = (() => {
832
965
  await importLink(childlink, observer);
833
966
  }
834
967
  if (unhide) dom.body.removeAttribute("hidden");
835
- createComponent(as, dom.body, {observer});
968
+ createComponent(as, dom.body, {observer,href:url.href});
836
969
  }
837
970
  return {as};
838
971
  }