@dinoreic/fez 0.4.1 → 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 +707 -209
- 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 +249 -146
- package/src/fez/defaults.js +272 -92
- 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 +279 -209
- 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/svelte-cde-adapter.coffee +10 -2
- 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
|
-
}
|
|
173
|
+
connect() {}
|
|
174
|
+
onMount() {}
|
|
175
|
+
beforeRender() {}
|
|
176
|
+
afterRender() {}
|
|
177
|
+
onDestroy() {}
|
|
178
|
+
onStateChange() {}
|
|
179
|
+
onGlobalStateChange() {}
|
|
180
|
+
onPropsChange() {}
|
|
142
181
|
|
|
143
|
-
|
|
144
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Centralized destroy logic - called by MutationObserver when element is removed
|
|
184
|
+
*/
|
|
145
185
|
fezOnDestroy() {
|
|
146
|
-
//
|
|
186
|
+
// Guard against double-cleanup
|
|
187
|
+
if (this._destroyed) return;
|
|
188
|
+
this._destroyed = true;
|
|
189
|
+
|
|
190
|
+
// Execute cleanup callbacks (intervals, observers, event listeners)
|
|
147
191
|
if (this._onDestroyCallbacks) {
|
|
148
|
-
this._onDestroyCallbacks.forEach(callback => {
|
|
192
|
+
this._onDestroyCallbacks.forEach((callback) => {
|
|
149
193
|
try {
|
|
150
194
|
callback();
|
|
151
195
|
} catch (e) {
|
|
152
|
-
|
|
196
|
+
this.fezError("destroy", "Error in cleanup callback", e);
|
|
153
197
|
}
|
|
154
198
|
});
|
|
155
199
|
this._onDestroyCallbacks = [];
|
|
156
200
|
}
|
|
157
201
|
|
|
158
|
-
// Call user's onDestroy
|
|
159
|
-
this.onDestroy()
|
|
160
|
-
this.onDestroy = () => {}
|
|
202
|
+
// Call user's onDestroy hook
|
|
203
|
+
this.onDestroy();
|
|
204
|
+
this.onDestroy = () => {};
|
|
161
205
|
|
|
162
206
|
// Clean up root references
|
|
163
207
|
if (this.root) {
|
|
164
|
-
this.root.fez = undefined
|
|
208
|
+
this.root.fez = undefined;
|
|
165
209
|
}
|
|
166
|
-
|
|
167
|
-
this.root = undefined
|
|
210
|
+
this.root = undefined;
|
|
168
211
|
}
|
|
169
212
|
|
|
170
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Add a cleanup callback for destroy
|
|
215
|
+
*/
|
|
171
216
|
addOnDestroy(callback) {
|
|
172
217
|
this._onDestroyCallbacks = this._onDestroyCallbacks || [];
|
|
173
218
|
this._onDestroyCallbacks.push(callback);
|
|
174
219
|
}
|
|
175
220
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (this._eventHandlers[eventName]) {
|
|
181
|
-
window.removeEventListener(eventName, this._eventHandlers[eventName]);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const throttledFunc = Fez.throttle(() => {
|
|
185
|
-
if (this.isConnected) {
|
|
186
|
-
func.call(this);
|
|
187
|
-
}
|
|
188
|
-
}, delay);
|
|
189
|
-
|
|
190
|
-
this._eventHandlers[eventName] = throttledFunc;
|
|
191
|
-
window.addEventListener(eventName, throttledFunc);
|
|
192
|
-
|
|
193
|
-
this.addOnDestroy(() => {
|
|
194
|
-
window.removeEventListener(eventName, throttledFunc);
|
|
195
|
-
delete this._eventHandlers[eventName];
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Helper function for resize events
|
|
200
|
-
onWindowResize(func, delay) {
|
|
201
|
-
this.on('resize', func, delay);
|
|
202
|
-
func();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Helper function for scroll events
|
|
206
|
-
onWindowScroll(func, delay) {
|
|
207
|
-
this.on('scroll', func, delay);
|
|
208
|
-
func();
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Helper function for element resize events using ResizeObserver
|
|
212
|
-
onElementResize(el, func, delay = 200) {
|
|
213
|
-
const throttledFunc = Fez.throttle(() => {
|
|
214
|
-
if (this.isConnected) {
|
|
215
|
-
func.call(this, el.getBoundingClientRect(), el);
|
|
216
|
-
}
|
|
217
|
-
}, delay);
|
|
221
|
+
// ===========================================================================
|
|
222
|
+
// RENDERING
|
|
223
|
+
// ===========================================================================
|
|
218
224
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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();
|
|
227
234
|
}
|
|
228
235
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
target.appendChild(source.firstChild)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (isSlot) {
|
|
244
|
-
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);
|
|
245
246
|
} else {
|
|
246
|
-
|
|
247
|
+
window.requestAnimationFrame(func.bind(this));
|
|
247
248
|
}
|
|
248
|
-
|
|
249
|
-
return target
|
|
250
249
|
}
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
onMount() {}
|
|
258
|
-
beforeRender() {}
|
|
259
|
-
afterRender() {}
|
|
260
|
-
onDestroy() {}
|
|
261
|
-
onStateChange() {}
|
|
262
|
-
onGlobalStateChange() {}
|
|
263
|
-
|
|
264
|
-
// component publish will search for parent component that subscribes by name
|
|
265
|
-
publish(channel, ...args) {
|
|
266
|
-
const handle_publish = (component) => {
|
|
267
|
-
if (Fez._subs && Fez._subs[channel]) {
|
|
268
|
-
const sub = Fez._subs[channel].find(([comp]) => comp === component)
|
|
269
|
-
if (sub) {
|
|
270
|
-
sub[1].bind(component)(...args)
|
|
271
|
-
return true
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return false
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Check if current component has subscription
|
|
278
|
-
if (handle_publish(this)) {
|
|
279
|
-
return true
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Bubble up to parent components
|
|
283
|
-
let parent = this.root.parentElement
|
|
284
|
-
while (parent) {
|
|
285
|
-
if (parent.fez) {
|
|
286
|
-
if (handle_publish(parent.fez)) {
|
|
287
|
-
return true
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
parent = parent.parentElement
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// If no parent handled it, fall back to global publish
|
|
294
|
-
// Fez.publish(channel, ...args)
|
|
295
|
-
return false
|
|
251
|
+
/**
|
|
252
|
+
* Force a re-render on next frame
|
|
253
|
+
*/
|
|
254
|
+
fezRefresh() {
|
|
255
|
+
this.fezNextTick(() => this.fezRender(), "refresh");
|
|
296
256
|
}
|
|
297
257
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
text = text
|
|
304
|
-
.replace(/([!'"\s;])fez\.(\w)/g, `$1${base}$2`)
|
|
305
|
-
.replace(/>\s+</g, '><')
|
|
306
|
-
|
|
307
|
-
return text.trim()
|
|
258
|
+
/**
|
|
259
|
+
* Alias for fezRefresh - can be overwritten
|
|
260
|
+
*/
|
|
261
|
+
refresh() {
|
|
262
|
+
this.fezRefresh();
|
|
308
263
|
}
|
|
309
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;
|
|
310
272
|
|
|
311
|
-
|
|
312
|
-
nextTick(func, name) {
|
|
313
|
-
if (name) {
|
|
314
|
-
this._nextTicks ||= {}
|
|
315
|
-
this._nextTicks[name] ||= window.requestAnimationFrame(() => {
|
|
316
|
-
func.bind(this)()
|
|
317
|
-
this._nextTicks[name] = null
|
|
318
|
-
}, name)
|
|
319
|
-
} else {
|
|
320
|
-
window.requestAnimationFrame(func.bind(this))
|
|
321
|
-
}
|
|
322
|
-
}
|
|
273
|
+
if (!template || !this.root) return;
|
|
323
274
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// <slot></slot> will be replaced with current root
|
|
327
|
-
// this.render('...loading')
|
|
328
|
-
// this.render('.images', '...loading')
|
|
329
|
-
render(template) {
|
|
330
|
-
template ||= this?.class?.fezHtmlFunc
|
|
275
|
+
// Prevent re-render loops from state changes in beforeRender/afterRender
|
|
276
|
+
this._isRendering = true;
|
|
331
277
|
|
|
332
|
-
|
|
278
|
+
this.beforeRender();
|
|
333
279
|
|
|
334
|
-
|
|
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");
|
|
335
285
|
|
|
336
|
-
|
|
337
|
-
const newNode = document.createElement(nodeName || 'div')
|
|
338
|
-
|
|
339
|
-
let renderedTpl
|
|
286
|
+
let renderedTpl;
|
|
340
287
|
if (Array.isArray(template)) {
|
|
341
|
-
// array nodes this.n(...), look tabs example
|
|
342
288
|
if (template[0] instanceof Node) {
|
|
343
|
-
template.forEach(
|
|
344
|
-
} else{
|
|
345
|
-
renderedTpl = template.join(
|
|
289
|
+
template.forEach((n) => newNode.appendChild(n));
|
|
290
|
+
} else {
|
|
291
|
+
renderedTpl = template.join("");
|
|
346
292
|
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
renderedTpl = createTemplate(template)(this)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
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);
|
|
353
298
|
}
|
|
354
299
|
|
|
355
300
|
if (renderedTpl) {
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
}
|
|
358
320
|
}
|
|
359
321
|
|
|
360
|
-
|
|
361
|
-
this.fezKeepNode(newNode)
|
|
322
|
+
this.fezKeepNode(newNode);
|
|
362
323
|
|
|
363
|
-
//
|
|
364
|
-
|
|
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
|
+
});
|
|
365
334
|
|
|
366
|
-
Fez.morphdom(this.root, newNode)
|
|
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
|
+
});
|
|
348
|
+
|
|
349
|
+
this.fezRenderPostProcess();
|
|
350
|
+
this.afterRender();
|
|
369
351
|
|
|
370
|
-
this.
|
|
352
|
+
this._isRendering = false;
|
|
371
353
|
}
|
|
372
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Post-render processing for fez-* attributes
|
|
357
|
+
*/
|
|
373
358
|
fezRenderPostProcess() {
|
|
374
359
|
const fetchAttr = (name, func) => {
|
|
375
|
-
this.root.querySelectorAll(`*[${name}]`).forEach((n)=>{
|
|
376
|
-
let value = n.getAttribute(name)
|
|
377
|
-
n.removeAttribute(name)
|
|
360
|
+
this.root.querySelectorAll(`*[${name}]`).forEach((n) => {
|
|
361
|
+
let value = n.getAttribute(name);
|
|
362
|
+
n.removeAttribute(name);
|
|
378
363
|
if (value) {
|
|
379
|
-
func.bind(this)(value, n)
|
|
364
|
+
func.bind(this)(value, n);
|
|
380
365
|
}
|
|
381
|
-
})
|
|
382
|
-
}
|
|
366
|
+
});
|
|
367
|
+
};
|
|
383
368
|
|
|
384
|
-
//
|
|
385
|
-
fetchAttr(
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
});
|
|
388
375
|
|
|
389
|
-
//
|
|
390
|
-
fetchAttr(
|
|
391
|
-
if (value.includes(
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
else {
|
|
401
|
-
// fez-use="animate"
|
|
402
|
-
const target = this[value]
|
|
403
|
-
if (typeof target == 'function') {
|
|
404
|
-
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);
|
|
405
387
|
} else {
|
|
406
|
-
|
|
388
|
+
this.fezError("fez-use", `"${value}" is not a function`);
|
|
407
389
|
}
|
|
408
390
|
}
|
|
409
391
|
}
|
|
410
|
-
})
|
|
392
|
+
});
|
|
411
393
|
|
|
412
|
-
//
|
|
413
|
-
fetchAttr(
|
|
414
|
-
let classes = value.split(/\s+/)
|
|
415
|
-
let lastClass = classes.pop()
|
|
416
|
-
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));
|
|
417
399
|
if (lastClass) {
|
|
418
|
-
setTimeout(()=>{
|
|
419
|
-
n.classList.add(lastClass)
|
|
420
|
-
}, 1)
|
|
421
|
-
}
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
// <input fez-bind="state.inputNode" -> this.state.inputNode will be the value of input
|
|
425
|
-
fetchAttr('fez-bind', (text, n) => {
|
|
426
|
-
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(n.nodeName)) {
|
|
427
|
-
const value = (new Function(`return this.${text}`)).bind(this)()
|
|
428
|
-
const isCb = n.type.toLowerCase() == 'checkbox'
|
|
429
|
-
const eventName = ['SELECT'].includes(n.nodeName) || isCb ? 'onchange' : 'onkeyup'
|
|
430
|
-
n.setAttribute(eventName, `${this.fezHtmlRoot}${text} = this.${isCb ? 'checked' : 'value'}`)
|
|
431
|
-
this.val(n, value)
|
|
432
|
-
} else {
|
|
433
|
-
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);
|
|
434
403
|
}
|
|
435
|
-
})
|
|
404
|
+
});
|
|
436
405
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if ([
|
|
440
|
-
|
|
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;
|
|
441
420
|
} else {
|
|
442
|
-
|
|
421
|
+
this.fezError(
|
|
422
|
+
"fez-bind",
|
|
423
|
+
`Can't bind "${text}" to ${n.nodeName} (needs INPUT, SELECT or TEXTAREA)`,
|
|
424
|
+
);
|
|
443
425
|
}
|
|
444
|
-
})
|
|
445
|
-
}
|
|
426
|
+
});
|
|
446
427
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (newEl.getAttribute('hide')) {
|
|
457
|
-
// You cant use state any more
|
|
458
|
-
this.state = null
|
|
459
|
-
|
|
460
|
-
const parent = newEl.parentNode
|
|
461
|
-
|
|
462
|
-
// Insert all root children before the slot's next sibling
|
|
463
|
-
Array.from(this.root.childNodes).forEach(child => {
|
|
464
|
-
parent.insertBefore(child, newEl)
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
// Remove the slot element
|
|
468
|
-
newEl.remove()
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
// First render - populate the slot with current root children
|
|
472
|
-
Array.from(this.root.childNodes).forEach(
|
|
473
|
-
child => {
|
|
474
|
-
newEl.appendChild(child)
|
|
475
|
-
}
|
|
476
|
-
)
|
|
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);
|
|
477
437
|
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
480
440
|
}
|
|
481
441
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
}
|
|
486
461
|
|
|
487
|
-
|
|
462
|
+
// ===========================================================================
|
|
463
|
+
// REACTIVE STATE
|
|
464
|
+
// ===========================================================================
|
|
488
465
|
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
473
|
|
|
492
|
-
if (
|
|
493
|
-
Fez.
|
|
494
|
-
newMemoEl.parentNode.replaceChild(storedNode.cloneNode(true), newMemoEl)
|
|
495
|
-
} else {
|
|
496
|
-
const oldMemoEl = this.root.querySelector('[fez-memoize]:not(.fez)')
|
|
497
|
-
if (oldMemoEl) {
|
|
498
|
-
const oldMemoElKey = oldMemoEl.getAttribute('fez-memoize')
|
|
499
|
-
this.fezMemoStore.set(oldMemoElKey, oldMemoEl.cloneNode(true))
|
|
500
|
-
}
|
|
474
|
+
if (this.class.css) {
|
|
475
|
+
this.class.css = Fez.globalCss(this.class.css, { name: this.fezName });
|
|
501
476
|
}
|
|
477
|
+
|
|
478
|
+
this.state ||= this.fezReactiveStore();
|
|
479
|
+
this.globalState = Fez.state.createProxy(this);
|
|
480
|
+
this.fezRegisterBindMethods();
|
|
502
481
|
}
|
|
503
482
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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)));
|
|
514
494
|
}
|
|
515
495
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
496
|
+
/**
|
|
497
|
+
* Create a reactive store that triggers re-renders on changes
|
|
498
|
+
*/
|
|
499
|
+
fezReactiveStore(obj, handler) {
|
|
500
|
+
obj ||= {};
|
|
521
501
|
|
|
522
|
-
|
|
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
|
+
};
|
|
523
511
|
|
|
524
|
-
this
|
|
525
|
-
clearInterval(this._setIntervalCache[name])
|
|
512
|
+
handler.bind(this);
|
|
526
513
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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;
|
|
530
526
|
}
|
|
531
|
-
}, tick)
|
|
532
527
|
|
|
533
|
-
|
|
528
|
+
return new Proxy(obj, {
|
|
529
|
+
set(target, property, value, receiver) {
|
|
530
|
+
const currentValue = Reflect.get(target, property, receiver);
|
|
534
531
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
532
|
+
if (currentValue !== value) {
|
|
533
|
+
if (shouldProxy(value)) {
|
|
534
|
+
value = createReactive(value, handler);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const result = Reflect.set(target, property, value, receiver);
|
|
538
|
+
handler(target, property, value, currentValue);
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
540
541
|
|
|
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);
|
|
542
555
|
}
|
|
543
556
|
|
|
557
|
+
// ===========================================================================
|
|
558
|
+
// DOM HELPERS
|
|
559
|
+
// ===========================================================================
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Find element by selector
|
|
563
|
+
*/
|
|
544
564
|
find(selector) {
|
|
545
|
-
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));
|
|
575
|
+
}
|
|
576
|
+
|
|
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);
|
|
546
582
|
}
|
|
547
583
|
|
|
548
|
-
|
|
584
|
+
/**
|
|
585
|
+
* Get or set node value (input/textarea/select or innerHTML)
|
|
586
|
+
*/
|
|
549
587
|
val(selector, data) {
|
|
550
|
-
const node = this.find(selector)
|
|
588
|
+
const node = this.find(selector);
|
|
551
589
|
|
|
552
590
|
if (node) {
|
|
553
|
-
if ([
|
|
554
|
-
if (typeof data !=
|
|
555
|
-
if (node.type ==
|
|
556
|
-
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;
|
|
557
595
|
} else {
|
|
558
|
-
node.value = data
|
|
596
|
+
node.value = data;
|
|
559
597
|
}
|
|
560
598
|
} else {
|
|
561
|
-
return node.value
|
|
599
|
+
return node.value;
|
|
562
600
|
}
|
|
563
601
|
} else {
|
|
564
|
-
if (typeof data !=
|
|
565
|
-
node.innerHTML = data
|
|
602
|
+
if (typeof data != "undefined") {
|
|
603
|
+
node.innerHTML = data;
|
|
566
604
|
} else {
|
|
567
|
-
return node.innerHTML
|
|
605
|
+
return node.innerHTML;
|
|
568
606
|
}
|
|
569
607
|
}
|
|
570
608
|
}
|
|
571
609
|
}
|
|
572
610
|
|
|
611
|
+
/**
|
|
612
|
+
* Instance form data helper
|
|
613
|
+
*/
|
|
573
614
|
formData(node) {
|
|
574
|
-
return this.class.formData(node || this.root)
|
|
615
|
+
return this.class.formData(node || this.root);
|
|
575
616
|
}
|
|
576
617
|
|
|
577
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Get or set root attribute
|
|
620
|
+
*/
|
|
578
621
|
attr(name, value) {
|
|
579
|
-
if (typeof value ===
|
|
580
|
-
return this.root.getAttribute(name)
|
|
622
|
+
if (typeof value === "undefined") {
|
|
623
|
+
return this.root.getAttribute(name);
|
|
581
624
|
} else {
|
|
582
|
-
this.root.setAttribute(name, value)
|
|
583
|
-
return value
|
|
625
|
+
this.root.setAttribute(name, value);
|
|
626
|
+
return value;
|
|
584
627
|
}
|
|
585
628
|
}
|
|
586
629
|
|
|
587
|
-
|
|
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
|
+
*/
|
|
588
635
|
childNodes(func) {
|
|
589
|
-
let children = Array.from(this.root.children)
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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);
|
|
600
647
|
}
|
|
601
|
-
|
|
602
|
-
return children
|
|
648
|
+
return children;
|
|
603
649
|
}
|
|
604
650
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
+
}
|
|
610
662
|
}
|
|
611
663
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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];
|
|
617
670
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
+
}
|
|
622
678
|
|
|
623
|
-
|
|
624
|
-
|
|
679
|
+
if (typeof value == "string") {
|
|
680
|
+
this.root.setAttribute(name, value);
|
|
681
|
+
} else {
|
|
682
|
+
this.root[name] = value;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
625
685
|
}
|
|
626
|
-
|
|
627
|
-
this.state ||= this.reactiveStore()
|
|
628
|
-
this.globalState = Fez.state.createProxy(this)
|
|
629
|
-
this.fezRegisterBindMethods()
|
|
630
686
|
}
|
|
631
687
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
688
|
+
/**
|
|
689
|
+
* Get or set root ID
|
|
690
|
+
*/
|
|
691
|
+
rootId() {
|
|
692
|
+
this.root.id ||= `fez_${this.UID}`;
|
|
693
|
+
return this.root.id;
|
|
638
694
|
}
|
|
639
695
|
|
|
640
|
-
|
|
696
|
+
/**
|
|
697
|
+
* Dissolve component into parent
|
|
698
|
+
*/
|
|
641
699
|
dissolve(inNode) {
|
|
642
700
|
if (inNode) {
|
|
643
|
-
inNode.classList.add(
|
|
644
|
-
inNode.classList.add(`fez-${this.fezName}`)
|
|
645
|
-
inNode.fez = this
|
|
646
|
-
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"));
|
|
647
705
|
|
|
648
|
-
this.root.innerHTML =
|
|
649
|
-
this.root.appendChild(inNode)
|
|
706
|
+
this.root.innerHTML = "";
|
|
707
|
+
this.root.appendChild(inNode);
|
|
650
708
|
}
|
|
651
709
|
|
|
652
|
-
const node = this.root
|
|
653
|
-
const nodes = this.childNodes()
|
|
654
|
-
const parent = this.root.parentNode
|
|
710
|
+
const node = this.root;
|
|
711
|
+
const nodes = this.childNodes();
|
|
712
|
+
const parent = this.root.parentNode;
|
|
655
713
|
|
|
656
|
-
nodes.reverse().forEach(el => parent.insertBefore(el, node.nextSibling))
|
|
714
|
+
nodes.reverse().forEach((el) => parent.insertBefore(el, node.nextSibling));
|
|
657
715
|
|
|
658
|
-
this.root.remove()
|
|
659
|
-
this.root = undefined
|
|
716
|
+
this.root.remove();
|
|
717
|
+
this.root = undefined;
|
|
660
718
|
|
|
661
719
|
if (inNode) {
|
|
662
|
-
this.root = inNode
|
|
720
|
+
this.root = inNode;
|
|
663
721
|
}
|
|
664
722
|
|
|
665
|
-
return nodes
|
|
723
|
+
return nodes;
|
|
666
724
|
}
|
|
667
725
|
|
|
668
|
-
|
|
669
|
-
|
|
726
|
+
// ===========================================================================
|
|
727
|
+
// EVENTS
|
|
728
|
+
// ===========================================================================
|
|
670
729
|
|
|
671
|
-
|
|
672
|
-
|
|
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
|
}
|