@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/README.md +723 -198
- package/bin/fez +16 -6
- package/bin/fez-compile +347 -0
- package/bin/fez-debug +25 -0
- package/bin/fez-index +16 -4
- package/bin/refactor +699 -0
- package/dist/fez.js +142 -33
- package/dist/fez.js.map +4 -4
- package/fez.d.ts +533 -0
- package/package.json +25 -15
- package/src/fez/compile.js +396 -164
- package/src/fez/connect.js +250 -143
- package/src/fez/defaults.js +275 -84
- package/src/fez/instance.js +673 -514
- package/src/fez/lib/await-helper.js +64 -0
- package/src/fez/lib/global-state.js +22 -4
- package/src/fez/lib/index.js +140 -0
- package/src/fez/lib/localstorage.js +44 -0
- package/src/fez/lib/n.js +38 -23
- package/src/fez/lib/pubsub.js +208 -0
- package/src/fez/lib/svelte-template-lib.js +339 -0
- package/src/fez/lib/svelte-template.js +472 -0
- package/src/fez/lib/template.js +114 -119
- package/src/fez/morph.js +384 -0
- package/src/fez/root.js +284 -164
- package/src/fez/utility.js +319 -149
- package/src/fez/utils/dump.js +114 -84
- package/src/fez/utils/highlight_all.js +1 -1
- package/src/fez.js +65 -43
- package/src/rollup.js +1 -1
- package/src/svelte-cde-adapter.coffee +21 -12
- package/src/fez/vendor/idiomorph.js +0 -860
package/src/fez/connect.js
CHANGED
|
@@ -1,211 +1,318 @@
|
|
|
1
|
-
import createTemplate from './lib/template.js'
|
|
2
|
-
import FezBase from './instance.js'
|
|
3
|
-
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Fez Component Registration & Connection
|
|
3
|
+
*
|
|
4
|
+
* This file handles:
|
|
5
|
+
* - Registering components with customElements
|
|
6
|
+
* - Transforming plain classes to FezBase subclasses
|
|
7
|
+
* - Instantiating components when they appear in DOM
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. connect(name, class) - registers custom element
|
|
11
|
+
* 2. connectedCallback() - when element appears in DOM
|
|
12
|
+
* 3. connectNode() - creates instance, renders, calls lifecycle
|
|
7
13
|
*/
|
|
8
|
-
const observer = new MutationObserver((mutationsList, _) => {
|
|
9
|
-
for (const mutation of mutationsList) {
|
|
10
|
-
if (mutation.type === 'attributes') {
|
|
11
|
-
const fez = mutation.target.fez
|
|
12
|
-
const name = mutation.attributeName
|
|
13
|
-
const value = mutation.target.getAttribute(name)
|
|
14
14
|
|
|
15
|
+
import createTemplate from "./lib/template.js";
|
|
16
|
+
import FezBase from "./instance.js";
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// CONSTANTS
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
const SELF_CLOSING_TAGS = new Set([
|
|
23
|
+
"area",
|
|
24
|
+
"base",
|
|
25
|
+
"br",
|
|
26
|
+
"col",
|
|
27
|
+
"embed",
|
|
28
|
+
"hr",
|
|
29
|
+
"img",
|
|
30
|
+
"input",
|
|
31
|
+
"link",
|
|
32
|
+
"meta",
|
|
33
|
+
"source",
|
|
34
|
+
"track",
|
|
35
|
+
"wbr",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Attribute observer for reactive props
|
|
39
|
+
const attrObserver = new MutationObserver((mutations) => {
|
|
40
|
+
for (const mutation of mutations) {
|
|
41
|
+
if (mutation.type === "attributes") {
|
|
42
|
+
const fez = mutation.target.fez;
|
|
15
43
|
if (fez) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
const name = mutation.attributeName;
|
|
45
|
+
const value = mutation.target.getAttribute(name);
|
|
46
|
+
fez.props[name] = value;
|
|
47
|
+
fez.onPropsChange(name, value);
|
|
19
48
|
}
|
|
20
49
|
}
|
|
21
50
|
}
|
|
22
51
|
});
|
|
23
52
|
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// MAIN CONNECT FUNCTION
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
24
57
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @param {
|
|
58
|
+
* Register a Fez component
|
|
59
|
+
*
|
|
60
|
+
* @param {string} name - Custom element name (must contain dash)
|
|
61
|
+
* @param {Class} klass - Component class
|
|
62
|
+
*
|
|
28
63
|
* @example
|
|
29
|
-
* Fez('
|
|
30
|
-
* HTML = '<
|
|
31
|
-
* CSS = '
|
|
64
|
+
* Fez('ui-button', class {
|
|
65
|
+
* HTML = '<button><slot /></button>'
|
|
66
|
+
* CSS = 'button { color: blue; }'
|
|
67
|
+
* init() { console.log('created') }
|
|
32
68
|
* })
|
|
33
69
|
*/
|
|
34
70
|
export default function connect(name, klass) {
|
|
35
71
|
const Fez = globalThis.window?.Fez || globalThis.Fez;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
72
|
+
|
|
73
|
+
// Validate name
|
|
74
|
+
if (!name.includes("-")) {
|
|
75
|
+
console.error(`Fez: Invalid name "${name}". Must contain a dash.`);
|
|
76
|
+
return;
|
|
39
77
|
}
|
|
40
78
|
|
|
41
|
-
// Transform
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
79
|
+
// Transform to FezBase subclass
|
|
80
|
+
klass = ensureFezBase(Fez, name, klass);
|
|
81
|
+
|
|
82
|
+
// Process HTML template
|
|
83
|
+
if (klass.html) {
|
|
84
|
+
klass.html = klass.html
|
|
85
|
+
.replace(
|
|
86
|
+
/<slot(\s[^>]*)?>/,
|
|
87
|
+
`<div class="fez-slot" fez-keep="default-slot"$1>`,
|
|
88
|
+
)
|
|
89
|
+
.replace("</slot>", `</div>`);
|
|
45
90
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.concat(Object.getOwnPropertyNames(klass.prototype))
|
|
49
|
-
.filter(el => !['constructor', 'prototype'].includes(el))
|
|
91
|
+
klass.fezHtmlFunc = createTemplate(klass.html, { name });
|
|
92
|
+
}
|
|
50
93
|
|
|
51
|
-
|
|
94
|
+
// Register CSS
|
|
95
|
+
if (klass.css) {
|
|
96
|
+
klass.css = Fez.globalCss(klass.css, { name });
|
|
97
|
+
}
|
|
52
98
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (klassObj.GLOBAL) { newKlass.GLOBAL = klassObj.GLOBAL } // Global instance reference
|
|
56
|
-
if (klassObj.CSS) { newKlass.css = klassObj.CSS } // Component styles
|
|
57
|
-
if (klassObj.HTML) {
|
|
58
|
-
newKlass.html = closeCustomTags(klassObj.HTML) // Component template
|
|
59
|
-
}
|
|
60
|
-
if (klassObj.NAME) { newKlass.nodeName = klassObj.NAME } // Custom DOM node name
|
|
99
|
+
// Store class in index
|
|
100
|
+
Fez.index.ensure(name).class = klass;
|
|
61
101
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
102
|
+
// Register custom element
|
|
103
|
+
if (!customElements.get(name)) {
|
|
104
|
+
customElements.define(
|
|
105
|
+
name,
|
|
106
|
+
class extends HTMLElement {
|
|
107
|
+
connectedCallback() {
|
|
108
|
+
if (shouldRenderFast(this, klass)) {
|
|
109
|
+
connectNode(name, this);
|
|
110
|
+
} else {
|
|
111
|
+
requestAnimationFrame(() => connectNode(name, this));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
66
118
|
|
|
67
|
-
|
|
119
|
+
// =============================================================================
|
|
120
|
+
// CLASS TRANSFORMATION
|
|
121
|
+
// =============================================================================
|
|
68
122
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Transform plain class to FezBase subclass
|
|
125
|
+
* Maps uppercase config props (HTML, CSS, etc.)
|
|
126
|
+
*/
|
|
127
|
+
function ensureFezBase(Fez, name, klass) {
|
|
128
|
+
// Already a FezBase subclass
|
|
129
|
+
if (klass.prototype instanceof FezBase) {
|
|
130
|
+
if (klass.html) klass.html = closeCustomTags(klass.html);
|
|
131
|
+
return klass;
|
|
73
132
|
}
|
|
74
133
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
134
|
+
// Create FezBase subclass
|
|
135
|
+
const instance = new klass();
|
|
136
|
+
const newKlass = class extends FezBase {};
|
|
78
137
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
138
|
+
// Copy properties and methods
|
|
139
|
+
const props = [
|
|
140
|
+
...Object.getOwnPropertyNames(instance),
|
|
141
|
+
...Object.getOwnPropertyNames(klass.prototype),
|
|
142
|
+
].filter((p) => p !== "constructor" && p !== "prototype");
|
|
82
143
|
|
|
83
|
-
|
|
84
|
-
|
|
144
|
+
for (const prop of props) {
|
|
145
|
+
newKlass.prototype[prop] = instance[prop];
|
|
85
146
|
}
|
|
86
147
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
148
|
+
// Map config properties
|
|
149
|
+
const configMap = {
|
|
150
|
+
FAST: "FAST",
|
|
151
|
+
GLOBAL: "GLOBAL",
|
|
152
|
+
NAME: "nodeName",
|
|
153
|
+
};
|
|
154
|
+
for (const [from, to] of Object.entries(configMap)) {
|
|
155
|
+
if (instance[from]) newKlass[to] = instance[from];
|
|
90
156
|
}
|
|
91
157
|
|
|
92
|
-
|
|
158
|
+
// Handle CSS (can be string or function)
|
|
159
|
+
if (instance.CSS) {
|
|
160
|
+
newKlass.css =
|
|
161
|
+
typeof instance.CSS === "function" ? instance.CSS() : instance.CSS;
|
|
162
|
+
}
|
|
93
163
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
})
|
|
164
|
+
// Handle HTML (can be string or function)
|
|
165
|
+
if (instance.HTML) {
|
|
166
|
+
const html =
|
|
167
|
+
typeof instance.HTML === "function" ? instance.HTML() : instance.HTML;
|
|
168
|
+
newKlass.html = closeCustomTags(html);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle META (generic metadata object)
|
|
172
|
+
if (instance.META) {
|
|
173
|
+
newKlass.META = instance.META;
|
|
174
|
+
Fez.index.ensure(name).meta = instance.META;
|
|
108
175
|
}
|
|
109
|
-
}
|
|
110
176
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (fezFast == 'false') {
|
|
115
|
-
return false
|
|
116
|
-
} else {
|
|
117
|
-
return fezFast || isFast
|
|
177
|
+
// Auto-mount global components
|
|
178
|
+
if (instance.GLOBAL) {
|
|
179
|
+
Fez.onReady(() => document.body.appendChild(document.createElement(name)));
|
|
118
180
|
}
|
|
181
|
+
|
|
182
|
+
Fez.consoleLog(`${name} compiled`);
|
|
183
|
+
return newKlass;
|
|
119
184
|
}
|
|
120
185
|
|
|
121
186
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
187
|
+
* Convert self-closing custom tags to full format
|
|
188
|
+
* <my-comp /> -> <my-comp></my-comp>
|
|
189
|
+
* Uses (?:[^>]|=>) to skip => (arrow functions) inside attributes
|
|
124
190
|
*/
|
|
125
191
|
function closeCustomTags(html) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
192
|
+
return html.replace(
|
|
193
|
+
/<([a-z][a-z-]*)\b((?:=>|[^>])*)>/g,
|
|
194
|
+
(match, tag, attrs) => {
|
|
195
|
+
if (!attrs.trimEnd().endsWith("/")) return match;
|
|
196
|
+
if (SELF_CLOSING_TAGS.has(tag)) return match;
|
|
197
|
+
return `<${tag}${attrs.replace(/\s*\/$/, "")}></${tag}>`;
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Determine if component should render synchronously
|
|
204
|
+
*/
|
|
205
|
+
function shouldRenderFast(node, klass) {
|
|
206
|
+
const attr = node.getAttribute("fez-fast");
|
|
207
|
+
if (attr === "false") return false;
|
|
129
208
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
209
|
+
const klassFast =
|
|
210
|
+
typeof klass.FAST === "function" ? klass.FAST(node) : klass.FAST;
|
|
211
|
+
return !!(attr || klassFast || node.childNodes[0] || node.nextSibling);
|
|
133
212
|
}
|
|
134
213
|
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// NODE CONNECTION (Instantiation)
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
135
218
|
/**
|
|
136
|
-
*
|
|
137
|
-
* Replaces the custom element with the component's rendered content
|
|
219
|
+
* Initialize component instance from DOM node
|
|
138
220
|
*/
|
|
139
221
|
function connectNode(name, node) {
|
|
140
|
-
|
|
141
|
-
|
|
222
|
+
if (!node.isConnected) return;
|
|
223
|
+
if (node.classList?.contains("fez")) return;
|
|
142
224
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
225
|
+
const klass = Fez.index[name]?.class;
|
|
226
|
+
const nodeName =
|
|
227
|
+
typeof klass.nodeName === "function"
|
|
228
|
+
? klass.nodeName(node)
|
|
229
|
+
: klass.nodeName;
|
|
230
|
+
const newNode = document.createElement(nodeName || "div");
|
|
146
231
|
|
|
147
|
-
|
|
148
|
-
newNode.classList.add(`fez-${name}`)
|
|
232
|
+
newNode.classList.add("fez", `fez-${name}`);
|
|
149
233
|
|
|
150
|
-
|
|
234
|
+
if (!node.parentNode) {
|
|
235
|
+
console.warn(`Fez: ${name} has no parent, skipping`);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
151
238
|
|
|
152
|
-
|
|
239
|
+
// Replace custom element with component node
|
|
240
|
+
node.parentNode.replaceChild(newNode, node);
|
|
153
241
|
|
|
154
|
-
|
|
155
|
-
|
|
242
|
+
// Create instance
|
|
243
|
+
const fez = new klass();
|
|
244
|
+
fez.UID = ++Fez.instanceCount;
|
|
245
|
+
Fez.instances.set(fez.UID, fez);
|
|
156
246
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
247
|
+
fez.oldRoot = node;
|
|
248
|
+
fez.fezName = name;
|
|
249
|
+
fez.root = newNode;
|
|
250
|
+
fez.props = klass.getProps(node, newNode);
|
|
251
|
+
fez.class = klass;
|
|
162
252
|
|
|
163
|
-
|
|
164
|
-
|
|
253
|
+
// Move children (slot content)
|
|
254
|
+
fez.fezSlot(node, newNode);
|
|
165
255
|
|
|
166
|
-
|
|
256
|
+
newNode.fez = fez;
|
|
167
257
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
258
|
+
// Global component reference
|
|
259
|
+
if (klass.GLOBAL && klass.GLOBAL !== true) {
|
|
260
|
+
window[klass.GLOBAL] ||= fez;
|
|
261
|
+
}
|
|
171
262
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
263
|
+
// jQuery compatibility
|
|
264
|
+
if (window.$) fez.$root = $(newNode);
|
|
175
265
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
266
|
+
// Copy ID
|
|
267
|
+
if (fez.props.id) newNode.setAttribute("id", fez.props.id);
|
|
268
|
+
|
|
269
|
+
// Copy fez-keep for DOM differ preservation
|
|
270
|
+
const fezKeep = node.getAttribute("fez-keep");
|
|
271
|
+
if (fezKeep) newNode.setAttribute("fez-keep", fezKeep);
|
|
272
|
+
|
|
273
|
+
// === LIFECYCLE ===
|
|
274
|
+
|
|
275
|
+
// Setup reactive state
|
|
276
|
+
fez.fezRegister();
|
|
277
|
+
|
|
278
|
+
// Capture children before rendering replaces them
|
|
279
|
+
if (fez.root.childNodes.length) {
|
|
280
|
+
fez._fezSlotNodes = Array.from(fez.root.childNodes);
|
|
281
|
+
fez._fezChildNodes = fez._fezSlotNodes.filter((n) => n.nodeType === 1);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Prevent state changes during init/mount from scheduling extra renders
|
|
285
|
+
fez._isInitializing = true;
|
|
179
286
|
|
|
180
|
-
|
|
181
|
-
|
|
287
|
+
// Init (supports multiple naming conventions)
|
|
288
|
+
const initMethod = fez.onInit || fez.init || fez.created || fez.connect;
|
|
289
|
+
initMethod.call(fez, fez.props);
|
|
182
290
|
|
|
183
|
-
|
|
184
|
-
|
|
291
|
+
// Render
|
|
292
|
+
fez.fezRender();
|
|
185
293
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
fez.firstRender = true
|
|
294
|
+
// Done initializing - state changes in onMount will now trigger renders
|
|
295
|
+
fez._isInitializing = false;
|
|
189
296
|
|
|
190
|
-
|
|
191
|
-
|
|
297
|
+
// Mount
|
|
298
|
+
fez.onMount(fez.props);
|
|
192
299
|
|
|
193
|
-
|
|
194
|
-
|
|
300
|
+
// Form submit handling
|
|
301
|
+
if (fez.onSubmit) {
|
|
302
|
+
const form = fez.root.nodeName === "FORM" ? fez.root : fez.find("form");
|
|
303
|
+
if (form) {
|
|
195
304
|
form.onsubmit = (e) => {
|
|
196
|
-
e.preventDefault()
|
|
197
|
-
fez.onSubmit(fez.formData())
|
|
198
|
-
}
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
fez.onSubmit(fez.formData());
|
|
307
|
+
};
|
|
199
308
|
}
|
|
309
|
+
}
|
|
200
310
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
for (const [key, value] of Object.entries(fez.props)) {
|
|
207
|
-
fez.onPropsChange(key, value)
|
|
208
|
-
}
|
|
311
|
+
// Watch for attribute changes
|
|
312
|
+
if (fez.onPropsChange) {
|
|
313
|
+
attrObserver.observe(newNode, { attributes: true });
|
|
314
|
+
for (const [key, value] of Object.entries(fez.props)) {
|
|
315
|
+
fez.onPropsChange(key, value);
|
|
209
316
|
}
|
|
210
317
|
}
|
|
211
318
|
}
|