@dinoreic/fez 0.4.0 → 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,206 +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
 
105
+ return node.fez;
106
+ };
107
+
108
+ // =============================================================================
109
+ // COMPONENT REGISTRY
110
+ // =============================================================================
111
+
112
+ /** Unified component index - Fez.index['name'] = { class, meta, demo, info, source } */
113
+ Fez.index = index;
114
+
115
+ /** Counter for unique instance IDs */
116
+ Fez.instanceCount = 0;
117
+
118
+ /** Active component instances by UID */
119
+ Fez.instances = new Map();
120
+
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;
130
+
131
+ // jQuery compatibility
132
+ if (typeof node.val === "function") node = node[0];
133
+
134
+ const selector = name ? `.fez.fez-${name}` : ".fez";
135
+ const closestNode = node.closest(selector);
136
+
137
+ if (closestNode?.fez) return closestNode.fez;
138
+
139
+ Fez.onError("find", `Node connector not found. Selector: "${selector}", node: ${onode}`, {
140
+ original: onode,
141
+ resolved: node,
142
+ selector,
143
+ });
144
+ };
145
+
146
+ // =============================================================================
147
+ // CSS UTILITIES
148
+ // =============================================================================
149
+
150
+ /**
151
+ * Generate unique CSS class from CSS text (via Goober)
152
+ * @param {string} text - CSS rules
153
+ * @returns {string} Generated class name
154
+ */
86
155
  Fez.cssClass = (text) => {
87
- return Gobber.css(text)
88
- }
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);
167
+ }
168
+ };
89
169
 
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
+ */
90
176
  Fez.globalCss = (cssClass, opts = {}) => {
91
- if (typeof cssClass === 'function') {
92
- cssClass = cssClass()
93
- }
177
+ if (typeof cssClass === "function") cssClass = cssClass();
94
178
 
95
- if (cssClass.includes(':')) {
179
+ if (cssClass.includes(":")) {
96
180
  let text = cssClass
97
181
  .split("\n")
98
- .filter(line => !(/^\s*\/\//.test(line)))
99
- .join("\n")
182
+ .filter((line) => !/^\s*\/\//.test(line))
183
+ .join("\n");
100
184
 
101
- if (opts.wrap) {
102
- text = `:fez { ${text} }`
103
- }
185
+ if (opts.wrap) text = `:fez { ${text} }`;
186
+ text = text.replace(/\:fez|\:host/, `.fez.fez-${opts.name}`);
187
+ cssClass = Fez.cssClass(text);
188
+ }
104
189
 
105
- text = text.replace(/\:fez|\:host/, `.fez.fez-${opts.name}`)
190
+ Fez.onReady(() => document.body.parentElement.classList.add(cssClass));
191
+ return cssClass;
192
+ };
106
193
 
107
- cssClass = Fez.cssClass(text)
108
- }
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
+ },
223
+
224
+ // Cleanup destroyed fez components
225
+ beforeRemove: (node) => {
226
+ if (node.classList?.contains("fez") && node.fez) {
227
+ node.fez.fezOnDestroy();
228
+ }
229
+ },
230
+ });
231
+ };
109
232
 
110
- Fez.onReady(() => {
111
- document.body.parentElement.classList.add(cssClass)
112
- })
233
+ // =============================================================================
234
+ // PUB/SUB SYSTEM (see lib/pubsub.js)
235
+ // =============================================================================
113
236
 
114
- return cssClass
115
- }
237
+ Fez.subscribe = subscribe;
238
+ Fez.publish = publish;
116
239
 
117
- Fez.info = () => {
118
- console.log('Fez components:', Object.keys(Fez.classes || {}))
119
- }
240
+ // =============================================================================
241
+ // LOCAL STORAGE (see lib/localstorage.js)
242
+ // =============================================================================
120
243
 
121
- Fez.morphdom = (target, newNode, opts = {}) => {
122
- Array.from(target.attributes).forEach(attr => {
123
- newNode.setAttribute(attr.name, attr.value)
124
- })
244
+ Fez.localStorage = fezLocalStorage;
125
245
 
126
- Idiomorph.morph(target, newNode, {
127
- morphStyle: 'outerHTML'
128
- })
246
+ // =============================================================================
247
+ // ASYNC AWAIT HELPER (see lib/await-helper.js)
248
+ // =============================================================================
129
249
 
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.publish = (channel, ...args) => {
138
- Fez._subs ||= {}
139
- Fez._subs[channel] ||= []
140
- Fez._subs[channel].forEach((el) => {
141
- el[1].bind(el[0])(...args)
142
- })
143
- }
144
-
145
- Fez.error = (text, show) => {
146
- text = `Fez: ${text}`
147
- console.error(text)
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);
148
259
  if (show) {
149
- 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>`;
150
261
  }
151
- }
262
+ };
152
263
 
153
- Fez.log = (text) => {
154
- if (Fez.LOG === true) {
155
- text = String(text).substring(0, 180)
156
- console.log(`Fez: ${text}`)
264
+ Fez.consoleLog = (text) => {
265
+ if (Fez.LOG) {
266
+ console.log(`Fez: ${String(text).substring(0, 180)}`);
157
267
  }
158
- }
268
+ };
159
269
 
160
- Fez.onError = (kind, message) => {
161
- // Ensure kind is always a string
162
- if (typeof kind !== 'string') {
163
- 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];
164
285
  }
165
286
 
166
- console.error(`${kind}: ${message.toString()}`);
167
- }
168
-
169
- // work with tmp store
170
- Fez.store = {
171
- store: new Map(),
172
- counter: 0,
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}`;
173
292
 
174
- set(value) {
175
- const key = this.counter++;
176
- this.store.set(key, value);
177
- return key;
178
- },
179
-
180
- get(key) {
181
- return this.store.get(key);
182
- },
293
+ // Log with context if available
294
+ if (context && Fez.LOG) {
295
+ console.error(fullMessage, context);
296
+ } else {
297
+ console.error(fullMessage);
298
+ }
183
299
 
184
- delete(key) {
185
- const value = this.store.get(key);
186
- this.store.delete(key)
187
- return value;
300
+ // Include stack trace for Error objects
301
+ if (message instanceof Error && message.stack && Fez.LOG) {
302
+ console.error(message.stack);
188
303
  }
304
+
305
+ return fullMessage;
189
306
  };
190
307
 
191
- // Load utility functions
192
- import addUtilities from './utility.js'
193
- import cssMixin from './utils/css_mixin.js'
194
- addUtilities(Fez)
195
- 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);
196
317
 
197
- Fez.compile = compile
198
- Fez.state = state
199
- Fez.dump = objectDump
200
- Fez.dump = highlightAll
318
+ Fez.compile = compile;
319
+ Fez.createTemplate = createTemplate;
320
+ Fez.state = state;
321
+ Fez.log = objectDump;
322
+ Fez.highlightAll = highlightAll;
201
323
 
202
- Fez.onReady(() => {
203
- Fez.log('Fez.LOG === true, logging enabled.')
204
- })
324
+ Fez.onReady(() => Fez.consoleLog("Fez.LOG === true, logging enabled."));
205
325
 
206
- export default Fez
326
+ export default Fez;