lightview 1.4.5-b → 1.4.10-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 +2 -2
- package/codepen-no-tabs-embed.css +2 -0
- package/{counter.html → examples/counter.html} +1 -0
- package/{directives.html → examples/directives.html} +1 -1
- package/{remote.html → examples/foreign.html} +1 -1
- package/{remoteform.html → examples/forgeinform.html} +1 -1
- package/examples/form.html +65 -0
- package/examples/message.html +13 -0
- package/examples/nested.html +11 -0
- package/examples/remote-server.js +51 -0
- package/examples/remote.html +30 -0
- package/examples/remote.json +1 -0
- package/{scratch.html → examples/scratch.html} +1 -1
- package/examples/template.html +33 -0
- package/examples/top.html +10 -0
- package/examples/types.html +73 -0
- package/{xor.html → examples/xor.html} +1 -1
- package/lightview.js +334 -135
- package/package.json +2 -2
- package/test/basic.html +4 -2
- package/test/basic.test.mjs +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# lightview v1.4.
|
|
1
|
+
# lightview v1.4.10b (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: <
|
|
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
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<head>
|
|
4
4
|
<title>Form</title>
|
|
5
|
-
<script src="
|
|
5
|
+
<script src="../lightview.js?as=x-body"></script>
|
|
6
6
|
<script>Lightview.whenFramed(({as,unhide,importAnchors,isolated,enableFrames}) => {
|
|
7
7
|
Lightview.bodyAsComponent({as,unhide,importAnchors,isolated,enableFrames});
|
|
8
8
|
})</script>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<title>Form</title>
|
|
6
|
+
<script src="../lightview.js?as=x-body"></script>
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<div style="margin:20px;padding:5px;border:1px;border-style:solid;border-color:${color}">
|
|
11
|
+
<p>
|
|
12
|
+
<input type="text" value="${color}">
|
|
13
|
+
<input type="radio" name="color" value="red">
|
|
14
|
+
<input type="radio" name="color" value="yellow">
|
|
15
|
+
<input type="radio" name="color" value="green">
|
|
16
|
+
<select value="${color}">
|
|
17
|
+
<option value="red">red</option>
|
|
18
|
+
<option>yellow</option>
|
|
19
|
+
<option> green</option>
|
|
20
|
+
</select>
|
|
21
|
+
<div>Hamburger options:</div>
|
|
22
|
+
<select value="${hamburger}" multiple>
|
|
23
|
+
<option value="lettuce">lettuce</option>
|
|
24
|
+
<option value="tomato">tomato</option>
|
|
25
|
+
<option>cheese</option>
|
|
26
|
+
</select>
|
|
27
|
+
</p>
|
|
28
|
+
Expose: <input type="checkbox" value="${checked}">
|
|
29
|
+
<p l-if="${checked}">
|
|
30
|
+
Now you've done it. You've exposed me.
|
|
31
|
+
</p>
|
|
32
|
+
<p id="variables">
|
|
33
|
+
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<script type="lightview/module">
|
|
37
|
+
self.variables({
|
|
38
|
+
color: string,
|
|
39
|
+
checked: boolean,
|
|
40
|
+
hamburger: Array
|
|
41
|
+
}, {
|
|
42
|
+
reactive
|
|
43
|
+
});
|
|
44
|
+
color = "red";
|
|
45
|
+
checked = true;
|
|
46
|
+
hamburger = ["cheese"];
|
|
47
|
+
|
|
48
|
+
// demo instrumentation
|
|
49
|
+
const variableValues = () => {
|
|
50
|
+
const el = self.getElementById("variables");
|
|
51
|
+
while (el.lastElementChild) el.lastElementChild.remove();
|
|
52
|
+
self.getVariableNames().forEach((name) => {
|
|
53
|
+
const line = document.createElement("div");
|
|
54
|
+
line.innerText = `${name} = ${JSON.stringify(self.getValue(name))}`;
|
|
55
|
+
el.appendChild(line);
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
variableValues();
|
|
59
|
+
addEventListener("change", () => {
|
|
60
|
+
variableValues()
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
63
|
+
</body>
|
|
64
|
+
|
|
65
|
+
</html>
|
|
@@ -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>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const http = require("http"),
|
|
2
|
+
fs = require("fs"),
|
|
3
|
+
host = 'localhost',
|
|
4
|
+
port = 8000,
|
|
5
|
+
requestListener = async function (req, res) {
|
|
6
|
+
const path = `.${req.url}`;
|
|
7
|
+
res.setHeader("Access-Control-Allow-Origin","*");
|
|
8
|
+
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
9
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
10
|
+
res.setHeader("Content-Type", "application/json");
|
|
11
|
+
if(req.method==="OPTIONS") {
|
|
12
|
+
res.end();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if(req.method==="GET") {
|
|
16
|
+
console.log("GET",req.url);
|
|
17
|
+
res.write(fs.readFileSync(path));
|
|
18
|
+
res.end();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const buffers = [];
|
|
22
|
+
for await(const chunk of req) {
|
|
23
|
+
buffers.push(chunk);
|
|
24
|
+
}
|
|
25
|
+
const data = JSON.parse(Buffer.concat(buffers).toString());
|
|
26
|
+
console.log(req.method,req.url,data);
|
|
27
|
+
if(req.method==="PUT") {
|
|
28
|
+
const string = JSON.stringify(data);
|
|
29
|
+
fs.writeFileSync(path,string);
|
|
30
|
+
res.write(string);
|
|
31
|
+
res.end();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if(req.method==="PATCH") {
|
|
35
|
+
const {property,value,oldValue} = data,
|
|
36
|
+
json = JSON.parse(fs.readFileSync(path));
|
|
37
|
+
if(property!==undefined && json[property]===oldValue) { // probably need a deepEqual for a production use
|
|
38
|
+
json[property] = value;
|
|
39
|
+
fs.writeFileSync(path,JSON.stringify(json))
|
|
40
|
+
}
|
|
41
|
+
res.write(JSON.stringify(json));
|
|
42
|
+
res.end();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
server = http.createServer(requestListener);
|
|
47
|
+
server.listen(port, host, () => {
|
|
48
|
+
console.log(`Server is running on http://${host}:${port}`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>Remote</title>
|
|
6
|
+
<script src="../lightview.js?as=x-body"></script>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
|
|
10
|
+
<input id="myRemote" type=text" value="${JSON.stringify(myRemote)}" size="${JSON.stringify(myRemote).length}"><br>
|
|
11
|
+
<button l-on:click="patch">Patch</button><br>
|
|
12
|
+
<button l-on:click="replace">Replace</button>
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
<script type="lightview/module">
|
|
16
|
+
self.variables({myRemote:object},{reactive,remote:"http://localhost:8000/remote.json"});
|
|
17
|
+
|
|
18
|
+
await myRemote; // must await remotes before the first time they are used, e.g. before HTML is rendered
|
|
19
|
+
|
|
20
|
+
self.patch = () => {
|
|
21
|
+
const json = JSON.parse(document.body.getElementById("myRemote").value);
|
|
22
|
+
Object.assign(myRemote,json);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
self.replace = () => {
|
|
26
|
+
myRemote = JSON.parse(document.body.getElementById("myRemote").value);
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"joe","age":20}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<title>Scratch</title>
|
|
6
|
-
<script src="
|
|
6
|
+
<script src="../lightview.js?as=x-body"></script>
|
|
7
7
|
</head>
|
|
8
8
|
<body>
|
|
9
9
|
<div style="margin:20px;padding:5px;border:1px;border-style:solid;border-color:${color}">
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<title>Template</title>
|
|
6
|
+
<template id="local-component">
|
|
7
|
+
<p>
|
|
8
|
+
<button l-on:click="click">Click Me</button>
|
|
9
|
+
</p>
|
|
10
|
+
<p>
|
|
11
|
+
${message ? message : ""}
|
|
12
|
+
</p>
|
|
13
|
+
<script type="lightview/module">
|
|
14
|
+
self.variables({message: string}, {reactive});
|
|
15
|
+
|
|
16
|
+
self.click = (event) => {
|
|
17
|
+
message = "Hi there!";
|
|
18
|
+
};
|
|
19
|
+
</script>
|
|
20
|
+
</template>
|
|
21
|
+
<script src="../lightview.js"></script>
|
|
22
|
+
<script>
|
|
23
|
+
Lightview.createComponent("x-hello", document.getElementById("local-component"));
|
|
24
|
+
</script>
|
|
25
|
+
</head>
|
|
26
|
+
|
|
27
|
+
<body>
|
|
28
|
+
<div style="margin:20px">
|
|
29
|
+
<x-hello></x-hello>
|
|
30
|
+
</div>
|
|
31
|
+
</body>
|
|
32
|
+
|
|
33
|
+
</html>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<title>Types</title>
|
|
6
|
+
<script src="../lightview.js?as=x-body"></script>
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<div style="margin:20px">
|
|
11
|
+
<p>
|
|
12
|
+
<button l-on:click="run">Run</button>
|
|
13
|
+
<button l-on:click="clear">Clear</button>
|
|
14
|
+
</p>
|
|
15
|
+
<p id="console"></p>
|
|
16
|
+
</div>
|
|
17
|
+
<script type="lightview/module">
|
|
18
|
+
self.variables({
|
|
19
|
+
astring: string,
|
|
20
|
+
aDate: Date,
|
|
21
|
+
err: Error
|
|
22
|
+
});
|
|
23
|
+
self.run = () => {
|
|
24
|
+
try {
|
|
25
|
+
astring = "my string";
|
|
26
|
+
} catch (e) {
|
|
27
|
+
err = e;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
astring = 1;
|
|
31
|
+
} catch (e) {
|
|
32
|
+
err = e;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
aDate = new Date();
|
|
36
|
+
} catch (e) {
|
|
37
|
+
err = e;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
aDate = 1;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
err = e;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
err = 1;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
err = e;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// demo instrumentation
|
|
51
|
+
self.clear = () => {
|
|
52
|
+
const cnsl = self.getElementById("console");
|
|
53
|
+
while (cnsl.lastChild) cnsl.lastChild.remove();
|
|
54
|
+
};
|
|
55
|
+
addEventListener("change", ({
|
|
56
|
+
variableName,
|
|
57
|
+
value
|
|
58
|
+
}) => {
|
|
59
|
+
const cnsl = self.getElementById("console");
|
|
60
|
+
if (cnsl) {
|
|
61
|
+
const message = document.createElement("div");
|
|
62
|
+
if (variableName === "err") {
|
|
63
|
+
message.innerHTML = `<b>></b> ${value}<br>`;
|
|
64
|
+
} else {
|
|
65
|
+
message.innerHTML = `<b>></b> ${variableName} = ${value}<br>`;
|
|
66
|
+
}
|
|
67
|
+
cnsl.appendChild(message);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
</body>
|
|
72
|
+
|
|
73
|
+
</html>
|
package/lightview.js
CHANGED
|
@@ -30,6 +30,24 @@ const {observe} = (() => {
|
|
|
30
30
|
let CURRENTOBSERVER;
|
|
31
31
|
const parser = new DOMParser();
|
|
32
32
|
|
|
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
|
+
|
|
44
|
+
function escapeHTML(html) {
|
|
45
|
+
escaper.textContent = html;
|
|
46
|
+
return escaper.innerHTML;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Lightview.escapeHTML = escapeHTML;
|
|
50
|
+
|
|
33
51
|
const addListener = (node, eventName, callback) => {
|
|
34
52
|
node.addEventListener(eventName, callback); // just used to make code footprint smaller
|
|
35
53
|
}
|
|
@@ -52,7 +70,8 @@ const {observe} = (() => {
|
|
|
52
70
|
return "l-" + name;
|
|
53
71
|
}
|
|
54
72
|
const observe = (f, thisArg, argsList = []) => {
|
|
55
|
-
|
|
73
|
+
const observer = (...args) => {
|
|
74
|
+
if(observer.cancelled) return;
|
|
56
75
|
CURRENTOBSERVER = observer;
|
|
57
76
|
try {
|
|
58
77
|
f.call(thisArg || this, ...argsList, ...args);
|
|
@@ -61,7 +80,6 @@ const {observe} = (() => {
|
|
|
61
80
|
}
|
|
62
81
|
CURRENTOBSERVER = null;
|
|
63
82
|
}
|
|
64
|
-
|
|
65
83
|
observer.cancel = () => observer.cancelled = true;
|
|
66
84
|
observer();
|
|
67
85
|
return observer;
|
|
@@ -73,7 +91,7 @@ const {observe} = (() => {
|
|
|
73
91
|
if (toType === "number") return parseFloat(value + "");
|
|
74
92
|
if (toType === "boolean") {
|
|
75
93
|
if (["on", "checked", "selected"].includes(value)) return true;
|
|
76
|
-
if(value==null || value==="") return false;
|
|
94
|
+
if (value == null || value === "") return false;
|
|
77
95
|
try {
|
|
78
96
|
const parsed = JSON.parse(value + "");
|
|
79
97
|
if (typeof (parsed) === "boolean") return parsed;
|
|
@@ -96,10 +114,10 @@ const {observe} = (() => {
|
|
|
96
114
|
if (instance instanceof Array) {
|
|
97
115
|
let parsed = tryParse(value.startsWith("[") ? value : `[${value}]`);
|
|
98
116
|
if (!Array.isArray(parsed)) {
|
|
99
|
-
if(value.includes(",")) parsed = value.split(",");
|
|
117
|
+
if (value.includes(",")) parsed = value.split(",");
|
|
100
118
|
else {
|
|
101
119
|
parsed = tryParse(`["${value}"]`);
|
|
102
|
-
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;
|
|
103
121
|
}
|
|
104
122
|
}
|
|
105
123
|
if (!Array.isArray(parsed)) {
|
|
@@ -128,14 +146,16 @@ const {observe} = (() => {
|
|
|
128
146
|
}
|
|
129
147
|
throw new TypeError(`Unable to coerce ${value} to ${toType}`)
|
|
130
148
|
}
|
|
131
|
-
const Reactor = (
|
|
132
|
-
if (
|
|
133
|
-
if (
|
|
149
|
+
const Reactor = (data) => {
|
|
150
|
+
if (data && typeof (data) === "object") {
|
|
151
|
+
if (data.__isReactor__) return data;
|
|
134
152
|
const childReactors = [],
|
|
135
153
|
dependents = {},
|
|
136
|
-
proxy = new Proxy(
|
|
154
|
+
proxy = new Proxy(data, {
|
|
137
155
|
get(target, property) {
|
|
138
156
|
if (property === "__isReactor__") return true;
|
|
157
|
+
if(property=== "__dependents__") return dependents;
|
|
158
|
+
if(property=== "__reactorProxyTarget__") return data;
|
|
139
159
|
if (target instanceof Array) {
|
|
140
160
|
if (property === "toJSON") return function toJSON() {
|
|
141
161
|
return [...target];
|
|
@@ -150,9 +170,10 @@ const {observe} = (() => {
|
|
|
150
170
|
const observers = dependents[property] ||= new Set();
|
|
151
171
|
observers.add(CURRENTOBSERVER)
|
|
152
172
|
}
|
|
173
|
+
if(value===undefined) return;
|
|
153
174
|
if (childReactors.includes(value) || (value && type !== "object") || typeof (property) === "symbol") {
|
|
154
|
-
//
|
|
155
|
-
if (type === "function" && [Date].includes(value)) value = value.bind(target)
|
|
175
|
+
// Dates and Promises must be bound to work with proxies
|
|
176
|
+
if (type === "function" && ([Date].includes(value) || property==="then")) value = value.bind(target)
|
|
156
177
|
return value;
|
|
157
178
|
}
|
|
158
179
|
if (value && type === "object") {
|
|
@@ -162,8 +183,14 @@ const {observe} = (() => {
|
|
|
162
183
|
target[property] = value;
|
|
163
184
|
return value;
|
|
164
185
|
},
|
|
165
|
-
set(target, property, value) {
|
|
186
|
+
async set(target, property, value) {
|
|
187
|
+
if(target instanceof Promise) {
|
|
188
|
+
console.warn(`Setting ${property} = ${value} on a Promise in Reactor`);
|
|
189
|
+
}
|
|
166
190
|
const type = typeof (value);
|
|
191
|
+
if(value && type==="object" && value instanceof Promise) {
|
|
192
|
+
value = await value;
|
|
193
|
+
}
|
|
167
194
|
if (target[property] !== value) {
|
|
168
195
|
if (value && type === "object") {
|
|
169
196
|
value = Reactor(value);
|
|
@@ -181,7 +208,7 @@ const {observe} = (() => {
|
|
|
181
208
|
});
|
|
182
209
|
return proxy;
|
|
183
210
|
}
|
|
184
|
-
return
|
|
211
|
+
return data;
|
|
185
212
|
}
|
|
186
213
|
|
|
187
214
|
class VariableEvent {
|
|
@@ -198,6 +225,7 @@ const {observe} = (() => {
|
|
|
198
225
|
return value;
|
|
199
226
|
},
|
|
200
227
|
set(target, property, newValue) {
|
|
228
|
+
//if(newValue && typeof(newValue)==="object" && newValue instanceof Promise) newValue = await newValue;
|
|
201
229
|
const event = new VariableEvent({variableName: property, value: newValue});
|
|
202
230
|
if (target[property] === undefined) {
|
|
203
231
|
target[property] = {type: "any", value: newValue}; // should we allow this, do first to prevent loops
|
|
@@ -205,7 +233,7 @@ const {observe} = (() => {
|
|
|
205
233
|
if (event.defaultPrevented) delete target[property].value;
|
|
206
234
|
return true;
|
|
207
235
|
}
|
|
208
|
-
const {type, value, shared, exported, constant, reactive} = target[property];
|
|
236
|
+
const {type, value, shared, exported, constant, reactive, remote} = target[property];
|
|
209
237
|
if (constant) throw new TypeError(`${property}:${type} is a constant`);
|
|
210
238
|
const newtype = typeof (newValue),
|
|
211
239
|
typetype = typeof (type);
|
|
@@ -214,7 +242,11 @@ const {observe} = (() => {
|
|
|
214
242
|
event.oldValue = value;
|
|
215
243
|
target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
|
|
216
244
|
target.postEvent.value("change", event);
|
|
217
|
-
if (event.defaultPrevented)
|
|
245
|
+
if (event.defaultPrevented) {
|
|
246
|
+
target[property].value = value;
|
|
247
|
+
} else if(remote) {
|
|
248
|
+
handleRemote({variable:target[property],remote,reactive},true);
|
|
249
|
+
}
|
|
218
250
|
}
|
|
219
251
|
return true;
|
|
220
252
|
}
|
|
@@ -232,7 +264,7 @@ const {observe} = (() => {
|
|
|
232
264
|
const observer = new MutationObserver((mutations) => {
|
|
233
265
|
mutations.forEach((mutation) => {
|
|
234
266
|
if (mutation.type === "attributes") {
|
|
235
|
-
if (framed) debugger;
|
|
267
|
+
//if (framed) debugger;
|
|
236
268
|
const name = mutation.attributeName,
|
|
237
269
|
target = mutation.target,
|
|
238
270
|
value = target.getAttribute(name);
|
|
@@ -275,39 +307,48 @@ const {observe} = (() => {
|
|
|
275
307
|
nodes.push(root, ...getNodes(root.shadowRoot))
|
|
276
308
|
} else {
|
|
277
309
|
for (const node of root.childNodes) {
|
|
310
|
+
if (node.tagName === "SCRIPT") continue;
|
|
278
311
|
if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.includes("${")) {
|
|
279
312
|
node.template ||= node.nodeValue;
|
|
280
313
|
nodes.push(node);
|
|
281
314
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
282
|
-
let skip;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
315
|
+
let skip, pushed;
|
|
316
|
+
[...node.attributes].forEach((attr) => {
|
|
317
|
+
if (attr.value.includes("${")) {
|
|
318
|
+
attr.template ||= attr.value;
|
|
319
|
+
pushed = true;
|
|
320
|
+
nodes.push(node);
|
|
321
|
+
} else if (attr.name.includes(":") || attr.name.startsWith("l-")) {
|
|
322
|
+
skip = attr.name.includes("l-for:");
|
|
323
|
+
pushed = true;
|
|
324
|
+
nodes.push(node)
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
if (!pushed && node.getAttribute("type") === "radio") nodes.push(node);
|
|
293
328
|
if (!skip && !node.shadowRoot) nodes.push(...getNodes(node));
|
|
294
329
|
}
|
|
295
330
|
}
|
|
296
331
|
}
|
|
297
332
|
return nodes;
|
|
298
333
|
}
|
|
299
|
-
const
|
|
300
|
-
|
|
334
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
335
|
+
const resolveNodeOrText = (node, component, safe) => {
|
|
336
|
+
const type = typeof (node),
|
|
337
|
+
template = type === "string" ? node.trim() : node.template;
|
|
338
|
+
if (template) {
|
|
301
339
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
340
|
+
let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
|
|
341
|
+
value = node.nodeType === Node.TEXT_NODE || !safe ? value : Lightview.escapeHTML(value);
|
|
342
|
+
if (type === "string") return value;
|
|
343
|
+
node.nodeValue = value == "null" || value == "undefined" ? "" : value;
|
|
304
344
|
} catch (e) {
|
|
345
|
+
console.warn(e);
|
|
305
346
|
if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
|
|
306
347
|
}
|
|
307
348
|
}
|
|
308
349
|
return node?.nodeValue;
|
|
309
350
|
}
|
|
310
|
-
const render =
|
|
351
|
+
const render = (hasTemplate, render) => {
|
|
311
352
|
let observer;
|
|
312
353
|
if (hasTemplate) {
|
|
313
354
|
if (observer) observer.cancel();
|
|
@@ -324,43 +365,47 @@ const {observe} = (() => {
|
|
|
324
365
|
if (["checkbox"].includes(inputType)) return "boolean";
|
|
325
366
|
return "any";
|
|
326
367
|
}
|
|
327
|
-
const
|
|
328
|
-
[...node.querySelectorAll('a[href][target^="#"]')].forEach((node) => {
|
|
368
|
+
const importAnchors = (node, component) => {
|
|
369
|
+
[...node.querySelectorAll('a[href$=".html"][target^="#"]')].forEach((node) => {
|
|
329
370
|
node.removeEventListener("click", anchorHandler);
|
|
330
371
|
addListener(node, "click", anchorHandler);
|
|
331
372
|
})
|
|
332
373
|
}
|
|
333
|
-
const
|
|
374
|
+
const bound = new WeakSet();
|
|
375
|
+
const bindInput = (input, variableName, component, value) => {
|
|
376
|
+
if (bound.has(input)) return;
|
|
377
|
+
bound.add(input);
|
|
334
378
|
const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
|
|
335
379
|
type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype),
|
|
336
380
|
deflt = input.getAttribute("default");
|
|
337
381
|
value ||= input.getAttribute("value");
|
|
338
|
-
let variable = component.vars[
|
|
382
|
+
let variable = component.vars[variableName] || {type};
|
|
339
383
|
if (type !== variable.type) {
|
|
340
384
|
if (variable.type === "any" || variable.type === "unknown") variable.type = type;
|
|
341
|
-
else throw new TypeError(`Attempt to bind <input name="${
|
|
385
|
+
else throw new TypeError(`Attempt to bind <input name="${variableName}" type="${type}"> to variable ${variableName}:${variable.type}`)
|
|
342
386
|
}
|
|
343
|
-
component.variables({[
|
|
344
|
-
component.setValue(
|
|
387
|
+
component.variables({[variableName]: type});
|
|
388
|
+
if(inputtype!=="radio") component.setValue(variableName, value);
|
|
345
389
|
let eventname = "change";
|
|
346
390
|
if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
|
|
347
391
|
eventname = "input";
|
|
348
392
|
}
|
|
349
|
-
|
|
350
|
-
event.stopImmediatePropagation();
|
|
351
|
-
|
|
352
|
-
let value = target.value;
|
|
393
|
+
const listener = (event) => {
|
|
394
|
+
if (event) event.stopImmediatePropagation();
|
|
395
|
+
let value = input.value;
|
|
353
396
|
if (inputtype === "checkbox") {
|
|
354
397
|
value = input.checked
|
|
355
|
-
} else if (
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
398
|
+
} else if (input.tagName === "SELECT") {
|
|
399
|
+
if (input.hasAttribute("multiple")) {
|
|
400
|
+
const varvalue = component.varsProxy[variableName];
|
|
401
|
+
value = [...input.querySelectorAll("option")]
|
|
402
|
+
.filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value) //todo make sync comopat
|
|
359
403
|
.map((option) => option.getAttribute("value") || option.innerText);
|
|
360
404
|
}
|
|
361
405
|
}
|
|
362
|
-
component.varsProxy[
|
|
363
|
-
}
|
|
406
|
+
component.varsProxy[variableName] = coerce(value, type);
|
|
407
|
+
};
|
|
408
|
+
addListener(input, eventname, listener);
|
|
364
409
|
}
|
|
365
410
|
const tryParse = (value) => {
|
|
366
411
|
try {
|
|
@@ -370,16 +415,19 @@ const {observe} = (() => {
|
|
|
370
415
|
}
|
|
371
416
|
}
|
|
372
417
|
let reserved = {
|
|
418
|
+
any: {value: "any", constant: true},
|
|
373
419
|
boolean: {value: "boolean", constant: true},
|
|
374
420
|
string: {value: "string", constant: true},
|
|
375
421
|
number: {value: "number", constant: true},
|
|
422
|
+
object: {value: "object", constant: true},
|
|
376
423
|
observed: {value: true, constant: true},
|
|
377
424
|
reactive: {value: true, constant: true},
|
|
378
425
|
shared: {value: true, constant: true},
|
|
379
426
|
exported: {value: true, constant: true},
|
|
380
|
-
imported: {value: true, constant: true}
|
|
427
|
+
imported: {value: true, constant: true},
|
|
428
|
+
remote: {}
|
|
381
429
|
};
|
|
382
|
-
const createClass = (domElementNode, {observer,
|
|
430
|
+
const createClass = (domElementNode, {observer, framed}) => {
|
|
383
431
|
const instances = new Set(),
|
|
384
432
|
dom = domElementNode.tagName === "TEMPLATE"
|
|
385
433
|
? domElementNode.content.cloneNode(true)
|
|
@@ -399,6 +447,17 @@ const {observe} = (() => {
|
|
|
399
447
|
eventlisteners = {};
|
|
400
448
|
this.vars = {
|
|
401
449
|
...reserved,
|
|
450
|
+
changeListener: {
|
|
451
|
+
value: ({variableName, value}) => {
|
|
452
|
+
if (currentComponent.vars.changeListener.value.targets.has(variableName)) {
|
|
453
|
+
value = typeof (value) === "string" || !value ? value : JSON.stringify(value);
|
|
454
|
+
if (value == null) removeComponentAttribute(this, variableName);
|
|
455
|
+
else setComponentAttribute(this, variableName, value);
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
type: "function",
|
|
459
|
+
constant: true
|
|
460
|
+
},
|
|
402
461
|
addEventListener: {
|
|
403
462
|
value: (eventName, listener) => {
|
|
404
463
|
const listeners = eventlisteners[eventName] ||= new Set();
|
|
@@ -423,6 +482,8 @@ const {observe} = (() => {
|
|
|
423
482
|
};
|
|
424
483
|
this.defaultAttributes = domElementNode.tagName === "TEMPLATE" ? domElementNode.attributes : dom.attributes;
|
|
425
484
|
this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
|
|
485
|
+
this.vars.changeListener.value.targets = new Set();
|
|
486
|
+
this.varsProxy.addEventListener("change", this.varsProxy.changeListener);
|
|
426
487
|
if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported: true});
|
|
427
488
|
["getElementById", "querySelector", "querySelectorAll"]
|
|
428
489
|
.forEach((fname) => {
|
|
@@ -433,7 +494,7 @@ const {observe} = (() => {
|
|
|
433
494
|
})
|
|
434
495
|
});
|
|
435
496
|
[...dom.childNodes].forEach((child) => shadow.appendChild(child.cloneNode(true)));
|
|
436
|
-
|
|
497
|
+
importAnchors(shadow, this);
|
|
437
498
|
}
|
|
438
499
|
|
|
439
500
|
get siblings() {
|
|
@@ -464,7 +525,7 @@ const {observe} = (() => {
|
|
|
464
525
|
}
|
|
465
526
|
currentScript.classList.remove("lightview");
|
|
466
527
|
const text = script.innerHTML.replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1").replaceAll(/\r?\n/g, "");
|
|
467
|
-
currentScript.innerHTML = `
|
|
528
|
+
currentScript.innerHTML = `Object.getPrototypeOf(async function(){}).constructor('if(window["${scriptid}"]?.ctx) { with(window["${scriptid}"].ctx) { ${text}; } window["${scriptid}"](); }')(); `;
|
|
468
529
|
let resolver;
|
|
469
530
|
promises.push(new Promise((resolve) => resolver = resolve));
|
|
470
531
|
window[scriptid] = () => {
|
|
@@ -479,19 +540,19 @@ const {observe} = (() => {
|
|
|
479
540
|
const nodes = getNodes(ctx);
|
|
480
541
|
nodes.forEach((node) => {
|
|
481
542
|
if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
|
|
482
|
-
render(!!node.template,
|
|
543
|
+
render(!!node.template, () => resolveNodeOrText(node, this))
|
|
483
544
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
484
545
|
// resolve the value before all else;
|
|
485
546
|
const attr = node.attributes.value;
|
|
486
547
|
if (attr && attr.template) {
|
|
487
|
-
render(!!attr.template,
|
|
488
|
-
const value =
|
|
489
|
-
eltype =
|
|
490
|
-
if(node.attributes.value) {
|
|
548
|
+
render(!!attr.template, () => {
|
|
549
|
+
const value = resolveNodeOrText(attr, this),
|
|
550
|
+
eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
|
|
551
|
+
if (node.attributes.value) {
|
|
491
552
|
const template = attr.template;
|
|
492
|
-
if(/\$\{[a-zA-z_]+\}/g.test(template)) {
|
|
493
|
-
const name = template.substring(2,template.length-1);
|
|
494
|
-
|
|
553
|
+
if (/\$\{[a-zA-z_]+\}/g.test(template)) {
|
|
554
|
+
const name = template.substring(2, template.length - 1);
|
|
555
|
+
bindInput(node, name, this, value);
|
|
495
556
|
}
|
|
496
557
|
}
|
|
497
558
|
if (eltype === "checkbox") {
|
|
@@ -502,19 +563,19 @@ const {observe} = (() => {
|
|
|
502
563
|
node.removeAttribute("checked");
|
|
503
564
|
node.checked = false;
|
|
504
565
|
}
|
|
505
|
-
const vname =
|
|
566
|
+
const vname = resolveNodeOrText(node.attributes.name, ctx);
|
|
506
567
|
if (vname) ctx.setValue(vname, node.checked, {coerceTo: "boolean"});
|
|
507
568
|
}
|
|
508
569
|
if (node.tagName === "SELECT") {
|
|
509
570
|
let values = [value];
|
|
510
571
|
if (node.hasAttribute("multiple")) values = coerce(value, Array);
|
|
511
|
-
[...node.querySelectorAll("option")].forEach((option) => {
|
|
572
|
+
[...node.querySelectorAll("option")].forEach(async (option) => {
|
|
512
573
|
if (option.hasAttribute("value")) {
|
|
513
|
-
if (values.includes(
|
|
574
|
+
if (values.includes(resolveNodeOrText(option.attributes.value, ctx))) {
|
|
514
575
|
option.setAttribute("selected", "");
|
|
515
576
|
option.selected = true;
|
|
516
577
|
}
|
|
517
|
-
} else if (option.innerText
|
|
578
|
+
} else if (values.includes(resolveNodeOrText(option.innerText, ctx))) {
|
|
518
579
|
option.setAttribute("selected", "");
|
|
519
580
|
option.selected = true;
|
|
520
581
|
}
|
|
@@ -522,18 +583,18 @@ const {observe} = (() => {
|
|
|
522
583
|
}
|
|
523
584
|
});
|
|
524
585
|
}
|
|
525
|
-
[...node.attributes].forEach((attr) => {
|
|
526
|
-
if (attr.name === "value") return;
|
|
586
|
+
[...node.attributes].forEach(async (attr) => {
|
|
587
|
+
if (attr.name === "value" && attr.template) return;
|
|
527
588
|
const {name, value} = attr;
|
|
528
589
|
if (name === "type") {
|
|
529
|
-
if (value === "radio") {
|
|
530
|
-
const name =
|
|
590
|
+
if (value === "radio" && node.attributes.name) {
|
|
591
|
+
const name = resolveNodeOrText(node.attributes.name, ctx);
|
|
531
592
|
for (const vname of this.getVariableNames()) {
|
|
532
593
|
if (vname === name) {
|
|
533
|
-
render(true,
|
|
534
|
-
const name =
|
|
535
|
-
varvalue =
|
|
536
|
-
if (varvalue ==
|
|
594
|
+
render(true, () => {
|
|
595
|
+
const name = resolveNodeOrText(node.attributes.name, ctx),
|
|
596
|
+
varvalue = Function("context", "with(context) { return `${" + name + "}` }")(ctx.varsProxy);
|
|
597
|
+
if (node.attributes.value && varvalue == resolveNodeOrText(node.attributes.value, ctx)) {
|
|
537
598
|
node.setAttribute("checked", "");
|
|
538
599
|
node.checked = true;
|
|
539
600
|
} else {
|
|
@@ -550,45 +611,48 @@ const {observe} = (() => {
|
|
|
550
611
|
|
|
551
612
|
const [type, ...params] = name.split(":");
|
|
552
613
|
if (type === "") { // name is :something
|
|
553
|
-
render(!!attr.template,
|
|
554
|
-
const value = attr.value
|
|
555
|
-
elvalue = resolveNode(node.attributes.value, ctx),
|
|
556
|
-
eltype = resolveNode(node.attributes.type, ctx),
|
|
557
|
-
elname = resolveNode(node.attributes.name, ctx);
|
|
614
|
+
render(!!attr.template, () => {
|
|
615
|
+
const value = attr.value;
|
|
558
616
|
if (params[0]) {
|
|
559
617
|
if (value === "true") node.setAttribute(params[0], "")
|
|
560
618
|
else node.removeAttribute(params[0]);
|
|
561
|
-
} else
|
|
562
|
-
|
|
563
|
-
|
|
619
|
+
} else {
|
|
620
|
+
const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx) : null,
|
|
621
|
+
eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx) : null;
|
|
622
|
+
if (eltype === "checkbox" || node.tagName === "OPTION") {
|
|
623
|
+
if (elvalue === "true") node.setAttribute("checked", "")
|
|
624
|
+
else node.removeAttribute("checked");
|
|
625
|
+
}
|
|
564
626
|
}
|
|
565
627
|
})
|
|
566
628
|
} else if (type === "l-on") {
|
|
567
629
|
let listener;
|
|
568
|
-
render(!!attr.template,
|
|
569
|
-
const value =
|
|
630
|
+
render(!!attr.template, () => {
|
|
631
|
+
const value = resolveNodeOrText(attr, this);
|
|
570
632
|
if (listener) node.removeEventListener(params[0], listener);
|
|
571
633
|
listener = this[value] || window[value] || Function(value);
|
|
572
634
|
addListener(node, params[0], listener);
|
|
573
635
|
})
|
|
574
636
|
} else if (type === "l-if") {
|
|
575
|
-
render(!!attr.template,
|
|
576
|
-
const value =
|
|
637
|
+
render(!!attr.template, () => {
|
|
638
|
+
const value = resolveNodeOrText(attr, this);
|
|
577
639
|
node.style.setProperty("display", value === "true" ? "revert" : "none");
|
|
578
640
|
})
|
|
579
641
|
} else if (type === "l-for") {
|
|
580
642
|
node.template ||= node.innerHTML;
|
|
581
|
-
render(!!attr.template,
|
|
643
|
+
render(!!attr.template, () => {
|
|
582
644
|
const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
|
|
583
|
-
value =
|
|
645
|
+
value = resolveNodeOrText(attr, this),
|
|
584
646
|
coerced = coerce(value, what === "each" ? Array : "object"),
|
|
585
647
|
target = what === "each" ? coerced : Object[what](coerced),
|
|
586
|
-
html = target.reduce((html, item, i, target) => {
|
|
587
|
-
return html += Function("context", "with(context) { return `" + node.template + "` }")(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
648
|
+
html = target.reduce( (html, item, i, target) => {
|
|
649
|
+
return html += Function("vars", "context", "with(vars) { with(context) { return `" + node.template + "` }}")(
|
|
650
|
+
ctx.varsProxy,
|
|
651
|
+
{
|
|
652
|
+
[vname]: item,
|
|
653
|
+
[index]: i,
|
|
654
|
+
[array]: target
|
|
655
|
+
})
|
|
592
656
|
}, ""),
|
|
593
657
|
parsed = parser.parseFromString(html, "text/html");
|
|
594
658
|
if (!window.lightviewDebug) {
|
|
@@ -604,7 +668,7 @@ const {observe} = (() => {
|
|
|
604
668
|
}
|
|
605
669
|
})
|
|
606
670
|
} else if (attr.template) {
|
|
607
|
-
render(!!attr.template,
|
|
671
|
+
render(!!attr.template, () => resolveNodeOrText(attr, this));
|
|
608
672
|
}
|
|
609
673
|
})
|
|
610
674
|
}
|
|
@@ -620,23 +684,23 @@ const {observe} = (() => {
|
|
|
620
684
|
//Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
|
|
621
685
|
}
|
|
622
686
|
|
|
623
|
-
connected(
|
|
624
|
-
this.connectedCallback =
|
|
687
|
+
connected(callback) {
|
|
688
|
+
this.connectedCallback = callback;
|
|
625
689
|
//Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
|
|
626
690
|
}
|
|
627
691
|
|
|
628
|
-
attributeChanged(
|
|
629
|
-
this.attributeChangedCallback =
|
|
692
|
+
attributeChanged(callback) {
|
|
693
|
+
this.attributeChangedCallback = callback;
|
|
630
694
|
//Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
|
|
631
695
|
}
|
|
632
696
|
|
|
633
|
-
disconnected(
|
|
697
|
+
disconnected(callback) {
|
|
634
698
|
Object.defineProperty(this, "disconnectedCallback", {
|
|
635
699
|
configurable: true,
|
|
636
700
|
writable: true,
|
|
637
701
|
value: () => {
|
|
638
702
|
value();
|
|
639
|
-
super.disconnectedCallback(
|
|
703
|
+
super.disconnectedCallback(callback);
|
|
640
704
|
}
|
|
641
705
|
});
|
|
642
706
|
}
|
|
@@ -647,19 +711,19 @@ const {observe} = (() => {
|
|
|
647
711
|
})
|
|
648
712
|
}
|
|
649
713
|
|
|
650
|
-
setValue(
|
|
714
|
+
setValue(variableName, value, {coerceTo = typeof (value)} = {}) {
|
|
651
715
|
if (!this.isConnected) {
|
|
652
716
|
instances.delete(this);
|
|
653
717
|
return false;
|
|
654
718
|
}
|
|
655
|
-
let {type} = this.vars[
|
|
719
|
+
let {type} = this.vars[variableName] || {};
|
|
656
720
|
if (type) {
|
|
657
721
|
value = coerce(value, type);
|
|
658
|
-
if (this.varsProxy[
|
|
659
|
-
const variable = this.vars[
|
|
722
|
+
if (this.varsProxy[variableName] !== value) {
|
|
723
|
+
const variable = this.vars[variableName];
|
|
660
724
|
if (variable.shared) {
|
|
661
725
|
const event = new VariableEvent({
|
|
662
|
-
variableName:
|
|
726
|
+
variableName: variableName,
|
|
663
727
|
value: value,
|
|
664
728
|
oldValue: variable.value
|
|
665
729
|
});
|
|
@@ -667,12 +731,12 @@ const {observe} = (() => {
|
|
|
667
731
|
this.vars.postEvent.value("change", event);
|
|
668
732
|
if (event.defaultPrevented) variable.value = value;
|
|
669
733
|
} else {
|
|
670
|
-
this.varsProxy[
|
|
734
|
+
this.varsProxy[variableName] = value;
|
|
671
735
|
}
|
|
672
736
|
}
|
|
673
737
|
return true;
|
|
674
738
|
}
|
|
675
|
-
this.vars[
|
|
739
|
+
this.vars[variableName] = {name, type: coerceTo, value: coerce(value, coerceTo)};
|
|
676
740
|
return false;
|
|
677
741
|
}
|
|
678
742
|
|
|
@@ -680,7 +744,7 @@ const {observe} = (() => {
|
|
|
680
744
|
return this.vars[variableName]?.value;
|
|
681
745
|
}
|
|
682
746
|
|
|
683
|
-
variables(variables, {observed, reactive, shared, exported, imported} = {}) { // options = {observed,reactive,shared,exported,imported}
|
|
747
|
+
variables(variables, {observed, reactive, shared, exported, imported, remote} = {}) { // options = {observed,reactive,shared,exported,imported}
|
|
684
748
|
const addEventListener = this.varsProxy.addEventListener;
|
|
685
749
|
if (variables !== undefined) {
|
|
686
750
|
Object.entries(variables)
|
|
@@ -707,11 +771,11 @@ const {observe} = (() => {
|
|
|
707
771
|
variable.exported = true;
|
|
708
772
|
// in case the export goes up to an iframe
|
|
709
773
|
if (variable.value != null) setComponentAttribute(this, key, variable.value);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
})
|
|
774
|
+
this.vars.changeListener.value.targets.add(key);
|
|
775
|
+
}
|
|
776
|
+
if (remote) {
|
|
777
|
+
variable.remote = remote;
|
|
778
|
+
handleRemote({variable, remote, reactive});
|
|
715
779
|
}
|
|
716
780
|
});
|
|
717
781
|
}
|
|
@@ -741,7 +805,136 @@ const {observe} = (() => {
|
|
|
741
805
|
}
|
|
742
806
|
}
|
|
743
807
|
}
|
|
744
|
-
|
|
808
|
+
|
|
809
|
+
const remoteProxy = ({json, variable,remote, reactive}) => {
|
|
810
|
+
const type = typeof (remote);
|
|
811
|
+
return new Proxy(json, {
|
|
812
|
+
get(target,property) {
|
|
813
|
+
if(property==="__remoteProxytarget__") return json;
|
|
814
|
+
return target[property];
|
|
815
|
+
},
|
|
816
|
+
async set(target, property, value) {
|
|
817
|
+
if(value && typeof(value)==="object" && value instanceof Promise) value = await value;
|
|
818
|
+
const oldValue = target[property];
|
|
819
|
+
if (oldValue !== value) {
|
|
820
|
+
let remotevalue;
|
|
821
|
+
if (type === "string") {
|
|
822
|
+
const href = new URL(remote,window.location.href).href;
|
|
823
|
+
remotevalue = patch({target,property,value,oldValue},href);
|
|
824
|
+
} else if(remote && type==="object") {
|
|
825
|
+
let href;
|
|
826
|
+
if(remote.path) href = new URL(remote.path,window.location.href).href;
|
|
827
|
+
if(!remote.patch) {
|
|
828
|
+
if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
|
|
829
|
+
remote.patch = patch;
|
|
830
|
+
}
|
|
831
|
+
remotevalue = remote.patch({target,property,value,oldValue},href);
|
|
832
|
+
}
|
|
833
|
+
if(remotevalue) {
|
|
834
|
+
await remotevalue.then((newjson) => {
|
|
835
|
+
if (newjson && typeof (newjson) === "object" && reactive) {
|
|
836
|
+
const target = variable.value?.__reactorProxyTarget__ ? json : variable.value;
|
|
837
|
+
Object.entries(newjson).forEach(([key,newValue]) => {
|
|
838
|
+
if(target[key]!==newValue) {
|
|
839
|
+
target[key] = newValue;
|
|
840
|
+
}
|
|
841
|
+
})
|
|
842
|
+
Object.keys(target).forEach((key) => {
|
|
843
|
+
if(!(key in newjson)) {
|
|
844
|
+
delete target[key];
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
if(variable.value?.__reactorProxyTarget__) {
|
|
848
|
+
const dependents = variable.value.__dependents__,
|
|
849
|
+
observers = dependents[property] || [];
|
|
850
|
+
[...observers].forEach((f) => {
|
|
851
|
+
if (f.cancelled) dependents[property].delete(f);
|
|
852
|
+
else f();
|
|
853
|
+
})
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
variable.value = json;
|
|
857
|
+
}
|
|
858
|
+
})
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return true;
|
|
862
|
+
}
|
|
863
|
+
})
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const patch = ({target,property,value,oldValue},href) => {
|
|
867
|
+
return fetch(href, {
|
|
868
|
+
method: "PATCH",
|
|
869
|
+
body: JSON.stringify({property,value,oldValue}),
|
|
870
|
+
headers: {
|
|
871
|
+
"Content-Type": "application/json"
|
|
872
|
+
}
|
|
873
|
+
}).then((response) => {
|
|
874
|
+
if (response.status < 400) return response.json();
|
|
875
|
+
})
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const get = ({variable,remote,reactive},path) => {
|
|
879
|
+
return fetch(path)
|
|
880
|
+
.then((response) => {
|
|
881
|
+
if (response.status < 400) return response.json();
|
|
882
|
+
})
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const put = ({variable,remote,reactive},href) => {
|
|
886
|
+
return fetch(href, {
|
|
887
|
+
method: "PUT",
|
|
888
|
+
body: JSON.stringify(variable.value),
|
|
889
|
+
headers: {
|
|
890
|
+
"Content-Type": "application/json"
|
|
891
|
+
}
|
|
892
|
+
}).then((response) => {
|
|
893
|
+
if (response.status === 200) {
|
|
894
|
+
return response.json();
|
|
895
|
+
}
|
|
896
|
+
})
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const handleRemote = async ({variable, remote, reactive},doput) => {
|
|
900
|
+
const type = typeof (remote);
|
|
901
|
+
let value;
|
|
902
|
+
if (type === "string") {
|
|
903
|
+
const href = new URL(remote,window.location.href).href;
|
|
904
|
+
value = (doput
|
|
905
|
+
? put({variable, remote, reactive},href)
|
|
906
|
+
: get({variable, remote, reactive},href));
|
|
907
|
+
if(variable.value===undefined) variable.value = value;
|
|
908
|
+
} else if (remote && type === "object") {
|
|
909
|
+
let href;
|
|
910
|
+
if(remote.path) href = new URL(remote.path,window.location.href).href;
|
|
911
|
+
if(!remote.get || !remote.put) {
|
|
912
|
+
if(!href) throw new Error(`A remote path is required is no put function is provided for remote data`)
|
|
913
|
+
if(!remote.get) remote.get = get;
|
|
914
|
+
if(!remote.put) remote.put = put;
|
|
915
|
+
}
|
|
916
|
+
value = (doput
|
|
917
|
+
? remote.put({variable, remote, reactive},href)
|
|
918
|
+
: remote.get({variable, remote, reactive},href));
|
|
919
|
+
if(remote.ttl && !doput && !remote.intervalId) {
|
|
920
|
+
remote.intervalId = setInterval(async () => {
|
|
921
|
+
await handleRemote({variable, remote, reactive});
|
|
922
|
+
})
|
|
923
|
+
}
|
|
924
|
+
if(variable.value===undefined) variable.value = value;
|
|
925
|
+
}
|
|
926
|
+
if(value) {
|
|
927
|
+
variable.value = await value.then((json) => {
|
|
928
|
+
if (json && typeof (json) === "object" && reactive) {
|
|
929
|
+
return remoteProxy({json, variable,remote, reactive})
|
|
930
|
+
} else {
|
|
931
|
+
return json;
|
|
932
|
+
}
|
|
933
|
+
})
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const createComponent = (name, node, {framed, observer} = {}) => {
|
|
745
938
|
let ctor = customElements.get(name);
|
|
746
939
|
if (ctor) {
|
|
747
940
|
if (framed && !ctor.lightviewFramed) {
|
|
@@ -751,38 +944,47 @@ const {observe} = (() => {
|
|
|
751
944
|
}
|
|
752
945
|
return ctor;
|
|
753
946
|
}
|
|
754
|
-
ctor = createClass(node, {observer,
|
|
947
|
+
ctor = createClass(node, {observer, framed});
|
|
755
948
|
customElements.define(name, ctor);
|
|
949
|
+
Lightview.customElements.set(name, ctor);
|
|
756
950
|
return ctor;
|
|
757
951
|
}
|
|
952
|
+
Lightview.customElements = new Map();
|
|
758
953
|
Lightview.createComponent = createComponent;
|
|
759
954
|
//Object.defineProperty(Lightview, "createComponent", {writable: true, configurable: true, value: createComponent})
|
|
760
955
|
const importLink = async (link, observer) => {
|
|
761
956
|
const url = (new URL(link.getAttribute("href"), window.location.href)),
|
|
762
957
|
as = link.getAttribute("as") || getNameFromPath(url.pathname);
|
|
763
|
-
if (url.hostname !== window.location.hostname) {
|
|
764
|
-
throw new URIError(`importLink:HTML imports must be from same domain: ${url.hostname}!=${location.hostname}
|
|
958
|
+
if (url.hostname !== window.location.hostname && !link.getAttribute("crossorigin")) {
|
|
959
|
+
throw new URIError(`importLink:HTML imports must be from same domain: ${url.hostname}!=${location.hostname} unless 'crossorigin' attribute is set.`)
|
|
765
960
|
}
|
|
766
961
|
if (!customElements.get(as)) {
|
|
767
962
|
const html = await (await fetch(url.href)).text(),
|
|
768
963
|
dom = parser.parseFromString(html, "text/html"),
|
|
769
|
-
|
|
770
|
-
|
|
964
|
+
unhide = !!dom.head.querySelector('meta[name="l-unhide"]'),
|
|
965
|
+
links = dom.head.querySelectorAll('link[href$=".html"][rel=module]');
|
|
966
|
+
for (const childlink of links) {
|
|
967
|
+
const href = childlink.getAttribute("href"),
|
|
968
|
+
childurl = new URL(href, url.href);
|
|
969
|
+
childlink.setAttribute("href", childurl.href);
|
|
970
|
+
if (link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin", link.getAttribute("crossorigin"))
|
|
971
|
+
await importLink(childlink, observer);
|
|
972
|
+
}
|
|
771
973
|
if (unhide) dom.body.removeAttribute("hidden");
|
|
772
|
-
createComponent(as, dom.body, {observer
|
|
974
|
+
createComponent(as, dom.body, {observer});
|
|
773
975
|
}
|
|
774
976
|
return {as};
|
|
775
977
|
}
|
|
776
978
|
const importLinks = async () => {
|
|
777
979
|
const observer = createObserver(document.body);
|
|
778
|
-
for (const link of [...document.querySelectorAll(
|
|
779
|
-
await importLink(link);
|
|
980
|
+
for (const link of [...document.querySelectorAll(`link[href$=".html"][rel=module]`)]) {
|
|
981
|
+
await importLink(link, observer);
|
|
780
982
|
}
|
|
781
983
|
}
|
|
782
984
|
|
|
783
|
-
const bodyAsComponent = ({as = "x-body", unhide,
|
|
985
|
+
const bodyAsComponent = ({as = "x-body", unhide, framed} = {}) => {
|
|
784
986
|
const parent = document.body.parentElement;
|
|
785
|
-
createComponent(as, document.body, {
|
|
987
|
+
createComponent(as, document.body, {framed});
|
|
786
988
|
const component = document.createElement(as);
|
|
787
989
|
parent.replaceChild(component, document.body);
|
|
788
990
|
Object.defineProperty(document, "body", {
|
|
@@ -827,13 +1029,12 @@ const {observe} = (() => {
|
|
|
827
1029
|
if (!domContentLoadedEvent) addListener(window, "DOMContentLoaded", (event) => domContentLoadedEvent = event);
|
|
828
1030
|
let OBSERVER;
|
|
829
1031
|
const loader = async (whenFramed) => {
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
unhide = !!document.querySelector('meta[name="l-unhide"]'),
|
|
1032
|
+
await importLinks();
|
|
1033
|
+
const unhide = !!document.querySelector('meta[name="l-unhide"]'),
|
|
833
1034
|
isolated = !!document.querySelector('meta[name="l-isolate"]'),
|
|
834
1035
|
enableFrames = !!document.querySelector('meta[name="l-enableFrames"]');
|
|
835
1036
|
if (whenFramed) {
|
|
836
|
-
whenFramed({unhide,
|
|
1037
|
+
whenFramed({unhide, isolated, enableFrames, framed: true});
|
|
837
1038
|
if (!isolated) {
|
|
838
1039
|
postMessage.enabled = true;
|
|
839
1040
|
addListener(window, "message", ({data}) => {
|
|
@@ -868,7 +1069,7 @@ const {observe} = (() => {
|
|
|
868
1069
|
postMessage({type: "DOMContentLoaded"})
|
|
869
1070
|
}
|
|
870
1071
|
} else if (url.searchParams.has("as")) {
|
|
871
|
-
bodyAsComponent({as: url.searchParams.get("as"), unhide
|
|
1072
|
+
bodyAsComponent({as: url.searchParams.get("as"), unhide});
|
|
872
1073
|
}
|
|
873
1074
|
if (enableFrames) {
|
|
874
1075
|
postMessage.enabled = true;
|
|
@@ -942,12 +1143,12 @@ const {observe} = (() => {
|
|
|
942
1143
|
}
|
|
943
1144
|
}
|
|
944
1145
|
}
|
|
945
|
-
const whenFramed = (
|
|
1146
|
+
const whenFramed = (callback, {isolated} = {}) => {
|
|
946
1147
|
// loads for framed content
|
|
947
|
-
addListener(document, "DOMContentLoaded", (event) => loader(
|
|
1148
|
+
addListener(document, "DOMContentLoaded", (event) => loader(callback));
|
|
948
1149
|
}
|
|
949
1150
|
Lightview.whenFramed = whenFramed;
|
|
950
|
-
|
|
1151
|
+
|
|
951
1152
|
if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) {
|
|
952
1153
|
// loads for unframed content
|
|
953
1154
|
// CodePen mucks with window.parent
|
|
@@ -955,6 +1156,4 @@ const {observe} = (() => {
|
|
|
955
1156
|
}
|
|
956
1157
|
|
|
957
1158
|
return {observe}
|
|
958
|
-
})();
|
|
959
|
-
|
|
960
|
-
|
|
1159
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightview",
|
|
3
|
-
"version": "1.4.
|
|
4
|
-
"description": "Small, simple, powerful web UI and micro front end creation ...
|
|
3
|
+
"version": "1.4.10b",
|
|
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
7
|
"test": "set NODE_OPTIONS=--experimental-vm-modules && jest ./test"
|
package/test/basic.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<title>Basic</title>
|
|
6
6
|
<template id="x-test" name="joe" open="true" count=1 children='["mary"]' l-on:click="bump">
|
|
7
|
+
|
|
7
8
|
<span id="name">${name}</span>
|
|
8
9
|
<span id="open">${open}</span>
|
|
9
10
|
<span id="count">${count}</span>
|
|
@@ -18,8 +19,9 @@
|
|
|
18
19
|
<input id="itel" type="tel" value="${itel}">
|
|
19
20
|
<input id="iemail" type="email" value="${iemail}">
|
|
20
21
|
<input id="iurl" type="url" value="${iurl}">
|
|
21
|
-
<input id="isearch" type="search" value="${isearch}">
|
|
22
22
|
<input id="iradio" type="radio" value="${iradio}">
|
|
23
|
+
<input id="isearch" type="search" value="${isearch}">
|
|
24
|
+
|
|
23
25
|
<input id="icolor" type="color" value="${icolor}">
|
|
24
26
|
<input id="ipassword" type="password" value="${ipassword}">
|
|
25
27
|
|
|
@@ -31,7 +33,7 @@
|
|
|
31
33
|
<input id="icheckbox" type="checkbox" value="${icheckbox}">
|
|
32
34
|
|
|
33
35
|
<script type="lightview/module">
|
|
34
|
-
debugger;
|
|
36
|
+
//debugger;
|
|
35
37
|
self.variables({name:string,open:boolean,count:number,children:Array},{imported,reactive});
|
|
36
38
|
self.variables({color:string,checked:boolean,age:number,hamburger:Array},{exported,reactive});
|
|
37
39
|
self.variables({counter:number},{reactive});
|
package/test/basic.test.mjs
CHANGED
|
@@ -194,7 +194,7 @@ describe('Lightview', () => {
|
|
|
194
194
|
expect(value).toBe("test");
|
|
195
195
|
expect(variable.name).toBe("i${type}");
|
|
196
196
|
expect(variable.type).toBe("string");
|
|
197
|
-
expect(variable.value).toBe(value);
|
|
197
|
+
if("${type}"!=="radio") expect(variable.value).toBe(value);
|
|
198
198
|
}`)();
|
|
199
199
|
test(`${type} input - i${type} should be "test"`,f);
|
|
200
200
|
});
|