@dinoreic/fez 0.1.1 → 0.2.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.
@@ -19,6 +19,7 @@ export default class FezBase {
19
19
 
20
20
  for (const [key, val] of Object.entries(attrs)) {
21
21
  if ([':'].includes(key[0])) {
22
+ // LOG([key, val])
22
23
  delete attrs[key]
23
24
  try {
24
25
  const newVal = new Function(`return (${val})`).bind(newNode)()
@@ -110,14 +111,29 @@ export default class FezBase {
110
111
  }
111
112
  }
112
113
 
114
+ // clear all node references
113
115
  fezRemoveSelf() {
114
116
  this._setIntervalCache ||= {}
115
117
  Object.keys(this._setIntervalCache).forEach((key)=> {
116
118
  clearInterval(this._setIntervalCache[key])
117
119
  })
118
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
+
119
135
  this.onDestroy()
120
- this.onDestroy = ()=> {}
136
+ this.onDestroy = () => {}
121
137
 
122
138
  if (this.root) {
123
139
  this.root.fez = undefined
@@ -162,43 +178,78 @@ export default class FezBase {
162
178
  }
163
179
  }
164
180
 
165
- // helper function to execute stuff on window resize, and clean after node is not connected any more
166
- // if delay given, throttle, if not debounce
167
- onResize(func, delay) {
168
- let timeoutId;
169
- let lastRun = 0;
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]);
191
+ }
192
+
193
+ if (this._timeouts[eventName]) {
194
+ clearTimeout(this._timeouts[eventName]);
195
+ }
170
196
 
171
- func()
197
+ let lastRun = 0;
172
198
 
173
- const checkAndExecute = () => {
199
+ const doExecute = () => {
174
200
  if (!this.isConnected) {
175
- window.removeEventListener('resize', handleResize);
176
- return;
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;
177
210
  }
178
211
  func.call(this);
212
+ return true;
179
213
  };
180
214
 
181
- const handleResize = () => {
182
- if (!this.isConnected) {
183
- window.removeEventListener('resize', handleResize);
184
- return;
185
- }
215
+ const handleEvent = () => {
216
+ const now = Date.now();
186
217
 
187
- if (delay) {
188
- // Throttle
189
- const now = Date.now();
190
- if (now - lastRun >= delay) {
191
- checkAndExecute();
218
+ if (now - lastRun >= delay) {
219
+ if (doExecute()) {
192
220
  lastRun = now;
221
+ } else {
222
+ return;
193
223
  }
194
- } else {
195
- // Debounce
196
- clearTimeout(timeoutId);
197
- timeoutId = setTimeout(checkAndExecute, 200);
198
224
  }
225
+
226
+ // Clear previous timeout and set new one to ensure final event
227
+ if (this._timeouts[eventName]) {
228
+ clearTimeout(this._timeouts[eventName]);
229
+ }
230
+
231
+ this._timeouts[eventName] = setTimeout(() => {
232
+ if (now > lastRun && doExecute()) {
233
+ lastRun = Date.now();
234
+ }
235
+ delete this._timeouts[eventName];
236
+ }, delay);
199
237
  };
200
238
 
201
- window.addEventListener('resize', handleResize);
239
+ this._eventHandlers[eventName] = handleEvent;
240
+ window.addEventListener(eventName, handleEvent);
241
+ }
242
+
243
+ // Helper function for resize events
244
+ onResize(func, delay) {
245
+ this.on('resize', func, delay);
246
+ func();
247
+ }
248
+
249
+ // Helper function for scroll events
250
+ onScroll(func, delay) {
251
+ this.on('scroll', func, delay);
252
+ func();
202
253
  }
203
254
 
204
255
  // copy child nodes, natively to preserve bound events
@@ -235,14 +286,48 @@ export default class FezBase {
235
286
  onDestroy() {}
236
287
  onStateChange() {}
237
288
  onGlobalStateChange() {}
238
- publish = Fez.publish
289
+
290
+ // component publish will search for parent component that subscribes by name
291
+ publish(channel, ...args) {
292
+ const handle_publish = (component) => {
293
+ if (Fez._subs && Fez._subs[channel]) {
294
+ const sub = Fez._subs[channel].find(([comp]) => comp === component)
295
+ if (sub) {
296
+ sub[1].bind(component)(...args)
297
+ return true
298
+ }
299
+ }
300
+ return false
301
+ }
302
+
303
+ // Check if current component has subscription
304
+ if (handle_publish(this)) {
305
+ return true
306
+ }
307
+
308
+ // Bubble up to parent components
309
+ let parent = this.root.parentElement
310
+ while (parent) {
311
+ if (parent.fez) {
312
+ if (handle_publish(parent.fez)) {
313
+ return true
314
+ }
315
+ }
316
+ parent = parent.parentElement
317
+ }
318
+
319
+ // If no parent handled it, fall back to global publish
320
+ // Fez.publish(channel, ...args)
321
+ return false
322
+ }
323
+
239
324
  fezBlocks = {}
240
325
 
241
326
  parseHtml(text) {
242
327
  const base = this.fezHtmlRoot.replaceAll('"', '"')
243
328
 
244
329
  text = text
245
- .replace(/([^\w\.])fez\./g, `$1${base}`)
330
+ .replace(/([!'"\s;])fez\.(\w)/g, `$1${base}$2`)
246
331
  .replace(/>\s+</g, '><')
247
332
 
248
333
  return text.trim()
@@ -297,21 +382,18 @@ export default class FezBase {
297
382
  newNode.innerHTML = this.parseHtml(renderedTpl)
298
383
  }
299
384
 
300
- // this comes only from array nodes this.n(...)
301
- const slot = newNode.querySelector('slot')
302
- if (slot) {
303
- this.slot(this.root, slot.parentNode)
304
- slot.parentNode.removeChild(slot)
305
- }
385
+ newNode.querySelectorAll('[fez-keep]').forEach(newEl => {
386
+ const key = newEl.getAttribute('fez-keep')
387
+ const oldEl = this.root.querySelector(`[fez-keep="${key}"]`)
306
388
 
307
- //let currentSlot = this.root.querySelector(':not(span.fez):not(div.fez) > .fez-slot, .fez-slot:not(span.fez *):not(div.fez *)');
308
- let currentSlot = this.find('.fez-slot')
309
- if (currentSlot) {
310
- const newSLot = newNode.querySelector('.fez-slot')
311
- if (newSLot) {
312
- newSLot.parentNode.replaceChild(currentSlot, newSLot)
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))
313
395
  }
314
- }
396
+ })
315
397
 
316
398
  Fez.morphdom(this.root, newNode)
317
399
 
@@ -385,8 +467,7 @@ export default class FezBase {
385
467
  refresh(selector) {
386
468
  alert('NEEDS FIX and remove htmlTemplate')
387
469
  if (selector) {
388
- const n = document.createElement('div')
389
- n.innerHTML = this.class.htmlTemplate
470
+ const n = Fez.domRoot(this.class.htmlTemplate)
390
471
  const tpl = n.querySelector(selector).innerHTML
391
472
  this.render(selector, tpl)
392
473
  } else {
@@ -459,7 +540,7 @@ export default class FezBase {
459
540
 
460
541
  // get root node child nodes as array
461
542
  childNodes(func) {
462
- const children = Array.from(this.root.children)
543
+ let children = Array.from(this.root.children)
463
544
 
464
545
  if (func) {
465
546
  // Create temporary container to avoid ancestor-parent errors
@@ -468,12 +549,11 @@ export default class FezBase {
468
549
  document.body.appendChild(tmpContainer)
469
550
  children.forEach(child => tmpContainer.appendChild(child))
470
551
 
471
- let list = Array.from(tmpContainer.children).map(func)
552
+ children = Array.from(tmpContainer.children).map(func)
472
553
  document.body.removeChild(tmpContainer)
473
- return list
474
- } else {
475
- return children
476
554
  }
555
+
556
+ return children
477
557
  }
478
558
 
479
559
  subscribe(channel, func) {
@@ -513,19 +593,14 @@ export default class FezBase {
513
593
 
514
594
  fezHide() {
515
595
  const node = this.root
596
+ const nodes = this.childNodes()
516
597
  const parent = this.root.parentNode
517
- const fragment = document.createDocumentFragment();
518
598
 
519
- while (node.firstChild) {
520
- fragment.appendChild(node.firstChild)
521
- }
599
+ nodes.reverse().forEach(el => parent.insertBefore(el, node.nextSibling))
522
600
 
523
- // Replace the target element with the fragment (which contains the child elements)
524
- node.parentNode.replaceChild(fragment, node);
525
- // parent.classList.add('fez')
526
- // parent.classList.add(`fez-${this.fezName}`)
601
+ this.root.remove()
527
602
  this.root = parent
528
- return Array.from(this.root.children)
603
+ return nodes
529
604
  }
530
605
 
531
606
  reactiveStore(obj, handler) {
@@ -40,6 +40,11 @@ function parseBlock(data, ifStack) {
40
40
  else {
41
41
  const prefix = '@html '
42
42
 
43
+ if (data.startsWith('json ')) {
44
+ data = data.replace('json ', "@html '<pre class=json>'+JSON.stringify(")
45
+ data += ", null, 2) + '</pre>'"
46
+ }
47
+
43
48
  if (data.startsWith(prefix)) {
44
49
  data = data.replace(prefix, '')
45
50
  } else {
@@ -93,6 +98,10 @@ export default function createTemplate(text, opts = {}) {
93
98
  return parsedData
94
99
  });
95
100
 
101
+ result = result
102
+ .replace(/<!\-\-.*?\-\->/g, '')
103
+ .replace(/>\s+</g, '><')
104
+
96
105
  result = '`' + result.trim() + '`'
97
106
 
98
107
  try {
package/src/fez/root.js CHANGED
@@ -141,6 +141,7 @@ Fez.htmlEscape = (text) => {
141
141
  text = text
142
142
  // .replaceAll('&', "&amp;")
143
143
  .replace(/font-family\s*:\s*(?:&[^;]+;|[^;])*?;/gi, '')
144
+ .replaceAll("&", '&amp;')
144
145
  .replaceAll("'", '&apos;')
145
146
  .replaceAll('"', '&quot;')
146
147
  .replaceAll('<', '&lt;')
@@ -191,6 +192,7 @@ Fez.error = (text, show) => {
191
192
  }
192
193
  Fez.log = (text) => {
193
194
  if (Fez.LOG === true) {
195
+ text = String(text).substring(0, 180)
194
196
  console.log(`Fez: ${text}`)
195
197
  }
196
198
  }
@@ -210,20 +212,34 @@ Fez.untilTrue = (func, pingRate) => {
210
212
  }
211
213
 
212
214
  // Script from URL
213
- // head({ js: 'https://example.com/script.js' });
215
+ // Fez.head({ js: 'https://example.com/script.js' });
214
216
  // Script with attributes
215
- // head({ js: 'https://example.com/script.js', type: 'module', async: true });
217
+ // Fez.head({ js: 'https://example.com/script.js', type: 'module', async: true });
216
218
  // Script with callback
217
- // head({ js: 'https://example.com/script.js' }, () => { console.log('loaded') });
219
+ // Fez.head({ js: 'https://example.com/script.js' }, () => { console.log('loaded') });
218
220
  // Module loading with auto-import to window
219
- // head({ js: 'https://example.com/module.js', module: 'MyModule' }); // imports and sets window.MyModule
221
+ // Fez.head({ js: 'https://example.com/module.js', module: 'MyModule' }); // imports and sets window.MyModule
220
222
  // CSS inclusion
221
- // head({ css: 'https://example.com/styles.css' });
223
+ // Fez.head({ css: 'https://example.com/styles.css' });
222
224
  // CSS with additional attributes and callback
223
- // head({ css: 'https://example.com/styles.css', media: 'print' }, () => { console.log('CSS loaded') })
225
+ // Fez.head({ css: 'https://example.com/styles.css', media: 'print' }, () => { console.log('CSS loaded') })
224
226
  // Inline script evaluation
225
- // head({ script: 'console.log("Hello world")' })
227
+ // Fez.head({ script: 'console.log("Hello world")' })
228
+ // Extract from nodes
229
+ // Fez.head(domNode)
226
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
+ }
239
+
240
+ return
241
+ }
242
+
227
243
  if (typeof config !== 'object' || config === null) {
228
244
  throw new Error('head requires an object parameter');
229
245
  }
@@ -459,6 +475,25 @@ Fez.store = {
459
475
  }
460
476
  };
461
477
 
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
+ }
496
+
462
497
  Fez.compile = compile
463
498
  Fez.state = state
464
499
 
package/src/fez.js CHANGED
@@ -6,6 +6,9 @@ if (typeof window !== 'undefined') window.FezBase = FezBase
6
6
  import Fez from './fez/root.js'
7
7
  if (typeof window !== 'undefined') window.Fez = Fez
8
8
 
9
+ // Load defaults after Fez is properly initialized
10
+ import('./fez/defaults.js')
11
+
9
12
  // clear all unattached nodes
10
13
  setInterval(() => {
11
14
  for (const [key, el] of Fez.instances) {
@@ -22,18 +25,23 @@ const observer = new MutationObserver((mutations) => {
22
25
  for (const { addedNodes, removedNodes } of mutations) {
23
26
  addedNodes.forEach((node) => {
24
27
  if (node.nodeType !== 1) return; // only elements
25
- // check the node itself
28
+
26
29
  if (node.matches('template[fez], xmp[fez], script[fez]')) {
27
- window.requestAnimationFrame(()=>{
28
- Fez.compile(node);
29
- node.remove();
30
- })
30
+ Fez.compile(node);
31
+ node.remove();
32
+ }
33
+
34
+ if (node.querySelectorAll) {
35
+ const nestedTemplates = node.querySelectorAll('template[fez], xmp[fez], script[fez]');
36
+ nestedTemplates.forEach(template => {
37
+ Fez.compile(template);
38
+ template.remove();
39
+ });
31
40
  }
32
41
  });
33
42
 
34
43
  removedNodes.forEach((node) => {
35
44
  if (node.nodeType === 1 && node.querySelectorAll) {
36
- // check both the node itself and its descendants
37
45
  const fezElements = node.querySelectorAll('.fez, :scope.fez');
38
46
  fezElements
39
47
  .forEach(el => {
@@ -53,22 +61,5 @@ observer.observe(document.documentElement, {
53
61
  subtree: true
54
62
  });
55
63
 
56
- // fez custom tags
57
-
58
- //<fez-component name="some-node" :props="fez.props"></fez-node>
59
- Fez('fez-component', class {
60
- init(props) {
61
- const tag = document.createElement(props.name)
62
- tag.props = props.props || props['data-props'] || props
63
-
64
- while (this.root.firstChild) {
65
- this.root.parentNode.insertBefore(this.root.lastChild, tag.nextSibling);
66
- }
67
-
68
- this.root.innerHTML = ''
69
- this.root.appendChild(tag)
70
- }
71
- })
72
-
73
64
  export default Fez
74
65
  export { Fez }
package/src/log.js ADDED
@@ -0,0 +1,154 @@
1
+ // pretty print HTML
2
+ const LOG_PP = (html) => {
3
+ const parts = html
4
+ .split(/(<\/?[^>]+>)/g)
5
+ .map(p => p.trim())
6
+ .filter(p => p);
7
+
8
+ let indent = 0;
9
+ const lines = [];
10
+
11
+ for (let i = 0; i < parts.length; i++) {
12
+ const part = parts[i];
13
+ const nextPart = parts[i + 1];
14
+ const nextNextPart = parts[i + 2];
15
+
16
+ // Check if it's a tag
17
+ if (part.startsWith('<')) {
18
+ // Check if this is an opening tag followed by text and then its closing tag
19
+ if (!part.startsWith('</') && !part.endsWith('/>') && nextPart && !nextPart.startsWith('<') && nextNextPart && nextNextPart.startsWith('</')) {
20
+ // Combine them on one line
21
+ const actualIndent = Math.max(0, indent);
22
+ lines.push(' '.repeat(actualIndent) + part + nextPart + nextNextPart);
23
+ i += 2; // Skip the next two parts
24
+ }
25
+ // Closing tag
26
+ else if (part.startsWith('</')) {
27
+ indent--;
28
+ const actualIndent = Math.max(0, indent);
29
+ lines.push(' '.repeat(actualIndent) + part);
30
+ }
31
+ // Self-closing tag
32
+ else if (part.endsWith('/>') || part.includes(' />')) {
33
+ const actualIndent = Math.max(0, indent);
34
+ lines.push(' '.repeat(actualIndent) + part);
35
+ }
36
+ // Opening tag
37
+ else {
38
+ const actualIndent = Math.max(0, indent);
39
+ lines.push(' '.repeat(actualIndent) + part);
40
+ indent++;
41
+ }
42
+ }
43
+ // Text node
44
+ else if (part) {
45
+ const actualIndent = Math.max(0, indent);
46
+ lines.push(' '.repeat(actualIndent) + part);
47
+ }
48
+ }
49
+
50
+ return lines.join('\n');
51
+ }
52
+
53
+ const LOG = (() => {
54
+ const logs = [];
55
+ const logTypes = []; // Track the original type of each log
56
+ let currentIndex = 0;
57
+
58
+ return o => {
59
+ if (!document.body) {
60
+ window.requestAnimationFrame( () => LOG(o) )
61
+ return
62
+ }
63
+
64
+ if (o instanceof Node) {
65
+ o = LOG_PP(o.outerHTML)
66
+ }
67
+
68
+ // Store the original type
69
+ let originalType = typeof o;
70
+
71
+ if (o === undefined) { o = 'undefined' }
72
+ if (o === null) { o = 'null' }
73
+
74
+ if (Array.isArray(o)) {
75
+ originalType = 'array';
76
+ } else if (typeof o === 'object' && o !== null) {
77
+ originalType = 'object';
78
+ }
79
+
80
+ if (typeof o != 'string') {
81
+ o = JSON.stringify(o, (key, value) => {
82
+ if (typeof value === 'function') {
83
+ return String(value);
84
+ }
85
+ return value;
86
+ }, 2).replaceAll('<', '&lt;')
87
+ }
88
+
89
+ o = o.trim()
90
+
91
+ logs.push(o + `\n\ntype: ${originalType}`);
92
+ logTypes.push(originalType);
93
+
94
+ let d = document.getElementById('dump-dialog');
95
+ if (!d) {
96
+ d = document.body.appendChild(document.createElement('div'));
97
+ d.id = 'dump-dialog';
98
+ d.style.cssText =
99
+ 'position:fixed;top:30px;left:30px;right:50px;bottom:50px;' +
100
+ 'background:#fff;border:1px solid#333;box-shadow:0 0 10px rgba(0,0,0,0.5);' +
101
+ 'padding:20px;overflow:auto;z-index:9999;font:13px/1.4 monospace;white-space:pre';
102
+ }
103
+
104
+ // Check if we have a saved index and it's still valid
105
+ const savedIndex = parseInt(localStorage.getItem('_LOG_INDEX'));
106
+ if (!isNaN(savedIndex) && savedIndex >= 0 && savedIndex < logs.length) {
107
+ currentIndex = savedIndex;
108
+ } else {
109
+ currentIndex = logs.length - 1;
110
+ }
111
+
112
+ const renderContent = () => {
113
+ const buttons = logs.map((_, i) => {
114
+ let bgColor = '#f0f0f0'; // default
115
+ if (i !== currentIndex) {
116
+ if (logTypes[i] === 'object') {
117
+ bgColor = '#d6e3ef'; // super light blue
118
+ } else if (logTypes[i] === 'array') {
119
+ bgColor = '#d8d5ef'; // super light indigo
120
+ }
121
+ }
122
+ return `<button style="padding:4px 8px;margin:0;cursor:pointer;background:${i === currentIndex ? '#333' : bgColor};color:${i === currentIndex ? '#fff' : '#000'}" data-index="${i}">${i + 1}</button>`
123
+ }).join('');
124
+
125
+ d.innerHTML =
126
+ '<div style="display:flex;flex-direction:column;height:100%">' +
127
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px">' +
128
+ '<div style="display:flex;flex-wrap:wrap;gap:4px;flex:1;margin-right:10px">' + buttons + '</div>' +
129
+ '<button style="padding:4px 8px;cursor:pointer;flex-shrink:0">&times;</button>' +
130
+ '</div>' +
131
+ '<xmp style="flex:1;overflow:auto;margin:0;padding:0;color:#000;background:#fff;font-size:14px;line-height:22px">' + logs[currentIndex] + '</xmp>' +
132
+ '</div>';
133
+
134
+ d.querySelector('button[style*="flex-shrink:0"]').onclick = () => d.remove();
135
+
136
+ d.querySelectorAll('button[data-index]').forEach(btn => {
137
+ btn.onclick = () => {
138
+ currentIndex = parseInt(btn.dataset.index);
139
+ localStorage.setItem('_LOG_INDEX', currentIndex);
140
+ renderContent();
141
+ };
142
+ });
143
+ };
144
+
145
+ renderContent();
146
+ };
147
+ })();
148
+
149
+ if (typeof window !== 'undefined') {
150
+ window.LOG = LOG
151
+ window.LOG_PP = LOG_PP
152
+ }
153
+
154
+ export default LOG
package/src/rollup.js CHANGED
@@ -22,10 +22,4 @@ function fezPlugin() {
22
22
  };
23
23
  }
24
24
 
25
- // Export for ES modules
26
25
  export default fezPlugin;
27
-
28
- // Export for CommonJS
29
- // if (typeof module !== 'undefined') {
30
- // module.exports = fezPlugin;
31
- // }