@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/src/fez/root.js CHANGED
@@ -1,256 +1,326 @@
1
- // runtime scss
2
- import Gobber from './vendor/gobber.js'
3
-
4
- // morph dom from one state to another
5
- import { Idiomorph } from './vendor/idiomorph.js'
6
-
7
- import objectDump from './utils/dump.js'
8
- import highlightAll from './utils/highlight_all.js'
9
- import connect from './connect.js'
10
- import compile from './compile.js'
11
- import state from './lib/global-state.js'
12
-
13
- // Fez('ui-slider') # first slider
14
- // Fez('ui-slider', (n)=>alert(n)) # find all and execute
15
- // Fez(this, 'ui-slider') # first parent ui-slider
16
- // Fez('ui-slider', class { init() { ... }}) # create Fez dom node
1
+ /**
2
+ * Fez - Main Framework Object
3
+ *
4
+ * This file contains:
5
+ * - Main Fez function (component registration and lookup)
6
+ * - Component registry
7
+ * - CSS utilities
8
+ * - Pub/Sub system
9
+ * - DOM morphing
10
+ * - Error handling
11
+ * - Temporary store
12
+ *
13
+ * For component instance methods, see instance.js
14
+ */
15
+
16
+ // =============================================================================
17
+ // IMPORTS
18
+ // =============================================================================
19
+
20
+ import Gobber from "./vendor/gobber.js";
21
+ import { fezMorph } from "./morph.js";
22
+ import objectDump from "./utils/dump.js";
23
+ import highlightAll from "./utils/highlight_all.js";
24
+ import connect from "./connect.js";
25
+ import compile from "./compile.js";
26
+ import state from "./lib/global-state.js";
27
+ import createTemplate from "./lib/template.js";
28
+ import { subscribe, publish } from "./lib/pubsub.js";
29
+ import fezLocalStorage from "./lib/localstorage.js";
30
+ import fezAwait from "./lib/await-helper.js";
31
+ import index from "./lib/index.js";
32
+
33
+ // =============================================================================
34
+ // MAIN FEZ FUNCTION
35
+ // =============================================================================
36
+
37
+ /**
38
+ * Main Fez function - register or find components
39
+ *
40
+ * @example
41
+ * Fez('ui-foo', class { ... }) // Register component
42
+ * Fez('ui-foo') // Find first instance
43
+ * Fez(123) // Find by UID
44
+ * Fez(domNode) // Find from DOM node
45
+ * Fez('ui-foo', fn) // Find all & execute callback
46
+ *
47
+ * @param {string|number|Node} name - Component name, UID, or DOM node
48
+ * @param {Class|Function} [klass] - Component class or callback
49
+ * @returns {FezBase|Array|void}
50
+ */
17
51
  const Fez = (name, klass) => {
18
- if(typeof name === 'number') {
19
- const fez = Fez.instances.get(name)
20
- if (fez) {
21
- return fez
22
- } else {
23
- Fez.error(`Instance with UID "${name}" not found.`)
24
- }
52
+ // Find by UID
53
+ if (typeof name === "number") {
54
+ const fez = Fez.instances.get(name);
55
+ if (fez) return fez;
56
+ Fez.onError("lookup", `Instance with UID "${name}" not found. Component may have been destroyed or never created.`, { uid: name });
57
+ return;
25
58
  }
26
- else if (name) {
27
- if (klass) {
28
- const isPureFn = typeof klass === 'function' && !/^\s*class/.test(klass.toString()) && !/\b(this|new)\b/.test(klass.toString())
29
-
30
- if (isPureFn) {
31
- const list = Array
32
- .from(document.querySelectorAll(`.fez.fez-${name}`))
33
- .filter( n => n.fez )
34
-
35
- list.forEach( el => klass(el.fez) )
36
- return list
37
- } else if (typeof klass != 'function') {
38
- return Fez.find(name, klass)
39
- } else {
40
- return connect(name, klass)
41
- }
42
- } else {
43
- const node = name.nodeName ? name.closest('.fez') : (
44
- document.querySelector( name.includes('#') ? name : `.fez.fez-${name}` )
45
- )
46
- if (node) {
47
- if (node.fez) {
48
- return node.fez
49
- } else {
50
- Fez.error(`node "${name}" has no Fez attached.`)
51
- }
52
- } else {
53
- Fez.error(`node "${name}" not found.`)
54
- }
55
- }
56
- } else {
57
- Fez.error('Fez() ?')
59
+
60
+ if (!name) {
61
+ Fez.onError("lookup", "Fez() called without arguments. Expected component name, UID, or DOM node.");
62
+ return;
58
63
  }
59
- }
60
64
 
61
- Fez.classes = {}
62
- Fez.instanceCount = 0
63
- Fez.instances = new Map()
65
+ // With second argument
66
+ if (klass) {
67
+ const isPureFn =
68
+ typeof klass === "function" &&
69
+ !/^\s*class/.test(klass.toString()) &&
70
+ !/\b(this|new)\b/.test(klass.toString());
71
+
72
+ // Fez('name', callback) - find all & execute
73
+ if (isPureFn) {
74
+ const list = Array.from(
75
+ document.querySelectorAll(`.fez.fez-${name}`),
76
+ ).filter((n) => n.fez);
77
+ list.forEach((el) => klass(el.fez));
78
+ return list;
79
+ }
64
80
 
65
- Fez.find = (onode, name) => {
66
- let node = onode
81
+ // Fez('name', selector) - find with context
82
+ if (typeof klass !== "function") {
83
+ return Fez.find(name, klass);
84
+ }
67
85
 
68
- if (typeof node == 'string') {
69
- node = document.body.querySelector(node)
86
+ // Fez('name', class) - register component
87
+ return connect(name, klass);
70
88
  }
71
89
 
72
- if (typeof node.val == 'function') {
73
- node = node[0]
74
- }
90
+ // Find instance by name or node
91
+ const node = name.nodeName
92
+ ? name.closest(".fez")
93
+ : document.querySelector(name.includes("#") ? name : `.fez.fez-${name}`);
75
94
 
76
- const klass = name ? `.fez.fez-${name}` : '.fez'
95
+ if (!node) {
96
+ Fez.onError("lookup", `Component "${name}" not found in DOM. Ensure the component is defined and rendered.`, { componentName: name });
97
+ return;
98
+ }
77
99
 
78
- const closestNode = node.closest(klass)
79
- if (closestNode && closestNode.fez) {
80
- return closestNode.fez
81
- } else {
82
- console.error('Fez node connector not found', onode, node)
100
+ if (!node.fez) {
101
+ Fez.onError("lookup", `DOM node "${name}" exists but has no Fez instance attached. Component may not be initialized yet.`, { node, tagName: name });
102
+ return;
83
103
  }
84
- }
85
104
 
86
- Fez.cssClass = (text) => {
87
- return Gobber.css(text)
88
- }
105
+ return node.fez;
106
+ };
89
107
 
90
- Fez.globalCss = (cssClass, opts = {}) => {
91
- if (typeof cssClass === 'function') {
92
- cssClass = cssClass()
93
- }
108
+ // =============================================================================
109
+ // COMPONENT REGISTRY
110
+ // =============================================================================
94
111
 
95
- if (cssClass.includes(':')) {
96
- let text = cssClass
97
- .split("\n")
98
- .filter(line => !(/^\s*\/\//.test(line)))
99
- .join("\n")
112
+ /** Unified component index - Fez.index['name'] = { class, meta, demo, info, source } */
113
+ Fez.index = index;
100
114
 
101
- if (opts.wrap) {
102
- text = `:fez { ${text} }`
103
- }
115
+ /** Counter for unique instance IDs */
116
+ Fez.instanceCount = 0;
104
117
 
105
- text = text.replace(/\:fez|\:host/, `.fez.fez-${opts.name}`)
118
+ /** Active component instances by UID */
119
+ Fez.instances = new Map();
106
120
 
107
- cssClass = Fez.cssClass(text)
108
- }
121
+ /**
122
+ * Find a component instance from a DOM node
123
+ * @param {Node|string} onode - DOM node or selector
124
+ * @param {string} [name] - Optional component name filter
125
+ * @returns {FezBase|undefined}
126
+ */
127
+ Fez.find = (onode, name) => {
128
+ let node =
129
+ typeof onode === "string" ? document.body.querySelector(onode) : onode;
109
130
 
110
- Fez.onReady(() => {
111
- document.body.parentElement.classList.add(cssClass)
112
- })
131
+ // jQuery compatibility
132
+ if (typeof node.val === "function") node = node[0];
113
133
 
114
- return cssClass
115
- }
134
+ const selector = name ? `.fez.fez-${name}` : ".fez";
135
+ const closestNode = node.closest(selector);
116
136
 
117
- Fez.info = () => {
118
- console.log('Fez components:', Object.keys(Fez.classes || {}))
119
- }
137
+ if (closestNode?.fez) return closestNode.fez;
120
138
 
121
- Fez.morphdom = (target, newNode, opts = {}) => {
122
- Array.from(target.attributes).forEach(attr => {
123
- newNode.setAttribute(attr.name, attr.value)
124
- })
139
+ Fez.onError("find", `Node connector not found. Selector: "${selector}", node: ${onode}`, {
140
+ original: onode,
141
+ resolved: node,
142
+ selector,
143
+ });
144
+ };
125
145
 
126
- Idiomorph.morph(target, newNode, {
127
- morphStyle: 'outerHTML'
128
- })
146
+ // =============================================================================
147
+ // CSS UTILITIES
148
+ // =============================================================================
129
149
 
130
- // remove whitespace on next node, if exists (you never want this)
131
- const nextSibling = target.nextSibling
132
- if (nextSibling?.nodeType === Node.TEXT_NODE && nextSibling.textContent.trim() === '') {
133
- nextSibling.remove();
134
- }
135
- }
136
-
137
- Fez._globalSubs ||= new Map()
138
-
139
- Fez.publish = (channel, ...args) => {
140
- Fez._subs ||= {}
141
- Fez._subs[channel] ||= []
142
- Fez._subs[channel].forEach((el) => {
143
- el[1].bind(el[0])(...args)
144
- })
145
-
146
- // Trigger global subscriptions
147
- const subs = Fez._globalSubs.get(channel)
148
- if (subs) {
149
- subs.forEach((sub) => {
150
- if (sub.node.isConnected) {
151
- sub.callback.call(sub.node, ...args)
152
- } else {
153
- // Remove disconnected nodes from subscriptions
154
- subs.delete(sub)
155
- }
156
- })
157
- }
158
- }
159
-
160
- Fez.subscribe = (node, eventName, callback) => {
161
- // If second arg is function, shift arguments
162
- if (typeof eventName === 'function') {
163
- callback = eventName
164
- eventName = node
165
- node = document.body
150
+ /**
151
+ * Generate unique CSS class from CSS text (via Goober)
152
+ * @param {string} text - CSS rules
153
+ * @returns {string} Generated class name
154
+ */
155
+ Fez.cssClass = (text) => {
156
+ // In test environments without proper DOM, goober may fail
157
+ // Return a placeholder class name based on hash
158
+ try {
159
+ return Gobber.css(text);
160
+ } catch {
161
+ // Fallback: generate simple hash-based class name
162
+ let hash = 0;
163
+ for (let i = 0; i < text.length; i++) {
164
+ hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
165
+ }
166
+ return "fez-" + Math.abs(hash).toString(36);
166
167
  }
168
+ };
167
169
 
168
- // Handle string selectors
169
- if (typeof node === 'string') {
170
- node = document.querySelector(node)
171
- }
170
+ /**
171
+ * Register global CSS styles
172
+ * @param {string|Function} cssClass - CSS text or function
173
+ * @param {Object} opts - { name, wrap }
174
+ * @returns {string} Generated class name
175
+ */
176
+ Fez.globalCss = (cssClass, opts = {}) => {
177
+ if (typeof cssClass === "function") cssClass = cssClass();
172
178
 
173
- if (!Fez._globalSubs.has(eventName)) {
174
- Fez._globalSubs.set(eventName, new Set())
179
+ if (cssClass.includes(":")) {
180
+ let text = cssClass
181
+ .split("\n")
182
+ .filter((line) => !/^\s*\/\//.test(line))
183
+ .join("\n");
184
+
185
+ if (opts.wrap) text = `:fez { ${text} }`;
186
+ text = text.replace(/\:fez|\:host/, `.fez.fez-${opts.name}`);
187
+ cssClass = Fez.cssClass(text);
175
188
  }
176
189
 
177
- const subs = Fez._globalSubs.get(eventName)
190
+ Fez.onReady(() => document.body.parentElement.classList.add(cssClass));
191
+ return cssClass;
192
+ };
193
+
194
+ // =============================================================================
195
+ // DOM MORPHING
196
+ // =============================================================================
197
+
198
+ /**
199
+ * Morph DOM node to new state (via fez-morph)
200
+ * Child fez components are automatically preserved (skipped from morphing)
201
+ * Use fez-keep attribute for explicit element preservation
202
+ * @param {Element} target - Element to morph
203
+ * @param {Element} newNode - New state
204
+ */
205
+ Fez.morphdom = (target, newNode) => {
206
+ fezMorph(target, newNode, {
207
+ // Preserve child fez components - skip morphing them entirely
208
+ skipNode: (oldNode) => {
209
+ if (
210
+ oldNode.classList?.contains("fez") &&
211
+ oldNode.fez &&
212
+ !oldNode.fez._destroyed
213
+ ) {
214
+ if (Fez.LOG) {
215
+ console.log(
216
+ `Fez: preserved child component ${oldNode.fez.fezName} (UID ${oldNode.fez.UID})`,
217
+ );
218
+ }
219
+ return true;
220
+ }
221
+ return false;
222
+ },
178
223
 
179
- // Remove existing subscription for same node and callback
180
- subs.forEach(sub => {
181
- if (sub.node === node && sub.callback === callback) {
182
- subs.delete(sub)
183
- }
184
- })
224
+ // Cleanup destroyed fez components
225
+ beforeRemove: (node) => {
226
+ if (node.classList?.contains("fez") && node.fez) {
227
+ node.fez.fezOnDestroy();
228
+ }
229
+ },
230
+ });
231
+ };
185
232
 
186
- const subscription = { node, callback }
187
- subs.add(subscription)
233
+ // =============================================================================
234
+ // PUB/SUB SYSTEM (see lib/pubsub.js)
235
+ // =============================================================================
188
236
 
189
- // Return unsubscribe function
190
- return () => {
191
- subs.delete(subscription)
192
- }
193
- }
237
+ Fez.subscribe = subscribe;
238
+ Fez.publish = publish;
194
239
 
195
- Fez.error = (text, show) => {
196
- text = `Fez: ${text}`
197
- console.error(text)
240
+ // =============================================================================
241
+ // LOCAL STORAGE (see lib/localstorage.js)
242
+ // =============================================================================
243
+
244
+ Fez.localStorage = fezLocalStorage;
245
+
246
+ // =============================================================================
247
+ // ASYNC AWAIT HELPER (see lib/await-helper.js)
248
+ // =============================================================================
249
+
250
+ Fez.fezAwait = fezAwait;
251
+
252
+ // =============================================================================
253
+ // ERROR HANDLING & LOGGING
254
+ // =============================================================================
255
+
256
+ Fez.consoleError = (text, show) => {
257
+ text = `Fez: ${text}`;
258
+ console.error(text);
198
259
  if (show) {
199
- return `<span style="border: 1px solid red; font-size: 14px; padding: 3px 7px; background: #fee; border-radius: 4px;">${text}</span>`
260
+ return `<span style="border: 1px solid red; font-size: 14px; padding: 3px 7px; background: #fee; border-radius: 4px;">${text}</span>`;
200
261
  }
201
- }
262
+ };
202
263
 
203
- Fez.log = (text) => {
204
- if (Fez.LOG === true) {
205
- text = String(text).substring(0, 180)
206
- console.log(`Fez: ${text}`)
264
+ Fez.consoleLog = (text) => {
265
+ if (Fez.LOG) {
266
+ console.log(`Fez: ${String(text).substring(0, 180)}`);
207
267
  }
208
- }
268
+ };
209
269
 
210
- Fez.onError = (kind, message) => {
211
- // Ensure kind is always a string
212
- if (typeof kind !== 'string') {
213
- throw new Error('Fez.onError: kind must be a string');
270
+ /**
271
+ * Enhanced error handler with component context
272
+ * @param {string} kind - Error category (e.g., 'template', 'lifecycle', 'morph')
273
+ * @param {string|Error} message - Error message or Error object
274
+ * @param {Object} [context] - Additional context (component name, props, etc.)
275
+ * @returns {string} Formatted error message
276
+ */
277
+ Fez.onError = (kind, message, context) => {
278
+ // Extract component name from context or message
279
+ let componentName = context?.componentName || context?.name;
280
+
281
+ // Try to extract component name from message if not in context
282
+ if (!componentName && typeof message === "string") {
283
+ const match = message.match(/<([^>]+)>/);
284
+ if (match) componentName = match[1];
214
285
  }
215
286
 
216
- console.error(`${kind}: ${message.toString()}`);
217
- }
287
+ // Format the error message with component context
288
+ const prefix = componentName ? ` [${componentName}]` : "";
289
+ const errorMsg =
290
+ typeof message === "string" ? message : message?.message || String(message);
291
+ const fullMessage = `Fez ${kind}:${prefix} ${errorMsg}`;
218
292
 
219
- // work with tmp store
220
- Fez.store = {
221
- store: new Map(),
222
- counter: 0,
223
-
224
- set(value) {
225
- const key = this.counter++;
226
- this.store.set(key, value);
227
- return key;
228
- },
229
-
230
- get(key) {
231
- return this.store.get(key);
232
- },
293
+ // Log with context if available
294
+ if (context && Fez.LOG) {
295
+ console.error(fullMessage, context);
296
+ } else {
297
+ console.error(fullMessage);
298
+ }
233
299
 
234
- delete(key) {
235
- const value = this.store.get(key);
236
- this.store.delete(key)
237
- return value;
300
+ // Include stack trace for Error objects
301
+ if (message instanceof Error && message.stack && Fez.LOG) {
302
+ console.error(message.stack);
238
303
  }
304
+
305
+ return fullMessage;
239
306
  };
240
307
 
241
- // Load utility functions
242
- import addUtilities from './utility.js'
243
- import cssMixin from './utils/css_mixin.js'
244
- addUtilities(Fez)
245
- cssMixin(Fez)
308
+ // =============================================================================
309
+ // LOAD UTILITIES & EXPORTS
310
+ // =============================================================================
311
+
312
+ import addUtilities from "./utility.js";
313
+ import cssMixin from "./utils/css_mixin.js";
314
+
315
+ addUtilities(Fez);
316
+ cssMixin(Fez);
246
317
 
247
- Fez.compile = compile
248
- Fez.state = state
249
- Fez.dump = objectDump
250
- Fez.highlightAll = highlightAll
318
+ Fez.compile = compile;
319
+ Fez.createTemplate = createTemplate;
320
+ Fez.state = state;
321
+ Fez.log = objectDump;
322
+ Fez.highlightAll = highlightAll;
251
323
 
252
- Fez.onReady(() => {
253
- Fez.log('Fez.LOG === true, logging enabled.')
254
- })
324
+ Fez.onReady(() => Fez.consoleLog("Fez.LOG === true, logging enabled."));
255
325
 
256
- export default Fez
326
+ export default Fez;