@dinoreic/fez 0.4.0 → 0.5.2
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 +723 -198
- package/bin/fez +16 -6
- package/bin/fez-compile +347 -0
- package/bin/fez-debug +25 -0
- package/bin/fez-index +16 -4
- package/bin/refactor +699 -0
- package/dist/fez.js +142 -33
- package/dist/fez.js.map +4 -4
- package/fez.d.ts +533 -0
- package/package.json +25 -15
- package/src/fez/compile.js +396 -164
- package/src/fez/connect.js +250 -143
- package/src/fez/defaults.js +275 -84
- package/src/fez/instance.js +673 -514
- package/src/fez/lib/await-helper.js +64 -0
- package/src/fez/lib/global-state.js +22 -4
- package/src/fez/lib/index.js +140 -0
- package/src/fez/lib/localstorage.js +44 -0
- package/src/fez/lib/n.js +38 -23
- package/src/fez/lib/pubsub.js +208 -0
- package/src/fez/lib/svelte-template-lib.js +339 -0
- package/src/fez/lib/svelte-template.js +472 -0
- package/src/fez/lib/template.js +114 -119
- package/src/fez/morph.js +384 -0
- package/src/fez/root.js +284 -164
- package/src/fez/utility.js +319 -149
- package/src/fez/utils/dump.js +114 -84
- package/src/fez/utils/highlight_all.js +1 -1
- package/src/fez.js +65 -43
- package/src/rollup.js +1 -1
- package/src/svelte-cde-adapter.coffee +21 -12
- package/src/fez/vendor/idiomorph.js +0 -860
package/src/fez/utility.js
CHANGED
|
@@ -14,69 +14,136 @@ export default (Fez) => {
|
|
|
14
14
|
// Fez.head({ css: 'https://example.com/styles.css', media: 'print' }, () => { console.log('CSS loaded') })
|
|
15
15
|
// Inline script evaluation
|
|
16
16
|
// Fez.head({ script: 'console.log("Hello world")' })
|
|
17
|
+
// Fez component loading
|
|
18
|
+
// Fez.head({ fez: 'path/to/component.fez' })
|
|
17
19
|
// Extract from nodes
|
|
18
20
|
// Fez.head(domNode)
|
|
19
21
|
Fez.head = (config, callback) => {
|
|
20
22
|
if (config.nodeName) {
|
|
21
|
-
if (config.nodeName ==
|
|
22
|
-
Fez.head({script: config.innerText})
|
|
23
|
-
config.remove()
|
|
23
|
+
if (config.nodeName == "SCRIPT") {
|
|
24
|
+
Fez.head({ script: config.innerText });
|
|
25
|
+
config.remove();
|
|
24
26
|
} else {
|
|
25
|
-
config.querySelectorAll(
|
|
26
|
-
config
|
|
27
|
+
config.querySelectorAll("script").forEach((n) => Fez.head(n));
|
|
28
|
+
config
|
|
29
|
+
.querySelectorAll("template[fez], xmp[fez], script[fez]")
|
|
30
|
+
.forEach((n) => Fez.compile(n));
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
return
|
|
33
|
+
return;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
if (typeof config !==
|
|
33
|
-
throw new Error(
|
|
36
|
+
if (typeof config !== "object" || config === null) {
|
|
37
|
+
throw new Error("head requires an object parameter");
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
let src,
|
|
40
|
+
let src,
|
|
41
|
+
attributes = {},
|
|
42
|
+
elementType;
|
|
43
|
+
|
|
44
|
+
// Load Fez component(s) from URL
|
|
45
|
+
// Supports:
|
|
46
|
+
// - Single component: { fez: 'path/to/component.fez' }
|
|
47
|
+
// - Component list: { fez: 'path/to/components.txt' }
|
|
48
|
+
// txt file contains one component path per line (relative to txt location or absolute if starts with /)
|
|
49
|
+
if (config.fez) {
|
|
50
|
+
const fezPath = config.fez;
|
|
51
|
+
|
|
52
|
+
// If it's a txt file, load it as a component list
|
|
53
|
+
if (fezPath.endsWith(".txt")) {
|
|
54
|
+
Fez.fetch(fezPath).then((content) => {
|
|
55
|
+
// Get base path from txt file location
|
|
56
|
+
const basePath = fezPath.substring(0, fezPath.lastIndexOf("/") + 1);
|
|
57
|
+
|
|
58
|
+
// Parse lines, filter empty lines and comments
|
|
59
|
+
const lines = content
|
|
60
|
+
.split("\n")
|
|
61
|
+
.map((line) => line.trim())
|
|
62
|
+
.filter((line) => line && !line.startsWith("#"));
|
|
63
|
+
|
|
64
|
+
// Load each component
|
|
65
|
+
let loaded = 0;
|
|
66
|
+
const total = lines.length;
|
|
67
|
+
|
|
68
|
+
lines.forEach((line) => {
|
|
69
|
+
// Determine full path
|
|
70
|
+
// - If starts with /, it's absolute from root
|
|
71
|
+
// - Otherwise, relative to txt file location
|
|
72
|
+
let componentPath;
|
|
73
|
+
if (line.startsWith("/")) {
|
|
74
|
+
componentPath = line;
|
|
75
|
+
} else {
|
|
76
|
+
// Add .fez extension if not present
|
|
77
|
+
const path = line.endsWith(".fez") ? line : line + ".fez";
|
|
78
|
+
componentPath = basePath + path;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract component name from path
|
|
82
|
+
const name = componentPath.split("/").pop().split(".")[0];
|
|
83
|
+
|
|
84
|
+
Fez.fetch(componentPath).then((componentContent) => {
|
|
85
|
+
Fez.compile(name, componentContent);
|
|
86
|
+
loaded++;
|
|
87
|
+
if (loaded === total && callback) callback();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Single .fez component
|
|
95
|
+
Fez.fetch(fezPath).then((content) => {
|
|
96
|
+
const name = fezPath.split("/").pop().split(".")[0];
|
|
97
|
+
Fez.compile(name, content);
|
|
98
|
+
if (callback) callback();
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
37
102
|
|
|
38
103
|
if (config.script) {
|
|
39
|
-
if (config.script.includes(
|
|
104
|
+
if (config.script.includes("import ")) {
|
|
40
105
|
if (callback) {
|
|
41
|
-
Fez.
|
|
106
|
+
Fez.consoleError(
|
|
107
|
+
"Fez.head callback is not supported when script with import is passed (module context).",
|
|
108
|
+
);
|
|
42
109
|
}
|
|
43
110
|
|
|
44
111
|
// Evaluate inline script in context in the module
|
|
45
|
-
const script = document.createElement(
|
|
46
|
-
script.type =
|
|
112
|
+
const script = document.createElement("script");
|
|
113
|
+
script.type = "module";
|
|
47
114
|
script.textContent = config.script;
|
|
48
115
|
document.head.appendChild(script);
|
|
49
|
-
requestAnimationFrame(()=>script.remove())
|
|
116
|
+
requestAnimationFrame(() => script.remove());
|
|
50
117
|
} else {
|
|
51
118
|
try {
|
|
52
119
|
new Function(config.script)();
|
|
53
120
|
if (callback) callback();
|
|
54
121
|
} catch (error) {
|
|
55
|
-
Fez.
|
|
122
|
+
Fez.consoleError("Error executing script:", error);
|
|
56
123
|
console.log(config.script);
|
|
57
124
|
}
|
|
58
125
|
}
|
|
59
126
|
return;
|
|
60
127
|
} else if (config.js) {
|
|
61
128
|
src = config.js;
|
|
62
|
-
elementType =
|
|
129
|
+
elementType = "script";
|
|
63
130
|
// Copy all properties except 'js' as attributes
|
|
64
131
|
for (const [key, value] of Object.entries(config)) {
|
|
65
|
-
if (key !==
|
|
132
|
+
if (key !== "js" && key !== "module") {
|
|
66
133
|
attributes[key] = value;
|
|
67
134
|
}
|
|
68
135
|
}
|
|
69
136
|
// Handle module loading
|
|
70
137
|
if (config.module) {
|
|
71
|
-
attributes.type =
|
|
138
|
+
attributes.type = "module";
|
|
72
139
|
}
|
|
73
140
|
} else if (config.css) {
|
|
74
141
|
src = config.css;
|
|
75
|
-
elementType =
|
|
76
|
-
attributes.rel =
|
|
142
|
+
elementType = "link";
|
|
143
|
+
attributes.rel = "stylesheet";
|
|
77
144
|
// Copy all properties except 'css' as attributes
|
|
78
145
|
for (const [key, value] of Object.entries(config)) {
|
|
79
|
-
if (key !==
|
|
146
|
+
if (key !== "css") {
|
|
80
147
|
attributes[key] = value;
|
|
81
148
|
}
|
|
82
149
|
}
|
|
@@ -84,7 +151,9 @@ export default (Fez) => {
|
|
|
84
151
|
throw new Error('head requires either "script", "js" or "css" property');
|
|
85
152
|
}
|
|
86
153
|
|
|
87
|
-
const existingNode = document.querySelector(
|
|
154
|
+
const existingNode = document.querySelector(
|
|
155
|
+
`${elementType}[src="${src}"], ${elementType}[href="${src}"]`,
|
|
156
|
+
);
|
|
88
157
|
if (existingNode) {
|
|
89
158
|
if (callback) callback();
|
|
90
159
|
return existingNode;
|
|
@@ -92,7 +161,7 @@ export default (Fez) => {
|
|
|
92
161
|
|
|
93
162
|
const element = document.createElement(elementType);
|
|
94
163
|
|
|
95
|
-
if (elementType ===
|
|
164
|
+
if (elementType === "link") {
|
|
96
165
|
element.href = src;
|
|
97
166
|
} else {
|
|
98
167
|
element.src = src;
|
|
@@ -105,12 +174,15 @@ export default (Fez) => {
|
|
|
105
174
|
if (callback || config.module) {
|
|
106
175
|
element.onload = () => {
|
|
107
176
|
// If module name is provided, import it and assign to window
|
|
108
|
-
if (config.module && elementType ===
|
|
109
|
-
import(src)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
177
|
+
if (config.module && elementType === "script") {
|
|
178
|
+
import(src)
|
|
179
|
+
.then((module) => {
|
|
180
|
+
window[config.module] =
|
|
181
|
+
module.default || module[config.module] || module;
|
|
182
|
+
})
|
|
183
|
+
.catch((error) => {
|
|
184
|
+
console.error(`Error importing module ${config.module}:`, error);
|
|
185
|
+
});
|
|
114
186
|
}
|
|
115
187
|
if (callback) callback();
|
|
116
188
|
};
|
|
@@ -119,7 +191,11 @@ export default (Fez) => {
|
|
|
119
191
|
document.head.appendChild(element);
|
|
120
192
|
|
|
121
193
|
return element;
|
|
122
|
-
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Cache configuration
|
|
197
|
+
const FETCH_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
198
|
+
const FETCH_CACHE_MAX_SIZE = 100;
|
|
123
199
|
|
|
124
200
|
// Fetch wrapper with automatic caching and data handling
|
|
125
201
|
// Usage:
|
|
@@ -132,16 +208,16 @@ export default (Fez) => {
|
|
|
132
208
|
// Data object is automatically converted:
|
|
133
209
|
// - GET: appended as URL query parameters
|
|
134
210
|
// - POST: sent as FormData (multipart/form-data) without custom headers
|
|
135
|
-
Fez.fetch = function(...args) {
|
|
211
|
+
Fez.fetch = function (...args) {
|
|
136
212
|
// Initialize cache if not exists
|
|
137
|
-
Fez._fetchCache ||=
|
|
213
|
+
Fez._fetchCache ||= new Map();
|
|
138
214
|
|
|
139
|
-
let method =
|
|
215
|
+
let method = "GET";
|
|
140
216
|
let url;
|
|
141
217
|
let callback;
|
|
142
218
|
|
|
143
219
|
// Check if first arg is HTTP method (uppercase letters)
|
|
144
|
-
if (typeof args[0] ===
|
|
220
|
+
if (typeof args[0] === "string" && /^[A-Z]+$/.test(args[0])) {
|
|
145
221
|
method = args.shift();
|
|
146
222
|
}
|
|
147
223
|
|
|
@@ -151,22 +227,22 @@ export default (Fez) => {
|
|
|
151
227
|
// Check for data/options object
|
|
152
228
|
let opts = {};
|
|
153
229
|
let data = null;
|
|
154
|
-
if (typeof args[0] ===
|
|
230
|
+
if (typeof args[0] === "object") {
|
|
155
231
|
data = args.shift();
|
|
156
232
|
}
|
|
157
233
|
|
|
158
234
|
// Check for callback function
|
|
159
|
-
if (typeof args[0] ===
|
|
235
|
+
if (typeof args[0] === "function") {
|
|
160
236
|
callback = args.shift();
|
|
161
237
|
}
|
|
162
238
|
|
|
163
239
|
// Handle data based on method
|
|
164
240
|
if (data) {
|
|
165
|
-
if (method ===
|
|
241
|
+
if (method === "GET") {
|
|
166
242
|
// For GET, append data as query parameters
|
|
167
243
|
const params = new URLSearchParams(data);
|
|
168
|
-
url += (url.includes(
|
|
169
|
-
} else if (method ===
|
|
244
|
+
url += (url.includes("?") ? "&" : "?") + params.toString();
|
|
245
|
+
} else if (method === "POST") {
|
|
170
246
|
// For POST, convert to FormData
|
|
171
247
|
const formData = new FormData();
|
|
172
248
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -182,127 +258,197 @@ export default (Fez) => {
|
|
|
182
258
|
// Create cache key from method, url, and stringified opts
|
|
183
259
|
const cacheKey = `${method}:${url}:${JSON.stringify(opts)}`;
|
|
184
260
|
|
|
185
|
-
// Check cache first
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
Fez.
|
|
261
|
+
// Check cache first (with TTL validation)
|
|
262
|
+
const cached = Fez._fetchCache.get(cacheKey);
|
|
263
|
+
if (cached && Date.now() - cached.timestamp < FETCH_CACHE_TTL) {
|
|
264
|
+
Fez.consoleLog(`fetch cache hit: ${method} ${url}`);
|
|
189
265
|
if (callback) {
|
|
190
|
-
callback(
|
|
266
|
+
callback(cached.data);
|
|
191
267
|
return;
|
|
192
268
|
}
|
|
193
|
-
return Promise.resolve(
|
|
269
|
+
return Promise.resolve(cached.data);
|
|
194
270
|
}
|
|
195
271
|
|
|
196
272
|
// Log live fetch
|
|
197
|
-
Fez.
|
|
273
|
+
Fez.consoleLog(`fetch live: ${method} ${url}`);
|
|
198
274
|
|
|
199
275
|
// Helper to process and cache response
|
|
200
276
|
const processResponse = (response) => {
|
|
201
|
-
if (response.headers.get(
|
|
277
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
202
278
|
return response.json();
|
|
203
279
|
}
|
|
204
280
|
return response.text();
|
|
205
281
|
};
|
|
206
282
|
|
|
283
|
+
// Helper to store in cache with size limit
|
|
284
|
+
const storeInCache = (key, data) => {
|
|
285
|
+
// Enforce max cache size by removing oldest entries
|
|
286
|
+
if (Fez._fetchCache.size >= FETCH_CACHE_MAX_SIZE) {
|
|
287
|
+
const oldestKey = Fez._fetchCache.keys().next().value;
|
|
288
|
+
Fez._fetchCache.delete(oldestKey);
|
|
289
|
+
}
|
|
290
|
+
Fez._fetchCache.set(key, { data, timestamp: Date.now() });
|
|
291
|
+
};
|
|
292
|
+
|
|
207
293
|
// If callback provided, execute and handle
|
|
208
294
|
if (callback) {
|
|
209
295
|
fetch(url, opts)
|
|
210
296
|
.then(processResponse)
|
|
211
|
-
.then(data => {
|
|
212
|
-
|
|
297
|
+
.then((data) => {
|
|
298
|
+
storeInCache(cacheKey, data);
|
|
213
299
|
callback(data);
|
|
214
300
|
})
|
|
215
|
-
.catch(error => Fez.onError(
|
|
301
|
+
.catch((error) => Fez.onError("fetch", error));
|
|
216
302
|
return;
|
|
217
303
|
}
|
|
218
304
|
|
|
219
305
|
// Return promise with automatic JSON parsing
|
|
220
306
|
return fetch(url, opts)
|
|
221
307
|
.then(processResponse)
|
|
222
|
-
.then(data => {
|
|
223
|
-
|
|
308
|
+
.then((data) => {
|
|
309
|
+
storeInCache(cacheKey, data);
|
|
224
310
|
return data;
|
|
225
311
|
});
|
|
226
|
-
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Clear fetch cache (useful for testing or manual cache invalidation)
|
|
315
|
+
Fez.clearFetchCache = () => {
|
|
316
|
+
Fez._fetchCache?.clear();
|
|
317
|
+
};
|
|
227
318
|
|
|
228
319
|
Fez.darkenColor = (color, percent = 20) => {
|
|
229
320
|
// Convert hex to RGB
|
|
230
|
-
const num = parseInt(color.replace("#", ""), 16)
|
|
231
|
-
const amt = Math.round(2.55 * percent)
|
|
232
|
-
const R = (num >> 16) - amt
|
|
233
|
-
const G = (num >> 8 &
|
|
234
|
-
const B = (num &
|
|
235
|
-
return
|
|
236
|
-
|
|
321
|
+
const num = parseInt(color.replace("#", ""), 16);
|
|
322
|
+
const amt = Math.round(2.55 * percent);
|
|
323
|
+
const R = (num >> 16) - amt;
|
|
324
|
+
const G = ((num >> 8) & 0x00ff) - amt;
|
|
325
|
+
const B = (num & 0x0000ff) - amt;
|
|
326
|
+
return (
|
|
327
|
+
"#" +
|
|
328
|
+
(
|
|
329
|
+
0x1000000 +
|
|
330
|
+
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
|
331
|
+
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
|
332
|
+
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
|
333
|
+
)
|
|
334
|
+
.toString(16)
|
|
335
|
+
.slice(1)
|
|
336
|
+
);
|
|
337
|
+
};
|
|
237
338
|
|
|
238
339
|
Fez.lightenColor = (color, percent = 20) => {
|
|
239
340
|
// Convert hex to RGB
|
|
240
|
-
const num = parseInt(color.replace("#", ""), 16)
|
|
241
|
-
const amt = Math.round(2.55 * percent)
|
|
242
|
-
const R = (num >> 16) + amt
|
|
243
|
-
const G = (num >> 8 &
|
|
244
|
-
const B = (num &
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
341
|
+
const num = parseInt(color.replace("#", ""), 16);
|
|
342
|
+
const amt = Math.round(2.55 * percent);
|
|
343
|
+
const R = (num >> 16) + amt;
|
|
344
|
+
const G = ((num >> 8) & 0x00ff) + amt;
|
|
345
|
+
const B = (num & 0x0000ff) + amt;
|
|
346
|
+
return (
|
|
347
|
+
"#" +
|
|
348
|
+
(
|
|
349
|
+
0x1000000 +
|
|
350
|
+
(R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
|
|
351
|
+
(G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
|
|
352
|
+
(B < 255 ? (B < 1 ? 0 : B) : 255)
|
|
353
|
+
)
|
|
354
|
+
.toString(16)
|
|
355
|
+
.slice(1)
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Escapes HTML special characters in a string
|
|
361
|
+
* Also strips font-family styles (common source of XSS via CSS)
|
|
362
|
+
*/
|
|
248
363
|
Fez.htmlEscape = (text) => {
|
|
249
|
-
if (typeof text
|
|
250
|
-
text = text
|
|
251
|
-
// .replaceAll('&', "&")
|
|
252
|
-
.replace(/font-family\s*:\s*(?:&[^;]+;|[^;])*?;/gi, '')
|
|
253
|
-
.replaceAll("&", '&')
|
|
254
|
-
.replaceAll("'", ''')
|
|
255
|
-
.replaceAll('"', '"')
|
|
256
|
-
.replaceAll('<', '<')
|
|
257
|
-
.replaceAll('>', '>')
|
|
258
|
-
// .replaceAll('@', '@') // needed for template escaping
|
|
259
|
-
|
|
364
|
+
if (typeof text === "string") {
|
|
260
365
|
return text
|
|
261
|
-
|
|
262
|
-
|
|
366
|
+
.replace(/font-family\s*:\s*(?:&[^;]+;|[^;])*?;/gi, "") // Strip font-family (CSS safety)
|
|
367
|
+
.replaceAll("&", "&")
|
|
368
|
+
.replaceAll("'", "'")
|
|
369
|
+
.replaceAll('"', """)
|
|
370
|
+
.replaceAll("<", "<")
|
|
371
|
+
.replaceAll(">", ">");
|
|
263
372
|
}
|
|
264
|
-
|
|
373
|
+
return text === undefined ? "" : text;
|
|
374
|
+
};
|
|
265
375
|
|
|
266
376
|
// create dom root and return it
|
|
267
|
-
Fez.domRoot = (data, name =
|
|
377
|
+
Fez.domRoot = (data, name = "div") => {
|
|
268
378
|
if (data instanceof Node) {
|
|
269
|
-
return data
|
|
379
|
+
return data;
|
|
270
380
|
} else {
|
|
271
|
-
const root = document.createElement(name)
|
|
272
|
-
root.innerHTML = data
|
|
273
|
-
return root
|
|
381
|
+
const root = document.createElement(name);
|
|
382
|
+
root.innerHTML = data;
|
|
383
|
+
return root;
|
|
274
384
|
}
|
|
275
|
-
}
|
|
385
|
+
};
|
|
276
386
|
|
|
277
387
|
// add class by name to node and remove it from siblings
|
|
278
|
-
Fez.activateNode = (node, klass =
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
388
|
+
Fez.activateNode = (node, klass = "active") => {
|
|
389
|
+
if (!node || !node.parentElement) return;
|
|
390
|
+
Array.from(node.parentElement.children).forEach((child) => {
|
|
391
|
+
child.classList.remove(klass);
|
|
392
|
+
});
|
|
393
|
+
node.classList.add(klass);
|
|
394
|
+
};
|
|
284
395
|
|
|
285
396
|
Fez.isTrue = (val) => {
|
|
286
|
-
return [
|
|
287
|
-
}
|
|
397
|
+
return ["1", "true", "on"].includes(String(val).toLowerCase());
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
// get document unique ID
|
|
401
|
+
Fez.UID = 111;
|
|
402
|
+
Fez.uid = () => {
|
|
403
|
+
return "fez_uid_" + (++Fez.UID).toString(32);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// get global function pointer, used to pass functions to nested or inline elements
|
|
407
|
+
// <some-node :callback="${Fez.pointer(opts.callback)}" ...>
|
|
408
|
+
// Pointers are automatically cleaned up after first use (one-time use by default)
|
|
409
|
+
// Use Fez.pointer(func, { persist: true }) to keep the pointer
|
|
410
|
+
Fez.POINTER_SEQ = 0;
|
|
411
|
+
Fez.POINTER = {};
|
|
412
|
+
Fez.pointer = (func, opts = {}) => {
|
|
413
|
+
if (typeof func == "function") {
|
|
414
|
+
const uid = ++Fez.POINTER_SEQ;
|
|
415
|
+
|
|
416
|
+
if (opts.persist) {
|
|
417
|
+
// Persistent pointer - stays until manually removed
|
|
418
|
+
Fez.POINTER[uid] = func;
|
|
419
|
+
} else {
|
|
420
|
+
// One-time use pointer - auto-cleanup after first call
|
|
421
|
+
Fez.POINTER[uid] = (...args) => {
|
|
422
|
+
const result = func(...args);
|
|
423
|
+
delete Fez.POINTER[uid];
|
|
424
|
+
return result;
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return `Fez.POINTER[${uid}]`;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Manually clear all pointers (useful for testing or cleanup)
|
|
433
|
+
Fez.clearPointers = () => {
|
|
434
|
+
Fez.POINTER = {};
|
|
435
|
+
};
|
|
288
436
|
|
|
289
437
|
// Resolve a function from a string or function reference
|
|
290
438
|
Fez.getFunction = (pointer) => {
|
|
291
439
|
if (!pointer) {
|
|
292
|
-
return ()=>{}
|
|
293
|
-
}
|
|
294
|
-
else if (typeof pointer === 'function') {
|
|
440
|
+
return () => {};
|
|
441
|
+
} else if (typeof pointer === "function") {
|
|
295
442
|
return pointer;
|
|
296
|
-
}
|
|
297
|
-
else if (typeof pointer === 'string') {
|
|
443
|
+
} else if (typeof pointer === "string") {
|
|
298
444
|
// Check if it's a function expression (arrow function or function keyword)
|
|
299
445
|
// Arrow function: (args) => or args =>
|
|
300
446
|
const arrowFuncPattern = /^\s*\(?\s*\w+(\s*,\s*\w+)*\s*\)?\s*=>/;
|
|
301
447
|
const functionPattern = /^\s*function\s*\(/;
|
|
302
448
|
|
|
303
449
|
if (arrowFuncPattern.test(pointer) || functionPattern.test(pointer)) {
|
|
304
|
-
return new Function(
|
|
305
|
-
} else if (pointer.includes(
|
|
450
|
+
return new Function("return " + pointer)();
|
|
451
|
+
} else if (pointer.includes(".") && !pointer.includes("(")) {
|
|
306
452
|
// It's a property access like "this.focus" - return a function that calls it
|
|
307
453
|
return new Function(`return function() { return ${pointer}(); }`);
|
|
308
454
|
} else {
|
|
@@ -310,57 +456,63 @@ export default (Fez) => {
|
|
|
310
456
|
return new Function(pointer);
|
|
311
457
|
}
|
|
312
458
|
}
|
|
313
|
-
}
|
|
459
|
+
};
|
|
314
460
|
|
|
315
461
|
// Execute a function when DOM is ready or immediately if already loaded
|
|
316
462
|
Fez.onReady = (callback) => {
|
|
317
|
-
if (document.readyState ===
|
|
318
|
-
document.addEventListener(
|
|
319
|
-
|
|
320
|
-
|
|
463
|
+
if (document.readyState === "loading") {
|
|
464
|
+
document.addEventListener(
|
|
465
|
+
"DOMContentLoaded",
|
|
466
|
+
() => {
|
|
467
|
+
callback();
|
|
468
|
+
},
|
|
469
|
+
{ once: true },
|
|
470
|
+
);
|
|
321
471
|
} else {
|
|
322
|
-
callback()
|
|
472
|
+
callback();
|
|
323
473
|
}
|
|
324
|
-
}
|
|
474
|
+
};
|
|
325
475
|
|
|
326
476
|
// get unique id from string
|
|
327
477
|
Fez.fnv1 = (str) => {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
for (i = j = 0, ref = str.length - 1; (0 <= ref ? j <= ref : j >= ref); i = 0 <= ref ? ++j : --j) {
|
|
478
|
+
let FNV_OFFSET_BASIS = 2166136261;
|
|
479
|
+
let FNV_PRIME = 16777619;
|
|
480
|
+
let hash = FNV_OFFSET_BASIS;
|
|
481
|
+
for (let i = 0; i < str.length; i++) {
|
|
333
482
|
hash ^= str.charCodeAt(i);
|
|
334
483
|
hash *= FNV_PRIME;
|
|
335
484
|
}
|
|
336
|
-
return hash.toString(36).replaceAll(
|
|
337
|
-
}
|
|
485
|
+
return hash.toString(36).replaceAll("-", "");
|
|
486
|
+
};
|
|
338
487
|
|
|
339
|
-
Fez.tag = (tag, opts = {}, html =
|
|
340
|
-
const json = encodeURIComponent(JSON.stringify(opts))
|
|
341
|
-
return `<${tag} data-props="${json}">${html}</${tag}
|
|
488
|
+
Fez.tag = (tag, opts = {}, html = "") => {
|
|
489
|
+
const json = encodeURIComponent(JSON.stringify(opts));
|
|
490
|
+
return `<${tag} data-props="${json}">${html}</${tag}>`;
|
|
342
491
|
// const json = JSON.stringify(opts, null, 2)
|
|
343
492
|
// const data = `<script type="text/template">${json}</script><${tag} data-json-template="true">${html}</${tag}>`
|
|
344
493
|
// return data
|
|
345
|
-
}
|
|
494
|
+
};
|
|
346
495
|
|
|
347
496
|
// execute function until it returns true
|
|
348
497
|
Fez.untilTrue = (func, pingRate) => {
|
|
349
|
-
pingRate ||= 200
|
|
498
|
+
pingRate ||= 200;
|
|
350
499
|
|
|
351
500
|
if (!func()) {
|
|
352
|
-
setTimeout(()=>{
|
|
353
|
-
Fez.untilTrue(func, pingRate)
|
|
354
|
-
}
|
|
501
|
+
setTimeout(() => {
|
|
502
|
+
Fez.untilTrue(func, pingRate);
|
|
503
|
+
}, pingRate);
|
|
355
504
|
}
|
|
356
|
-
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
// Default throttle delay in ms
|
|
508
|
+
const DEFAULT_THROTTLE_DELAY = 200;
|
|
357
509
|
|
|
358
510
|
// throttle function calls
|
|
359
|
-
Fez.throttle = (func, delay =
|
|
511
|
+
Fez.throttle = (func, delay = DEFAULT_THROTTLE_DELAY) => {
|
|
360
512
|
let lastRun = 0;
|
|
361
513
|
let timeout;
|
|
362
514
|
|
|
363
|
-
return function(...args) {
|
|
515
|
+
return function (...args) {
|
|
364
516
|
const now = Date.now();
|
|
365
517
|
|
|
366
518
|
if (now - lastRun >= delay) {
|
|
@@ -368,26 +520,44 @@ export default (Fez) => {
|
|
|
368
520
|
lastRun = now;
|
|
369
521
|
} else {
|
|
370
522
|
clearTimeout(timeout);
|
|
371
|
-
timeout = setTimeout(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
523
|
+
timeout = setTimeout(
|
|
524
|
+
() => {
|
|
525
|
+
func.apply(this, args);
|
|
526
|
+
lastRun = Date.now();
|
|
527
|
+
},
|
|
528
|
+
delay - (now - lastRun),
|
|
529
|
+
);
|
|
375
530
|
}
|
|
376
531
|
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Enhanced truthiness check for template conditionals (#if, #unless)
|
|
535
|
+
// Empty arrays and empty objects are falsy, everything else uses standard JS truthiness
|
|
536
|
+
Fez.isTruthy = (v) => {
|
|
537
|
+
if (Array.isArray(v)) return v.length > 0;
|
|
538
|
+
if (v && typeof v === "object") return Object.keys(v).length > 0;
|
|
539
|
+
return !!v;
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// Convert any collection to pairs for loop destructuring
|
|
543
|
+
// Array: ['a', 'b'] → [['a', 0], ['b', 1]] (value, index)
|
|
544
|
+
// Object: {x: 1} → [['x', 1]] (key, value)
|
|
545
|
+
Fez.toPairs = (c) => {
|
|
546
|
+
if (Array.isArray(c)) return c.map((v, i) => [v, i]);
|
|
547
|
+
if (c && typeof c === "object") return Object.entries(c);
|
|
548
|
+
return [];
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// Returns short type identifier for data:
|
|
552
|
+
// 'o' - object, 'f' - function, 's' - string, 'a' - array, 'i' - integer, 'n' - float/number, 'u' - undefined/null
|
|
553
|
+
Fez.typeof = (data) => {
|
|
554
|
+
if (data === null || data === undefined) return "u";
|
|
555
|
+
if (Array.isArray(data)) return "a";
|
|
556
|
+
const t = typeof data;
|
|
557
|
+
if (t === "function") return "f";
|
|
558
|
+
if (t === "string") return "s";
|
|
559
|
+
if (t === "number") return Number.isInteger(data) ? "i" : "n";
|
|
560
|
+
if (t === "object") return "o";
|
|
561
|
+
return t[0];
|
|
562
|
+
};
|
|
563
|
+
};
|