lightview 1.4.2-b → 1.4.7-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/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # lightview v1.4.2b (BETA)
1
+ # lightview v1.4.7b (BETA)
2
2
 
3
3
  Small, simple, powerful web UI and micro front end creation ...
4
4
 
5
- Great ideas from Svelte, React, Vue and Riot combined into one small tool: < 6K (minified/gzipped).
5
+ Great ideas from Svelte, React, Vue and Riot combined into one small tool: < 7K (minified/gzipped).
6
6
 
7
7
  See the docs and examples at [https://lightview.dev](https://lightview.dev).
8
8
 
@@ -0,0 +1,2 @@
1
+ body.split-output #output > .active { width: 75%; }
2
+ body.split-output #output > #result-box.active { width: 25% }
package/counter.html CHANGED
@@ -9,6 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <script type="lightview/module">
12
+ debugger;
12
13
  self.variables({
13
14
  count: number
14
15
  }, {
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ server: {
3
+ command: 'http-server'
4
+ }
5
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "moduleFileExtensions": ["mjs", "js", "jsx", "ts", "tsx", "json", "node"],
3
+ "testMatch": [
4
+ "**/?(*.)test.?js"
5
+ ],
6
+ "verbose": true,
7
+ "transform": {},
8
+ "testEnvironment": "jest-environment-node",
9
+ "globalSetup": "jest-environment-puppeteer/setup",
10
+ "globalTeardown": "jest-environment-puppeteer/teardown",
11
+ "testEnvironment": "jest-environment-puppeteer"
12
+ }
package/lightview.js CHANGED
@@ -30,8 +30,24 @@ const {observe} = (() => {
30
30
  let CURRENTOBSERVER;
31
31
  const parser = new DOMParser();
32
32
 
33
- const addListener = (node,eventName,callback) => {
34
- node.addEventListener(eventName,callback); // just used to make code footprint smaller
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,"");
39
+ }
40
+ Lightview.sanitizeTemplate = templateSanitizer;
41
+
42
+ const escaper = document.createElement('textarea');
43
+ function escapeHTML(html) {
44
+ escaper.textContent = html;
45
+ return escaper.innerHTML;
46
+ }
47
+ Lightview.escapeHTML = escapeHTML;
48
+
49
+ const addListener = (node, eventName, callback) => {
50
+ node.addEventListener(eventName, callback); // just used to make code footprint smaller
35
51
  }
36
52
  const anchorHandler = async (event) => {
37
53
  event.preventDefault();
@@ -73,6 +89,7 @@ const {observe} = (() => {
73
89
  if (toType === "number") return parseFloat(value + "");
74
90
  if (toType === "boolean") {
75
91
  if (["on", "checked", "selected"].includes(value)) return true;
92
+ if(value==null || value==="") return false;
76
93
  try {
77
94
  const parsed = JSON.parse(value + "");
78
95
  if (typeof (parsed) === "boolean") return parsed;
@@ -93,9 +110,18 @@ const {observe} = (() => {
93
110
  if (isfunction) {
94
111
  const instance = toType === Date ? new Date() : Object.create(toType.prototype);
95
112
  if (instance instanceof Array) {
96
- const parsed = JSON.parse(value.startsWith("[") ? value : `[${value}]`);
97
- if (!Array.isArray(parsed)) throw new TypeError(`Expected an Array for parsed data`)
98
- parsed.forEach((item) => instance.push(item))
113
+ let parsed = tryParse(value.startsWith("[") ? value : `[${value}]`);
114
+ if (!Array.isArray(parsed)) {
115
+ if(value.includes(",")) parsed = value.split(",");
116
+ else {
117
+ parsed = tryParse(`["${value}"]`);
118
+ if(!Array.isArray(parsed) || parsed[0]!==value && parsed.length!==1) parsed = null;
119
+ }
120
+ }
121
+ if (!Array.isArray(parsed)) {
122
+ throw new TypeError(`Expected an Array for parsed data`)
123
+ }
124
+ instance.push(...parsed);
99
125
  } else if (instance instanceof Date) {
100
126
  instance.setTime(Date.parse(value));
101
127
  } else {
@@ -131,7 +157,7 @@ const {observe} = (() => {
131
157
  return [...target];
132
158
  }
133
159
  if (property === "toString") return function toString() {
134
- return JSON.stringify(target);
160
+ return JSON.stringify([...target]);
135
161
  }
136
162
  }
137
163
  let value = target[property];
@@ -218,18 +244,18 @@ const {observe} = (() => {
218
244
  }
219
245
  });
220
246
  }
221
- const createObserver = (domNode,framed) => {
247
+ const createObserver = (domNode, framed) => {
222
248
  const observer = new MutationObserver((mutations) => {
223
249
  mutations.forEach((mutation) => {
224
250
  if (mutation.type === "attributes") {
225
- if(framed) debugger;
251
+ //if (framed) debugger;
226
252
  const name = mutation.attributeName,
227
253
  target = mutation.target,
228
254
  value = target.getAttribute(name);
229
- if(framed && name==="message" && target instanceof IFrameElement) {
230
- if(value) console.log("message",value);
255
+ if (framed && name === "message" && target instanceof IFrameElement) {
256
+ if (value) console.log("message", value);
231
257
  target.removeAttribute(name);
232
- target.dispatchEvent("message",new CustomEvent("message",{detail:JSON.parse(value)}))
258
+ target.dispatchEvent(new CustomEvent("message", {detail: JSON.parse(value)}))
233
259
  }
234
260
  if (target.observedAttributes && target.observedAttributes.includes(name)) {
235
261
  if (value !== mutation.oldValue) {
@@ -270,7 +296,7 @@ const {observe} = (() => {
270
296
  nodes.push(node);
271
297
  } else if (node.nodeType === Node.ELEMENT_NODE) {
272
298
  let skip;
273
- if(node.getAttribute("type")==="radio") nodes.push(node);
299
+ if (node.getAttribute("type") === "radio") nodes.push(node);
274
300
  [...node.attributes].forEach((attr) => {
275
301
  if (attr.value.includes("${")) {
276
302
  attr.template ||= attr.value;
@@ -286,12 +312,14 @@ const {observe} = (() => {
286
312
  }
287
313
  return nodes;
288
314
  }
289
- const resolveNode = (node, component) => {
315
+ const resolveNode = (node, component,safe) => {
290
316
  if (node?.template) {
291
317
  try {
292
- const value = Function("context", "with(context) { return `" + node.template + "` }")(component.varsProxy);
293
- node.nodeValue = value === "null" || value === "undefined" ? "" : value;
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;
294
321
  } catch (e) {
322
+ console.warn(e);
295
323
  if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
296
324
  }
297
325
  }
@@ -308,34 +336,35 @@ const {observe} = (() => {
308
336
  }
309
337
  const inputTypeToType = (inputType) => {
310
338
  if (!inputType) return "any"
311
- if (["text", "tel", "email", "url", "search", "radio","color","password"].includes(inputType)) return "string";
339
+ if (["text", "tel", "email", "url", "search", "radio", "color", "password"].includes(inputType)) return "string";
312
340
  if (["number", "range"].includes(inputType)) return "number";
313
341
  if (["datetime"].includes(inputType)) return Date;
314
342
  if (["checkbox"].includes(inputType)) return "boolean";
315
343
  return "any";
316
344
  }
317
- const _importAnchors = (node, component) => {
318
- [...node.querySelectorAll('a[href][target^="#"]')].forEach((node) => {
345
+ const importAnchors = (node, component) => {
346
+ [...node.querySelectorAll('a[href$=".html"][target^="#"]')].forEach((node) => {
319
347
  node.removeEventListener("click", anchorHandler);
320
- addListener(node,"click", anchorHandler);
348
+ addListener(node, "click", anchorHandler);
321
349
  })
322
350
  }
323
- const bindInput = (input, name, component) => {
324
- const inputtype = input.tagName === "SELECT" ? "text" : input.getAttribute("type"),
351
+ const bindInput = (input, name, component,value) => {
352
+ const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
325
353
  type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype),
326
- deflt = input.getAttribute("default"),
327
- value = input.getAttribute("value");
354
+ deflt = input.getAttribute("default");
355
+ value ||= input.getAttribute("value");
328
356
  let variable = component.vars[name] || {type};
329
357
  if (type !== variable.type) {
330
358
  if (variable.type === "any" || variable.type === "unknown") variable.type = type;
331
359
  else throw new TypeError(`Attempt to bind <input name="${name}" type="${type}"> to variable ${name}:${variable.type}`)
332
360
  }
333
361
  component.variables({[name]: type});
362
+ component.setValue(name,value);
334
363
  let eventname = "change";
335
- if(input.tagName!=="SELECT" && (!inputtype || ["text","number","tel","email","url","search","password"].includes(inputtype))) {
364
+ if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
336
365
  eventname = "input";
337
366
  }
338
- addListener(input,eventname, (event) => {
367
+ addListener(input, eventname, (event) => {
339
368
  event.stopImmediatePropagation();
340
369
  const target = event.target;
341
370
  let value = target.value;
@@ -344,7 +373,7 @@ const {observe} = (() => {
344
373
  } else if (target.tagName === "SELECT") {
345
374
  if (target.hasAttribute("multiple")) {
346
375
  value = [...target.querySelectorAll("option")]
347
- .filter((option) => option.selected || resolveNode(option.attributes.value,component)==value || option.innerText == value)
376
+ .filter((option) => option.selected || resolveNode(option.attributes.value, component) == value || option.innerText == value)
348
377
  .map((option) => option.getAttribute("value") || option.innerText);
349
378
  }
350
379
  }
@@ -354,21 +383,23 @@ const {observe} = (() => {
354
383
  const tryParse = (value) => {
355
384
  try {
356
385
  return JSON.parse(value);
357
- } catch(e) {
386
+ } catch (e) {
358
387
  return value;
359
388
  }
360
389
  }
361
390
  let reserved = {
391
+ any: {value: "any",constant: true},
362
392
  boolean: {value: "boolean", constant: true},
363
393
  string: {value: "string", constant: true},
364
394
  number: {value: "number", constant: true},
395
+ object: {value: "object", constant: true},
365
396
  observed: {value: true, constant: true},
366
397
  reactive: {value: true, constant: true},
367
398
  shared: {value: true, constant: true},
368
399
  exported: {value: true, constant: true},
369
400
  imported: {value: true, constant: true}
370
401
  };
371
- const createClass = (domElementNode, {observer, importAnchors, framed}) => {
402
+ const createClass = (domElementNode, {observer, framed}) => {
372
403
  const instances = new Set(),
373
404
  dom = domElementNode.tagName === "TEMPLATE"
374
405
  ? domElementNode.content.cloneNode(true)
@@ -382,7 +413,7 @@ const {observe} = (() => {
382
413
  constructor() {
383
414
  super();
384
415
  instances.add(this);
385
- observer ||= createObserver(this,framed);
416
+ observer ||= createObserver(this, framed);
386
417
  const currentComponent = this,
387
418
  shadow = this.attachShadow({mode: "open"}),
388
419
  eventlisteners = {};
@@ -412,7 +443,7 @@ const {observe} = (() => {
412
443
  };
413
444
  this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
414
445
  this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
415
- if(framed || CustomElement.lightviewFramed) this.variables({message:Object},{exported:true});
446
+ if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported: true});
416
447
  ["getElementById", "querySelector", "querySelectorAll"]
417
448
  .forEach((fname) => {
418
449
  Object.defineProperty(this, fname, {
@@ -422,7 +453,7 @@ const {observe} = (() => {
422
453
  })
423
454
  });
424
455
  [...dom.childNodes].forEach((child) => shadow.appendChild(child.cloneNode(true)));
425
- if (importAnchors) _importAnchors(shadow, this);
456
+ importAnchors(shadow, this);
426
457
  }
427
458
 
428
459
  get siblings() {
@@ -472,51 +503,49 @@ const {observe} = (() => {
472
503
  } else if (node.nodeType === Node.ELEMENT_NODE) {
473
504
  // resolve the value before all else;
474
505
  const attr = node.attributes.value;
475
- let name;
476
- if(attr && attr.template) {
477
- render(!!attr.template,() => {
506
+ if (attr && attr.template) {
507
+ render(!!attr.template, () => {
478
508
  const value = resolveNode(attr, this),
479
- eltype = resolveNode(node.attributes.type,ctx);
480
- if(eltype==="checkbox") {
481
- if(coerce(value,"boolean")===true) {
482
- node.setAttribute("checked","");
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", "");
483
520
  node.checked = true;
484
521
  } else {
485
522
  node.removeAttribute("checked");
486
523
  node.checked = false;
487
524
  }
488
- const vname = resolveNode(node.attributes.name,ctx);
489
- if(vname) ctx.setValue(vname,node.checked,{coerceTo:"boolean"});
525
+ const vname = resolveNode(node.attributes.name, ctx);
526
+ if (vname) ctx.setValue(vname, node.checked, {coerceTo: "boolean"});
490
527
  }
491
- if(node.tagName==="SELECT") {
528
+ if (node.tagName === "SELECT") {
492
529
  let values = [value];
493
- if(node.hasAttribute("multiple")) values = coerce(value,Array);
530
+ if (node.hasAttribute("multiple")) values = coerce(value, Array);
494
531
  [...node.querySelectorAll("option")].forEach((option) => {
495
- if(option.hasAttribute("value")) {
532
+ if (option.hasAttribute("value")) {
496
533
  if (values.includes(resolveNode(option.attributes.value, ctx))) {
497
534
  option.setAttribute("selected", "");
498
535
  option.selected = true;
499
536
  }
500
- } else if(option.innerText.trim()===value) {
501
- option.setAttribute("selected","");
537
+ } else if (option.innerText.trim() === value) {
538
+ option.setAttribute("selected", "");
502
539
  option.selected = true;
503
540
  }
504
541
  })
505
542
  }
506
543
  });
507
- let name;
508
- for(const vname of this.getVariableNames()) {
509
- if("${" + vname + "}" === attr.template) {
510
- name = vname;
511
- break;
512
- }
513
- }
514
- if(name) bindInput(node,name,ctx);
515
544
  }
516
545
  [...node.attributes].forEach((attr) => {
517
- if(attr.name==="value") return;
546
+ if (attr.name === "value" && attr.template) return;
518
547
  const {name, value} = attr;
519
- if(name==="type") {
548
+ if (name === "type") {
520
549
  if (value === "radio") {
521
550
  const name = resolveNode(node.attributes.name, ctx);
522
551
  for (const vname of this.getVariableNames()) {
@@ -524,7 +553,7 @@ const {observe} = (() => {
524
553
  render(true, () => {
525
554
  const name = resolveNode(node.attributes.name, ctx),
526
555
  varvalue = Function("context", "with(context) { return `${" + name + "}` }")(ctx.varsProxy);
527
- if (varvalue == resolveNode(node.attributes.value,ctx)) {
556
+ if (varvalue == resolveNode(node.attributes.value, ctx)) {
528
557
  node.setAttribute("checked", "");
529
558
  node.checked = true;
530
559
  } else {
@@ -543,13 +572,13 @@ const {observe} = (() => {
543
572
  if (type === "") { // name is :something
544
573
  render(!!attr.template, () => {
545
574
  const value = attr.value,
546
- elvalue = resolveNode(node.attributes.value,ctx),
547
- eltype = resolveNode(node.attributes.type,ctx),
548
- elname = resolveNode(node.attributes.name,ctx);
575
+ elvalue = resolveNode(node.attributes.value, ctx),
576
+ eltype = resolveNode(node.attributes.type, ctx),
577
+ elname = resolveNode(node.attributes.name, ctx);
549
578
  if (params[0]) {
550
579
  if (value === "true") node.setAttribute(params[0], "")
551
580
  else node.removeAttribute(params[0]);
552
- } else if (eltype=== "checkbox" || node.tagName === "OPTION") {
581
+ } else if (eltype === "checkbox" || node.tagName === "OPTION") {
553
582
  if (value === "true") node.setAttribute("checked", "")
554
583
  else node.removeAttribute("checked");
555
584
  }
@@ -560,7 +589,7 @@ const {observe} = (() => {
560
589
  const value = resolveNode(attr, this);
561
590
  if (listener) node.removeEventListener(params[0], listener);
562
591
  listener = this[value] || window[value] || Function(value);
563
- addListener(node,params[0], listener);
592
+ addListener(node, params[0], listener);
564
593
  })
565
594
  } else if (type === "l-if") {
566
595
  render(!!attr.template, () => {
@@ -575,11 +604,13 @@ const {observe} = (() => {
575
604
  coerced = coerce(value, what === "each" ? Array : "object"),
576
605
  target = what === "each" ? coerced : Object[what](coerced),
577
606
  html = target.reduce((html, item, i, target) => {
578
- return html += Function("context", "with(context) { return `" + node.template + "` }")({
579
- [vname]: item,
580
- [index]: i,
581
- [array]: target
582
- })
607
+ return html += Function("vars","context", "with(vars) { with(context) { return `" + node.template + "` }}")(
608
+ ctx.varsProxy,
609
+ {
610
+ [vname]: item,
611
+ [index]: i,
612
+ [array]: target
613
+ })
583
614
  }, ""),
584
615
  parsed = parser.parseFromString(html, "text/html");
585
616
  if (!window.lightviewDebug) {
@@ -611,46 +642,46 @@ const {observe} = (() => {
611
642
  //Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
612
643
  }
613
644
 
614
- connected(value) {
615
- this.connectedCallback = value;
645
+ connected(callback) {
646
+ this.connectedCallback = callback;
616
647
  //Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
617
648
  }
618
649
 
619
- attributeChanged(value) {
620
- this.attributeChangedCallback = value;
650
+ attributeChanged(callback) {
651
+ this.attributeChangedCallback = callback;
621
652
  //Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
622
653
  }
623
654
 
624
- disconnected(value) {
655
+ disconnected(callback) {
625
656
  Object.defineProperty(this, "disconnectedCallback", {
626
657
  configurable: true,
627
658
  writable: true,
628
659
  value: () => {
629
660
  value();
630
- super.disconnectedCallback(value);
661
+ super.disconnectedCallback(callback);
631
662
  }
632
663
  });
633
664
  }
634
665
 
635
666
  getVariableNames() {
636
667
  return Object.keys(this.vars).filter((name) => {
637
- return !(name in reserved) && !["self","addEventListener","postEvent"].includes(name)
668
+ return !(name in reserved) && !["self", "addEventListener", "postEvent"].includes(name)
638
669
  })
639
670
  }
640
671
 
641
- setValue(name, value, {shared, coerceTo = typeof (value)} = {}) {
672
+ setValue(variableName, value, {coerceTo = typeof (value)} = {}) {
642
673
  if (!this.isConnected) {
643
674
  instances.delete(this);
644
675
  return false;
645
676
  }
646
- let {type} = this.vars[name] || {};
677
+ let {type} = this.vars[variableName] || {};
647
678
  if (type) {
648
679
  value = coerce(value, type);
649
- if (this.varsProxy[name] !== value) {
650
- const variable = this.vars[name];
680
+ if (this.varsProxy[variableName] !== value) {
681
+ const variable = this.vars[variableName];
651
682
  if (variable.shared) {
652
683
  const event = new VariableEvent({
653
- variableName: name,
684
+ variableName: variableName,
654
685
  value: value,
655
686
  oldValue: variable.value
656
687
  });
@@ -658,12 +689,12 @@ const {observe} = (() => {
658
689
  this.vars.postEvent.value("change", event);
659
690
  if (event.defaultPrevented) variable.value = value;
660
691
  } else {
661
- this.varsProxy[name] = value;
692
+ this.varsProxy[variableName] = value;
662
693
  }
663
694
  }
664
695
  return true;
665
696
  }
666
- this.vars[name] = {name, type: coerceTo, value: coerce(value, coerceTo)};
697
+ this.vars[variableName] = {name, type: coerceTo, value: coerce(value, coerceTo)};
667
698
  return false;
668
699
  }
669
700
 
@@ -732,48 +763,57 @@ const {observe} = (() => {
732
763
  }
733
764
  }
734
765
  }
735
- const createComponent = (name, node, {observer, importAnchors,framed} = {}) => {
766
+ const createComponent = (name, node, {framed,observer} = {}) => {
736
767
  let ctor = customElements.get(name);
737
768
  if (ctor) {
738
- if(framed && !ctor.lightviewFramed) {
769
+ if (framed && !ctor.lightviewFramed) {
739
770
  ctor.lightviewFramed = true;
740
771
  } else {
741
772
  console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
742
773
  }
743
774
  return ctor;
744
775
  }
745
- ctor = createClass(node, {observer, importAnchors,framed});
776
+ ctor = createClass(node, {observer, framed});
746
777
  customElements.define(name, ctor);
778
+ Lightview.customElements.set(name,ctor);
747
779
  return ctor;
748
780
  }
781
+ Lightview.customElements = new Map();
749
782
  Lightview.createComponent = createComponent;
750
783
  //Object.defineProperty(Lightview, "createComponent", {writable: true, configurable: true, value: createComponent})
751
784
  const importLink = async (link, observer) => {
752
785
  const url = (new URL(link.getAttribute("href"), window.location.href)),
753
786
  as = link.getAttribute("as") || getNameFromPath(url.pathname);
754
- if (url.hostname !== window.location.hostname) {
755
- throw new URIError(`importLink:HTML imports must be from same domain: ${url.hostname}!=${location.hostname}`)
787
+ if (url.hostname !== window.location.hostname && !link.getAttribute("crossorigin")) {
788
+ throw new URIError(`importLink:HTML imports must be from same domain: ${url.hostname}!=${location.hostname} unless 'crossorigin' attribute is set.`)
756
789
  }
757
790
  if (!customElements.get(as)) {
758
791
  const html = await (await fetch(url.href)).text(),
759
792
  dom = parser.parseFromString(html, "text/html"),
760
- importAnchors = !!dom.head.querySelector('meta[name="l-importAnchors"]'),
761
- unhide = !!dom.head.querySelector('meta[name="l-unhide"]');
793
+ unhide = !!dom.head.querySelector('meta[name="l-unhide"]'),
794
+ links = dom.head.querySelectorAll('link[href$=".html"][rel=module]');
795
+ for(const childlink of links) {
796
+ 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);
801
+ }
762
802
  if (unhide) dom.body.removeAttribute("hidden");
763
- createComponent(as, dom.body, {observer, importAnchors});
803
+ createComponent(as, dom.body, {observer});
764
804
  }
765
805
  return {as};
766
806
  }
767
807
  const importLinks = async () => {
768
808
  const observer = createObserver(document.body);
769
- for (const link of [...document.querySelectorAll("link[href][rel=module]")]) {
770
- await importLink(link);
809
+ for (const link of [...document.querySelectorAll(`link[href$=".html"][rel=module]`)]) {
810
+ await importLink(link,observer);
771
811
  }
772
812
  }
773
813
 
774
- const bodyAsComponent = ({as = "x-body", unhide, importAnchors,framed} = {}) => {
814
+ const bodyAsComponent = ({as = "x-body", unhide, framed} = {}) => {
775
815
  const parent = document.body.parentElement;
776
- createComponent(as, document.body, {importAnchors,framed});
816
+ createComponent(as, document.body, {framed});
777
817
  const component = document.createElement(as);
778
818
  parent.replaceChild(component, document.body);
779
819
  Object.defineProperty(document, "body", {
@@ -815,19 +855,18 @@ const {observe} = (() => {
815
855
 
816
856
  const url = new URL(document.currentScript.getAttribute("src"), window.location.href);
817
857
  let domContentLoadedEvent;
818
- if(!domContentLoadedEvent) addListener(window,"DOMContentLoaded", (event) => domContentLoadedEvent = event);
858
+ if (!domContentLoadedEvent) addListener(window, "DOMContentLoaded", (event) => domContentLoadedEvent = event);
819
859
  let OBSERVER;
820
860
  const loader = async (whenFramed) => {
821
- if (!!document.querySelector('meta[name="l-importLinks"]')) await importLinks();
822
- const importAnchors = !!document.querySelector('meta[name="l-importAnchors"]'),
823
- unhide = !!document.querySelector('meta[name="l-unhide"]'),
861
+ await importLinks();
862
+ const unhide = !!document.querySelector('meta[name="l-unhide"]'),
824
863
  isolated = !!document.querySelector('meta[name="l-isolate"]'),
825
864
  enableFrames = !!document.querySelector('meta[name="l-enableFrames"]');
826
865
  if (whenFramed) {
827
- whenFramed({unhide, importAnchors, isolated, enableFrames, framed:true});
866
+ whenFramed({unhide, isolated, enableFrames, framed: true});
828
867
  if (!isolated) {
829
868
  postMessage.enabled = true;
830
- addListener(window,"message", ({data}) => {
869
+ addListener(window, "message", ({data}) => {
831
870
  const {type, argsList} = JSON.parse(data);
832
871
  if (type === "framed") {
833
872
  const resize = () => {
@@ -859,11 +898,11 @@ const {observe} = (() => {
859
898
  postMessage({type: "DOMContentLoaded"})
860
899
  }
861
900
  } else if (url.searchParams.has("as")) {
862
- bodyAsComponent({as: url.searchParams.get("as"), unhide, importAnchors});
901
+ bodyAsComponent({as: url.searchParams.get("as"), unhide});
863
902
  }
864
903
  if (enableFrames) {
865
904
  postMessage.enabled = true;
866
- addListener(window,"message", (message) => {
905
+ addListener(window, "message", (message) => {
867
906
  const {type, iframeId, argsList, href} = JSON.parse(message.data),
868
907
  iframe = document.getElementById(iframeId);
869
908
  if (iframe) {
@@ -900,7 +939,7 @@ const {observe} = (() => {
900
939
  }
901
940
  console.warn("iframe posted a message without providing an id", message);
902
941
  });
903
- if(!OBSERVER) {
942
+ if (!OBSERVER) {
904
943
  const mutationCallback = (mutationsList) => {
905
944
  const console = document.getElementById("console");
906
945
  for (const {target, attributeName, oldValue} of mutationsList) {
@@ -914,13 +953,16 @@ const {observe} = (() => {
914
953
  }, iframe)
915
954
  }
916
955
  }
917
- if(attributeName==="message") {
918
- if(value) {
956
+ if (attributeName === "message") {
957
+ if (value) {
919
958
  target.removeAttribute("message");
920
- target.dispatchEvent(new CustomEvent("message",{target,detail:JSON.parse(value)}))
959
+ target.dispatchEvent(new CustomEvent("message", {target, detail: JSON.parse(value)}))
921
960
  }
922
961
  } else {
923
- target.dispatchEvent(new CustomEvent("attribute.changed",{target,detail:{attributeName,value,oldValue}}))
962
+ target.dispatchEvent(new CustomEvent("attribute.changed", {
963
+ target,
964
+ detail: {attributeName, value, oldValue}
965
+ }))
924
966
  }
925
967
  }
926
968
  };
@@ -930,17 +972,17 @@ const {observe} = (() => {
930
972
  }
931
973
  }
932
974
  }
933
- const whenFramed = (f, {isolated} = {}) => {
975
+ const whenFramed = (callback, {isolated} = {}) => {
934
976
  // loads for framed content
935
- addListener(document,"DOMContentLoaded", (event) => loader(f));
977
+ addListener(document, "DOMContentLoaded", (event) => loader(callback));
936
978
  }
937
979
  Lightview.whenFramed = whenFramed;
938
980
  //Object.defineProperty(Lightview, "whenFramed", {configurable: true, writable: true, value: whenFramed});
939
981
  if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) {
940
982
  // loads for unframed content
941
983
  // CodePen mucks with window.parent
942
- addListener(document,"DOMContentLoaded", () => loader())
984
+ addListener(document, "DOMContentLoaded", () => loader())
943
985
  }
944
986
 
945
987
  return {observe}
946
- })();
988
+ })();
package/message.html ADDED
@@ -0,0 +1,13 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="UTF-8">
4
+ <title>Message</title>
5
+ </head>
6
+ <body>
7
+ ${value}
8
+ <script type="lightview/module">
9
+ debugger;
10
+ self.variables({value:string},{imported});
11
+ </script>
12
+ </body>
13
+ </html>
package/nested.html ADDED
@@ -0,0 +1,11 @@
1
+ <!DOCTYPE html>
2
+ <head>
3
+ <title>Nested</title>
4
+ <link href="./message.html" rel="module">
5
+ <script src="./lightview.js?as=x-body"></script>
6
+ </head>
7
+ <body>
8
+ <l-message value="Hello One"></l-message>
9
+ <l-message value="Hello Two"></l-message>
10
+ </body>
11
+ </html>
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "lightview",
3
- "version": "1.4.2b",
4
- "description": "Small, simple, powerful web UI and micro front end creation ... imagine a blend of Svelte, React, Vue, Riot and more.",
3
+ "version": "1.4.7b",
4
+ "description": "Small, simple, powerful web UI and micro front end creation ... Great ideas from Svelte, React, Vue and Riot combined.",
5
5
  "main": "lightview.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "set NODE_OPTIONS=--experimental-vm-modules && jest ./test"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -28,5 +28,9 @@
28
28
  "bugs": {
29
29
  "url": "https://github.com/anywhichway/lightview/issues"
30
30
  },
31
- "homepage": "https://github.com/anywhichway/lightview#readme"
31
+ "homepage": "https://github.com/anywhichway/lightview#readme",
32
+ "devDependencies": {
33
+ "jest": "^27.5.1",
34
+ "jest-puppeteer": "^6.1.0"
35
+ }
32
36
  }
@@ -0,0 +1,79 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Basic</title>
6
+ <template id="x-test" name="joe" open="true" count=1 children='["mary"]' l-on:click="bump">
7
+ <span id="name">${name}</span>
8
+ <span id="open">${open}</span>
9
+ <span id="count">${count}</span>
10
+ <span id="children">${children}</span>
11
+ <span id="color">${color}</span>
12
+ <span id="checked">${checked}</span>
13
+ <span id="age">${age}</span>
14
+ <span id="hamburger">${hamburger}</span>
15
+
16
+ <input id="iuntyped" value="${iuntyped}">
17
+ <input id="itext" type="text" value="${itext}">
18
+ <input id="itel" type="tel" value="${itel}">
19
+ <input id="iemail" type="email" value="${iemail}">
20
+ <input id="iurl" type="url" value="${iurl}">
21
+ <input id="isearch" type="search" value="${isearch}">
22
+ <input id="iradio" type="radio" value="${iradio}">
23
+ <input id="icolor" type="color" value="${icolor}">
24
+ <input id="ipassword" type="password" value="${ipassword}">
25
+
26
+ <input id="inumber" type="number" value="${inumber}">
27
+ <input id="irange" type="range" value="${irange}">
28
+
29
+ <input id="idatetime" type="datetime" value="${idatetime}">
30
+
31
+ <input id="icheckbox" type="checkbox" value="${icheckbox}">
32
+
33
+ <script type="lightview/module">
34
+ debugger;
35
+ self.variables({name:string,open:boolean,count:number,children:Array},{imported,reactive});
36
+ self.variables({color:string,checked:boolean,age:number,hamburger:Array},{exported,reactive});
37
+ self.variables({counter:number},{reactive});
38
+ self.variables({myshare:number},{shared});
39
+
40
+ color = "green";
41
+ checked = true;
42
+ age = 27;
43
+ hamburger = ["lettuce"];
44
+ counter = 0;
45
+ myshare = 1;
46
+
47
+ iuntyped = "test";
48
+ itext = "test";
49
+ itel = "test";
50
+ iemail = "test";
51
+ iurl = "test";
52
+ isearch = "test";
53
+ iradio = "test";
54
+ icolor = "test";
55
+ ipassword = "test";
56
+
57
+ inumber = 1;
58
+ irange = 1;
59
+
60
+ idatetime = new Date();
61
+
62
+ icheckbox = true;
63
+
64
+ self.bump = () => {
65
+ counter++;
66
+ };
67
+ </script>
68
+ </template>
69
+ <script src="../lightview.js"></script>
70
+ <script>
71
+ Lightview.createComponent("x-test",document.getElementById("x-test"));
72
+ </script>
73
+ </head>
74
+ <body>
75
+ <p><x-test id="test"></x-test></p>
76
+
77
+ <p><x-test id="test1"></x-test></p>
78
+ </body>
79
+ </html>
@@ -0,0 +1,248 @@
1
+ import 'expect-puppeteer';
2
+
3
+ describe('Google', () => {
4
+ beforeAll(async () => {
5
+ await page.goto('https://google.com');
6
+ });
7
+
8
+ test('should be titled "Google"', async () => {
9
+ await expect(page.title()).resolves.toMatch('Google');
10
+ });
11
+ });
12
+
13
+ describe('Lightview', () => {
14
+ beforeAll(async () => {
15
+ await page.goto('http://localhost:8080/test/basic.html');
16
+ });
17
+
18
+ test('should be titled "Basic"', async () => {
19
+ await expect(page.title()).resolves.toMatch('Basic');
20
+ });
21
+
22
+ test('boolean - open should be imported', async () => {
23
+ const result = await page.evaluate(() => {
24
+ const el = document.getElementById("test");
25
+ return JSON.parse(el.getValue("open"));
26
+ });
27
+ expect(result).toBe(true);
28
+ });
29
+
30
+ test('number - count should be imported', async () => {
31
+ const result = await page.evaluate(() => {
32
+ const el = document.getElementById("test");
33
+ return JSON.parse(el.getValue("count"));
34
+ });
35
+ expect(result).toBe(1);
36
+ });
37
+
38
+ test('string - name should be imported', async () => {
39
+ const result = await page.evaluate(() => {
40
+ const el = document.getElementById("test");
41
+ return el.getValue("name");
42
+ });
43
+ expect(result).toBe("joe");
44
+ });
45
+
46
+ test('object - children should be imported', async () => {
47
+ const result = await page.evaluate(() => {
48
+ const el = document.getElementById("test");
49
+ return el.getValue("children").toJSON();
50
+ });
51
+ expect(Array.isArray(result)).toBe(true);
52
+ expect(result[0]).toBe("mary");
53
+ expect(result.length).toBe(1);
54
+ });
55
+
56
+ test('boolean - checked should be exported', async () => {
57
+ const result = await page.evaluate(() => {
58
+ const el = document.getElementById("test");
59
+ return JSON.parse(el.getAttribute("checked"));
60
+ });
61
+ expect(result).toBe(true);
62
+ });
63
+
64
+ test('number - age should be exported', async () => {
65
+ const result = await page.evaluate(() => {
66
+ const el = document.getElementById("test");
67
+ return JSON.parse(el.getAttribute("age"));
68
+ });
69
+ expect(result).toBe(27);
70
+ });
71
+
72
+ test('string - color should be exported', async () => {
73
+ const result = await page.evaluate(() => {
74
+ const el = document.getElementById("test");
75
+ return el.getAttribute("color");
76
+ });
77
+ expect(result).toBe("green");
78
+ });
79
+
80
+ test('object - hamburger should be exported', async () => {
81
+ const result = await page.evaluate(() => {
82
+ const el = document.getElementById("test");
83
+ return JSON.parse(el.getAttribute("hamburger"));
84
+ });
85
+ expect(Array.isArray(result)).toBe(true);
86
+ expect(result[0]).toBe("lettuce");
87
+ expect(result.length).toBe(1);
88
+ });
89
+
90
+ test('boolean - open should be rendered', async () => {
91
+ const result = await page.evaluate(() => {
92
+ const el = document.getElementById("test"),
93
+ result = el.getElementById("open");
94
+ return JSON.parse(result.innerText);
95
+ });
96
+ expect(result).toBe(true);
97
+ });
98
+
99
+ test('number - count should be rendered', async () => {
100
+ const result = await page.evaluate(() => {
101
+ const el = document.getElementById("test"),
102
+ result = el.getElementById("count");
103
+ return JSON.parse(result.innerText);
104
+ });
105
+ expect(result).toBe(1);
106
+ });
107
+
108
+ test('string - name should be rendered', async () => {
109
+ const result = await page.evaluate(() => {
110
+ const el = document.getElementById("test"),
111
+ result = el.getElementById("name");
112
+ return result.innerText;
113
+ });
114
+ expect(result).toBe("joe");
115
+ });
116
+
117
+ test('object - children should be rendered', async () => {
118
+ const result = await page.evaluate(() => {
119
+ const el = document.getElementById("test"),
120
+ result = el.getElementById("children");
121
+ return JSON.parse(result.innerText);
122
+ });
123
+ expect(Array.isArray(result)).toBe(true);
124
+ expect(result[0]).toBe("mary");
125
+ expect(result.length).toBe(1);
126
+ });
127
+
128
+ test('boolean - checked should be rendered', async () => {
129
+ const result = await page.evaluate(() => {
130
+ const el = document.getElementById("test"),
131
+ result = el.getElementById("checked");
132
+ return JSON.parse(result.innerText);
133
+ });
134
+ expect(result).toBe(true);
135
+ });
136
+
137
+ test('number - age should be rendered', async () => {
138
+ const result = await page.evaluate(() => {
139
+ const el = document.getElementById("test"),
140
+ result = el.getElementById("age");
141
+ return JSON.parse(result.innerText);
142
+ });
143
+ expect(result).toBe(27);
144
+ });
145
+
146
+ test('string - color should be rendered', async () => {
147
+ const result = await page.evaluate(() => {
148
+ const el = document.getElementById("test"),
149
+ result = el.getElementById("color");
150
+ return result.innerText;
151
+ });
152
+ expect(result).toBe("green");
153
+ });
154
+
155
+ test('object - hamburger should be rendered', async () => {
156
+ const result = await page.evaluate(() => {
157
+ const el = document.getElementById("test"),
158
+ result = el.getElementById("hamburger");
159
+ return JSON.parse(result.innerText);
160
+ });
161
+ expect(Array.isArray(result)).toBe(true);
162
+ expect(result[0]).toBe("lettuce");
163
+ expect(result.length).toBe(1);
164
+ });
165
+
166
+ test('shared - myshare should be same', async () => {
167
+ const result = await page.evaluate(async () => {
168
+ const el0 = document.getElementById("test"),
169
+ el1 = document.getElementById("test1")
170
+ return [el0.getValue("myshare"),el1.getValue("myshare")];
171
+ });
172
+ expect(Array.isArray(result)).toBe(true);
173
+ expect(result[0]).toBe(result[1]);
174
+ });
175
+
176
+ test('untyped input - iuntyped should be "test"', async () => {
177
+ const result = await page.evaluate(async () => {
178
+ const el = document.getElementById("test"),
179
+ result = el.getElementById("iuntyped")
180
+ return result.getAttribute("value");
181
+ });
182
+ expect(result).toBe("test");
183
+ });
184
+
185
+ // "tel", "email", "url", "search", "radio", "color", "password"
186
+ ["text","tel","email", "url", "search", "radio", "color", "password"].forEach((type) => {
187
+ const f = Function(`return async () => {
188
+ const result = await page.evaluate(async () => {
189
+ const el = document.getElementById("test"),
190
+ result = el.getElementById("i${type}");
191
+ return {value:result.getAttribute("value"),variable:el.vars["i${type}"]};
192
+ });
193
+ const {value,variable} = result;
194
+ expect(value).toBe("test");
195
+ expect(variable.name).toBe("i${type}");
196
+ expect(variable.type).toBe("string");
197
+ expect(variable.value).toBe(value);
198
+ }`)();
199
+ test(`${type} input - i${type} should be "test"`,f);
200
+ });
201
+
202
+ test('number input - inumber should be 1', async () => {
203
+ const result = await page.evaluate(async () => {
204
+ const el = document.getElementById("test"),
205
+ result = el.getElementById("inumber")
206
+ return JSON.parse(result.getAttribute("value"));
207
+ });
208
+ expect(result).toBe(1);
209
+ });
210
+
211
+ test('range input - irange should be 1', async () => {
212
+ const result = await page.evaluate(async () => {
213
+ const el = document.getElementById("test"),
214
+ result = el.getElementById("irange")
215
+ return JSON.parse(result.getAttribute("value"));
216
+ });
217
+ expect(result).toBe(1);
218
+ });
219
+
220
+ test('datetime input - idatetime should be current date', async () => {
221
+ const result = await page.evaluate(async () => {
222
+ const el = document.getElementById("test"),
223
+ result = el.getElementById("idatetime")
224
+ return result.getAttribute("value");
225
+ });
226
+ const dt = new Date(result);
227
+ expect(dt).toBeInstanceOf(Date);
228
+ expect(dt.toString()).toBe(result);
229
+ });
230
+
231
+ test('checkbox input - icheckbox should be true', async () => {
232
+ const result = await page.evaluate(async () => {
233
+ const el = document.getElementById("test"),
234
+ result = el.getElementById("icheckbox")
235
+ return JSON.parse(result.getAttribute("value"));
236
+ });
237
+ expect(result).toBe(true);
238
+ });
239
+
240
+ test('on:<handler> - count should be bumped', async () => {
241
+ await page.click("#test",{waitUntil:"load"});
242
+ const result = await page.evaluate(async () => {
243
+ const el = document.getElementById("test");
244
+ return JSON.parse(el.getValue("counter"));
245
+ });
246
+ expect(result).toBe(1);
247
+ });
248
+ });
package/top.html ADDED
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <head>
3
+ <title>Top</title>
4
+ <link href="./nested.html" rel="module">
5
+ <script src="./lightview.js?as=x-body"></script>
6
+ </head>
7
+ <body>
8
+ <l-nested></l-nested>
9
+ </body>
10
+ </html>