@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/instance.js
CHANGED
|
@@ -1,721 +1,880 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* FezBase - Base class for all Fez components
|
|
3
|
+
*
|
|
4
|
+
* Provides lifecycle hooks, reactive state, DOM utilities, and template rendering
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import parseNode from "./lib/n.js";
|
|
8
|
+
import createTemplate from "./lib/template.js";
|
|
9
|
+
import { componentSubscribe, componentPublish } from "./lib/pubsub.js";
|
|
4
10
|
|
|
5
11
|
export default class FezBase {
|
|
6
|
-
//
|
|
12
|
+
// ===========================================================================
|
|
13
|
+
// STATIC METHODS
|
|
14
|
+
// ===========================================================================
|
|
15
|
+
|
|
16
|
+
static nodeName = "div";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extract props from a DOM node's attributes
|
|
20
|
+
* Handles :attr syntax for evaluated expressions and data-props JSON
|
|
21
|
+
*/
|
|
7
22
|
static getProps(node, newNode) {
|
|
8
|
-
let attrs = {}
|
|
23
|
+
let attrs = {};
|
|
9
24
|
|
|
10
|
-
//
|
|
25
|
+
// Direct props attachment
|
|
11
26
|
if (node.props) {
|
|
12
|
-
return node.props
|
|
27
|
+
return node.props;
|
|
13
28
|
}
|
|
14
29
|
|
|
15
|
-
//
|
|
30
|
+
// Collect attributes
|
|
16
31
|
for (const attr of node.attributes) {
|
|
17
|
-
attrs[attr.name] = attr.value
|
|
32
|
+
attrs[attr.name] = attr.value;
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
// Evaluate :attr expressions
|
|
20
36
|
for (const [key, val] of Object.entries(attrs)) {
|
|
21
|
-
if ([
|
|
22
|
-
|
|
23
|
-
delete attrs[key]
|
|
37
|
+
if ([":"].includes(key[0])) {
|
|
38
|
+
delete attrs[key];
|
|
24
39
|
try {
|
|
25
|
-
const newVal = new Function(`return (${val})`).bind(newNode)()
|
|
26
|
-
attrs[key.replace(/[\:_]/,
|
|
27
|
-
|
|
40
|
+
const newVal = new Function(`return (${val})`).bind(newNode)();
|
|
41
|
+
attrs[key.replace(/[\:_]/, "")] = newVal;
|
|
28
42
|
} catch (e) {
|
|
29
|
-
|
|
43
|
+
Fez.onError(
|
|
44
|
+
"attr",
|
|
45
|
+
`<${node.tagName.toLowerCase()}> Error evaluating ${key}="${val}": ${e.message}`,
|
|
46
|
+
);
|
|
30
47
|
}
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (typeof data ==
|
|
38
|
-
return data
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
data = decodeURIComponent(data)
|
|
51
|
+
// Handle data-props JSON
|
|
52
|
+
if (attrs["data-props"]) {
|
|
53
|
+
let data = attrs["data-props"];
|
|
54
|
+
if (typeof data == "object") {
|
|
55
|
+
return data;
|
|
56
|
+
} else {
|
|
57
|
+
if (data[0] != "{") {
|
|
58
|
+
data = decodeURIComponent(data);
|
|
43
59
|
}
|
|
44
60
|
try {
|
|
45
|
-
attrs = JSON.parse(data)
|
|
61
|
+
attrs = JSON.parse(data);
|
|
46
62
|
} catch (e) {
|
|
47
|
-
|
|
63
|
+
Fez.onError(
|
|
64
|
+
"props",
|
|
65
|
+
`<${node.tagName.toLowerCase()}> Invalid JSON in data-props: ${e.message}`,
|
|
66
|
+
);
|
|
48
67
|
}
|
|
49
68
|
}
|
|
50
69
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// <foo-bar data-json-template="true"></foo-bar>
|
|
55
|
-
else if (attrs['data-json-template']) {
|
|
56
|
-
const data = newNode.previousSibling?.textContent
|
|
70
|
+
// Handle JSON template
|
|
71
|
+
else if (attrs["data-json-template"]) {
|
|
72
|
+
const data = newNode.previousSibling?.textContent;
|
|
57
73
|
if (data) {
|
|
58
74
|
try {
|
|
59
|
-
attrs = JSON.parse(data)
|
|
60
|
-
newNode.previousSibling.remove()
|
|
75
|
+
attrs = JSON.parse(data);
|
|
76
|
+
newNode.previousSibling.remove();
|
|
61
77
|
} catch (e) {
|
|
62
|
-
|
|
78
|
+
Fez.onError(
|
|
79
|
+
"props",
|
|
80
|
+
`<${node.tagName.toLowerCase()}> Invalid JSON in template: ${e.message}`,
|
|
81
|
+
);
|
|
63
82
|
}
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
85
|
|
|
67
|
-
return attrs
|
|
86
|
+
return attrs;
|
|
68
87
|
}
|
|
69
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Get form data from closest/child form
|
|
91
|
+
*/
|
|
70
92
|
static formData(node) {
|
|
71
|
-
const formNode = node.closest(
|
|
93
|
+
const formNode = node.closest("form") || node.querySelector("form");
|
|
72
94
|
if (!formNode) {
|
|
73
|
-
Fez.
|
|
74
|
-
return {}
|
|
95
|
+
Fez.consoleLog("No form found for formData()");
|
|
96
|
+
return {};
|
|
75
97
|
}
|
|
76
|
-
const formData = new FormData(formNode)
|
|
77
|
-
const formObject = {}
|
|
98
|
+
const formData = new FormData(formNode);
|
|
99
|
+
const formObject = {};
|
|
78
100
|
formData.forEach((value, key) => {
|
|
79
|
-
formObject[key] = value
|
|
101
|
+
formObject[key] = value;
|
|
80
102
|
});
|
|
81
|
-
return formObject
|
|
103
|
+
return formObject;
|
|
82
104
|
}
|
|
83
105
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// instance methods
|
|
106
|
+
// ===========================================================================
|
|
107
|
+
// CONSTRUCTOR & CORE
|
|
108
|
+
// ===========================================================================
|
|
88
109
|
|
|
89
110
|
constructor() {}
|
|
90
111
|
|
|
91
|
-
n = parseNode
|
|
92
|
-
|
|
93
|
-
|
|
112
|
+
n = parseNode;
|
|
113
|
+
fezBlocks = {};
|
|
114
|
+
|
|
115
|
+
// Store for passing values to child components (e.g., loop vars)
|
|
116
|
+
fezGlobals = {
|
|
117
|
+
_data: new Map(),
|
|
118
|
+
_counter: 0,
|
|
119
|
+
set(value) {
|
|
120
|
+
const key = this._counter++;
|
|
121
|
+
this._data.set(key, value);
|
|
122
|
+
return key;
|
|
123
|
+
},
|
|
124
|
+
delete(key) {
|
|
125
|
+
const value = this._data.get(key);
|
|
126
|
+
this._data.delete(key);
|
|
127
|
+
return value;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Report error with component name always included
|
|
133
|
+
* @param {string} kind - Error category
|
|
134
|
+
* @param {string} message - Error message
|
|
135
|
+
* @param {Object} [context] - Additional context
|
|
136
|
+
* @returns {string} Formatted error message
|
|
137
|
+
*/
|
|
138
|
+
fezError(kind, message, context) {
|
|
139
|
+
const name = this.fezName || this.root?.tagName?.toLowerCase() || "unknown";
|
|
140
|
+
const enhancedContext = context ? { ...context, componentName: name } : { componentName: name };
|
|
141
|
+
return Fez.onError(kind, `<${name}> ${message}`, enhancedContext);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* String selector for use in HTML nodes
|
|
146
|
+
*/
|
|
94
147
|
get fezHtmlRoot() {
|
|
95
|
-
return `Fez(${this.UID})
|
|
96
|
-
// return this.props.id ? `Fez.find("#${this.props.id}").` : `Fez.find(this, "${this.fezName}").`
|
|
148
|
+
return `Fez(${this.UID}).`;
|
|
97
149
|
}
|
|
98
150
|
|
|
99
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Check if node is attached to DOM
|
|
153
|
+
*/
|
|
100
154
|
get isConnected() {
|
|
101
|
-
|
|
102
|
-
return true
|
|
103
|
-
} else {
|
|
104
|
-
this.fezOnDestroy()
|
|
105
|
-
return false
|
|
106
|
-
}
|
|
155
|
+
return !!this.root?.isConnected;
|
|
107
156
|
}
|
|
108
157
|
|
|
109
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Get single node property
|
|
160
|
+
*/
|
|
110
161
|
prop(name) {
|
|
111
|
-
let v = this.oldRoot[name] || this.props[name]
|
|
112
|
-
if (typeof v ==
|
|
113
|
-
|
|
114
|
-
v = v.bind(this.root)
|
|
162
|
+
let v = this.oldRoot[name] || this.props[name];
|
|
163
|
+
if (typeof v == "function") {
|
|
164
|
+
v = v.bind(this.root);
|
|
115
165
|
}
|
|
116
|
-
return v
|
|
166
|
+
return v;
|
|
117
167
|
}
|
|
118
168
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
let value = this.props[name]
|
|
123
|
-
|
|
124
|
-
if (value !== undefined) {
|
|
125
|
-
if (name == 'class') {
|
|
126
|
-
const klass = this.root.getAttribute(name, value)
|
|
127
|
-
|
|
128
|
-
if (klass) {
|
|
129
|
-
value = [klass, value].join(' ')
|
|
130
|
-
}
|
|
131
|
-
}
|
|
169
|
+
// ===========================================================================
|
|
170
|
+
// LIFECYCLE HOOKS
|
|
171
|
+
// ===========================================================================
|
|
132
172
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
173
|
+
connect() {}
|
|
174
|
+
onMount() {}
|
|
175
|
+
beforeRender() {}
|
|
176
|
+
afterRender() {}
|
|
177
|
+
onDestroy() {}
|
|
178
|
+
onStateChange() {}
|
|
179
|
+
onGlobalStateChange() {}
|
|
180
|
+
onPropsChange() {}
|
|
144
181
|
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Centralized destroy logic - called by MutationObserver when element is removed
|
|
184
|
+
*/
|
|
147
185
|
fezOnDestroy() {
|
|
148
|
-
//
|
|
186
|
+
// Guard against double-cleanup
|
|
187
|
+
if (this._destroyed) return;
|
|
188
|
+
this._destroyed = true;
|
|
189
|
+
|
|
190
|
+
// Execute cleanup callbacks (intervals, observers, event listeners)
|
|
149
191
|
if (this._onDestroyCallbacks) {
|
|
150
|
-
this._onDestroyCallbacks.forEach(callback => {
|
|
192
|
+
this._onDestroyCallbacks.forEach((callback) => {
|
|
151
193
|
try {
|
|
152
194
|
callback();
|
|
153
195
|
} catch (e) {
|
|
154
|
-
|
|
196
|
+
this.fezError("destroy", "Error in cleanup callback", e);
|
|
155
197
|
}
|
|
156
198
|
});
|
|
157
199
|
this._onDestroyCallbacks = [];
|
|
158
200
|
}
|
|
159
201
|
|
|
160
|
-
// Call user's onDestroy
|
|
161
|
-
this.onDestroy()
|
|
162
|
-
this.onDestroy = () => {}
|
|
202
|
+
// Call user's onDestroy hook
|
|
203
|
+
this.onDestroy();
|
|
204
|
+
this.onDestroy = () => {};
|
|
163
205
|
|
|
164
206
|
// Clean up root references
|
|
165
207
|
if (this.root) {
|
|
166
|
-
this.root.fez = undefined
|
|
208
|
+
this.root.fez = undefined;
|
|
167
209
|
}
|
|
168
|
-
|
|
169
|
-
this.root = undefined
|
|
210
|
+
this.root = undefined;
|
|
170
211
|
}
|
|
171
212
|
|
|
172
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Add a cleanup callback for destroy
|
|
215
|
+
*/
|
|
173
216
|
addOnDestroy(callback) {
|
|
174
217
|
this._onDestroyCallbacks = this._onDestroyCallbacks || [];
|
|
175
218
|
this._onDestroyCallbacks.push(callback);
|
|
176
219
|
}
|
|
177
220
|
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (this._eventHandlers[eventName]) {
|
|
183
|
-
window.removeEventListener(eventName, this._eventHandlers[eventName]);
|
|
184
|
-
}
|
|
221
|
+
// ===========================================================================
|
|
222
|
+
// RENDERING
|
|
223
|
+
// ===========================================================================
|
|
185
224
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
this.addOnDestroy(() => {
|
|
196
|
-
window.removeEventListener(eventName, throttledFunc);
|
|
197
|
-
delete this._eventHandlers[eventName];
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Helper function for resize events
|
|
202
|
-
onWindowResize(func, delay) {
|
|
203
|
-
this.on('resize', func, delay);
|
|
204
|
-
func();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Helper function for scroll events
|
|
208
|
-
onWindowScroll(func, delay) {
|
|
209
|
-
this.on('scroll', func, delay);
|
|
210
|
-
func();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Helper function for element resize events using ResizeObserver
|
|
214
|
-
onElementResize(el, func, delay = 200) {
|
|
215
|
-
const throttledFunc = Fez.throttle(() => {
|
|
216
|
-
if (this.isConnected) {
|
|
217
|
-
func.call(this, el.getBoundingClientRect(), el);
|
|
218
|
-
}
|
|
219
|
-
}, delay);
|
|
220
|
-
|
|
221
|
-
const observer = new ResizeObserver(throttledFunc);
|
|
222
|
-
observer.observe(el);
|
|
223
|
-
|
|
224
|
-
func.call(this, el.getBoundingClientRect(), el);
|
|
225
|
-
|
|
226
|
-
this.addOnDestroy(() => {
|
|
227
|
-
observer.disconnect();
|
|
228
|
-
});
|
|
225
|
+
/**
|
|
226
|
+
* Parse HTML and replace fez. references
|
|
227
|
+
*/
|
|
228
|
+
fezParseHtml(text) {
|
|
229
|
+
const base = this.fezHtmlRoot.replaceAll('"', """);
|
|
230
|
+
text = text
|
|
231
|
+
.replace(/([!'"\s;])fez\.(\w)/g, `$1${base}$2`)
|
|
232
|
+
.replace(/>\s+</g, "><");
|
|
233
|
+
return text.trim();
|
|
229
234
|
}
|
|
230
235
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
target.appendChild(source.firstChild)
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (isSlot) {
|
|
246
|
-
target.parentNode.removeChild(target)
|
|
236
|
+
/**
|
|
237
|
+
* Schedule work on next animation frame (debounced by name)
|
|
238
|
+
*/
|
|
239
|
+
fezNextTick(func, name) {
|
|
240
|
+
if (name) {
|
|
241
|
+
this._nextTicks ||= {};
|
|
242
|
+
this._nextTicks[name] ||= window.requestAnimationFrame(() => {
|
|
243
|
+
func.bind(this)();
|
|
244
|
+
this._nextTicks[name] = null;
|
|
245
|
+
}, name);
|
|
247
246
|
} else {
|
|
248
|
-
|
|
247
|
+
window.requestAnimationFrame(func.bind(this));
|
|
249
248
|
}
|
|
250
|
-
|
|
251
|
-
return target
|
|
252
249
|
}
|
|
253
250
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
onMount() {}
|
|
260
|
-
beforeRender() {}
|
|
261
|
-
afterRender() {}
|
|
262
|
-
onDestroy() {}
|
|
263
|
-
onStateChange() {}
|
|
264
|
-
onGlobalStateChange() {}
|
|
265
|
-
|
|
266
|
-
// component publish will search for parent component that subscribes by name
|
|
267
|
-
publish(channel, ...args) {
|
|
268
|
-
const handle_publish = (component) => {
|
|
269
|
-
if (Fez._subs && Fez._subs[channel]) {
|
|
270
|
-
const sub = Fez._subs[channel].find(([comp]) => comp === component)
|
|
271
|
-
if (sub) {
|
|
272
|
-
sub[1].bind(component)(...args)
|
|
273
|
-
return true
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return false
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Check if current component has subscription
|
|
280
|
-
if (handle_publish(this)) {
|
|
281
|
-
return true
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Bubble up to parent components
|
|
285
|
-
let parent = this.root.parentElement
|
|
286
|
-
while (parent) {
|
|
287
|
-
if (parent.fez) {
|
|
288
|
-
if (handle_publish(parent.fez)) {
|
|
289
|
-
return true
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
parent = parent.parentElement
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// If no parent handled it, fall back to global publish
|
|
296
|
-
// Fez.publish(channel, ...args)
|
|
297
|
-
return false
|
|
251
|
+
/**
|
|
252
|
+
* Force a re-render on next frame
|
|
253
|
+
*/
|
|
254
|
+
fezRefresh() {
|
|
255
|
+
this.fezNextTick(() => this.fezRender(), "refresh");
|
|
298
256
|
}
|
|
299
257
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
text = text
|
|
306
|
-
.replace(/([!'"\s;])fez\.(\w)/g, `$1${base}$2`)
|
|
307
|
-
.replace(/>\s+</g, '><')
|
|
308
|
-
|
|
309
|
-
return text.trim()
|
|
258
|
+
/**
|
|
259
|
+
* Alias for fezRefresh - can be overwritten
|
|
260
|
+
*/
|
|
261
|
+
refresh() {
|
|
262
|
+
this.fezRefresh();
|
|
310
263
|
}
|
|
311
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Render the component template to DOM
|
|
267
|
+
* Uses component-aware DOM differ with hash-based skip
|
|
268
|
+
*/
|
|
269
|
+
fezRender(template) {
|
|
270
|
+
// Check instance-level template first, then class-level
|
|
271
|
+
template ||= this.fezHtmlFunc || this?.class?.fezHtmlFunc;
|
|
312
272
|
|
|
313
|
-
|
|
314
|
-
nextTick(func, name) {
|
|
315
|
-
if (name) {
|
|
316
|
-
this._nextTicks ||= {}
|
|
317
|
-
this._nextTicks[name] ||= window.requestAnimationFrame(() => {
|
|
318
|
-
func.bind(this)()
|
|
319
|
-
this._nextTicks[name] = null
|
|
320
|
-
}, name)
|
|
321
|
-
} else {
|
|
322
|
-
window.requestAnimationFrame(func.bind(this))
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// inject htmlString as innerHTML and replace $$. with local pointer
|
|
327
|
-
// $$. will point to current fez instance
|
|
328
|
-
// <slot></slot> will be replaced with current root
|
|
329
|
-
// this.render('...loading')
|
|
330
|
-
// this.render('.images', '...loading')
|
|
331
|
-
render(template) {
|
|
332
|
-
template ||= this?.class?.fezHtmlFunc
|
|
273
|
+
if (!template || !this.root) return;
|
|
333
274
|
|
|
334
|
-
|
|
275
|
+
// Prevent re-render loops from state changes in beforeRender/afterRender
|
|
276
|
+
this._isRendering = true;
|
|
335
277
|
|
|
336
|
-
this.beforeRender()
|
|
278
|
+
this.beforeRender();
|
|
337
279
|
|
|
338
|
-
const nodeName =
|
|
339
|
-
|
|
280
|
+
const nodeName =
|
|
281
|
+
typeof this.class.nodeName == "function"
|
|
282
|
+
? this.class.nodeName(this.root)
|
|
283
|
+
: this.class.nodeName;
|
|
284
|
+
const newNode = document.createElement(nodeName || "div");
|
|
340
285
|
|
|
341
|
-
let renderedTpl
|
|
286
|
+
let renderedTpl;
|
|
342
287
|
if (Array.isArray(template)) {
|
|
343
|
-
// array nodes this.n(...), look tabs example
|
|
344
288
|
if (template[0] instanceof Node) {
|
|
345
|
-
template.forEach(
|
|
346
|
-
} else{
|
|
347
|
-
renderedTpl = template.join(
|
|
289
|
+
template.forEach((n) => newNode.appendChild(n));
|
|
290
|
+
} else {
|
|
291
|
+
renderedTpl = template.join("");
|
|
348
292
|
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
renderedTpl = createTemplate(template)(this)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
renderedTpl = template(this)
|
|
293
|
+
} else if (typeof template == "string") {
|
|
294
|
+
const name = this.root?.tagName?.toLowerCase();
|
|
295
|
+
renderedTpl = createTemplate(template, { name })(this);
|
|
296
|
+
} else if (typeof template == "function") {
|
|
297
|
+
renderedTpl = template(this);
|
|
355
298
|
}
|
|
356
299
|
|
|
357
300
|
if (renderedTpl) {
|
|
358
|
-
|
|
359
|
-
|
|
301
|
+
if (
|
|
302
|
+
renderedTpl instanceof DocumentFragment ||
|
|
303
|
+
renderedTpl instanceof Node
|
|
304
|
+
) {
|
|
305
|
+
newNode.appendChild(renderedTpl);
|
|
306
|
+
} else {
|
|
307
|
+
renderedTpl = renderedTpl.replace(/\s\w+="undefined"/g, "");
|
|
308
|
+
const parsedHtml = this.fezParseHtml(renderedTpl);
|
|
309
|
+
|
|
310
|
+
// Hash-skip: if template output is identical, skip the morph entirely
|
|
311
|
+
const newHash = Fez.fnv1(parsedHtml);
|
|
312
|
+
if (newHash === this._fezHash) {
|
|
313
|
+
this._isRendering = false;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this._fezHash = newHash;
|
|
317
|
+
|
|
318
|
+
newNode.innerHTML = parsedHtml;
|
|
319
|
+
}
|
|
360
320
|
}
|
|
361
321
|
|
|
362
|
-
|
|
363
|
-
this.fezKeepNode(newNode)
|
|
322
|
+
this.fezKeepNode(newNode);
|
|
364
323
|
|
|
365
|
-
//
|
|
366
|
-
|
|
324
|
+
// Save input values for fez-this/fez-bind bound elements before morph
|
|
325
|
+
const savedInputValues = new Map();
|
|
326
|
+
this.root.querySelectorAll("input, textarea, select").forEach((el) => {
|
|
327
|
+
if (el._fezThisName) {
|
|
328
|
+
savedInputValues.set(el._fezThisName, {
|
|
329
|
+
value: el.value,
|
|
330
|
+
checked: el.checked,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
Fez.morphdom(this.root, newNode);
|
|
367
336
|
|
|
368
|
-
|
|
337
|
+
// Restore input values after morph - find element by _fezThisName property
|
|
338
|
+
savedInputValues.forEach((saved, name) => {
|
|
339
|
+
let el = null;
|
|
340
|
+
this.root.querySelectorAll("input, textarea, select").forEach((input) => {
|
|
341
|
+
if (input._fezThisName === name) el = input;
|
|
342
|
+
});
|
|
343
|
+
if (el) {
|
|
344
|
+
el.value = saved.value;
|
|
345
|
+
if (saved.checked !== undefined) el.checked = saved.checked;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
369
348
|
|
|
370
|
-
this.fezRenderPostProcess()
|
|
349
|
+
this.fezRenderPostProcess();
|
|
350
|
+
this.afterRender();
|
|
371
351
|
|
|
372
|
-
this.
|
|
352
|
+
this._isRendering = false;
|
|
373
353
|
}
|
|
374
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Post-render processing for fez-* attributes
|
|
357
|
+
*/
|
|
375
358
|
fezRenderPostProcess() {
|
|
376
359
|
const fetchAttr = (name, func) => {
|
|
377
|
-
this.root.querySelectorAll(`*[${name}]`).forEach((n)=>{
|
|
378
|
-
let value = n.getAttribute(name)
|
|
379
|
-
n.removeAttribute(name)
|
|
360
|
+
this.root.querySelectorAll(`*[${name}]`).forEach((n) => {
|
|
361
|
+
let value = n.getAttribute(name);
|
|
362
|
+
n.removeAttribute(name);
|
|
380
363
|
if (value) {
|
|
381
|
-
func.bind(this)(value, n)
|
|
364
|
+
func.bind(this)(value, n);
|
|
382
365
|
}
|
|
383
|
-
})
|
|
384
|
-
}
|
|
366
|
+
});
|
|
367
|
+
};
|
|
385
368
|
|
|
386
|
-
//
|
|
387
|
-
fetchAttr(
|
|
388
|
-
|
|
389
|
-
|
|
369
|
+
// fez-this="button" -> this.button = node
|
|
370
|
+
fetchAttr("fez-this", (value, n) => {
|
|
371
|
+
new Function("n", `this.${value} = n`).bind(this)(n);
|
|
372
|
+
// Mark element for value preservation on re-render
|
|
373
|
+
n._fezThisName = value;
|
|
374
|
+
});
|
|
390
375
|
|
|
391
|
-
//
|
|
392
|
-
fetchAttr(
|
|
393
|
-
if (value.includes(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
else {
|
|
403
|
-
// fez-use="animate"
|
|
404
|
-
const target = this[value]
|
|
405
|
-
if (typeof target == 'function') {
|
|
406
|
-
target(n)
|
|
376
|
+
// fez-use="animate" -> this.animate(node)
|
|
377
|
+
fetchAttr("fez-use", (value, n) => {
|
|
378
|
+
if (value.includes("=>")) {
|
|
379
|
+
Fez.getFunction(value)(n);
|
|
380
|
+
} else {
|
|
381
|
+
if (value.includes(".")) {
|
|
382
|
+
Fez.getFunction(value).bind(n)();
|
|
383
|
+
} else {
|
|
384
|
+
const target = this[value];
|
|
385
|
+
if (typeof target == "function") {
|
|
386
|
+
target(n);
|
|
407
387
|
} else {
|
|
408
|
-
|
|
388
|
+
this.fezError("fez-use", `"${value}" is not a function`);
|
|
409
389
|
}
|
|
410
390
|
}
|
|
411
391
|
}
|
|
412
|
-
})
|
|
392
|
+
});
|
|
413
393
|
|
|
414
|
-
//
|
|
415
|
-
fetchAttr(
|
|
416
|
-
let classes = value.split(/\s+/)
|
|
417
|
-
let lastClass = classes.pop()
|
|
418
|
-
classes.forEach((c)=> n.classList.add(c)
|
|
394
|
+
// fez-class="dialog animate" -> add class after init for animation
|
|
395
|
+
fetchAttr("fez-class", (value, n) => {
|
|
396
|
+
let classes = value.split(/\s+/);
|
|
397
|
+
let lastClass = classes.pop();
|
|
398
|
+
classes.forEach((c) => n.classList.add(c));
|
|
419
399
|
if (lastClass) {
|
|
420
|
-
setTimeout(()=>{
|
|
421
|
-
n.classList.add(lastClass)
|
|
422
|
-
}, 1)
|
|
423
|
-
}
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
// <input fez-bind="state.inputNode" -> this.state.inputNode will be the value of input
|
|
427
|
-
fetchAttr('fez-bind', (text, n) => {
|
|
428
|
-
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(n.nodeName)) {
|
|
429
|
-
const value = (new Function(`return this.${text}`)).bind(this)()
|
|
430
|
-
const isCb = n.type.toLowerCase() == 'checkbox'
|
|
431
|
-
const eventName = ['SELECT'].includes(n.nodeName) || isCb ? 'onchange' : 'onkeyup'
|
|
432
|
-
n.setAttribute(eventName, `${this.fezHtmlRoot}${text} = this.${isCb ? 'checked' : 'value'}`)
|
|
433
|
-
this.val(n, value)
|
|
434
|
-
} else {
|
|
435
|
-
console.error(`Cant fez-bind="${text}" to ${n.nodeName} (needs INPUT, SELECT or TEXTAREA. Want to use fez-this?).`)
|
|
400
|
+
setTimeout(() => {
|
|
401
|
+
n.classList.add(lastClass);
|
|
402
|
+
}, 1);
|
|
436
403
|
}
|
|
437
|
-
})
|
|
404
|
+
});
|
|
438
405
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if ([
|
|
442
|
-
|
|
406
|
+
// fez-bind="state.inputNode" -> two-way binding
|
|
407
|
+
fetchAttr("fez-bind", (text, n) => {
|
|
408
|
+
if (["INPUT", "SELECT", "TEXTAREA"].includes(n.nodeName)) {
|
|
409
|
+
const value = new Function(`return this.${text}`).bind(this)();
|
|
410
|
+
const isCb = n.type.toLowerCase() == "checkbox";
|
|
411
|
+
const eventName =
|
|
412
|
+
["SELECT"].includes(n.nodeName) || isCb ? "onchange" : "onkeyup";
|
|
413
|
+
n.setAttribute(
|
|
414
|
+
eventName,
|
|
415
|
+
`${this.fezHtmlRoot}${text} = this.${isCb ? "checked" : "value"}`,
|
|
416
|
+
);
|
|
417
|
+
this.val(n, value);
|
|
418
|
+
// Mark element for value preservation on re-render
|
|
419
|
+
n._fezThisName = text;
|
|
443
420
|
} else {
|
|
444
|
-
|
|
421
|
+
this.fezError(
|
|
422
|
+
"fez-bind",
|
|
423
|
+
`Can't bind "${text}" to ${n.nodeName} (needs INPUT, SELECT or TEXTAREA)`,
|
|
424
|
+
);
|
|
445
425
|
}
|
|
446
|
-
})
|
|
447
|
-
}
|
|
426
|
+
});
|
|
448
427
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if (newEl.getAttribute('hide')) {
|
|
459
|
-
// You cant use state any more
|
|
460
|
-
this.state = null
|
|
461
|
-
|
|
462
|
-
const parent = newEl.parentNode
|
|
463
|
-
|
|
464
|
-
// Insert all root children before the slot's next sibling
|
|
465
|
-
Array.from(this.root.childNodes).forEach(child => {
|
|
466
|
-
parent.insertBefore(child, newEl)
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
// Remove the slot element
|
|
470
|
-
newEl.remove()
|
|
471
|
-
}
|
|
472
|
-
else {
|
|
473
|
-
// First render - populate the slot with current root children
|
|
474
|
-
Array.from(this.root.childNodes).forEach(
|
|
475
|
-
child => {
|
|
476
|
-
newEl.appendChild(child)
|
|
477
|
-
}
|
|
478
|
-
)
|
|
428
|
+
// Normalize boolean attributes (checked, disabled, selected)
|
|
429
|
+
for (const attr of ["checked", "disabled", "selected"]) {
|
|
430
|
+
this.root.querySelectorAll(`*[${attr}]`).forEach((n) => {
|
|
431
|
+
let value = n.getAttribute(attr);
|
|
432
|
+
if (["false", "null", "undefined"].includes(value)) {
|
|
433
|
+
n.removeAttribute(attr);
|
|
434
|
+
n[attr] = false;
|
|
435
|
+
} else {
|
|
436
|
+
n.setAttribute(attr, attr);
|
|
479
437
|
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
482
440
|
}
|
|
483
441
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
442
|
+
/**
|
|
443
|
+
* Handle slot initialization on first render.
|
|
444
|
+
* Moves captured children from _fezSlotNodes into the .fez-slot container.
|
|
445
|
+
* fez-keep matching is handled natively by the differ (morph.js).
|
|
446
|
+
*/
|
|
447
|
+
fezKeepNode(newNode) {
|
|
448
|
+
// First render only: move captured children into slot container.
|
|
449
|
+
// On subsequent renders the differ preserves the live .fez-slot via fez-keep.
|
|
450
|
+
if (this._fezSlotInitialized) return;
|
|
451
|
+
|
|
452
|
+
// Safe to use .querySelector - newNode is a fresh template with no nested components yet
|
|
453
|
+
const newSlot = newNode.querySelector(".fez-slot");
|
|
454
|
+
if (newSlot && this._fezSlotNodes) {
|
|
455
|
+
this._fezSlotInitialized = true;
|
|
456
|
+
this._fezSlotNodes.forEach((child) => {
|
|
457
|
+
newSlot.appendChild(child);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
488
461
|
|
|
489
|
-
|
|
462
|
+
// ===========================================================================
|
|
463
|
+
// REACTIVE STATE
|
|
464
|
+
// ===========================================================================
|
|
490
465
|
|
|
491
|
-
|
|
492
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Register component: setup CSS, state, and bind methods
|
|
468
|
+
*/
|
|
469
|
+
fezRegister() {
|
|
470
|
+
if (this.css) {
|
|
471
|
+
this.css = Fez.globalCss(this.css, { name: this.fezName, wrap: true });
|
|
472
|
+
}
|
|
493
473
|
|
|
494
|
-
if (
|
|
495
|
-
Fez.
|
|
496
|
-
newMemoEl.parentNode.replaceChild(storedNode.cloneNode(true), newMemoEl)
|
|
497
|
-
} else {
|
|
498
|
-
const oldMemoEl = this.root.querySelector('[fez-memoize]:not(.fez)')
|
|
499
|
-
if (oldMemoEl) {
|
|
500
|
-
const oldMemoElKey = oldMemoEl.getAttribute('fez-memoize')
|
|
501
|
-
this.fezMemoStore.set(oldMemoElKey, oldMemoEl.cloneNode(true))
|
|
502
|
-
}
|
|
474
|
+
if (this.class.css) {
|
|
475
|
+
this.class.css = Fez.globalCss(this.class.css, { name: this.fezName });
|
|
503
476
|
}
|
|
477
|
+
|
|
478
|
+
this.state ||= this.fezReactiveStore();
|
|
479
|
+
this.globalState = Fez.state.createProxy(this);
|
|
480
|
+
this.fezRegisterBindMethods();
|
|
504
481
|
}
|
|
505
482
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Bind all instance methods to this
|
|
485
|
+
*/
|
|
486
|
+
fezRegisterBindMethods() {
|
|
487
|
+
const methods = Object.getOwnPropertyNames(
|
|
488
|
+
Object.getPrototypeOf(this),
|
|
489
|
+
).filter(
|
|
490
|
+
(method) =>
|
|
491
|
+
method !== "constructor" && typeof this[method] === "function",
|
|
492
|
+
);
|
|
493
|
+
methods.forEach((method) => (this[method] = this[method].bind(this)));
|
|
516
494
|
}
|
|
517
495
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
496
|
+
/**
|
|
497
|
+
* Create a reactive store that triggers re-renders on changes
|
|
498
|
+
*/
|
|
499
|
+
fezReactiveStore(obj, handler) {
|
|
500
|
+
obj ||= {};
|
|
523
501
|
|
|
524
|
-
|
|
502
|
+
handler ||= (o, k, v, oldValue) => {
|
|
503
|
+
if (v != oldValue) {
|
|
504
|
+
this.onStateChange(k, v, oldValue);
|
|
505
|
+
// Don't schedule re-render during init/mount or if already rendering
|
|
506
|
+
if (!this._isRendering && !this._isInitializing) {
|
|
507
|
+
this.fezNextTick(this.fezRender, "fezRender");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
525
511
|
|
|
526
|
-
this
|
|
527
|
-
clearInterval(this._setIntervalCache[name])
|
|
512
|
+
handler.bind(this);
|
|
528
513
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
514
|
+
function shouldProxy(obj) {
|
|
515
|
+
return (
|
|
516
|
+
typeof obj === "object" &&
|
|
517
|
+
obj !== null &&
|
|
518
|
+
!(obj instanceof Promise) &&
|
|
519
|
+
!obj.nodeType
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function createReactive(obj, handler) {
|
|
524
|
+
if (!shouldProxy(obj)) {
|
|
525
|
+
return obj;
|
|
532
526
|
}
|
|
533
|
-
}, tick)
|
|
534
527
|
|
|
535
|
-
|
|
528
|
+
return new Proxy(obj, {
|
|
529
|
+
set(target, property, value, receiver) {
|
|
530
|
+
const currentValue = Reflect.get(target, property, receiver);
|
|
536
531
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
});
|
|
532
|
+
if (currentValue !== value) {
|
|
533
|
+
if (shouldProxy(value)) {
|
|
534
|
+
value = createReactive(value, handler);
|
|
535
|
+
}
|
|
542
536
|
|
|
543
|
-
|
|
537
|
+
const result = Reflect.set(target, property, value, receiver);
|
|
538
|
+
handler(target, property, value, currentValue);
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return true;
|
|
543
|
+
},
|
|
544
|
+
get(target, property, receiver) {
|
|
545
|
+
const value = Reflect.get(target, property, receiver);
|
|
546
|
+
if (shouldProxy(value)) {
|
|
547
|
+
return createReactive(value, handler);
|
|
548
|
+
}
|
|
549
|
+
return value;
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return createReactive(obj, handler);
|
|
544
555
|
}
|
|
545
556
|
|
|
557
|
+
// ===========================================================================
|
|
558
|
+
// DOM HELPERS
|
|
559
|
+
// ===========================================================================
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Find element by selector
|
|
563
|
+
*/
|
|
546
564
|
find(selector) {
|
|
547
|
-
return typeof selector ==
|
|
565
|
+
return typeof selector == "string"
|
|
566
|
+
? this.root.querySelector(selector)
|
|
567
|
+
: selector;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Add one or more classes (space-separated) to root or given node
|
|
572
|
+
*/
|
|
573
|
+
addClass(names, node) {
|
|
574
|
+
(node || this.root).classList.add(...names.split(/\s+/).filter(Boolean));
|
|
548
575
|
}
|
|
549
576
|
|
|
550
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Toggle a class on root or given node, with optional force boolean
|
|
579
|
+
*/
|
|
580
|
+
toggleClass(name, force, node) {
|
|
581
|
+
(node || this.root).classList.toggle(name, force);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get or set node value (input/textarea/select or innerHTML)
|
|
586
|
+
*/
|
|
551
587
|
val(selector, data) {
|
|
552
|
-
const node = this.find(selector)
|
|
588
|
+
const node = this.find(selector);
|
|
553
589
|
|
|
554
590
|
if (node) {
|
|
555
|
-
if ([
|
|
556
|
-
if (typeof data !=
|
|
557
|
-
if (node.type ==
|
|
558
|
-
node.checked = !!data
|
|
591
|
+
if (["INPUT", "TEXTAREA", "SELECT"].includes(node.nodeName)) {
|
|
592
|
+
if (typeof data != "undefined") {
|
|
593
|
+
if (node.type == "checkbox") {
|
|
594
|
+
node.checked = !!data;
|
|
559
595
|
} else {
|
|
560
|
-
node.value = data
|
|
596
|
+
node.value = data;
|
|
561
597
|
}
|
|
562
598
|
} else {
|
|
563
|
-
return node.value
|
|
599
|
+
return node.value;
|
|
564
600
|
}
|
|
565
601
|
} else {
|
|
566
|
-
if (typeof data !=
|
|
567
|
-
node.innerHTML = data
|
|
602
|
+
if (typeof data != "undefined") {
|
|
603
|
+
node.innerHTML = data;
|
|
568
604
|
} else {
|
|
569
|
-
return node.innerHTML
|
|
605
|
+
return node.innerHTML;
|
|
570
606
|
}
|
|
571
607
|
}
|
|
572
608
|
}
|
|
573
609
|
}
|
|
574
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Instance form data helper
|
|
613
|
+
*/
|
|
575
614
|
formData(node) {
|
|
576
|
-
return this.class.formData(node || this.root)
|
|
615
|
+
return this.class.formData(node || this.root);
|
|
577
616
|
}
|
|
578
617
|
|
|
579
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Get or set root attribute
|
|
620
|
+
*/
|
|
580
621
|
attr(name, value) {
|
|
581
|
-
if (typeof value ===
|
|
582
|
-
return this.root.getAttribute(name)
|
|
622
|
+
if (typeof value === "undefined") {
|
|
623
|
+
return this.root.getAttribute(name);
|
|
583
624
|
} else {
|
|
584
|
-
this.root.setAttribute(name, value)
|
|
585
|
-
return value
|
|
625
|
+
this.root.setAttribute(name, value);
|
|
626
|
+
return value;
|
|
586
627
|
}
|
|
587
628
|
}
|
|
588
629
|
|
|
589
|
-
|
|
630
|
+
/**
|
|
631
|
+
* Get root element children as array, optionally transform
|
|
632
|
+
* Returns only element nodes (nodeType === 1), text nodes are excluded.
|
|
633
|
+
* Pass true to convert children to objects with attrs as keys, innerHTML as .html, original node as .ROOT
|
|
634
|
+
*/
|
|
590
635
|
childNodes(func) {
|
|
591
|
-
let children = Array.from(this.root.children)
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
636
|
+
let children = this._fezChildNodes || Array.from(this.root.children);
|
|
637
|
+
if (func === true) {
|
|
638
|
+
children = children.map((node) => {
|
|
639
|
+
const obj = { html: node.innerHTML, ROOT: node };
|
|
640
|
+
for (const attr of node.attributes) {
|
|
641
|
+
obj[attr.name] = attr.value;
|
|
642
|
+
}
|
|
643
|
+
return obj;
|
|
644
|
+
});
|
|
645
|
+
} else if (func) {
|
|
646
|
+
children = children.map(func);
|
|
602
647
|
}
|
|
603
|
-
|
|
604
|
-
return children
|
|
648
|
+
return children;
|
|
605
649
|
}
|
|
606
650
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
651
|
+
/**
|
|
652
|
+
* Set CSS properties on root
|
|
653
|
+
*/
|
|
654
|
+
setStyle(key, value) {
|
|
655
|
+
if (key && typeof key == "object") {
|
|
656
|
+
Object.entries(key).forEach(([prop, val]) => {
|
|
657
|
+
this.root.style.setProperty(prop, val);
|
|
658
|
+
});
|
|
659
|
+
} else {
|
|
660
|
+
this.root.style.setProperty(key, value);
|
|
661
|
+
}
|
|
612
662
|
}
|
|
613
663
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Copy props as attributes to root
|
|
666
|
+
*/
|
|
667
|
+
copy() {
|
|
668
|
+
for (const name of Array.from(arguments)) {
|
|
669
|
+
let value = this.props[name];
|
|
619
670
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
671
|
+
if (value !== undefined) {
|
|
672
|
+
if (name == "class") {
|
|
673
|
+
const klass = this.root.getAttribute(name, value);
|
|
674
|
+
if (klass) {
|
|
675
|
+
value = [klass, value].join(" ");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
624
678
|
|
|
625
|
-
|
|
626
|
-
|
|
679
|
+
if (typeof value == "string") {
|
|
680
|
+
this.root.setAttribute(name, value);
|
|
681
|
+
} else {
|
|
682
|
+
this.root[name] = value;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
627
685
|
}
|
|
628
|
-
|
|
629
|
-
this.state ||= this.reactiveStore()
|
|
630
|
-
this.globalState = Fez.state.createProxy(this)
|
|
631
|
-
this.fezRegisterBindMethods()
|
|
632
686
|
}
|
|
633
687
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Get or set root ID
|
|
690
|
+
*/
|
|
691
|
+
rootId() {
|
|
692
|
+
this.root.id ||= `fez_${this.UID}`;
|
|
693
|
+
return this.root.id;
|
|
640
694
|
}
|
|
641
695
|
|
|
642
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Dissolve component into parent
|
|
698
|
+
*/
|
|
643
699
|
dissolve(inNode) {
|
|
644
700
|
if (inNode) {
|
|
645
|
-
inNode.classList.add(
|
|
646
|
-
inNode.classList.add(`fez-${this.fezName}`)
|
|
647
|
-
inNode.fez = this
|
|
648
|
-
if (this.attr(
|
|
701
|
+
inNode.classList.add("fez");
|
|
702
|
+
inNode.classList.add(`fez-${this.fezName}`);
|
|
703
|
+
inNode.fez = this;
|
|
704
|
+
if (this.attr("id")) inNode.setAttribute("id", this.attr("id"));
|
|
649
705
|
|
|
650
|
-
this.root.innerHTML =
|
|
651
|
-
this.root.appendChild(inNode)
|
|
706
|
+
this.root.innerHTML = "";
|
|
707
|
+
this.root.appendChild(inNode);
|
|
652
708
|
}
|
|
653
709
|
|
|
654
|
-
const node = this.root
|
|
655
|
-
const nodes = this.childNodes()
|
|
656
|
-
const parent = this.root.parentNode
|
|
710
|
+
const node = this.root;
|
|
711
|
+
const nodes = this.childNodes();
|
|
712
|
+
const parent = this.root.parentNode;
|
|
657
713
|
|
|
658
|
-
nodes.reverse().forEach(el => parent.insertBefore(el, node.nextSibling))
|
|
714
|
+
nodes.reverse().forEach((el) => parent.insertBefore(el, node.nextSibling));
|
|
659
715
|
|
|
660
|
-
this.root.remove()
|
|
661
|
-
this.root = undefined
|
|
716
|
+
this.root.remove();
|
|
717
|
+
this.root = undefined;
|
|
662
718
|
|
|
663
719
|
if (inNode) {
|
|
664
|
-
this.root = inNode
|
|
720
|
+
this.root = inNode;
|
|
665
721
|
}
|
|
666
722
|
|
|
667
|
-
return nodes
|
|
723
|
+
return nodes;
|
|
668
724
|
}
|
|
669
725
|
|
|
670
|
-
|
|
671
|
-
|
|
726
|
+
// ===========================================================================
|
|
727
|
+
// EVENTS
|
|
728
|
+
// ===========================================================================
|
|
672
729
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Add window event listener with auto-cleanup
|
|
732
|
+
*/
|
|
733
|
+
on(eventName, func, delay = 200) {
|
|
734
|
+
this._eventHandlers = this._eventHandlers || {};
|
|
735
|
+
|
|
736
|
+
if (this._eventHandlers[eventName]) {
|
|
737
|
+
window.removeEventListener(eventName, this._eventHandlers[eventName]);
|
|
676
738
|
}
|
|
677
739
|
|
|
678
|
-
|
|
740
|
+
const throttledFunc = Fez.throttle(() => {
|
|
741
|
+
if (this.isConnected) func.call(this);
|
|
742
|
+
}, delay);
|
|
679
743
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (typeof obj !== 'object' || obj === null) {
|
|
683
|
-
return obj;
|
|
684
|
-
}
|
|
744
|
+
this._eventHandlers[eventName] = throttledFunc;
|
|
745
|
+
window.addEventListener(eventName, throttledFunc);
|
|
685
746
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
747
|
+
this.addOnDestroy(() => {
|
|
748
|
+
window.removeEventListener(eventName, throttledFunc);
|
|
749
|
+
delete this._eventHandlers[eventName];
|
|
750
|
+
});
|
|
751
|
+
}
|
|
690
752
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
753
|
+
/**
|
|
754
|
+
* Window resize handler
|
|
755
|
+
*/
|
|
756
|
+
onWindowResize(func, delay) {
|
|
757
|
+
this.on("resize", func, delay);
|
|
758
|
+
func();
|
|
759
|
+
}
|
|
696
760
|
|
|
697
|
-
|
|
698
|
-
|
|
761
|
+
/**
|
|
762
|
+
* Window scroll handler
|
|
763
|
+
*/
|
|
764
|
+
onWindowScroll(func, delay) {
|
|
765
|
+
this.on("scroll", func, delay);
|
|
766
|
+
func();
|
|
767
|
+
}
|
|
699
768
|
|
|
700
|
-
|
|
701
|
-
|
|
769
|
+
/**
|
|
770
|
+
* Element resize handler using ResizeObserver
|
|
771
|
+
*/
|
|
772
|
+
onElementResize(el, func, delay = 200) {
|
|
773
|
+
const throttledFunc = Fez.throttle(() => {
|
|
774
|
+
if (this.isConnected) func.call(this, el.getBoundingClientRect(), el);
|
|
775
|
+
}, delay);
|
|
702
776
|
|
|
703
|
-
|
|
704
|
-
|
|
777
|
+
const observer = new ResizeObserver(throttledFunc);
|
|
778
|
+
observer.observe(el);
|
|
705
779
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
780
|
+
func.call(this, el.getBoundingClientRect(), el);
|
|
781
|
+
|
|
782
|
+
this.addOnDestroy(() => {
|
|
783
|
+
observer.disconnect();
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Timeout with auto-cleanup
|
|
789
|
+
*/
|
|
790
|
+
setTimeout(func, delay) {
|
|
791
|
+
const timeoutID = setTimeout(() => {
|
|
792
|
+
if (this.isConnected) func();
|
|
793
|
+
}, delay);
|
|
794
|
+
|
|
795
|
+
this.addOnDestroy(() => clearTimeout(timeoutID));
|
|
796
|
+
|
|
797
|
+
return timeoutID;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Interval with auto-cleanup
|
|
802
|
+
*/
|
|
803
|
+
setInterval(func, tick, name) {
|
|
804
|
+
if (typeof func == "number") {
|
|
805
|
+
[tick, func] = [func, tick];
|
|
717
806
|
}
|
|
718
807
|
|
|
719
|
-
|
|
808
|
+
name ||= Fez.fnv1(String(func));
|
|
809
|
+
|
|
810
|
+
this._setIntervalCache ||= {};
|
|
811
|
+
clearInterval(this._setIntervalCache[name]);
|
|
812
|
+
|
|
813
|
+
const intervalID = setInterval(() => {
|
|
814
|
+
if (this.isConnected) func();
|
|
815
|
+
}, tick);
|
|
816
|
+
|
|
817
|
+
this._setIntervalCache[name] = intervalID;
|
|
818
|
+
|
|
819
|
+
this.addOnDestroy(() => {
|
|
820
|
+
clearInterval(intervalID);
|
|
821
|
+
delete this._setIntervalCache[name];
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
return intervalID;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// ===========================================================================
|
|
828
|
+
// PUB/SUB
|
|
829
|
+
// ===========================================================================
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Publish to parent components (bubbles up through DOM)
|
|
833
|
+
* @param {string} channel - Event name
|
|
834
|
+
* @param {...any} args - Arguments to pass
|
|
835
|
+
* @returns {boolean} True if a parent handled the event
|
|
836
|
+
*/
|
|
837
|
+
publish(channel, ...args) {
|
|
838
|
+
return componentPublish(this, channel, ...args);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Subscribe to a channel (auto-cleanup on destroy)
|
|
843
|
+
* @param {string} channel - Event name
|
|
844
|
+
* @param {Function} func - Handler function
|
|
845
|
+
* @returns {Function} Unsubscribe function
|
|
846
|
+
*/
|
|
847
|
+
subscribe(channel, func) {
|
|
848
|
+
const unsubscribe = componentSubscribe(this, channel, func);
|
|
849
|
+
this.addOnDestroy(unsubscribe);
|
|
850
|
+
return unsubscribe;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ===========================================================================
|
|
854
|
+
// SLOTS
|
|
855
|
+
// ===========================================================================
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Copy child nodes natively to preserve bound events
|
|
859
|
+
*/
|
|
860
|
+
fezSlot(source, target) {
|
|
861
|
+
target ||= document.createElement("template");
|
|
862
|
+
const isSlot = target.nodeName == "SLOT";
|
|
863
|
+
|
|
864
|
+
while (source.firstChild) {
|
|
865
|
+
if (isSlot) {
|
|
866
|
+
target.parentNode.insertBefore(source.lastChild, target.nextSibling);
|
|
867
|
+
} else {
|
|
868
|
+
target.appendChild(source.firstChild);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (isSlot) {
|
|
873
|
+
target.parentNode.removeChild(target);
|
|
874
|
+
} else {
|
|
875
|
+
source.innerHTML = "";
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return target;
|
|
720
879
|
}
|
|
721
880
|
}
|