@dinoreic/fez 0.2.2 → 0.3.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.
@@ -81,11 +81,6 @@ export default class FezBase {
81
81
  return formObject
82
82
  }
83
83
 
84
- static fastBind() {
85
- // return true to bind without requestAnimationFrame
86
- // you can do this if you are sure you are not expecting innerHTML data
87
- return false
88
- }
89
84
 
90
85
  static nodeName = 'div'
91
86
 
@@ -106,42 +101,11 @@ export default class FezBase {
106
101
  if (this.root?.isConnected) {
107
102
  return true
108
103
  } else {
109
- this.fezRemoveSelf()
104
+ this.fezOnDestroy()
110
105
  return false
111
106
  }
112
107
  }
113
108
 
114
- // clear all node references
115
- fezRemoveSelf() {
116
- this._setIntervalCache ||= {}
117
- Object.keys(this._setIntervalCache).forEach((key)=> {
118
- clearInterval(this._setIntervalCache[key])
119
- })
120
-
121
- if (this._eventHandlers) {
122
- Object.entries(this._eventHandlers).forEach(([eventName, handler]) => {
123
- window.removeEventListener(eventName, handler);
124
- });
125
- this._eventHandlers = {};
126
- }
127
-
128
- if (this._timeouts) {
129
- Object.values(this._timeouts).forEach(timeoutId => {
130
- clearTimeout(timeoutId);
131
- });
132
- this._timeouts = {};
133
- }
134
-
135
- this.onDestroy()
136
- this.onDestroy = () => {}
137
-
138
- if (this.root) {
139
- this.root.fez = undefined
140
- }
141
-
142
- this.root = undefined
143
- }
144
-
145
109
  // get single node property
146
110
  prop(name) {
147
111
  let v = this.oldRoot[name] || this.props[name]
@@ -178,80 +142,92 @@ export default class FezBase {
178
142
  }
179
143
  }
180
144
 
181
- // Generic function to handle window events with automatic cleanup
182
- // eventName: 'resize', 'scroll', etc.
183
- // func: callback function to execute
184
- // delay: throttle delay in ms (default: 100ms)
185
- on(eventName, func, delay = 200) {
186
- this._eventHandlers = this._eventHandlers || {};
187
- this._timeouts = this._timeouts || {};
188
-
189
- if (this._eventHandlers[eventName]) {
190
- window.removeEventListener(eventName, this._eventHandlers[eventName]);
145
+ // clear all node references
146
+ // Centralized destroy logic
147
+ fezOnDestroy() {
148
+ // Execute all registered cleanup callbacks
149
+ if (this._onDestroyCallbacks) {
150
+ this._onDestroyCallbacks.forEach(callback => {
151
+ try {
152
+ callback();
153
+ } catch (e) {
154
+ console.error('Fez: Error in cleanup callback:', e);
155
+ }
156
+ });
157
+ this._onDestroyCallbacks = [];
191
158
  }
192
159
 
193
- if (this._timeouts[eventName]) {
194
- clearTimeout(this._timeouts[eventName]);
160
+ // Call user's onDestroy lifecycle hook
161
+ this.onDestroy()
162
+ this.onDestroy = () => {}
163
+
164
+ // Clean up root references
165
+ if (this.root) {
166
+ this.root.fez = undefined
195
167
  }
196
168
 
197
- let lastRun = 0;
169
+ this.root = undefined
170
+ }
198
171
 
199
- const doExecute = () => {
200
- if (!this.isConnected) {
201
- if (this._eventHandlers[eventName]) {
202
- window.removeEventListener(eventName, this._eventHandlers[eventName]);
203
- delete this._eventHandlers[eventName];
204
- }
205
- if (this._timeouts[eventName]) {
206
- clearTimeout(this._timeouts[eventName]);
207
- delete this._timeouts[eventName];
208
- }
209
- return false;
210
- }
211
- func.call(this);
212
- return true;
213
- };
172
+ // Add a cleanup callback to be executed on destroy
173
+ addOnDestroy(callback) {
174
+ this._onDestroyCallbacks = this._onDestroyCallbacks || [];
175
+ this._onDestroyCallbacks.push(callback);
176
+ }
214
177
 
215
- const handleEvent = () => {
216
- const now = Date.now();
178
+ // Generic function to handle window events with automatic cleanup
179
+ on(eventName, func, delay = 200) {
180
+ this._eventHandlers = this._eventHandlers || {};
217
181
 
218
- if (now - lastRun >= delay) {
219
- if (doExecute()) {
220
- lastRun = now;
221
- } else {
222
- return;
223
- }
224
- }
182
+ if (this._eventHandlers[eventName]) {
183
+ window.removeEventListener(eventName, this._eventHandlers[eventName]);
184
+ }
225
185
 
226
- // Clear previous timeout and set new one to ensure final event
227
- if (this._timeouts[eventName]) {
228
- clearTimeout(this._timeouts[eventName]);
186
+ const throttledFunc = Fez.throttle(() => {
187
+ if (this.isConnected) {
188
+ func.call(this);
229
189
  }
190
+ }, delay);
230
191
 
231
- this._timeouts[eventName] = setTimeout(() => {
232
- if (now > lastRun && doExecute()) {
233
- lastRun = Date.now();
234
- }
235
- delete this._timeouts[eventName];
236
- }, delay);
237
- };
192
+ this._eventHandlers[eventName] = throttledFunc;
193
+ window.addEventListener(eventName, throttledFunc);
238
194
 
239
- this._eventHandlers[eventName] = handleEvent;
240
- window.addEventListener(eventName, handleEvent);
195
+ this.addOnDestroy(() => {
196
+ window.removeEventListener(eventName, throttledFunc);
197
+ delete this._eventHandlers[eventName];
198
+ });
241
199
  }
242
200
 
243
201
  // Helper function for resize events
244
- onResize(func, delay) {
202
+ onWindowResize(func, delay) {
245
203
  this.on('resize', func, delay);
246
204
  func();
247
205
  }
248
206
 
249
207
  // Helper function for scroll events
250
- onScroll(func, delay) {
208
+ onWindowScroll(func, delay) {
251
209
  this.on('scroll', func, delay);
252
210
  func();
253
211
  }
254
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
+ });
229
+ }
230
+
255
231
  // copy child nodes, natively to preserve bound events
256
232
  // if node name is SLOT insert adjacent and remove SLOT, else as a child nodes
257
233
  slot(source, target) {
@@ -359,7 +335,8 @@ export default class FezBase {
359
335
 
360
336
  this.beforeRender()
361
337
 
362
- const newNode = document.createElement(this.class.nodeName || 'div')
338
+ const nodeName = typeof this.class.nodeName == 'function' ? this.class.nodeName(this.root) : this.class.nodeName
339
+ const newNode = document.createElement(nodeName || 'div')
363
340
 
364
341
  let renderedTpl
365
342
  if (Array.isArray(template)) {
@@ -382,27 +359,20 @@ export default class FezBase {
382
359
  newNode.innerHTML = this.parseHtml(renderedTpl)
383
360
  }
384
361
 
385
- newNode.querySelectorAll('[fez-keep]').forEach(newEl => {
386
- const key = newEl.getAttribute('fez-keep')
387
- const oldEl = this.root.querySelector(`[fez-keep="${key}"]`)
362
+ // Handle fez-keep attributes
363
+ this.fezKeepNode(newNode)
388
364
 
389
- if (oldEl) {
390
- // Keep the old element in place of the new one
391
- newEl.parentNode.replaceChild(oldEl, newEl)
392
- } else if (key === 'default-slot') {
393
- // First render - populate the slot with current root children
394
- Array.from(this.root.childNodes).forEach(child => newEl.appendChild(child))
395
- }
396
- })
365
+ // Handle fez-memoize attributes
366
+ this.fezMemoization(newNode)
397
367
 
398
368
  Fez.morphdom(this.root, newNode)
399
369
 
400
- this.renderFezPostProcess()
370
+ this.fezRenderPostProcess()
401
371
 
402
372
  this.afterRender()
403
373
  }
404
374
 
405
- renderFezPostProcess() {
375
+ fezRenderPostProcess() {
406
376
  const fetchAttr = (name, func) => {
407
377
  this.root.querySelectorAll(`*[${name}]`).forEach((n)=>{
408
378
  let value = n.getAttribute(name)
@@ -463,6 +433,43 @@ export default class FezBase {
463
433
  })
464
434
  }
465
435
 
436
+ fezKeepNode(newNode) {
437
+ newNode.querySelectorAll('[fez-keep]').forEach(newEl => {
438
+ const key = newEl.getAttribute('fez-keep')
439
+ const oldEl = this.root.querySelector(`[fez-keep="${key}"]`)
440
+
441
+ if (oldEl) {
442
+ // Keep the old element in place of the new one
443
+ newEl.parentNode.replaceChild(oldEl, newEl)
444
+ } else if (key === 'default-slot') {
445
+ // First render - populate the slot with current root children
446
+ Array.from(this.root.childNodes).forEach(child => newEl.appendChild(child))
447
+ }
448
+ })
449
+ }
450
+
451
+ fezMemoization(newNode) {
452
+ // Find the single memoize element in new DOM (excluding fez components)
453
+ const newMemoEl = newNode.querySelector('[fez-memoize]:not(.fez)')
454
+ if (!newMemoEl) return
455
+
456
+ this.fezMemoStore ||= new Map()
457
+
458
+ const newMemoElKey = newMemoEl.getAttribute('fez-memoize')
459
+ const storedNode = this.fezMemoStore.get(newMemoElKey)
460
+
461
+ if (storedNode) {
462
+ Fez.log(`Memoize restore ${this.fezName}: ${newMemoElKey}`)
463
+ newMemoEl.parentNode.replaceChild(storedNode.cloneNode(true), newMemoEl)
464
+ } else {
465
+ const oldMemoEl = this.root.querySelector('[fez-memoize]:not(.fez)')
466
+ if (oldMemoEl) {
467
+ const oldMemoElKey = oldMemoEl.getAttribute('fez-memoize')
468
+ this.fezMemoStore.set(oldMemoElKey, oldMemoEl.cloneNode(true))
469
+ }
470
+ }
471
+ }
472
+
466
473
  // refresh single node only
467
474
  refresh(selector) {
468
475
  alert('NEEDS FIX and remove htmlTemplate')
@@ -486,13 +493,21 @@ export default class FezBase {
486
493
  this._setIntervalCache ||= {}
487
494
  clearInterval(this._setIntervalCache[name])
488
495
 
489
- this._setIntervalCache[name] = setInterval(() => {
496
+ const intervalID = setInterval(() => {
490
497
  if (this.isConnected) {
491
498
  func()
492
499
  }
493
500
  }, tick)
494
501
 
495
- return this._setIntervalCache[name]
502
+ this._setIntervalCache[name] = intervalID
503
+
504
+ // Register cleanup callback
505
+ this.addOnDestroy(() => {
506
+ clearInterval(intervalID);
507
+ delete this._setIntervalCache[name];
508
+ });
509
+
510
+ return intervalID
496
511
  }
497
512
 
498
513
  find(selector) {
package/src/fez/root.js CHANGED
@@ -4,6 +4,7 @@ import Gobber from './vendor/gobber.js'
4
4
  // morph dom from one state to another
5
5
  import { Idiomorph } from './vendor/idiomorph.js'
6
6
 
7
+ import objectDump from './utils/dump.js'
7
8
  import connect from './connect.js'
8
9
  import compile from './compile.js'
9
10
  import state from './lib/global-state.js'
@@ -117,7 +118,7 @@ Fez.globalCss = (cssClass, opts = {}) => {
117
118
  }
118
119
 
119
120
  Fez.info = () => {
120
- console.log(JSON.stringify(Fez.fastBindInfo, null, 2))
121
+ console.log('Fez components:', Object.keys(Fez.classes || {}))
121
122
  }
122
123
 
123
124
  Fez.morphdom = (target, newNode, opts = {}) => {
@@ -136,24 +137,6 @@ Fez.morphdom = (target, newNode, opts = {}) => {
136
137
  }
137
138
  }
138
139
 
139
- Fez.htmlEscape = (text) => {
140
- if (typeof text == 'string') {
141
- text = text
142
- // .replaceAll('&', "&")
143
- .replace(/font-family\s*:\s*(?:&[^;]+;|[^;])*?;/gi, '')
144
- .replaceAll("&", '&')
145
- .replaceAll("'", ''')
146
- .replaceAll('"', '"')
147
- .replaceAll('<', '&lt;')
148
- .replaceAll('>', '&gt;')
149
- // .replaceAll('@', '&#64;') // needed for template escaping
150
-
151
- return text
152
- } else {
153
- return text === undefined ? '' : text
154
- }
155
- }
156
-
157
140
  Fez.publish = (channel, ...args) => {
158
141
  Fez._subs ||= {}
159
142
  Fez._subs[channel] ||= []
@@ -211,125 +194,25 @@ Fez.untilTrue = (func, pingRate) => {
211
194
  }
212
195
  }
213
196
 
214
- // Script from URL
215
- // Fez.head({ js: 'https://example.com/script.js' });
216
- // Script with attributes
217
- // Fez.head({ js: 'https://example.com/script.js', type: 'module', async: true });
218
- // Script with callback
219
- // Fez.head({ js: 'https://example.com/script.js' }, () => { console.log('loaded') });
220
- // Module loading with auto-import to window
221
- // Fez.head({ js: 'https://example.com/module.js', module: 'MyModule' }); // imports and sets window.MyModule
222
- // CSS inclusion
223
- // Fez.head({ css: 'https://example.com/styles.css' });
224
- // CSS with additional attributes and callback
225
- // Fez.head({ css: 'https://example.com/styles.css', media: 'print' }, () => { console.log('CSS loaded') })
226
- // Inline script evaluation
227
- // Fez.head({ script: 'console.log("Hello world")' })
228
- // Extract from nodes
229
- // Fez.head(domNode)
230
- Fez.head = (config, callback) => {
231
- if (config.nodeName) {
232
- if (config.nodeName == 'SCRIPT') {
233
- Fez.head({script: config.innerText})
234
- config.remove()
235
- } else {
236
- config.querySelectorAll('script').forEach((n) => Fez.head(n) )
237
- config.querySelectorAll('template[fez], xmp[fez], script[fez]').forEach((n) => Fez.compile(n) )
238
- }
197
+ // throttle function calls
198
+ Fez.throttle = (func, delay = 200) => {
199
+ let lastRun = 0;
200
+ let timeout;
239
201
 
240
- return
241
- }
242
-
243
- if (typeof config !== 'object' || config === null) {
244
- throw new Error('head requires an object parameter');
245
- }
202
+ return function(...args) {
203
+ const now = Date.now();
246
204
 
247
- let src, attributes = {}, elementType;
248
-
249
- if (config.script) {
250
- if (config.script.includes('import ')) {
251
- if (callback) {
252
- Fez.error('Fez.head callback is not supported when script with import is passed (module context).')
253
- }
254
-
255
- // Evaluate inline script in context in the module
256
- const script = document.createElement('script');
257
- script.type = 'module';
258
- script.textContent = config.script;
259
- document.head.appendChild(script);
260
- setTimeout(()=>script.remove(), 100)
205
+ if (now - lastRun >= delay) {
206
+ func.apply(this, args);
207
+ lastRun = now;
261
208
  } else {
262
- try {
263
- new Function(config.script)();
264
- if (callback) callback();
265
- } catch (error) {
266
- Fez.error('Error executing script:', error);
267
- console.log(config.script);
268
- }
269
- }
270
- return;
271
- } else if (config.js) {
272
- src = config.js;
273
- elementType = 'script';
274
- // Copy all properties except 'js' as attributes
275
- for (const [key, value] of Object.entries(config)) {
276
- if (key !== 'js' && key !== 'module') {
277
- attributes[key] = value;
278
- }
209
+ clearTimeout(timeout);
210
+ timeout = setTimeout(() => {
211
+ func.apply(this, args);
212
+ lastRun = Date.now();
213
+ }, delay - (now - lastRun));
279
214
  }
280
- // Handle module loading
281
- if (config.module) {
282
- attributes.type = 'module';
283
- }
284
- } else if (config.css) {
285
- src = config.css;
286
- elementType = 'link';
287
- attributes.rel = 'stylesheet';
288
- // Copy all properties except 'css' as attributes
289
- for (const [key, value] of Object.entries(config)) {
290
- if (key !== 'css') {
291
- attributes[key] = value;
292
- }
293
- }
294
- } else {
295
- throw new Error('head requires either "script", "js" or "css" property');
296
- }
297
-
298
- const existingNode = document.querySelector(`${elementType}[src="${src}"], ${elementType}[href="${src}"]`);
299
- if (existingNode) {
300
- if (callback) callback();
301
- return existingNode;
302
- }
303
-
304
- const element = document.createElement(elementType);
305
-
306
- if (elementType === 'link') {
307
- element.href = src;
308
- } else {
309
- element.src = src;
310
- }
311
-
312
- for (const [key, value] of Object.entries(attributes)) {
313
- element.setAttribute(key, value);
314
- }
315
-
316
- if (callback || config.module) {
317
- element.onload = () => {
318
- // If module name is provided, import it and assign to window
319
- if (config.module && elementType === 'script') {
320
- import(src).then(module => {
321
- window[config.module] = module.default || module[config.module] || module;
322
- }).catch(error => {
323
- console.error(`Error importing module ${config.module}:`, error);
324
- });
325
- }
326
- if (callback) callback();
327
- };
328
- }
329
-
330
- document.head.appendChild(element);
331
-
332
- return element;
215
+ };
333
216
  }
334
217
 
335
218
  // Fetch wrapper with automatic caching and data handling
@@ -475,26 +358,12 @@ Fez.store = {
475
358
  }
476
359
  };
477
360
 
478
- // create dom root and return it
479
- Fez.domRoot = (data, name = 'div') => {
480
- if (data instanceof Node) {
481
- return data
482
- } else {
483
- const root = document.createElement(name)
484
- root.innerHTML = data
485
- return root
486
- }
487
- }
488
-
489
- // add class by name to node and remove it from siblings
490
- Fez.activateNode = (node, klass = 'active') => {
491
- Array.from(node.parentElement.children).forEach(child => {
492
- child.classList.remove(klass)
493
- })
494
- node.classList.add(klass)
495
- }
361
+ // Load utility functions
362
+ import addUtilities from './utility.js'
363
+ addUtilities(Fez)
496
364
 
497
365
  Fez.compile = compile
498
366
  Fez.state = state
367
+ Fez.dump = objectDump
499
368
 
500
369
  export default Fez