elegance-js 2.1.23 → 2.1.26
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/dist/client/effect.d.ts +27 -0
- package/dist/client/effect.js +37 -0
- package/dist/client/eventListener.d.ts +39 -0
- package/dist/client/eventListener.js +52 -0
- package/dist/client/loadHook.d.ts +34 -0
- package/dist/client/loadHook.js +52 -0
- package/dist/client/observer.d.ts +36 -0
- package/dist/client/observer.js +66 -0
- package/dist/client/runtime.d.ts +105 -0
- package/dist/client/runtime.js +620 -0
- package/dist/client/state.d.ts +40 -0
- package/dist/client/state.js +110 -0
- package/dist/compilation/compiler.d.ts +155 -0
- package/dist/compilation/compiler.js +1153 -0
- package/dist/components/ClientComponent.d.ts +22 -0
- package/dist/components/ClientComponent.js +55 -0
- package/dist/components/Link.d.ts +16 -1
- package/dist/components/Link.js +22 -0
- package/dist/components/Portal.d.ts +2 -0
- package/dist/components/Portal.js +2 -0
- package/dist/elements/element.d.ts +87 -0
- package/dist/elements/element.js +33 -0
- package/dist/elements/element_list.d.ts +7 -0
- package/dist/elements/element_list.js +65 -0
- package/dist/elements/raw.d.ts +14 -0
- package/dist/elements/raw.js +78 -0
- package/dist/elements/specific_props.d.ts +750 -0
- package/dist/global.d.ts +221 -327
- package/dist/index.d.ts +15 -3
- package/dist/index.js +11 -0
- package/dist/server/layout.d.ts +34 -3
- package/dist/server/layout.js +6 -0
- package/dist/server/log.d.ts +12 -0
- package/dist/server/log.js +64 -0
- package/dist/server/page.d.ts +32 -0
- package/dist/server/page.js +6 -0
- package/dist/server/runtime.d.ts +6 -0
- package/dist/server/runtime.js +72 -0
- package/dist/server/server.d.ts +103 -11
- package/dist/server/server.js +709 -0
- package/package.json +13 -13
- package/scripts/bootstrap.js +37 -273
- package/scripts/bootstrap_files/elegance.txt +40 -0
- package/scripts/bootstrap_files/index.txt +3 -0
- package/scripts/bootstrap_files/layout.txt +46 -0
- package/scripts/bootstrap_files/middleware.txt +18 -0
- package/scripts/bootstrap_files/page.txt +123 -0
- package/scripts/bootstrap_files/route.txt +6 -0
- package/scripts/elegance_dev.ts +40 -0
- package/scripts/elegance_prod.ts +40 -0
- package/scripts/elegance_static.ts +24 -0
- package/scripts/prod.js +9 -26
- package/scripts/run.js +13 -0
- package/scripts/static.js +13 -0
- package/dist/build.d.ts +0 -2
- package/dist/build.mjs +0 -202
- package/dist/client/client.d.ts +0 -1
- package/dist/client/client.mjs +0 -574
- package/dist/client/processPageElements.d.ts +0 -1
- package/dist/client/processPageElements.mjs +0 -117
- package/dist/client/render.d.ts +0 -1
- package/dist/client/render.mjs +0 -40
- package/dist/client/watcher.d.ts +0 -1
- package/dist/client/watcher.mjs +0 -26
- package/dist/compilation/compilation.d.ts +0 -139
- package/dist/compilation/compilation.mjs +0 -751
- package/dist/compilation/compiler_process.d.ts +0 -3
- package/dist/compilation/compiler_process.mjs +0 -102
- package/dist/compilation/dynamic_compiler.d.ts +0 -10
- package/dist/compilation/dynamic_compiler.mjs +0 -93
- package/dist/compile_docs.mjs +0 -34
- package/dist/components/Link.mjs +0 -65
- package/dist/global.mjs +0 -0
- package/dist/helpers/ObjectAttributeType.d.ts +0 -7
- package/dist/helpers/ObjectAttributeType.mjs +0 -11
- package/dist/helpers/camelToKebab.d.ts +0 -1
- package/dist/helpers/camelToKebab.mjs +0 -6
- package/dist/index.mjs +0 -3
- package/dist/internal/deprecate.d.ts +0 -1
- package/dist/internal/deprecate.mjs +0 -7
- package/dist/log.d.ts +0 -10
- package/dist/log.mjs +0 -38
- package/dist/server/generateHTMLTemplate.d.ts +0 -12
- package/dist/server/generateHTMLTemplate.mjs +0 -41
- package/dist/server/layout.mjs +0 -19
- package/dist/server/loadHook.d.ts +0 -30
- package/dist/server/loadHook.mjs +0 -50
- package/dist/server/observe.d.ts +0 -19
- package/dist/server/observe.mjs +0 -16
- package/dist/server/render.d.ts +0 -5
- package/dist/server/render.mjs +0 -61
- package/dist/server/server.mjs +0 -429
- package/dist/server/state.d.ts +0 -61
- package/dist/server/state.mjs +0 -146
- package/dist/shared/bindServerElements.mjs +0 -3
- package/dist/shared/serverElements.d.ts +0 -11
- package/dist/shared/serverElements.mjs +0 -164
- package/scripts/dev.js +0 -33
- package/scripts/export.js +0 -20
- package/scripts/ts-arc-dev.js +0 -9
- package/scripts/ts-arc-prod.js +0 -9
- /package/dist/{compile_docs.d.ts → elements/specific_props.js} +0 -0
- /package/dist/{shared/bindServerElements.d.ts → global.js} +0 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { allElements } from "../elements/element_list.js";
|
|
2
|
+
import { SpecialElementOption, EleganceElement } from "../elements/element.js";
|
|
3
|
+
Object.assign(window, allElements);
|
|
4
|
+
const newArray = Array.from;
|
|
5
|
+
let idCounter = 0;
|
|
6
|
+
/**
|
|
7
|
+
* Generate a non-deterministic unique id that can be used for browser specific things like custom client observers.
|
|
8
|
+
* Unique, but may change between builds; depends on order of creation.
|
|
9
|
+
* @returns A unique id
|
|
10
|
+
*/
|
|
11
|
+
function genLocalID() {
|
|
12
|
+
idCounter++;
|
|
13
|
+
return idCounter;
|
|
14
|
+
}
|
|
15
|
+
function createHTMLElementFromEleganceElement(element) {
|
|
16
|
+
let specialElementOptions = [];
|
|
17
|
+
const domElement = document.createElement(element.tag);
|
|
18
|
+
// Process options.
|
|
19
|
+
{
|
|
20
|
+
const entries = Object.entries(element.options);
|
|
21
|
+
for (const [optionName, optionValue] of entries) {
|
|
22
|
+
if (optionValue instanceof SpecialElementOption) {
|
|
23
|
+
optionValue.mutate(element, optionName);
|
|
24
|
+
const elementKey = genLocalID().toString();
|
|
25
|
+
specialElementOptions.push({ elementKey, optionName, optionValue });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
domElement.setAttribute(optionName, `${optionValue}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (element.key) {
|
|
33
|
+
domElement.setAttribute("key", element.key);
|
|
34
|
+
}
|
|
35
|
+
// Process children.
|
|
36
|
+
{
|
|
37
|
+
if (element.children !== null) {
|
|
38
|
+
for (const child of element.children) {
|
|
39
|
+
const result = createHTMLElementFromElement(child);
|
|
40
|
+
domElement.appendChild(result.root);
|
|
41
|
+
specialElementOptions.push(...result.specialElementOptions);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { root: domElement, specialElementOptions };
|
|
46
|
+
}
|
|
47
|
+
function createHTMLElementFromElement(element) {
|
|
48
|
+
let specialElementOptions = [];
|
|
49
|
+
if (element === undefined || element === null) {
|
|
50
|
+
return { root: document.createTextNode(""), specialElementOptions: [], };
|
|
51
|
+
}
|
|
52
|
+
switch (typeof element) {
|
|
53
|
+
case "object":
|
|
54
|
+
if (Array.isArray(element)) {
|
|
55
|
+
const fragment = document.createDocumentFragment();
|
|
56
|
+
for (const subElement of element) {
|
|
57
|
+
const result = createHTMLElementFromElement(subElement);
|
|
58
|
+
fragment.appendChild(result.root);
|
|
59
|
+
specialElementOptions.push(...result.specialElementOptions);
|
|
60
|
+
}
|
|
61
|
+
return { root: fragment, specialElementOptions };
|
|
62
|
+
}
|
|
63
|
+
if (element instanceof EleganceElement) {
|
|
64
|
+
return createHTMLElementFromEleganceElement(element);
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`This element is an arbitrary object, and arbitrary objects are not valid children. Please make sure all elements are one of: EleganceElement, boolean, number, string or Array. Also note that currently in client components like reactiveMap, state subject references are not valid children.`);
|
|
67
|
+
case "boolean":
|
|
68
|
+
case "number":
|
|
69
|
+
case "string":
|
|
70
|
+
const text = typeof element === "string" ? element : element.toString();
|
|
71
|
+
const textNode = document.createTextNode(text);
|
|
72
|
+
return { root: textNode, specialElementOptions: [] };
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`The typeof of this element is not one of EleganceElement, boolean, number, string or Array. Please convert it into one of these types.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
DEV_BUILD && (() => {
|
|
78
|
+
let isErrored = false;
|
|
79
|
+
(function connect() {
|
|
80
|
+
const es = new EventSource("http://localhost:4000/elegance-hot-reload");
|
|
81
|
+
es.onopen = () => {
|
|
82
|
+
if (isErrored) {
|
|
83
|
+
window.location.reload();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
es.onmessage = (event) => {
|
|
87
|
+
if (event.data === "hot-reload") {
|
|
88
|
+
window.location.reload();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
es.onerror = () => {
|
|
92
|
+
isErrored = true;
|
|
93
|
+
es.close();
|
|
94
|
+
setTimeout(connect, 1000);
|
|
95
|
+
};
|
|
96
|
+
})();
|
|
97
|
+
})();
|
|
98
|
+
/**
|
|
99
|
+
* A ServerSubject that has been serialized, shipped to the browser, and re-created as it's final form.
|
|
100
|
+
*
|
|
101
|
+
* Setting the `value` of this ClientSubject will trigger it's observers callbacks.
|
|
102
|
+
*
|
|
103
|
+
* To listen for changes in `value`, you may call the `observe()` method.
|
|
104
|
+
*/
|
|
105
|
+
class ClientSubject {
|
|
106
|
+
constructor(id, value) {
|
|
107
|
+
this.observers = new Map();
|
|
108
|
+
this._value = value;
|
|
109
|
+
this.id = id;
|
|
110
|
+
}
|
|
111
|
+
get value() {
|
|
112
|
+
return this._value;
|
|
113
|
+
}
|
|
114
|
+
set value(newValue) {
|
|
115
|
+
this._value = newValue;
|
|
116
|
+
for (const observer of this.observers.values()) {
|
|
117
|
+
observer(newValue);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Manually trigger each of this subject's observers, with the subject's current value.
|
|
122
|
+
*
|
|
123
|
+
* Useful if you're mutating for example fields of an object, or pushing to an array.
|
|
124
|
+
*/
|
|
125
|
+
triggerObservers() {
|
|
126
|
+
for (const observer of this.observers.values()) {
|
|
127
|
+
observer(this._value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Add a new observer to this subject, `callback` is called whenever the value setter is called on this subject.
|
|
132
|
+
*
|
|
133
|
+
* Note: if an ID is already in use it's callback will just be overwritten with whatever you give it.
|
|
134
|
+
*
|
|
135
|
+
* Note: this triggers `callback` with the current value of this subject.
|
|
136
|
+
*
|
|
137
|
+
* @param id The unique id of this observer
|
|
138
|
+
* @param callback Called whenever the value of this subject changes.
|
|
139
|
+
*/
|
|
140
|
+
observe(id, callback) {
|
|
141
|
+
if (this.observers.has(id)) {
|
|
142
|
+
this.observers.delete(id);
|
|
143
|
+
}
|
|
144
|
+
this.observers.set(id, callback);
|
|
145
|
+
callback(this.value);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Remove an observer from this subject.
|
|
149
|
+
* @param id The unique id of the observer.
|
|
150
|
+
*/
|
|
151
|
+
unobserve(id) {
|
|
152
|
+
this.observers.delete(id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
class StateManager {
|
|
156
|
+
constructor() {
|
|
157
|
+
this.subjects = new Map();
|
|
158
|
+
}
|
|
159
|
+
loadValues(values, doOverwrite = false) {
|
|
160
|
+
for (const value of values) {
|
|
161
|
+
if (this.subjects.has(value.id) && doOverwrite === false)
|
|
162
|
+
continue;
|
|
163
|
+
const clientSubject = new ClientSubject(value.id, value.value);
|
|
164
|
+
this.subjects.set(value.id, clientSubject);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
get(id) {
|
|
168
|
+
return this.subjects.get(id);
|
|
169
|
+
}
|
|
170
|
+
getAll(ids) {
|
|
171
|
+
const results = [];
|
|
172
|
+
for (const id of ids) {
|
|
173
|
+
results.push(this.get(id));
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* An event listener after it has been generated on the server, processed into pagedata, and reconstructed on the client.
|
|
180
|
+
*/
|
|
181
|
+
class ClientEventListener {
|
|
182
|
+
constructor(id, callback, depencencies) {
|
|
183
|
+
this.id = id;
|
|
184
|
+
this.callback = callback;
|
|
185
|
+
this.dependencies = depencencies;
|
|
186
|
+
}
|
|
187
|
+
call(ev) {
|
|
188
|
+
const dependencies = stateManager.getAll(this.dependencies);
|
|
189
|
+
this.callback(ev, ...dependencies);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
class EventListenerManager {
|
|
193
|
+
constructor() {
|
|
194
|
+
this.eventListeners = new Map();
|
|
195
|
+
}
|
|
196
|
+
loadValues(serverEventListeners, doOverride = false) {
|
|
197
|
+
for (const serverEventListener of serverEventListeners) {
|
|
198
|
+
if (this.eventListeners.has(serverEventListener.id) && doOverride === false)
|
|
199
|
+
continue;
|
|
200
|
+
const clientEventListener = new ClientEventListener(serverEventListener.id, serverEventListener.callback, serverEventListener.dependencies);
|
|
201
|
+
this.eventListeners.set(clientEventListener.id, clientEventListener);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
hookCallbacks(eventListenerOptions) {
|
|
205
|
+
for (const eventListenerOption of eventListenerOptions) {
|
|
206
|
+
const element = document.querySelector(`[key="${eventListenerOption.key}"]`);
|
|
207
|
+
if (!element) {
|
|
208
|
+
DEV_BUILD && errorOut("Possibly corrupted HTML, failed to find element with key " + eventListenerOption.key + " for event listener.");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const eventListener = this.eventListeners.get(eventListenerOption.id);
|
|
212
|
+
if (!eventListener) {
|
|
213
|
+
DEV_BUILD && errorOut("Invalid EventListenerOption: Event listener with id \”" + eventListenerOption.id + "\" does not exist.");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
element[eventListenerOption.option] = (ev) => {
|
|
217
|
+
eventListener.call(ev);
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
get(id) {
|
|
222
|
+
return this.eventListeners.get(id);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
class ClientObserver {
|
|
226
|
+
constructor(id, callback, depencencies) {
|
|
227
|
+
this.subjectValues = [];
|
|
228
|
+
this.elements = [];
|
|
229
|
+
this.id = id;
|
|
230
|
+
this.callback = callback;
|
|
231
|
+
this.dependencies = depencencies;
|
|
232
|
+
const initialValues = stateManager.getAll(this.dependencies);
|
|
233
|
+
for (const initialValue of initialValues) {
|
|
234
|
+
const idx = this.subjectValues.length;
|
|
235
|
+
this.subjectValues.push(initialValue.value);
|
|
236
|
+
initialValue.observe(this.id, (newValue) => {
|
|
237
|
+
this.subjectValues[idx] = newValue;
|
|
238
|
+
this.call();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
this.call();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Add an element to update when this observer updates.
|
|
245
|
+
*/
|
|
246
|
+
addElement(element, optionName) {
|
|
247
|
+
this.elements.push({ element, optionName });
|
|
248
|
+
}
|
|
249
|
+
setProp(element, key, value) {
|
|
250
|
+
if (key === "class") {
|
|
251
|
+
element.className = value;
|
|
252
|
+
}
|
|
253
|
+
else if (key === "style" && typeof value === "object") {
|
|
254
|
+
Object.assign(element.style, value);
|
|
255
|
+
}
|
|
256
|
+
else if (key.startsWith("on") && typeof value === "function") {
|
|
257
|
+
element.addEventListener(key.slice(2), value);
|
|
258
|
+
}
|
|
259
|
+
else if (key in element) {
|
|
260
|
+
const isTruthy = value === "true" || value === "false";
|
|
261
|
+
if (isTruthy) {
|
|
262
|
+
element[key] = Boolean(value);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
element[key] = value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
element.setAttribute(key, value);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
call() {
|
|
273
|
+
for (const { element, optionName } of this.elements) {
|
|
274
|
+
const getSelf = function getSelf() {
|
|
275
|
+
return element;
|
|
276
|
+
};
|
|
277
|
+
const newValue = this.callback.call(element, ...this.subjectValues);
|
|
278
|
+
this.setProp(element, optionName, newValue);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
class ObserverManager {
|
|
283
|
+
constructor() {
|
|
284
|
+
this.clientObservers = new Map();
|
|
285
|
+
}
|
|
286
|
+
loadValues(serverObservers, doOverride = false) {
|
|
287
|
+
for (const serverObserver of serverObservers) {
|
|
288
|
+
if (this.clientObservers.has(serverObserver.id) && doOverride === false)
|
|
289
|
+
continue;
|
|
290
|
+
const clientObserver = new ClientObserver(serverObserver.id, serverObserver.callback, serverObserver.dependencies);
|
|
291
|
+
this.clientObservers.set(clientObserver.id, clientObserver);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
hookCallbacks(observerOptions) {
|
|
295
|
+
for (const observerOption of observerOptions) {
|
|
296
|
+
const element = document.querySelector(`[key="${observerOption.key}"]`);
|
|
297
|
+
if (!element) {
|
|
298
|
+
DEV_BUILD && errorOut("Possibly corrupted HTML, failed to find element with key " + observerOption.key + " for event listener.");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const observer = this.clientObservers.get(observerOption.id);
|
|
302
|
+
if (!observer) {
|
|
303
|
+
DEV_BUILD && errorOut("Invalid ObserverOption: Observer with id \”" + observerOption.id + "\" does not exist.");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
observer.addElement(element, observerOption.option);
|
|
307
|
+
observer.call();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Take the results of ServerSubject.generateObserverNode(), replace their HTML placeins for text nodes, and turn those into observers.
|
|
312
|
+
*/
|
|
313
|
+
transformSubjectObserverNodes() {
|
|
314
|
+
const observerNodes = newArray(document.querySelectorAll("template[o]"));
|
|
315
|
+
for (const node of observerNodes) {
|
|
316
|
+
const subjectId = node.getAttribute("o");
|
|
317
|
+
const subject = stateManager.get(subjectId);
|
|
318
|
+
if (!subject) {
|
|
319
|
+
DEV_BUILD: errorOut("Failed to find subject with id " + subjectId + " for observerNode.");
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const textNode = document.createTextNode(subject.value);
|
|
323
|
+
const id = genLocalID().toString();
|
|
324
|
+
function update(value) {
|
|
325
|
+
textNode.textContent = value;
|
|
326
|
+
}
|
|
327
|
+
subject.observe(id, update);
|
|
328
|
+
update(subject.value);
|
|
329
|
+
node.replaceWith(textNode);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
var LoadHookKind;
|
|
334
|
+
(function (LoadHookKind) {
|
|
335
|
+
LoadHookKind[LoadHookKind["LAYOUT_LOADHOOK"] = 0] = "LAYOUT_LOADHOOK";
|
|
336
|
+
LoadHookKind[LoadHookKind["PAGE_LOADHOOK"] = 1] = "PAGE_LOADHOOK";
|
|
337
|
+
})(LoadHookKind || (LoadHookKind = {}));
|
|
338
|
+
;
|
|
339
|
+
class EffectManager {
|
|
340
|
+
constructor() {
|
|
341
|
+
this.activeEffects = [];
|
|
342
|
+
this.cleanupProcedures = new Map();
|
|
343
|
+
}
|
|
344
|
+
loadValues(effects) {
|
|
345
|
+
for (const effect of effects) {
|
|
346
|
+
const depencencies = stateManager.getAll(effect.dependencies);
|
|
347
|
+
if (this.activeEffects.includes(effect.id)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
this.activeEffects.push(effect.id);
|
|
351
|
+
const update = () => {
|
|
352
|
+
if (this.cleanupProcedures.has(effect.id)) {
|
|
353
|
+
this.cleanupProcedures.get(effect.id)();
|
|
354
|
+
}
|
|
355
|
+
effect.callback(...depencencies);
|
|
356
|
+
};
|
|
357
|
+
for (const dependency of depencencies) {
|
|
358
|
+
const id = genLocalID().toString();
|
|
359
|
+
dependency.observe(id, update);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
class LoadHookManager {
|
|
365
|
+
constructor() {
|
|
366
|
+
this.cleanupProcedures = [];
|
|
367
|
+
this.activeLoadHooks = [];
|
|
368
|
+
}
|
|
369
|
+
loadValues(loadHooks) {
|
|
370
|
+
for (const loadHook of loadHooks) {
|
|
371
|
+
const depencencies = stateManager.getAll(loadHook.dependencies);
|
|
372
|
+
if (this.activeLoadHooks.includes(loadHook.id)) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
this.activeLoadHooks.push(loadHook.id);
|
|
376
|
+
const cleanupFunction = loadHook.callback(...depencencies);
|
|
377
|
+
if (cleanupFunction) {
|
|
378
|
+
this.cleanupProcedures.push({
|
|
379
|
+
kind: loadHook.kind,
|
|
380
|
+
cleanupFunction: cleanupFunction,
|
|
381
|
+
pathname: loadHook.pathname,
|
|
382
|
+
loadHookIdx: this.activeLoadHooks.length - 1,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
callCleanupFunctions() {
|
|
388
|
+
let remainingProcedures = [];
|
|
389
|
+
for (const cleanupProcedure of this.cleanupProcedures) {
|
|
390
|
+
if (cleanupProcedure.kind === LoadHookKind.LAYOUT_LOADHOOK) {
|
|
391
|
+
const isInScope = sanitizePathname(window.location.pathname).startsWith(cleanupProcedure.pathname);
|
|
392
|
+
if (isInScope) {
|
|
393
|
+
remainingProcedures.push(cleanupProcedure);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
cleanupProcedure.cleanupFunction();
|
|
398
|
+
this.activeLoadHooks.splice(cleanupProcedure.loadHookIdx, 1);
|
|
399
|
+
}
|
|
400
|
+
this.cleanupProcedures = remainingProcedures;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const observerManager = new ObserverManager();
|
|
404
|
+
const eventListenerManager = new EventListenerManager();
|
|
405
|
+
const stateManager = new StateManager();
|
|
406
|
+
const loadHookManager = new LoadHookManager();
|
|
407
|
+
const effectManager = new EffectManager();
|
|
408
|
+
const pageStringCache = new Map();
|
|
409
|
+
const domParser = new DOMParser();
|
|
410
|
+
const xmlSerializer = new XMLSerializer();
|
|
411
|
+
const fetchPage = async (targetURL) => {
|
|
412
|
+
const pathname = sanitizePathname(targetURL.pathname);
|
|
413
|
+
if (pageStringCache.has(pathname)) {
|
|
414
|
+
return domParser.parseFromString(pageStringCache.get(pathname), "text/html");
|
|
415
|
+
}
|
|
416
|
+
const res = await fetch(targetURL);
|
|
417
|
+
const newDOM = domParser.parseFromString(await res.text(), "text/html");
|
|
418
|
+
{
|
|
419
|
+
const dataScripts = newArray(newDOM.querySelectorAll('script[data-package="true"]'));
|
|
420
|
+
const currentScripts = newArray(document.head.querySelectorAll('script[data-package="true"]'));
|
|
421
|
+
for (const dataScript of dataScripts) {
|
|
422
|
+
const existing = currentScripts.find(s => s.src === dataScript.src);
|
|
423
|
+
if (existing) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
document.head.appendChild(dataScript);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// get page script
|
|
430
|
+
{
|
|
431
|
+
const pageDataScript = newDOM.querySelector(`script[data-hook="true"][data-pathname="${pathname}"]`);
|
|
432
|
+
const text = pageDataScript.textContent;
|
|
433
|
+
pageDataScript.remove();
|
|
434
|
+
const blob = new Blob([text], { type: 'text/javascript' });
|
|
435
|
+
const url = URL.createObjectURL(blob);
|
|
436
|
+
const script = document.createElement("script");
|
|
437
|
+
script.src = url;
|
|
438
|
+
script.type = "module";
|
|
439
|
+
script.setAttribute("data-page", "true");
|
|
440
|
+
script.setAttribute("data-pathname", `${pathname}`);
|
|
441
|
+
newDOM.head.appendChild(script);
|
|
442
|
+
}
|
|
443
|
+
pageStringCache.set(pathname, xmlSerializer.serializeToString(newDOM));
|
|
444
|
+
return newDOM;
|
|
445
|
+
};
|
|
446
|
+
let navigationCallbacks = [];
|
|
447
|
+
function onNavigate(callback) {
|
|
448
|
+
navigationCallbacks.push(callback);
|
|
449
|
+
return navigationCallbacks.length - 1;
|
|
450
|
+
}
|
|
451
|
+
function removeNavigationCallback(idx) {
|
|
452
|
+
navigationCallbacks.splice(idx, 1);
|
|
453
|
+
}
|
|
454
|
+
const navigateLocally = async (target, pushState = true, isPopState = false) => {
|
|
455
|
+
const targetURL = new URL(target);
|
|
456
|
+
const pathname = sanitizePathname(targetURL.pathname);
|
|
457
|
+
if (!isPopState && pathname === sanitizePathname(window.location.pathname)) {
|
|
458
|
+
if (targetURL.hash) {
|
|
459
|
+
document.getElementById(targetURL.hash.slice(1))?.scrollIntoView();
|
|
460
|
+
}
|
|
461
|
+
if (pushState)
|
|
462
|
+
history.pushState(null, "", targetURL.href);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
let newPage = await fetchPage(targetURL);
|
|
466
|
+
if (!newPage)
|
|
467
|
+
return;
|
|
468
|
+
let oldPageLatest = document.body;
|
|
469
|
+
let newPageLatest = newPage.body;
|
|
470
|
+
{
|
|
471
|
+
const newPageLayouts = newArray(newPage.querySelectorAll("template[layout-id]"));
|
|
472
|
+
const oldPageLayouts = newArray(document.querySelectorAll("template[layout-id]"));
|
|
473
|
+
const size = Math.min(newPageLayouts.length, oldPageLayouts.length);
|
|
474
|
+
for (let i = 0; i < size; i++) {
|
|
475
|
+
const newPageLayout = newPageLayouts[i];
|
|
476
|
+
const oldPageLayout = oldPageLayouts[i];
|
|
477
|
+
const newLayoutId = newPageLayout.getAttribute("layout-id");
|
|
478
|
+
const oldLayoutId = oldPageLayout.getAttribute("layout-id");
|
|
479
|
+
if (newLayoutId !== oldLayoutId) {
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
oldPageLatest = oldPageLayout.nextElementSibling;
|
|
483
|
+
newPageLatest = newPageLayout.nextElementSibling;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const head = document.head;
|
|
487
|
+
const newHead = newPage.head;
|
|
488
|
+
oldPageLatest.replaceWith(newPageLatest);
|
|
489
|
+
// Gracefully replace head.
|
|
490
|
+
// document.head.replaceWith(); causes FOUC on Chromium browsers.
|
|
491
|
+
{
|
|
492
|
+
document.head.querySelector("title")?.replaceWith(newPage.head.querySelector("title") ?? "");
|
|
493
|
+
const update = (targetList, matchAgainst, action) => {
|
|
494
|
+
for (const target of targetList) {
|
|
495
|
+
const matching = matchAgainst.find(n => n.isEqualNode(target));
|
|
496
|
+
if (matching) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
action(target);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
// add new tags and reomve old ones
|
|
503
|
+
const oldTags = [
|
|
504
|
+
...newArray(head.querySelectorAll("link")),
|
|
505
|
+
...newArray(head.querySelectorAll("meta")),
|
|
506
|
+
...newArray(head.querySelectorAll("script")),
|
|
507
|
+
...newArray(head.querySelectorAll("base")),
|
|
508
|
+
...newArray(head.querySelectorAll("style")),
|
|
509
|
+
];
|
|
510
|
+
const newTags = [
|
|
511
|
+
...newArray(newHead.querySelectorAll("link")),
|
|
512
|
+
...newArray(newHead.querySelectorAll("meta")),
|
|
513
|
+
...newArray(newHead.querySelectorAll("script")),
|
|
514
|
+
...newArray(newHead.querySelectorAll("base")),
|
|
515
|
+
...newArray(newHead.querySelectorAll("style")),
|
|
516
|
+
];
|
|
517
|
+
update(newTags, oldTags, (node) => document.head.appendChild(node));
|
|
518
|
+
update(oldTags, newTags, (node) => node.remove());
|
|
519
|
+
}
|
|
520
|
+
if (pushState)
|
|
521
|
+
history.pushState(null, "", targetURL.href);
|
|
522
|
+
loadHookManager.callCleanupFunctions();
|
|
523
|
+
{
|
|
524
|
+
for (const callback of navigationCallbacks) {
|
|
525
|
+
callback(pathname);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
await loadPage();
|
|
529
|
+
if (targetURL.hash) {
|
|
530
|
+
document.getElementById(targetURL.hash.slice(1))?.scrollIntoView();
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
/** a simple path sanitizer that just ensures no repeat-slashes and no trailing slash */
|
|
534
|
+
function safePercentDecode(input) {
|
|
535
|
+
return input.replace(/%[0-9A-Fa-f]{2}/g, (m) => String.fromCharCode(parseInt(m.slice(1), 16)));
|
|
536
|
+
}
|
|
537
|
+
function sanitizePathname(pathname = "") {
|
|
538
|
+
if (!pathname)
|
|
539
|
+
return "/";
|
|
540
|
+
pathname = safePercentDecode(pathname);
|
|
541
|
+
pathname = "/" + pathname;
|
|
542
|
+
pathname = pathname.replace(/\/+/g, "/");
|
|
543
|
+
const segments = pathname.split("/");
|
|
544
|
+
const resolved = [];
|
|
545
|
+
for (const segment of segments) {
|
|
546
|
+
if (!segment || segment === ".")
|
|
547
|
+
continue;
|
|
548
|
+
if (segment === "..") {
|
|
549
|
+
resolved.pop();
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
resolved.push(segment);
|
|
553
|
+
}
|
|
554
|
+
const encoded = resolved.map((s) => encodeURIComponent(s));
|
|
555
|
+
return "/" + encoded.join("/");
|
|
556
|
+
}
|
|
557
|
+
async function getPageData(pathname) {
|
|
558
|
+
/** Find the correct script tag in head. */
|
|
559
|
+
const dataScriptTag = document.head.querySelector(`script[data-page="true"][data-pathname="${pathname}"]`);
|
|
560
|
+
if (!dataScriptTag) {
|
|
561
|
+
DEV_BUILD && errorOut("Failed to find script tag for query:" + `script[data-page="true"][data-pathname="${pathname}"]`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const { data } = await import(dataScriptTag.src);
|
|
565
|
+
const { subjects, eventListeners, eventListenerOptions, observers, observerOptions, effects, } = data;
|
|
566
|
+
if (!eventListenerOptions || !eventListeners || !observers || !subjects || !observerOptions || !effects) {
|
|
567
|
+
DEV_BUILD && errorOut(`Possibly malformed page data ${data}`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
return data;
|
|
571
|
+
}
|
|
572
|
+
function errorOut(message) {
|
|
573
|
+
throw new Error(message);
|
|
574
|
+
}
|
|
575
|
+
async function loadPage() {
|
|
576
|
+
window.onpopstate = async (event) => {
|
|
577
|
+
event.preventDefault();
|
|
578
|
+
const target = event.target;
|
|
579
|
+
await navigateLocally(target.location.href, false, true);
|
|
580
|
+
history.replaceState(null, "", target.location.href);
|
|
581
|
+
};
|
|
582
|
+
const pathname = sanitizePathname(window.location.pathname);
|
|
583
|
+
const { subjects, eventListenerOptions, eventListeners, observers, observerOptions, loadHooks, effects, } = await getPageData(pathname);
|
|
584
|
+
DEV_BUILD: {
|
|
585
|
+
globalThis.devtools = {
|
|
586
|
+
pageData: {
|
|
587
|
+
subjects,
|
|
588
|
+
eventListenerOptions,
|
|
589
|
+
eventListeners,
|
|
590
|
+
observers,
|
|
591
|
+
observerOptions,
|
|
592
|
+
loadHooks,
|
|
593
|
+
effects,
|
|
594
|
+
},
|
|
595
|
+
stateManager,
|
|
596
|
+
eventListenerManager,
|
|
597
|
+
observerManager,
|
|
598
|
+
loadHookManager,
|
|
599
|
+
effectManager,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
globalThis.eleganceClient = {
|
|
603
|
+
createHTMLElementFromElement,
|
|
604
|
+
fetchPage,
|
|
605
|
+
navigateLocally,
|
|
606
|
+
onNavigate,
|
|
607
|
+
removeNavigationCallback,
|
|
608
|
+
genLocalID,
|
|
609
|
+
};
|
|
610
|
+
stateManager.loadValues(subjects);
|
|
611
|
+
eventListenerManager.loadValues(eventListeners);
|
|
612
|
+
eventListenerManager.hookCallbacks(eventListenerOptions);
|
|
613
|
+
observerManager.loadValues(observers);
|
|
614
|
+
observerManager.hookCallbacks(observerOptions);
|
|
615
|
+
observerManager.transformSubjectObserverNodes();
|
|
616
|
+
loadHookManager.loadValues(loadHooks);
|
|
617
|
+
effectManager.loadValues(effects);
|
|
618
|
+
}
|
|
619
|
+
loadPage();
|
|
620
|
+
export { ClientSubject, StateManager, ObserverManager, LoadHookManager, EventListenerManager, EffectManager, };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { EleganceElement } from "../elements/element";
|
|
2
|
+
type StateCreationOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Override the default ID generation, this is not generally encouraged,
|
|
5
|
+
* but can be useful if you cannot guarantee order-dependent state() calls.
|
|
6
|
+
*/
|
|
7
|
+
explicitId?: string;
|
|
8
|
+
};
|
|
9
|
+
declare class ServerSubject<T extends any> {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
value: T;
|
|
12
|
+
constructor(id: string, value: T);
|
|
13
|
+
/**
|
|
14
|
+
* Create a client-side reactiveMap, that dynamically updates itself whenever the subject changes.
|
|
15
|
+
*
|
|
16
|
+
* **IMPORTANT** `callback` is sent literally to the browser, and thus doesn't have access to server-side variables, and is untrusted.
|
|
17
|
+
* @param callback Client side templating function that gets each entry of T, and returns an EleganceElement.
|
|
18
|
+
* @returns An HTML represent used to track the position of the reactive map.
|
|
19
|
+
*/
|
|
20
|
+
reactiveMap(callback: (entry: T extends (infer U)[] ? U : never) => EleganceElement<any, any>): EleganceElement<any, true>;
|
|
21
|
+
/**
|
|
22
|
+
* Allows the use of a ServerSubject as the child of an EleganceElement.
|
|
23
|
+
*
|
|
24
|
+
* Returns an HTML string that will be removed on page-load in the client and replaced with the appropriate value.
|
|
25
|
+
* @returns HTML string
|
|
26
|
+
*/
|
|
27
|
+
generateObserverNode(): string;
|
|
28
|
+
toString(): string;
|
|
29
|
+
serialize(): string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a reactive ServerSubject which will be serialized and sent to the browser.
|
|
33
|
+
*
|
|
34
|
+
* Once in the callback of a loadHook, eventListener, etc. it will become a *ClientSubject*.
|
|
35
|
+
* @param value Any value you want to be accessible in the browser. Value is sent literally as-is. Functions are supported.
|
|
36
|
+
* @param options Set options for the state (usually unused)
|
|
37
|
+
* @returns An instance of ServerSubject you can use as a reference to this state in functions like `loadHook()`
|
|
38
|
+
*/
|
|
39
|
+
declare function state<T>(value: T, options?: StateCreationOptions): ServerSubject<T>;
|
|
40
|
+
export { state, ServerSubject, };
|