@dinoreic/fez 0.2.0 → 0.3.0

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) {
@@ -286,14 +262,48 @@ export default class FezBase {
286
262
  onDestroy() {}
287
263
  onStateChange() {}
288
264
  onGlobalStateChange() {}
289
- publish = Fez.publish
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
298
+ }
299
+
290
300
  fezBlocks = {}
291
301
 
292
302
  parseHtml(text) {
293
303
  const base = this.fezHtmlRoot.replaceAll('"', '"')
294
304
 
295
305
  text = text
296
- .replace(/(['"\s;])fez\./g, `$1${base}`)
306
+ .replace(/([!'"\s;])fez\.(\w)/g, `$1${base}$2`)
297
307
  .replace(/>\s+</g, '><')
298
308
 
299
309
  return text.trim()
@@ -325,7 +335,8 @@ export default class FezBase {
325
335
 
326
336
  this.beforeRender()
327
337
 
328
- 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')
329
340
 
330
341
  let renderedTpl
331
342
  if (Array.isArray(template)) {
@@ -348,22 +359,18 @@ export default class FezBase {
348
359
  newNode.innerHTML = this.parseHtml(renderedTpl)
349
360
  }
350
361
 
351
- // this comes only from array nodes this.n(...)
352
- const slot = newNode.querySelector('slot')
353
- if (slot) {
354
- this.slot(this.root, slot.parentNode)
355
- slot.parentNode.removeChild(slot)
356
- }
362
+ newNode.querySelectorAll('[fez-keep]').forEach(newEl => {
363
+ const key = newEl.getAttribute('fez-keep')
364
+ const oldEl = this.root.querySelector(`[fez-keep="${key}"]`)
357
365
 
358
- let newSlot = newNode.querySelector('.fez-slot')
359
- if(newSlot) {
360
- let currentSlot = this.find('.fez-slot')
361
- if (currentSlot) {
362
- newSlot.parentNode.replaceChild(currentSlot, newSlot)
363
- } else {
364
- this.slot(this.root, newSlot)
366
+ if (oldEl) {
367
+ // Keep the old element in place of the new one
368
+ newEl.parentNode.replaceChild(oldEl, newEl)
369
+ } else if (key === 'default-slot') {
370
+ // First render - populate the slot with current root children
371
+ Array.from(this.root.childNodes).forEach(child => newEl.appendChild(child))
365
372
  }
366
- }
373
+ })
367
374
 
368
375
  Fez.morphdom(this.root, newNode)
369
376
 
@@ -437,8 +444,7 @@ export default class FezBase {
437
444
  refresh(selector) {
438
445
  alert('NEEDS FIX and remove htmlTemplate')
439
446
  if (selector) {
440
- const n = document.createElement('div')
441
- n.innerHTML = this.class.htmlTemplate
447
+ const n = Fez.domRoot(this.class.htmlTemplate)
442
448
  const tpl = n.querySelector(selector).innerHTML
443
449
  this.render(selector, tpl)
444
450
  } else {
@@ -457,13 +463,21 @@ export default class FezBase {
457
463
  this._setIntervalCache ||= {}
458
464
  clearInterval(this._setIntervalCache[name])
459
465
 
460
- this._setIntervalCache[name] = setInterval(() => {
466
+ const intervalID = setInterval(() => {
461
467
  if (this.isConnected) {
462
468
  func()
463
469
  }
464
470
  }, tick)
465
471
 
466
- return this._setIntervalCache[name]
472
+ this._setIntervalCache[name] = intervalID
473
+
474
+ // Register cleanup callback
475
+ this.addOnDestroy(() => {
476
+ clearInterval(intervalID);
477
+ delete this._setIntervalCache[name];
478
+ });
479
+
480
+ return intervalID
467
481
  }
468
482
 
469
483
  find(selector) {
@@ -511,7 +525,7 @@ export default class FezBase {
511
525
 
512
526
  // get root node child nodes as array
513
527
  childNodes(func) {
514
- const children = Array.from(this.root.children)
528
+ let children = Array.from(this.root.children)
515
529
 
516
530
  if (func) {
517
531
  // Create temporary container to avoid ancestor-parent errors
@@ -520,12 +534,11 @@ export default class FezBase {
520
534
  document.body.appendChild(tmpContainer)
521
535
  children.forEach(child => tmpContainer.appendChild(child))
522
536
 
523
- let list = Array.from(tmpContainer.children).map(func)
537
+ children = Array.from(tmpContainer.children).map(func)
524
538
  document.body.removeChild(tmpContainer)
525
- return list
526
- } else {
527
- return children
528
539
  }
540
+
541
+ return children
529
542
  }
530
543
 
531
544
  subscribe(channel, func) {
@@ -565,19 +578,14 @@ export default class FezBase {
565
578
 
566
579
  fezHide() {
567
580
  const node = this.root
581
+ const nodes = this.childNodes()
568
582
  const parent = this.root.parentNode
569
- const fragment = document.createDocumentFragment();
570
583
 
571
- while (node.firstChild) {
572
- fragment.appendChild(node.firstChild)
573
- }
584
+ nodes.reverse().forEach(el => parent.insertBefore(el, node.nextSibling))
574
585
 
575
- // Replace the target element with the fragment (which contains the child elements)
576
- node.parentNode.replaceChild(fragment, node);
577
- // parent.classList.add('fez')
578
- // parent.classList.add(`fez-${this.fezName}`)
586
+ this.root.remove()
579
587
  this.root = parent
580
- return Array.from(this.root.children)
588
+ return nodes
581
589
  }
582
590
 
583
591
  reactiveStore(obj, handler) {
@@ -98,6 +98,10 @@ export default function createTemplate(text, opts = {}) {
98
98
  return parsedData
99
99
  });
100
100
 
101
+ result = result
102
+ .replace(/<!\-\-.*?\-\->/g, '')
103
+ .replace(/>\s+</g, '><')
104
+
101
105
  result = '`' + result.trim() + '`'
102
106
 
103
107
  try {
package/src/fez/root.js CHANGED
@@ -117,7 +117,7 @@ Fez.globalCss = (cssClass, opts = {}) => {
117
117
  }
118
118
 
119
119
  Fez.info = () => {
120
- console.log(JSON.stringify(Fez.fastBindInfo, null, 2))
120
+ console.log('Fez components:', Object.keys(Fez.classes || {}))
121
121
  }
122
122
 
123
123
  Fez.morphdom = (target, newNode, opts = {}) => {
@@ -136,24 +136,6 @@ Fez.morphdom = (target, newNode, opts = {}) => {
136
136
  }
137
137
  }
138
138
 
139
- Fez.htmlEscape = (text) => {
140
- if (typeof text == 'string') {
141
- text = text
142
- // .replaceAll('&', "&amp;")
143
- .replace(/font-family\s*:\s*(?:&[^;]+;|[^;])*?;/gi, '')
144
- .replaceAll("&", '&amp;')
145
- .replaceAll("'", '&apos;')
146
- .replaceAll('"', '&quot;')
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
139
  Fez.publish = (channel, ...args) => {
158
140
  Fez._subs ||= {}
159
141
  Fez._subs[channel] ||= []
@@ -183,6 +165,19 @@ Fez.tag = (tag, opts = {}, html = '') => {
183
165
  // return data
184
166
  };
185
167
 
168
+ // Resolve a function from a string or function reference
169
+ Fez.getFunction = (pointer) => {
170
+ if (!pointer) {
171
+ return ()=>{}
172
+ }
173
+ else if (typeof pointer === 'function') {
174
+ return pointer;
175
+ }
176
+ else if (typeof pointer === 'string') {
177
+ return new Function(pointer);
178
+ }
179
+ };
180
+
186
181
  Fez.error = (text, show) => {
187
182
  text = `Fez: ${text}`
188
183
  console.error(text)
@@ -211,125 +206,25 @@ Fez.untilTrue = (func, pingRate) => {
211
206
  }
212
207
  }
213
208
 
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
- }
209
+ // throttle function calls
210
+ Fez.throttle = (func, delay = 200) => {
211
+ let lastRun = 0;
212
+ let timeout;
239
213
 
240
- return
241
- }
242
-
243
- if (typeof config !== 'object' || config === null) {
244
- throw new Error('head requires an object parameter');
245
- }
246
-
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
- }
214
+ return function(...args) {
215
+ const now = Date.now();
254
216
 
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)
217
+ if (now - lastRun >= delay) {
218
+ func.apply(this, args);
219
+ lastRun = now;
261
220
  } 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
- }
221
+ clearTimeout(timeout);
222
+ timeout = setTimeout(() => {
223
+ func.apply(this, args);
224
+ lastRun = Date.now();
225
+ }, delay - (now - lastRun));
279
226
  }
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;
227
+ };
333
228
  }
334
229
 
335
230
  // Fetch wrapper with automatic caching and data handling
@@ -475,6 +370,10 @@ Fez.store = {
475
370
  }
476
371
  };
477
372
 
373
+ // Load utility functions
374
+ import addUtilities from './utility.js'
375
+ addUtilities(Fez)
376
+
478
377
  Fez.compile = compile
479
378
  Fez.state = state
480
379