lightview 1.0.0-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/.idea/lightview.iml +12 -0
- package/.idea/modules.xml +8 -0
- package/.idea/vcs.xml +6 -0
- package/LICENSE +21 -0
- package/README.md +6 -0
- package/lightview.js +716 -0
- package/package.json +26 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="WEB_MODULE" version="4">
|
|
3
|
+
<component name="NewModuleRootManager">
|
|
4
|
+
<content url="file://$MODULE_DIR$">
|
|
5
|
+
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
|
6
|
+
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
|
7
|
+
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
|
8
|
+
</content>
|
|
9
|
+
<orderEntry type="inheritedJdk" />
|
|
10
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
11
|
+
</component>
|
|
12
|
+
</module>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/lightview.iml" filepath="$PROJECT_DIR$/.idea/lightview.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
package/.idea/vcs.xml
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Simon Y. Blackwell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/lightview.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
/*
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2020 Simon Y. Blackwell - Lightview Small, simple, powerful UI creation ...
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// <script src="https://000686818.codepen.website/lightview.js?as=x-body"></script>
|
|
26
|
+
|
|
27
|
+
const Lightview = {};
|
|
28
|
+
|
|
29
|
+
const {observe} = (() => {
|
|
30
|
+
let CURRENTOBSERVER;
|
|
31
|
+
const parser = new DOMParser();
|
|
32
|
+
const anchorHandler = async (event) => {
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
const target = event.target;
|
|
35
|
+
if (target === event.currentTarget) {
|
|
36
|
+
const {as} = await importLink(target),
|
|
37
|
+
targets = querySelectorAll(document, target.getAttribute("target"));
|
|
38
|
+
targets.forEach((target) => {
|
|
39
|
+
while (target.lastChild) target.lastChild.remove();
|
|
40
|
+
target.appendChild(document.createElement(as))
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const getNameFromPath = (path) => {
|
|
45
|
+
const file = path.split("/").pop(),
|
|
46
|
+
name = file.split(".")[0];
|
|
47
|
+
if (name.includes("-")) return name;
|
|
48
|
+
return "l-" + name;
|
|
49
|
+
}
|
|
50
|
+
const observe = (f, thisArg, argsList = []) => {
|
|
51
|
+
function observer(...args) {
|
|
52
|
+
CURRENTOBSERVER = observer;
|
|
53
|
+
try {
|
|
54
|
+
f.call(thisArg || this, ...argsList, ...args);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
CURRENTOBSERVER = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
observer.cancel = () => observer.cancelled = true;
|
|
62
|
+
observer();
|
|
63
|
+
return observer;
|
|
64
|
+
}
|
|
65
|
+
const coerce = (value, toType) => {
|
|
66
|
+
const type = typeof (value);
|
|
67
|
+
if (type === toType) return value;
|
|
68
|
+
if (toType === "number") return parseFloat(value + "");
|
|
69
|
+
if (toType === "boolean") {
|
|
70
|
+
if(["on","checked","selected"].includes(value)) return true;
|
|
71
|
+
try {
|
|
72
|
+
const parsed = JSON.parse(value + "");
|
|
73
|
+
if (typeof (parsed) === "boolean") return parsed;
|
|
74
|
+
return [1,"on","checked","selected"].includes(parsed);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
throw new TypeError(`Unable to convert ${value} into 'boolean'`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (toType === "string") return value + "";
|
|
80
|
+
const isfunction = typeof (toType) === "function";
|
|
81
|
+
if ((toType === "object" || isfunction)) {
|
|
82
|
+
if(type==="object") {
|
|
83
|
+
if(value instanceof toType) return value;
|
|
84
|
+
}
|
|
85
|
+
if(type === "string") {
|
|
86
|
+
value = value.trim();
|
|
87
|
+
try {
|
|
88
|
+
if (isfunction) {
|
|
89
|
+
const instance = toType === Date ? new Date() : Object.create(toType.prototype);
|
|
90
|
+
if (instance instanceof Array) {
|
|
91
|
+
const parsed = JSON.parse(value.startsWith("[") ? value : `[${value}]`);
|
|
92
|
+
if (!Array.isArray(parsed)) throw new TypeError(`Expected an Array for parsed data`)
|
|
93
|
+
parsed.forEach((item) => instance.push(item))
|
|
94
|
+
} else if (instance instanceof Date) {
|
|
95
|
+
instance.setTime(Date.parse(value));
|
|
96
|
+
} else {
|
|
97
|
+
Object.assign(instance, JSON.parse(value));
|
|
98
|
+
}
|
|
99
|
+
if (toType !== Date) {
|
|
100
|
+
Object.defineProperty(instance, "constructor", {
|
|
101
|
+
configurable: true,
|
|
102
|
+
writable: true,
|
|
103
|
+
value: toType.prototype.constructor || toType
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return instance;
|
|
107
|
+
}
|
|
108
|
+
return JSON.parse(value);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
throw new TypeError(`Unable to convert ${value} into ${isfunction ? toType.name : type}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new TypeError(`Unable to coerce ${value} to ${toType}`)
|
|
115
|
+
}
|
|
116
|
+
const Reactor = (value) => {
|
|
117
|
+
if (value && typeof (value) === "object") {
|
|
118
|
+
if (value.__isReactor__) return value;
|
|
119
|
+
const childReactors = [],
|
|
120
|
+
dependents = {},
|
|
121
|
+
proxy = new Proxy(value, {
|
|
122
|
+
get(target, property) {
|
|
123
|
+
if (property === "__isReactor__") return true;
|
|
124
|
+
if (property === "toJSON" && target instanceof Array) {
|
|
125
|
+
const toJSON = function() { return target.toJSON(); }
|
|
126
|
+
return toJSON;
|
|
127
|
+
}
|
|
128
|
+
let value = target[property];
|
|
129
|
+
const type = typeof (value);
|
|
130
|
+
if (CURRENTOBSERVER && typeof (property) !== "symbol" && type !== "function") {
|
|
131
|
+
const observers = dependents[property] ||= new Set();
|
|
132
|
+
observers.add(CURRENTOBSERVER)
|
|
133
|
+
}
|
|
134
|
+
if (childReactors.includes(value) || (value && type !== "object") || typeof (property) === "symbol") {
|
|
135
|
+
// Dated must be bound to work with proxies
|
|
136
|
+
if (type === "function" && [Date].includes(value)) value = value.bind(target)
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
if (value && type === "object") {
|
|
140
|
+
value = Reactor(value);
|
|
141
|
+
childReactors.push(value);
|
|
142
|
+
}
|
|
143
|
+
target[property] = value;
|
|
144
|
+
return value;
|
|
145
|
+
},
|
|
146
|
+
set(target, property, value) {
|
|
147
|
+
const type = typeof (value);
|
|
148
|
+
if (target[property] !== value) {
|
|
149
|
+
if (value && type === "object") {
|
|
150
|
+
value = Reactor(value);
|
|
151
|
+
childReactors.push(value);
|
|
152
|
+
}
|
|
153
|
+
target[property] = value;
|
|
154
|
+
const observers = dependents[property] || [];
|
|
155
|
+
[...observers].forEach((f) => {
|
|
156
|
+
if (f.cancelled) dependents[property].delete(f);
|
|
157
|
+
else f();
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return proxy;
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class VariableEvent {
|
|
169
|
+
constructor(config) {
|
|
170
|
+
Object.assign(this, config);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const createVarsProxy = (vars, component, constructor) => {
|
|
175
|
+
return new Proxy(vars, {
|
|
176
|
+
get(target, property) {
|
|
177
|
+
let {value} = target[property] || {};
|
|
178
|
+
if (typeof (value) === "function") return value.bind(target);
|
|
179
|
+
return value;
|
|
180
|
+
},
|
|
181
|
+
set(target, property, newValue) {
|
|
182
|
+
const event = new VariableEvent({variableName: property, value: newValue});
|
|
183
|
+
if (target[property] === undefined) {
|
|
184
|
+
target[property] = {type: "any", value: newValue}; // should we allow this, do first to prevent loops
|
|
185
|
+
target.postEvent.value("change", event);
|
|
186
|
+
if(event.defaultPrevented) delete target[property].value;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
const {type, value, shared, exported, constant,reactive} = target[property];
|
|
190
|
+
if (constant) throw new TypeError(`${property}:${type} is a constant`);
|
|
191
|
+
const newtype = typeof (newValue),
|
|
192
|
+
typetype = typeof (type);
|
|
193
|
+
if (type === "any" || newtype === type || (typetype === "function" && newValue && newtype === "object" && newValue instanceof type)) {
|
|
194
|
+
if (value !== newValue) {
|
|
195
|
+
event.oldValue = value;
|
|
196
|
+
target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
|
|
197
|
+
target.postEvent.value("change", event);
|
|
198
|
+
if(event.defaultPrevented) target[property].value = value;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
if (typetype === "function" && newValue && newtype === "object") {
|
|
203
|
+
throw new TypeError(`Can't assign instance of '${newValue.constructor.name}' to variable '${property}:${type.name.replace("bound ", "")}'`)
|
|
204
|
+
}
|
|
205
|
+
throw new TypeError(`Can't assign '${typeof (newValue)} ${newtype === "string" ? '"' + newValue + '"' : newValue}' to variable '${property}:${typetype === "function" ? type.name.replace("bound ", "") : type}'`)
|
|
206
|
+
},
|
|
207
|
+
keys() {
|
|
208
|
+
return [...Object.keys(vars)];
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
const createObserver = (domNode) => {
|
|
213
|
+
const observer = new MutationObserver((mutations) => {
|
|
214
|
+
mutations.forEach((mutation) => {
|
|
215
|
+
if (mutation.type === "attributes") {
|
|
216
|
+
const name = mutation.attributeName,
|
|
217
|
+
target = mutation.target;
|
|
218
|
+
if (target.observedAttributes && target.observedAttributes.includes(name)) {
|
|
219
|
+
const value = target.getAttribute(name);
|
|
220
|
+
if (value !== mutation.oldValue) {
|
|
221
|
+
target.setVariable(name, value);
|
|
222
|
+
if (target.attributeChangedCallback) target.attributeChangedCallback(name, value, mutation.oldValue);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (mutation.type === "childList") {
|
|
226
|
+
for (const target of mutation.removedNodes) {
|
|
227
|
+
if (target.disconnectedCallback) target.disconnectedCallback();
|
|
228
|
+
}
|
|
229
|
+
for (const target of mutation.addedNodes) {
|
|
230
|
+
if (target.connectedCallback) target.connectedCallback();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
observer.observe(domNode, {subtree: true, childList: true});
|
|
236
|
+
return observer;
|
|
237
|
+
}
|
|
238
|
+
const querySelectorAll = (node, selector) => {
|
|
239
|
+
const nodes = [...node.querySelectorAll(selector)],
|
|
240
|
+
nodeIterator = document.createNodeIterator(node, Node.ELEMENT_NODE);
|
|
241
|
+
let currentNode;
|
|
242
|
+
while (currentNode = nodeIterator.nextNode()) {
|
|
243
|
+
if (currentNode.shadowRoot) nodes.push(...querySelectorAll(currentNode.shadowRoot, selector));
|
|
244
|
+
}
|
|
245
|
+
return nodes;
|
|
246
|
+
}
|
|
247
|
+
const getNodes = (root) => {
|
|
248
|
+
const nodes = [];
|
|
249
|
+
if (root.shadowRoot) {
|
|
250
|
+
nodes.push(root, ...getNodes(root.shadowRoot))
|
|
251
|
+
} else {
|
|
252
|
+
for (const node of root.childNodes) {
|
|
253
|
+
if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.includes("${")) {
|
|
254
|
+
node.template ||= node.nodeValue;
|
|
255
|
+
nodes.push(node);
|
|
256
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
257
|
+
let skip;
|
|
258
|
+
[...node.attributes].forEach((attr) => {
|
|
259
|
+
if (attr.value.includes("${")) {
|
|
260
|
+
attr.template ||= attr.value;
|
|
261
|
+
if (!nodes.includes(node)) nodes.push(node);
|
|
262
|
+
}
|
|
263
|
+
if (attr.name.includes(":") || attr.name.startsWith("l-")) {
|
|
264
|
+
skip = attr.name.includes("l-for:");
|
|
265
|
+
if (!nodes.includes(node)) nodes.push(node);
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
if (!skip) {
|
|
269
|
+
if (!node.shadowRoot) nodes.push(...getNodes(node));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return nodes;
|
|
275
|
+
}
|
|
276
|
+
const resolveNode = (node, component) => {
|
|
277
|
+
if (node.template) {
|
|
278
|
+
try {
|
|
279
|
+
node.nodeValue = Function("context", "with(context) { return `" + node.template + "` }")(component.varsProxy);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
if (!e.message.includes("defined")) throw e;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return node.nodeValue;
|
|
285
|
+
}
|
|
286
|
+
const render = (template, render) => {
|
|
287
|
+
let observer;
|
|
288
|
+
if (template) {
|
|
289
|
+
if (observer) observer.cancel();
|
|
290
|
+
observer = observe(render)
|
|
291
|
+
} else {
|
|
292
|
+
render();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const inputTypeToType = (inputType) => {
|
|
296
|
+
if (!inputType) return "any"
|
|
297
|
+
if (["text", "tel", "email", "url", "search", "radio"].includes(inputType)) return "string";
|
|
298
|
+
if (["number", "range"].includes(inputType)) return "number";
|
|
299
|
+
if (["datetime"].includes(inputType)) return Date;
|
|
300
|
+
if(["checkbox"].includes(inputType)) return "boolean";
|
|
301
|
+
return "any";
|
|
302
|
+
}
|
|
303
|
+
const _importAnchors = (node,component) => {
|
|
304
|
+
[...node.querySelectorAll('a[href][target^="#"]')].forEach((node) => {
|
|
305
|
+
node.removeEventListener("click", anchorHandler);
|
|
306
|
+
node.addEventListener("click", anchorHandler);
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
const _bindForms = (node, component) => {
|
|
310
|
+
[...node.querySelectorAll("input")].forEach((input) => {
|
|
311
|
+
bindInput(input,component);
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
const bindInput = (input,component) => {
|
|
315
|
+
const name = input.getAttribute("name"),
|
|
316
|
+
vname = input.getAttribute("l-bind")||name;
|
|
317
|
+
if (name) {
|
|
318
|
+
if(!input.hasAttribute("l-bind")) input.setAttribute("l-bind",vname)
|
|
319
|
+
const type = inputTypeToType(input.getAttribute("type")),
|
|
320
|
+
deflt = input.getAttribute("default"),
|
|
321
|
+
value = input.getAttribute("value");
|
|
322
|
+
let variable = component.vars[vname] || {type};
|
|
323
|
+
if(type!==variable.type) {
|
|
324
|
+
if(variable.type==="any" || variable.type==="unknown") variable.type = type;
|
|
325
|
+
else throw new TypeError(`Attempt to bind <input name="${name}" type="${type}"> to variable ${vname}:${variable.type}`)
|
|
326
|
+
}
|
|
327
|
+
component.variables({[vname]:type});
|
|
328
|
+
variable = component.vars[vname]
|
|
329
|
+
if (value || deflt) {
|
|
330
|
+
if (value && !value.includes("${")) {
|
|
331
|
+
variable.value = coerce(value, type);
|
|
332
|
+
input.setAttribute("value", `\${${name}}`);
|
|
333
|
+
}
|
|
334
|
+
if (deflt && !deflt.includes("${")) {
|
|
335
|
+
variable.value = coerce(deflt, type);
|
|
336
|
+
input.setAttribute("default", `\${${name}}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
input.addEventListener("change",(event) => {
|
|
340
|
+
event.stopImmediatePropagation();
|
|
341
|
+
component.varsProxy[vname] = coerce(event.target.value,type);
|
|
342
|
+
})
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const createClass = (domElementNode, {observer,bindForms,importAnchors}) => {
|
|
346
|
+
const instances = new Set(),
|
|
347
|
+
dom = domElementNode.tagName==="TEMPLATE"
|
|
348
|
+
? domElementNode.content.cloneNode(true)
|
|
349
|
+
: domElementNode.cloneNode(true);
|
|
350
|
+
if(domElementNode.tagName==="TEMPLATE") domElementNode = domElementNode.cloneNode(true);
|
|
351
|
+
return class CustomElement extends HTMLElement {
|
|
352
|
+
static get instances() {
|
|
353
|
+
return instances;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
constructor() {
|
|
357
|
+
super();
|
|
358
|
+
instances.add(this);
|
|
359
|
+
observer ||= createObserver(this);
|
|
360
|
+
const currentComponent = this,
|
|
361
|
+
shadow = this.attachShadow({mode: "open"}),
|
|
362
|
+
eventlisteners = {};
|
|
363
|
+
this.vars = {
|
|
364
|
+
addEventListener: {
|
|
365
|
+
value: (eventName, listener) => {
|
|
366
|
+
const listeners = eventlisteners[eventName] ||= new Set();
|
|
367
|
+
[...listeners].forEach((f) => {
|
|
368
|
+
if (listener + "" === f + "") listeners.delete(f);
|
|
369
|
+
})
|
|
370
|
+
eventlisteners[eventName].add(listener);
|
|
371
|
+
},
|
|
372
|
+
type: "function",
|
|
373
|
+
constant: true
|
|
374
|
+
},
|
|
375
|
+
postEvent: {
|
|
376
|
+
value: (eventName, event) => {
|
|
377
|
+
//event = {...event}
|
|
378
|
+
event.type = eventName;
|
|
379
|
+
eventlisteners[eventName]?.forEach((f) => f(event));
|
|
380
|
+
},
|
|
381
|
+
type: "function",
|
|
382
|
+
constant: true
|
|
383
|
+
},
|
|
384
|
+
self: {value: currentComponent, type: CustomElement, constant: true},
|
|
385
|
+
boolean: {value: "boolean", constant: true},
|
|
386
|
+
string: {value: "string", constant: true},
|
|
387
|
+
number: {value: "number", constant: true},
|
|
388
|
+
observed: {value: true, constant: true},
|
|
389
|
+
reactive: {value: true, constant: true},
|
|
390
|
+
shared: {value: true, constant: true},
|
|
391
|
+
exported: {value: true, constant: true},
|
|
392
|
+
imported: {value: true, constant: true}
|
|
393
|
+
};
|
|
394
|
+
this.defaultAttributes = domElementNode.tagName==="TEMPLATE" ? domElementNode.attributes : dom.attributes;
|
|
395
|
+
this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
|
|
396
|
+
["getElementById", "querySelector", "querySelectorAll"]
|
|
397
|
+
.forEach((fname) => {
|
|
398
|
+
Object.defineProperty(this, fname, {
|
|
399
|
+
configurable: true,
|
|
400
|
+
writable: true,
|
|
401
|
+
value: (...args) => this.shadowRoot[fname](...args)
|
|
402
|
+
})
|
|
403
|
+
});
|
|
404
|
+
[...dom.childNodes].forEach((child) => {
|
|
405
|
+
shadow.appendChild(child.cloneNode(true));
|
|
406
|
+
})
|
|
407
|
+
if (bindForms) _bindForms(shadow, this);
|
|
408
|
+
if(importAnchors) _importAnchors(shadow,this);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
get siblings() {
|
|
412
|
+
return [...CustomElement.instances].filter((sibling) => sibling!=this);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
adoptedCallback() {
|
|
416
|
+
if (this.hasOwnProperty("adoptedCallback")) this.adoptedCallback();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
disconnectedCallback() {
|
|
420
|
+
instances.delete(this);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
connectedCallback() {
|
|
424
|
+
const ctx = this,
|
|
425
|
+
shadow = ctx.shadowRoot;
|
|
426
|
+
for (const attr of this.defaultAttributes) this.hasAttribute(attr.name) || this.setAttribute(attr.name, attr.value);
|
|
427
|
+
const scripts = shadow.querySelectorAll("script"),
|
|
428
|
+
promises = [];
|
|
429
|
+
for (const script of scripts) {
|
|
430
|
+
if (script.attributes.src?.value?.includes("/lightview.js")) continue;
|
|
431
|
+
if (script.className !== "lightview" && !((script.getAttribute("type") || "").includes("lightview/"))) continue;
|
|
432
|
+
const scriptid = Math.random() + "",
|
|
433
|
+
currentScript = document.createElement("script");
|
|
434
|
+
for (const attr of script.attributes) {
|
|
435
|
+
currentScript.setAttribute(attr.name, attr.name === "type" ? attr.value.replace("lightview/", "") : attr.value);
|
|
436
|
+
}
|
|
437
|
+
currentScript.classList.remove("lightview");
|
|
438
|
+
const text = script.innerHTML.replaceAll(/\/\*[\s\S]*?\*\/|([^:]|^)\/\/.*$/gm, "$1").replaceAll(/\r?\n/g, "");
|
|
439
|
+
currentScript.innerHTML = `Function('if(window["${scriptid}"]?.ctx) { with(window["${scriptid}"].ctx) { ${text}; } window["${scriptid}"](); }')(); `;
|
|
440
|
+
let resolver;
|
|
441
|
+
promises.push(new Promise((resolve) => {
|
|
442
|
+
resolver = resolve;
|
|
443
|
+
}));
|
|
444
|
+
window[scriptid] = () => {
|
|
445
|
+
delete window[scriptid];
|
|
446
|
+
currentScript.remove();
|
|
447
|
+
resolver();
|
|
448
|
+
}
|
|
449
|
+
window[scriptid].ctx = ctx.varsProxy;
|
|
450
|
+
ctx.appendChild(currentScript);
|
|
451
|
+
}
|
|
452
|
+
Promise.all(promises).then(() => {
|
|
453
|
+
const inputs = [...ctx.shadowRoot.querySelectorAll("input[l-bind]")];
|
|
454
|
+
inputs.forEach((input) => {
|
|
455
|
+
bindInput(input,ctx);
|
|
456
|
+
})
|
|
457
|
+
const nodes = getNodes(ctx);
|
|
458
|
+
nodes.forEach((node) => {
|
|
459
|
+
if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
|
|
460
|
+
render(!!node.template, () => resolveNode(node, this))
|
|
461
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
462
|
+
[...node.attributes].forEach((attr) => {
|
|
463
|
+
const {name, value} = attr,
|
|
464
|
+
[type, ...params] = name.split(":");
|
|
465
|
+
if (["checked","selected"].includes(type)) {
|
|
466
|
+
render(!!attr.template, () => {
|
|
467
|
+
const value = resolveNode(attr, this);
|
|
468
|
+
if (value === "true") node.setAttribute(name, "");
|
|
469
|
+
else node.removeAttribute(name);
|
|
470
|
+
})
|
|
471
|
+
} else if(type==="") {
|
|
472
|
+
render(!!attr.template, () => {
|
|
473
|
+
const value = resolveNode(attr, this);
|
|
474
|
+
if(params[0]) {
|
|
475
|
+
if(value==="true") node.setAttribute(params[0], "");
|
|
476
|
+
else node.removeAttribute(params[0]);
|
|
477
|
+
} else {
|
|
478
|
+
if(value!=="true") node.removeAttribute(name);
|
|
479
|
+
}
|
|
480
|
+
})
|
|
481
|
+
} else if (["checked","selected"].includes(name)) {
|
|
482
|
+
render(!!attr.template, () => {
|
|
483
|
+
const value = resolveNode(attr, this);
|
|
484
|
+
if (value === "true") node.setAttribute(name, "");
|
|
485
|
+
else node.removeAttribute(name);
|
|
486
|
+
})
|
|
487
|
+
} else if (type === "l-on") {
|
|
488
|
+
let listener;
|
|
489
|
+
render(!!attr.template, () => {
|
|
490
|
+
const value = resolveNode(attr, this);
|
|
491
|
+
if (listener) node.removeEventListener(params[0], listener);
|
|
492
|
+
listener = this[value] || window[value] || Function(value);
|
|
493
|
+
node.addEventListener(params[0], listener);
|
|
494
|
+
})
|
|
495
|
+
} else if (type === "l-if") {
|
|
496
|
+
render(!!attr.template, () => {
|
|
497
|
+
const value = resolveNode(attr, this);
|
|
498
|
+
node.style.setProperty("display", value === "true" ? "revert" : "none");
|
|
499
|
+
})
|
|
500
|
+
} else if (type === "l-for") {
|
|
501
|
+
node.template ||= node.innerHTML;
|
|
502
|
+
render(!!attr.template, () => {
|
|
503
|
+
const [what = "each", vname = "element", index = "index", array = "array", after = false] = params,
|
|
504
|
+
value = resolveNode(attr, this),
|
|
505
|
+
coerced = coerce(value, what === "each" ? Array : "object"),
|
|
506
|
+
target = what === "each" ? coerced : Object[what](coerced),
|
|
507
|
+
html = target.reduce((html, item, i, target) => {
|
|
508
|
+
return html += Function("context", "with(context) { return `" + node.template + "` }")({
|
|
509
|
+
[vname]: item,
|
|
510
|
+
[index]: i,
|
|
511
|
+
[array]: target
|
|
512
|
+
})
|
|
513
|
+
}, ""),
|
|
514
|
+
parsed = parser.parseFromString(html, "text/html");
|
|
515
|
+
if (!window.lightviewDebug) {
|
|
516
|
+
if (after) {
|
|
517
|
+
node.style.setProperty("display", "none")
|
|
518
|
+
} else {
|
|
519
|
+
while (node.lastElementChild) node.lastElementChild.remove();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
while (parsed.body.firstChild) {
|
|
523
|
+
if (after) node.parentElement.insertBefore(parsed.body.firstChild, node);
|
|
524
|
+
else node.appendChild(parsed.body.firstChild);
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
} else if (attr.template) {
|
|
528
|
+
render(!!attr.template, () => resolveNode(attr, this));
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
shadow.normalize();
|
|
534
|
+
observer.observe(ctx, {attributeOldValue: true});
|
|
535
|
+
if (ctx.hasOwnProperty("connectedCallback")) ctx.connectedCallback();
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
adopted(value) {
|
|
540
|
+
Object.defineProperty(this, "adoptedCallback", {configurable: true, writable: true, value});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
connected(value) {
|
|
544
|
+
Object.defineProperty(this, "connectedCallback", {configurable: true, writable: true, value});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
attributeChanged(value) {
|
|
548
|
+
Object.defineProperty(this, "attributeChangedCallback", {configurable: true, writable: true, value});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
disconnected(value) {
|
|
552
|
+
Object.defineProperty(this, "disconnectedCallback", {
|
|
553
|
+
configurable: true,
|
|
554
|
+
writable: true,
|
|
555
|
+
value:() => { value(); super.disconnectedCallback(value); }
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
setVariable(name, value, {shared,coerceTo = typeof (value)}={}) {
|
|
560
|
+
if(!this.isConnected) {
|
|
561
|
+
instances.delete(this);
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
let {type} = this.vars[name] || {};
|
|
565
|
+
if (type) {
|
|
566
|
+
value = coerce(value, type);
|
|
567
|
+
if (this.varsProxy[name] !== value) {
|
|
568
|
+
const variable = this.vars[name];
|
|
569
|
+
if(variable.shared) {
|
|
570
|
+
const event = new VariableEvent({variableName: name, value: value,oldValue:variable.value});
|
|
571
|
+
variable.value = value;
|
|
572
|
+
this.vars.postEvent.value("change",event);
|
|
573
|
+
if(event.defaultPrevented) variable.value = value;
|
|
574
|
+
} else {
|
|
575
|
+
this.varsProxy[name] = value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
this.vars[name] = {type:coerceTo, value: coerce(value, coerceTo)};
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
variables(variables, {observed, reactive, shared, exported, imported} = {}) { // options = {observed,reactive,shared,exported,imported}
|
|
585
|
+
const addEventListener = this.varsProxy.addEventListener;
|
|
586
|
+
if (variables !== undefined) {
|
|
587
|
+
Object.entries(variables)
|
|
588
|
+
.forEach(([key, type]) => {
|
|
589
|
+
const variable = this.vars[key] ||= {type};
|
|
590
|
+
if (observed || imported) {
|
|
591
|
+
variable.value = coerce(this.getAttribute(key), variable.type);
|
|
592
|
+
variable.observed = observed;
|
|
593
|
+
variable.imported = imported;
|
|
594
|
+
}
|
|
595
|
+
if (reactive) {
|
|
596
|
+
variable.reactive = true;
|
|
597
|
+
this.vars[key] = Reactor(variable);
|
|
598
|
+
}
|
|
599
|
+
if (shared) {
|
|
600
|
+
variable.shared = true;
|
|
601
|
+
addEventListener("change",({variableName,value}) => {
|
|
602
|
+
if(this.vars[variableName]?.shared) {
|
|
603
|
+
this.siblings.forEach((instance) => {
|
|
604
|
+
instance.setVariable(variableName, value);
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
if (exported) {
|
|
610
|
+
variable.exported = true;
|
|
611
|
+
addEventListener("change",({variableName,value}) => {
|
|
612
|
+
// Array.isArray will not work here, Proxies mess up JSON.stringify for Arrays
|
|
613
|
+
//value = value && typeof (value) === "object" ? (value instanceof Array ? JSON.stringify([...value]) : JSON.stringify(value)) : value+"";
|
|
614
|
+
value = typeof(value)==="string" ? value : JSON.stringify(value);
|
|
615
|
+
this.setAttribute(variableName,value);
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
addEventListener("change",({variableName,value}) => {
|
|
620
|
+
[...this.shadowRoot.querySelectorAll(`input[l-bind=${variableName}]`)].forEach((input) => {
|
|
621
|
+
if(input.getAttribute("type")==="checkbox") { // at el option selected
|
|
622
|
+
if(!value) input.removeAttribute("checked");
|
|
623
|
+
input.checked = value;
|
|
624
|
+
} else {
|
|
625
|
+
// Array.isArray will not work here, Proxies mess up JSON.stringify for Arrays
|
|
626
|
+
//value = value && typeof (value) === "object" ? (value instanceof Array ? JSON.stringify([...value]) : JSON.stringify(value)) : value+"";
|
|
627
|
+
value = typeof(value)==="string" ? value : JSON.stringify(value);
|
|
628
|
+
const oldvalue = input.getAttribute("value")||"";
|
|
629
|
+
if(oldvalue!==value) {
|
|
630
|
+
input.setAttribute("value",value);
|
|
631
|
+
try {
|
|
632
|
+
input.setSelectionRange(0, Math.max(oldvalue.length,value.length)); // shadowDom sometimes fails to rerender unless this is done;
|
|
633
|
+
input.setRangeText(value,0, Math.max(oldvalue.length,value.length));
|
|
634
|
+
} catch(e) {
|
|
635
|
+
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
})
|
|
641
|
+
}
|
|
642
|
+
return Object.entries(this.vars)
|
|
643
|
+
.reduce((result, [key, variable]) => {
|
|
644
|
+
result[key] = {...variable};
|
|
645
|
+
return result;
|
|
646
|
+
}, {});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
constants(variables) {
|
|
650
|
+
if (variables !== undefined) {
|
|
651
|
+
Object.entries(variables)
|
|
652
|
+
.forEach(([key, value]) => {
|
|
653
|
+
const type = typeof (value) === "function" ? value : typeof (value),
|
|
654
|
+
variable = this.vars[key];
|
|
655
|
+
if (variable !== undefined) throw new TypeError(`${variable.constant ? "const" : "let"} ${key}:${variable.type} already declared.`);
|
|
656
|
+
if (value === undefined) throw new TypeError(`const ${key}:undefined must be initialized.`);
|
|
657
|
+
this.vars[key] = {type, value, constant: true};
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
return Object.entries(this.vars)
|
|
661
|
+
.reduce((result, [key, variable]) => {
|
|
662
|
+
if (variable.constant) result[key] = {...variable};
|
|
663
|
+
return result;
|
|
664
|
+
}, {});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
const createComponent = (name, node, {observer,bindForms,importAnchors}={}) => {
|
|
669
|
+
if (!customElements.get(name)) customElements.define(name, createClass(node, {observer,bindForms,importAnchors}));
|
|
670
|
+
}
|
|
671
|
+
Object.defineProperty(Lightview,"createComponent",{writable:true,configurable:true,value:createComponent})
|
|
672
|
+
const importLink = async (link, observer) => {
|
|
673
|
+
const url = (new URL(link.getAttribute("href"), window.location.href)),
|
|
674
|
+
as = link.getAttribute("as") || getNameFromPath(url.pathname);
|
|
675
|
+
if (url.hostname !== window.location.hostname) {
|
|
676
|
+
throw new URIError(`importLink:HTML imports must be from same domain: ${url.hostname}!=${location.hostname}`)
|
|
677
|
+
}
|
|
678
|
+
if (!customElements.get(as)) {
|
|
679
|
+
const html = await (await fetch(url.href)).text(),
|
|
680
|
+
dom = parser.parseFromString(html, "text/html"),
|
|
681
|
+
importAnchors = !!dom.head.querySelector('meta[name="l-importAnchors"]'),
|
|
682
|
+
bindForms = !!dom.head.querySelector('meta[name="l-bindForms"]'),
|
|
683
|
+
unhide = !!dom.head.querySelector('meta[name="l-unhide"]');
|
|
684
|
+
if(unhide) dom.body.removeAttribute("hidden");
|
|
685
|
+
createComponent(as, dom.body, {observer,importAnchors,bindForms});
|
|
686
|
+
}
|
|
687
|
+
return {as};
|
|
688
|
+
}
|
|
689
|
+
const importLinks = async () => {
|
|
690
|
+
const observer = createObserver(document.body);
|
|
691
|
+
for (const link of [...document.querySelectorAll("link[href][rel=module]")]) {
|
|
692
|
+
await importLink(link);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const bodyAsComponent = (as, {unhide,importAnchors,bindForms}={}) => {
|
|
697
|
+
const parent = document.body.parentElement;
|
|
698
|
+
createComponent(as, document.body,{importAnchors,bindForms});
|
|
699
|
+
const component = document.createElement(as);
|
|
700
|
+
parent.replaceChild(component, document.body);
|
|
701
|
+
if (unhide) component.removeAttribute("hidden");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const url = new URL(document.currentScript.getAttribute("src"), window.location.href);
|
|
705
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
|
706
|
+
if (url.searchParams.has("importLinks")) await importLinks();
|
|
707
|
+
const importAnchors = !!document.querySelector('meta[name="l-importAnchors"]'),
|
|
708
|
+
bindForms = !!document.querySelector('meta[name="l-bindForms"]'),
|
|
709
|
+
unhide = !!document.querySelector('meta[name="l-unhide"]');
|
|
710
|
+
if (url.searchParams.has("as")) bodyAsComponent(url.searchParams.get("as"), {unhide,importAnchors,bindForms});
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
return {observe}
|
|
714
|
+
})();
|
|
715
|
+
|
|
716
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lightview",
|
|
3
|
+
"version": "1.0.0b",
|
|
4
|
+
"description": "Small, simple, powerful web UI creation ...",
|
|
5
|
+
"main": "lightview.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/anywhichway/lightview.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"svelte",
|
|
15
|
+
"react",
|
|
16
|
+
"angular",
|
|
17
|
+
"riot",
|
|
18
|
+
"vue"
|
|
19
|
+
],
|
|
20
|
+
"author": "Simon Y. Blackwell",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/anywhichway/lightview/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/anywhichway/lightview#readme"
|
|
26
|
+
}
|