@dinoreic/fez 0.1.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.
@@ -0,0 +1,582 @@
1
+ // HTML node builder
2
+ import parseNode from './lib/n.js'
3
+ import createTemplate from './lib/template.js'
4
+
5
+ export default class FezBase {
6
+ // get node attributes as object
7
+ static getProps(node, newNode) {
8
+ let attrs = {}
9
+
10
+ // we can directly attach props to DOM node instance
11
+ if (node.props) {
12
+ return node.props
13
+ }
14
+
15
+ // LOG(node.nodeName, node.attributes)
16
+ for (const attr of node.attributes) {
17
+ attrs[attr.name] = attr.value
18
+ }
19
+
20
+ for (const [key, val] of Object.entries(attrs)) {
21
+ if ([':'].includes(key[0])) {
22
+ delete attrs[key]
23
+ try {
24
+ const newVal = new Function(`return (${val})`).bind(newNode)()
25
+ attrs[key.replace(/[\:_]/, '')] = newVal
26
+
27
+ } catch (e) {
28
+ console.error(`Fez: Error evaluating attribute ${key}="${val}" for ${node.tagName}: ${e.message}`)
29
+ }
30
+ }
31
+ }
32
+
33
+ if (attrs['data-props']) {
34
+ let data = attrs['data-props']
35
+
36
+ if (typeof data == 'object') {
37
+ return data
38
+ }
39
+ else {
40
+ if (data[0] != '{') {
41
+ data = decodeURIComponent(data)
42
+ }
43
+ try {
44
+ attrs = JSON.parse(data)
45
+ } catch (e) {
46
+ console.error(`Fez: Invalid JSON in data-props for ${node.tagName}: ${e.message}`)
47
+ }
48
+ }
49
+ }
50
+
51
+ // pass props as json template
52
+ // <script type="text/template">{...}</script>
53
+ // <foo-bar data-json-template="true"></foo-bar>
54
+ else if (attrs['data-json-template']) {
55
+ const data = newNode.previousSibling?.textContent
56
+ if (data) {
57
+ try {
58
+ attrs = JSON.parse(data)
59
+ newNode.previousSibling.remove()
60
+ } catch (e) {
61
+ console.error(`Fez: Invalid JSON in template for ${node.tagName}: ${e.message}`)
62
+ }
63
+ }
64
+ }
65
+
66
+ return attrs
67
+ }
68
+
69
+ static formData(node) {
70
+ const formNode = node.closest('form') || node.querySelector('form')
71
+ if (!formNode) {
72
+ Fez.log('No form found for formData()')
73
+ return {}
74
+ }
75
+ const formData = new FormData(formNode)
76
+ const formObject = {}
77
+ formData.forEach((value, key) => {
78
+ formObject[key] = value
79
+ });
80
+ return formObject
81
+ }
82
+
83
+ static fastBind() {
84
+ // return true to bind without requestAnimationFrame
85
+ // you can do this if you are sure you are not expecting innerHTML data
86
+ return false
87
+ }
88
+
89
+ static nodeName = 'div'
90
+
91
+ // instance methods
92
+
93
+ constructor() {}
94
+
95
+ n = parseNode
96
+
97
+ // string selector for use in HTML nodes
98
+ get fezHtmlRoot() {
99
+ return `Fez(${this.UID}).`
100
+ // return this.props.id ? `Fez.find("#${this.props.id}").` : `Fez.find(this, "${this.fezName}").`
101
+ }
102
+
103
+ // checks if node is attached and clears all if not
104
+ get isConnected() {
105
+ if (this.root?.isConnected) {
106
+ return true
107
+ } else {
108
+ this.fezRemoveSelf()
109
+ return false
110
+ }
111
+ }
112
+
113
+ fezRemoveSelf() {
114
+ this._setIntervalCache ||= {}
115
+ Object.keys(this._setIntervalCache).forEach((key)=> {
116
+ clearInterval(this._setIntervalCache[key])
117
+ })
118
+
119
+ this.onDestroy()
120
+ this.onDestroy = ()=> {}
121
+
122
+ if (this.root) {
123
+ this.root.fez = undefined
124
+ }
125
+
126
+ this.root = undefined
127
+ }
128
+
129
+ // get single node property
130
+ prop(name) {
131
+ let v = this.oldRoot[name] || this.props[name]
132
+ if (typeof v == 'function') {
133
+ // if this.prop('onclick'), we want "this" to point to this.root (dom node)
134
+ v = v.bind(this.root)
135
+ }
136
+ return v
137
+ }
138
+
139
+ // copy attributes to root node
140
+ copy() {
141
+ for (const name of Array.from(arguments)) {
142
+ let value = this.props[name]
143
+
144
+ if (value !== undefined) {
145
+ if (name == 'class') {
146
+ const klass = this.root.getAttribute(name, value)
147
+
148
+ if (klass) {
149
+ value = [klass, value].join(' ')
150
+ }
151
+ }
152
+
153
+ if (name == 'style' || !this.root[name]) {
154
+ if (typeof value == 'string') {
155
+ this.root.setAttribute(name, value)
156
+ }
157
+ else {
158
+ this.root[name] = value
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+
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;
170
+
171
+ func()
172
+
173
+ const checkAndExecute = () => {
174
+ if (!this.isConnected) {
175
+ window.removeEventListener('resize', handleResize);
176
+ return;
177
+ }
178
+ func.call(this);
179
+ };
180
+
181
+ const handleResize = () => {
182
+ if (!this.isConnected) {
183
+ window.removeEventListener('resize', handleResize);
184
+ return;
185
+ }
186
+
187
+ if (delay) {
188
+ // Throttle
189
+ const now = Date.now();
190
+ if (now - lastRun >= delay) {
191
+ checkAndExecute();
192
+ lastRun = now;
193
+ }
194
+ } else {
195
+ // Debounce
196
+ clearTimeout(timeoutId);
197
+ timeoutId = setTimeout(checkAndExecute, 200);
198
+ }
199
+ };
200
+
201
+ window.addEventListener('resize', handleResize);
202
+ }
203
+
204
+ // copy child nodes, natively to preserve bound events
205
+ // if node name is SLOT insert adjacent and remove SLOT, else as a child nodes
206
+ slot(source, target) {
207
+ target ||= document.createElement('template')
208
+ const isSlot = target.nodeName == 'SLOT'
209
+
210
+ while (source.firstChild) {
211
+ if (isSlot) {
212
+ target.parentNode.insertBefore(source.lastChild, target.nextSibling);
213
+ } else {
214
+ target.appendChild(source.firstChild)
215
+ }
216
+ }
217
+
218
+ if (isSlot) {
219
+ target.parentNode.removeChild(target)
220
+ } else {
221
+ source.innerHTML = ''
222
+ }
223
+
224
+ return target
225
+ }
226
+
227
+ setStyle(key, value) {
228
+ this.root.style.setProperty(key, value);
229
+ }
230
+
231
+ connect() {}
232
+ onMount() {}
233
+ beforeRender() {}
234
+ afterRender() {}
235
+ onDestroy() {}
236
+ onStateChange() {}
237
+ onGlobalStateChange() {}
238
+ publish = Fez.publish
239
+ fezBlocks = {}
240
+
241
+ parseHtml(text) {
242
+ const base = this.fezHtmlRoot.replaceAll('"', '&quot;')
243
+
244
+ text = text
245
+ .replace(/([^\w\.])fez\./g, `$1${base}`)
246
+ .replace(/>\s+</g, '><')
247
+
248
+ return text.trim()
249
+ }
250
+
251
+
252
+ // pass name to have only one tick of a kind
253
+ nextTick(func, name) {
254
+ if (name) {
255
+ this._nextTicks ||= {}
256
+ this._nextTicks[name] ||= window.requestAnimationFrame(() => {
257
+ func.bind(this)()
258
+ this._nextTicks[name] = null
259
+ }, name)
260
+ } else {
261
+ window.requestAnimationFrame(func.bind(this))
262
+ }
263
+ }
264
+
265
+ // inject htmlString as innerHTML and replace $$. with local pointer
266
+ // $$. will point to current fez instance
267
+ // <slot></slot> will be replaced with current root
268
+ // this.render('...loading')
269
+ // this.render('.images', '...loading')
270
+ render(template) {
271
+ template ||= this?.class?.fezHtmlFunc
272
+
273
+ if (!template || !this.root) return
274
+
275
+ this.beforeRender()
276
+
277
+ const newNode = document.createElement(this.class.nodeName || 'div')
278
+
279
+ let renderedTpl
280
+ if (Array.isArray(template)) {
281
+ // array nodes this.n(...), look tabs example
282
+ if (template[0] instanceof Node) {
283
+ template.forEach( n => newNode.appendChild(n) )
284
+ } else{
285
+ renderedTpl = template.join('')
286
+ }
287
+ }
288
+ else if (typeof template == 'string') {
289
+ renderedTpl = createTemplate(template)(this)
290
+ }
291
+ else if (typeof template == 'function') {
292
+ renderedTpl = template(this)
293
+ }
294
+
295
+ if (renderedTpl) {
296
+ renderedTpl = renderedTpl.replace(/\s\w+="undefined"/g, '')
297
+ newNode.innerHTML = this.parseHtml(renderedTpl)
298
+ }
299
+
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
+ }
306
+
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)
313
+ }
314
+ }
315
+
316
+ Fez.morphdom(this.root, newNode)
317
+
318
+ this.renderFezPostProcess()
319
+
320
+ this.afterRender()
321
+ }
322
+
323
+ renderFezPostProcess() {
324
+ const fetchAttr = (name, func) => {
325
+ this.root.querySelectorAll(`*[${name}]`).forEach((n)=>{
326
+ let value = n.getAttribute(name)
327
+ n.removeAttribute(name)
328
+ if (value) {
329
+ func.bind(this)(value, n)
330
+ }
331
+ })
332
+ }
333
+
334
+ // <button fez-this="button" -> this.button = node
335
+ fetchAttr('fez-this', (value, n) => {
336
+ (new Function('n', `this.${value} = n`)).bind(this)(n)
337
+ })
338
+
339
+ // <button fez-use="animate" -> this.animate(node]
340
+ fetchAttr('fez-use', (value, n) => {
341
+ const target = this[value]
342
+ if (typeof target == 'function') {
343
+ target(n)
344
+ } else {
345
+ console.error(`Fez error: "${value}" is not a function in ${this.fezName}`)
346
+ }
347
+ })
348
+
349
+ // <button fez-class="dialog animate" -> add class "animate" after node init to trigger animation
350
+ fetchAttr('fez-class', (value, n) => {
351
+ let classes = value.split(/\s+/)
352
+ let lastClass = classes.pop()
353
+ classes.forEach((c)=> n.classList.add(c) )
354
+ if (lastClass) {
355
+ setTimeout(()=>{
356
+ n.classList.add(lastClass)
357
+ }, 300)
358
+ }
359
+ })
360
+
361
+ // <input fez-bind="state.inputNode" -> this.state.inputNode will be the value of input
362
+ fetchAttr('fez-bind', (text, n) => {
363
+ if (['INPUT', 'SELECT', 'TEXTAREA'].includes(n.nodeName)) {
364
+ const value = (new Function(`return this.${text}`)).bind(this)()
365
+ const isCb = n.type.toLowerCase() == 'checkbox'
366
+ const eventName = ['SELECT'].includes(n.nodeName) || isCb ? 'onchange' : 'onkeyup'
367
+ n.setAttribute(eventName, `${this.fezHtmlRoot}${text} = this.${isCb ? 'checked' : 'value'}`)
368
+ this.val(n, value)
369
+ } else {
370
+ console.error(`Cant fez-bind="${text}" to ${n.nodeName} (needs INPUT, SELECT or TEXTAREA. Want to use fez-this?).`)
371
+ }
372
+ })
373
+
374
+ this.root.querySelectorAll(`*[disabled]`).forEach((n)=>{
375
+ let value = n.getAttribute('disabled')
376
+ if (['false'].includes(value)) {
377
+ n.removeAttribute('disabled')
378
+ } else {
379
+ n.setAttribute('disabled', 'true')
380
+ }
381
+ })
382
+ }
383
+
384
+ // refresh single node only
385
+ refresh(selector) {
386
+ alert('NEEDS FIX and remove htmlTemplate')
387
+ if (selector) {
388
+ const n = document.createElement('div')
389
+ n.innerHTML = this.class.htmlTemplate
390
+ const tpl = n.querySelector(selector).innerHTML
391
+ this.render(selector, tpl)
392
+ } else {
393
+ this.render()
394
+ }
395
+ }
396
+
397
+ // run only if node is attached, clear otherwise
398
+ setInterval(func, tick, name) {
399
+ if (typeof func == 'number') {
400
+ [tick, func] = [func, tick]
401
+ }
402
+
403
+ name ||= Fez.fnv1(String(func))
404
+
405
+ this._setIntervalCache ||= {}
406
+ clearInterval(this._setIntervalCache[name])
407
+
408
+ this._setIntervalCache[name] = setInterval(() => {
409
+ if (this.isConnected) {
410
+ func()
411
+ }
412
+ }, tick)
413
+
414
+ return this._setIntervalCache[name]
415
+ }
416
+
417
+ find(selector) {
418
+ return typeof selector == 'string' ? this.root.querySelector(selector) : selector
419
+ }
420
+
421
+ // get or set node value
422
+ val(selector, data) {
423
+ const node = this.find(selector)
424
+
425
+ if (node) {
426
+ if (['INPUT', 'TEXTAREA', 'SELECT'].includes(node.nodeName)) {
427
+ if (typeof data != 'undefined') {
428
+ if (node.type == 'checkbox') {
429
+ node.checked = !!data
430
+ } else {
431
+ node.value = data
432
+ }
433
+ } else {
434
+ return node.value
435
+ }
436
+ } else {
437
+ if (typeof data != 'undefined') {
438
+ node.innerHTML = data
439
+ } else {
440
+ return node.innerHTML
441
+ }
442
+ }
443
+ }
444
+ }
445
+
446
+ formData(node) {
447
+ return this.class.formData(node || this.root)
448
+ }
449
+
450
+ // get or set attribute
451
+ attr(name, value) {
452
+ if (typeof value === 'undefined') {
453
+ return this.root.getAttribute(name)
454
+ } else {
455
+ this.root.setAttribute(name, value)
456
+ return value
457
+ }
458
+ }
459
+
460
+ // get root node child nodes as array
461
+ childNodes(func) {
462
+ const children = Array.from(this.root.children)
463
+
464
+ if (func) {
465
+ // Create temporary container to avoid ancestor-parent errors
466
+ const tmpContainer = document.createElement('div')
467
+ tmpContainer.style.display = 'none'
468
+ document.body.appendChild(tmpContainer)
469
+ children.forEach(child => tmpContainer.appendChild(child))
470
+
471
+ let list = Array.from(tmpContainer.children).map(func)
472
+ document.body.removeChild(tmpContainer)
473
+ return list
474
+ } else {
475
+ return children
476
+ }
477
+ }
478
+
479
+ subscribe(channel, func) {
480
+ Fez._subs ||= {}
481
+ Fez._subs[channel] ||= []
482
+ Fez._subs[channel] = Fez._subs[channel].filter((el) => el[0].isConnected)
483
+ Fez._subs[channel].push([this, func])
484
+ }
485
+
486
+ // get and set root node ID
487
+ rootId() {
488
+ this.root.id ||= `fez_${this.UID}`
489
+ return this.root.id
490
+ }
491
+
492
+ fezRegister() {
493
+ if (this.css) {
494
+ this.css = Fez.globalCss(this.css, {name: this.fezName, wrap: true})
495
+ }
496
+
497
+ if (this.class.css) {
498
+ this.class.css = Fez.globalCss(this.class.css, {name: this.fezName})
499
+ }
500
+
501
+ this.state ||= this.reactiveStore()
502
+ this.globalState = Fez.state.createProxy(this)
503
+ this.fezRegisterBindMethods()
504
+ }
505
+
506
+ // bind all instance method to this, to avoid calling with .bind(this)
507
+ fezRegisterBindMethods() {
508
+ const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this))
509
+ .filter(method => method !== 'constructor' && typeof this[method] === 'function')
510
+
511
+ methods.forEach(method => this[method] = this[method].bind(this))
512
+ }
513
+
514
+ fezHide() {
515
+ const node = this.root
516
+ const parent = this.root.parentNode
517
+ const fragment = document.createDocumentFragment();
518
+
519
+ while (node.firstChild) {
520
+ fragment.appendChild(node.firstChild)
521
+ }
522
+
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}`)
527
+ this.root = parent
528
+ return Array.from(this.root.children)
529
+ }
530
+
531
+ reactiveStore(obj, handler) {
532
+ obj ||= {}
533
+
534
+ handler ||= (o, k, v, oldValue) => {
535
+ this.onStateChange(k, v, oldValue)
536
+ this.nextTick(this.render, 'render')
537
+ }
538
+
539
+ handler.bind(this)
540
+
541
+ // licence ? -> generated by ChatGPT 2024
542
+ function createReactive(obj, handler) {
543
+ if (typeof obj !== 'object' || obj === null) {
544
+ return obj;
545
+ }
546
+
547
+ return new Proxy(obj, {
548
+ set(target, property, value, receiver) {
549
+ // Get the current value of the property
550
+ const currentValue = Reflect.get(target, property, receiver);
551
+
552
+ // Only proceed if the new value is different from the current value
553
+ if (currentValue !== value) {
554
+ if (typeof value === 'object' && value !== null) {
555
+ value = createReactive(value, handler); // Recursively make nested objects reactive
556
+ }
557
+
558
+ // Set the new value
559
+ const result = Reflect.set(target, property, value, receiver);
560
+
561
+ // Call the handler only if the value has changed
562
+ handler(target, property, value, currentValue);
563
+
564
+ return result;
565
+ }
566
+
567
+ // If the value hasn't changed, return true (indicating success) without calling the handler
568
+ return true;
569
+ },
570
+ get(target, property, receiver) {
571
+ const value = Reflect.get(target, property, receiver);
572
+ if (typeof value === 'object' && value !== null) {
573
+ return createReactive(value, handler); // Recursively make nested objects reactive
574
+ }
575
+ return value;
576
+ }
577
+ });
578
+ }
579
+
580
+ return createReactive(obj, handler);
581
+ }
582
+ }