@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.
- package/LICENSE +30 -0
- package/README.md +638 -0
- package/dist/fez.js +28 -0
- package/dist/fez.js.map +7 -0
- package/dist/rollup.js +3 -0
- package/dist/rollup.js.map +7 -0
- package/package.json +60 -0
- package/src/fez/compile.js +195 -0
- package/src/fez/connect.js +217 -0
- package/src/fez/instance.js +582 -0
- package/src/fez/lib/global-state.js +157 -0
- package/src/fez/lib/n.js +65 -0
- package/src/fez/lib/template.js +119 -0
- package/src/fez/root.js +465 -0
- package/src/fez/vendor/gobber.js +8 -0
- package/src/fez/vendor/idiomorph.js +860 -0
- package/src/fez.js +73 -0
- package/src/rollup.js +31 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const compileToClass = (html) => {
|
|
2
|
+
const result = { script: '', style: '', html: '', head: '' }
|
|
3
|
+
const lines = html.split('\n')
|
|
4
|
+
|
|
5
|
+
let currentBlock = []
|
|
6
|
+
let currentType = ''
|
|
7
|
+
|
|
8
|
+
for (var line of lines) {
|
|
9
|
+
line = line.trim()
|
|
10
|
+
if (line.startsWith('<script') && !result.script && currentType != 'head') {
|
|
11
|
+
currentType = 'script';
|
|
12
|
+
} else if (line.startsWith('<head') && !result.script) { // you must use XMP tag if you want to define <head> tag, and it has to be first
|
|
13
|
+
currentType = 'head';
|
|
14
|
+
} else if (line.startsWith('<style')) {
|
|
15
|
+
currentType = 'style';
|
|
16
|
+
} else if (line.endsWith('</script>') && currentType === 'script' && !result.script) {
|
|
17
|
+
result.script = currentBlock.join('\n');
|
|
18
|
+
currentBlock = [];
|
|
19
|
+
currentType = null;
|
|
20
|
+
} else if (line.endsWith('</style>') && currentType === 'style') {
|
|
21
|
+
result.style = currentBlock.join('\n');
|
|
22
|
+
currentBlock = [];
|
|
23
|
+
currentType = null;
|
|
24
|
+
} else if ((line.endsWith('</head>') || line.endsWith('</header>')) && currentType === 'head') {
|
|
25
|
+
result.head = currentBlock.join('\n');
|
|
26
|
+
currentBlock = [];
|
|
27
|
+
currentType = null;
|
|
28
|
+
} else if (currentType) {
|
|
29
|
+
currentBlock.push(line);
|
|
30
|
+
} else {
|
|
31
|
+
result.html += line + '\n';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (result.head) {
|
|
36
|
+
const container = document.createElement('div')
|
|
37
|
+
container.innerHTML = result.head
|
|
38
|
+
|
|
39
|
+
// Process all children of the container
|
|
40
|
+
Array.from(container.children).forEach(node => {
|
|
41
|
+
if (node.tagName === 'SCRIPT') {
|
|
42
|
+
const script = document.createElement('script')
|
|
43
|
+
// Copy all attributes
|
|
44
|
+
Array.from(node.attributes).forEach(attr => {
|
|
45
|
+
script.setAttribute(attr.name, attr.value)
|
|
46
|
+
})
|
|
47
|
+
script.type ||= 'text/javascript'
|
|
48
|
+
|
|
49
|
+
if (node.src) {
|
|
50
|
+
// External script - will load automatically
|
|
51
|
+
document.head.appendChild(script)
|
|
52
|
+
} else if (script.type.includes('javascript') || script.type == 'module') {
|
|
53
|
+
// Inline script - set content and execute
|
|
54
|
+
script.textContent = node.textContent
|
|
55
|
+
document.head.appendChild(script)
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
// For other elements (link, meta, etc.), just append them
|
|
59
|
+
document.head.appendChild(node.cloneNode(true))
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let klass = result.script
|
|
65
|
+
|
|
66
|
+
if (!/class\s+\{/.test(klass)) {
|
|
67
|
+
klass = `class {\n${klass}\n}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (String(result.style).includes(':')) {
|
|
71
|
+
Object.entries(Fez._styleMacros).forEach(([key, val])=>{
|
|
72
|
+
result.style = result.style.replaceAll(`:${key} `, `${val} `)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
result.style = result.style.includes(':fez') || /(?:^|\s)body\s*\{/.test(result.style) ? result.style : `:fez {\n${result.style}\n}`
|
|
76
|
+
klass = klass.replace(/\}\s*$/, `\n CSS = \`${result.style}\`\n}`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (/\w/.test(String(result.html))) {
|
|
80
|
+
// escape backticks in whole template block
|
|
81
|
+
result.html = result.html.replaceAll('`', '`')
|
|
82
|
+
result.html = result.html.replaceAll('$', '\\$')
|
|
83
|
+
klass = klass.replace(/\}\s*$/, `\n HTML = \`${result.html}\`\n}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return klass
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// <template fez="ui-form">
|
|
90
|
+
// <script>
|
|
91
|
+
// ...
|
|
92
|
+
// Fez.compile() # compile all
|
|
93
|
+
// Fez.compile(templateNode) # compile template node
|
|
94
|
+
// Fez.compile('ui-form', templateNode.innerHTML) # compile string
|
|
95
|
+
export default function (tagName, html) {
|
|
96
|
+
if (tagName instanceof Node) {
|
|
97
|
+
const node = tagName
|
|
98
|
+
node.remove()
|
|
99
|
+
|
|
100
|
+
const fezName = node.getAttribute('fez')
|
|
101
|
+
|
|
102
|
+
// Check if fezName contains dot or slash (indicates URL)
|
|
103
|
+
if (fezName && (fezName.includes('.') || fezName.includes('/'))) {
|
|
104
|
+
const url = fezName
|
|
105
|
+
|
|
106
|
+
Fez.log(`Loading from ${url}`)
|
|
107
|
+
|
|
108
|
+
// Load HTML content via AJAX from URL
|
|
109
|
+
fetch(url)
|
|
110
|
+
.then(response => {
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`Failed to load ${url}: ${response.status}`)
|
|
113
|
+
}
|
|
114
|
+
return response.text()
|
|
115
|
+
})
|
|
116
|
+
.then(htmlContent => {
|
|
117
|
+
// Check if remote HTML has template/xmp tags with fez attribute
|
|
118
|
+
const parser = new DOMParser()
|
|
119
|
+
const doc = parser.parseFromString(htmlContent, 'text/html')
|
|
120
|
+
const fezElements = doc.querySelectorAll('template[fez], xmp[fez]')
|
|
121
|
+
|
|
122
|
+
if (fezElements.length > 0) {
|
|
123
|
+
// Compile each found fez element
|
|
124
|
+
fezElements.forEach(el => {
|
|
125
|
+
const name = el.getAttribute('fez')
|
|
126
|
+
if (name && !name.includes('-') && !name.includes('.') && !name.includes('/')) {
|
|
127
|
+
console.error(`Fez: Invalid custom element name "${name}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
|
|
128
|
+
}
|
|
129
|
+
const content = el.innerHTML
|
|
130
|
+
Fez.compile(name, content)
|
|
131
|
+
})
|
|
132
|
+
} else {
|
|
133
|
+
// No fez elements found, use extracted name from URL
|
|
134
|
+
const name = url.split('/').pop().split('.')[0]
|
|
135
|
+
Fez.compile(name, htmlContent)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.catch(error => {
|
|
139
|
+
console.error(`FEZ template load error for "${fezName}": ${error.message}`)
|
|
140
|
+
})
|
|
141
|
+
return
|
|
142
|
+
} else {
|
|
143
|
+
// Validate fezName format for non-URL names
|
|
144
|
+
if (fezName && !fezName.includes('-')) {
|
|
145
|
+
console.error(`Fez: Invalid custom element name "${fezName}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
|
|
146
|
+
}
|
|
147
|
+
html = node.innerHTML
|
|
148
|
+
tagName = fezName
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (typeof html != 'string') {
|
|
152
|
+
document.body.querySelectorAll('template[fez], xmp[fez]').forEach((n) => Fez.compile(n))
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Validate element name if it's not a URL
|
|
157
|
+
if (tagName && !tagName.includes('-') && !tagName.includes('.') && !tagName.includes('/')) {
|
|
158
|
+
console.error(`Fez: Invalid custom element name "${tagName}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let klass = compileToClass(html)
|
|
162
|
+
let parts = klass.split(/class\s+\{/, 2)
|
|
163
|
+
|
|
164
|
+
klass = `${parts[0]};\n\nwindow.Fez('${tagName}', class {\n${parts[1]})`
|
|
165
|
+
|
|
166
|
+
// Add tag to global hidden styles container
|
|
167
|
+
if (tagName) {
|
|
168
|
+
let styleContainer = document.getElementById('fez-hidden-styles')
|
|
169
|
+
if (!styleContainer) {
|
|
170
|
+
styleContainer = document.createElement('style')
|
|
171
|
+
styleContainer.id = 'fez-hidden-styles'
|
|
172
|
+
document.head.appendChild(styleContainer)
|
|
173
|
+
}
|
|
174
|
+
styleContainer.textContent += `${tagName} { display: none; }\n`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// we cant try/catch javascript modules (they use imports)
|
|
178
|
+
if (klass.includes('import ')) {
|
|
179
|
+
Fez.head({script: klass})
|
|
180
|
+
|
|
181
|
+
// best we can do it inform that node did not compile, so we assume there is arrow
|
|
182
|
+
setTimeout(()=>{
|
|
183
|
+
if (!Fez.classes[tagName]) {
|
|
184
|
+
Fez.error(`Template "${tagName}" possible compile error. (can be a false positive, it imports are not loaded)`)
|
|
185
|
+
}
|
|
186
|
+
}, 2000)
|
|
187
|
+
} else {
|
|
188
|
+
try {
|
|
189
|
+
new Function(klass)()
|
|
190
|
+
} catch(e) {
|
|
191
|
+
Fez.error(`Template "${tagName}" compile error: ${e.message}`)
|
|
192
|
+
console.log(klass)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// templating
|
|
2
|
+
import createTemplate from './lib/template.js'
|
|
3
|
+
import FezBase from './instance.js'
|
|
4
|
+
|
|
5
|
+
// this function accepts custom tag name and class definition, creates and connects
|
|
6
|
+
// Fez(name, klass)
|
|
7
|
+
export default function(name, klass) {
|
|
8
|
+
const Fez = globalThis.window?.Fez || globalThis.Fez;
|
|
9
|
+
// Validate custom element name format (must contain a dash)
|
|
10
|
+
if (!name.includes('-')) {
|
|
11
|
+
console.error(`Fez: Invalid custom element name "${name}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// to allow anonymous class and then re-attach (does not work)
|
|
15
|
+
// Fez('ui-todo', class { ... # instead Fez('ui-todo', class extends FezBase {
|
|
16
|
+
if (!klass.fezHtmlRoot) {
|
|
17
|
+
const klassObj = new klass()
|
|
18
|
+
const newKlass = class extends FezBase {}
|
|
19
|
+
|
|
20
|
+
const props = Object.getOwnPropertyNames(klassObj)
|
|
21
|
+
.concat(Object.getOwnPropertyNames(klass.prototype))
|
|
22
|
+
.filter(el => !['constructor', 'prototype'].includes(el))
|
|
23
|
+
|
|
24
|
+
props.forEach(prop => newKlass.prototype[prop] = klassObj[prop])
|
|
25
|
+
|
|
26
|
+
Fez.fastBindInfo ||= {fast: [], slow: []}
|
|
27
|
+
|
|
28
|
+
if (klassObj.GLOBAL) { newKlass.fezGlobal = klassObj.GLOBAL }
|
|
29
|
+
if (klassObj.CSS) { newKlass.css = klassObj.CSS }
|
|
30
|
+
if (klassObj.HTML) { newKlass.html = klassObj.HTML }
|
|
31
|
+
if (klassObj.NAME) { newKlass.nodeName = klassObj.NAME }
|
|
32
|
+
if (klassObj.FAST) {
|
|
33
|
+
newKlass.fastBind = klassObj.FAST
|
|
34
|
+
Fez.fastBindInfo.fast.push(typeof klassObj.FAST == 'function' ? `${name} (func)` : name)
|
|
35
|
+
} else {
|
|
36
|
+
Fez.fastBindInfo.slow.push(name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (klassObj.GLOBAL) {
|
|
40
|
+
const func = () => document.body.appendChild(document.createElement(name))
|
|
41
|
+
|
|
42
|
+
if (document.readyState === 'loading') {
|
|
43
|
+
document.addEventListener('DOMContentLoaded', func);
|
|
44
|
+
} else {
|
|
45
|
+
func()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
klass = newKlass
|
|
50
|
+
|
|
51
|
+
let info = `${name} compiled`
|
|
52
|
+
if (klassObj.FAST) info += ' (fast bind)'
|
|
53
|
+
Fez.log(info)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (klass.html) {
|
|
57
|
+
klass.html = closeCustomTags(klass.html)
|
|
58
|
+
|
|
59
|
+
// wrap slot to enable reactive re-renders. It will use existing .fez-slot if found
|
|
60
|
+
klass.html = klass.html.replace(/<slot\s*\/>|<slot\s*>\s*<\/slot>/g, () => {
|
|
61
|
+
const name = klass.slotNodeName || 'div'
|
|
62
|
+
return `<${name} class="fez-slot"></${name}>`
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
klass.fezHtmlFunc = createTemplate(klass.html)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// we have to register global css on component init, because some other component can depend on it (it is global)
|
|
69
|
+
if (klass.css) {
|
|
70
|
+
klass.css = Fez.globalCss(klass.css, {name: name})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
Fez.classes[name] = klass
|
|
74
|
+
|
|
75
|
+
if (!customElements.get(name)) {
|
|
76
|
+
customElements.define(name, class extends HTMLElement {
|
|
77
|
+
connectedCallback() {
|
|
78
|
+
// if you want to force fast render (prevent page flickering), add static fastBind = true or FAST = true
|
|
79
|
+
// we can not fast load auto for all because that creates hard to debug problems in nested custom nodes
|
|
80
|
+
// problems with events and slots (I woke up at 2AM, now it is 5AM)
|
|
81
|
+
// this is usually safe for first order components, as page header or any components that do not have innerHTML or use slots
|
|
82
|
+
// Example: you can add FAST as a function - render fast nodes that have name attribute
|
|
83
|
+
// FAST(node) { return !!node.getAttribute('name') }
|
|
84
|
+
// to inspect fast / slow components use Fez.info() in console
|
|
85
|
+
if (useFastRender(this, klass)) {
|
|
86
|
+
connectNode(name, this)
|
|
87
|
+
} else {
|
|
88
|
+
window.requestAnimationFrame(()=>{
|
|
89
|
+
if (this.parentNode) {
|
|
90
|
+
connectNode(name, this)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//
|
|
100
|
+
|
|
101
|
+
function closeCustomTags(html) {
|
|
102
|
+
const selfClosingTags = new Set([
|
|
103
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr'
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
return html.replace(/<([a-z-]+)\b([^>]*)\/>/g, (match, tagName, attributes) => {
|
|
107
|
+
return selfClosingTags.has(tagName) ? match : `<${tagName}${attributes}></${tagName}>`
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function useFastRender(n, klass) {
|
|
112
|
+
const fezFast = n.getAttribute('fez-fast')
|
|
113
|
+
var isFast = typeof klass.fastBind === 'function' ? klass.fastBind(n) : klass.fastBind
|
|
114
|
+
|
|
115
|
+
if (fezFast == 'false') {
|
|
116
|
+
return false
|
|
117
|
+
} else {
|
|
118
|
+
return fezFast || isFast
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function connectNode(name, node) {
|
|
123
|
+
const klass = Fez.classes[name]
|
|
124
|
+
const parentNode = node.parentNode
|
|
125
|
+
|
|
126
|
+
if (node.isConnected) {
|
|
127
|
+
const nodeName = typeof klass.nodeName == 'function' ? klass.nodeName(node) : klass.nodeName
|
|
128
|
+
const newNode = document.createElement(nodeName || 'div')
|
|
129
|
+
|
|
130
|
+
newNode.classList.add('fez')
|
|
131
|
+
newNode.classList.add(`fez-${name}`)
|
|
132
|
+
|
|
133
|
+
parentNode.replaceChild(newNode, node);
|
|
134
|
+
|
|
135
|
+
const fez = new klass()
|
|
136
|
+
|
|
137
|
+
fez.UID = ++Fez.instanceCount
|
|
138
|
+
Fez.instances.set(fez.UID, fez)
|
|
139
|
+
|
|
140
|
+
fez.oldRoot = node
|
|
141
|
+
fez.fezName = name
|
|
142
|
+
fez.root = newNode
|
|
143
|
+
fez.props = klass.getProps(node, newNode)
|
|
144
|
+
fez.class = klass
|
|
145
|
+
|
|
146
|
+
// copy child nodes, natively to preserve bound events
|
|
147
|
+
fez.slot(node, newNode)
|
|
148
|
+
|
|
149
|
+
newNode.fez = fez
|
|
150
|
+
|
|
151
|
+
if (klass.fezGlobal && klass.fezGlobal != true) {
|
|
152
|
+
window[klass.fezGlobal] = fez
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (window.$) {
|
|
156
|
+
fez.$root = $(newNode)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (fez.props.id) {
|
|
160
|
+
newNode.setAttribute('id', fez.props.id)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fez.fezRegister();
|
|
164
|
+
(fez.init || fez.created || fez.connect).bind(fez)(fez.props);
|
|
165
|
+
|
|
166
|
+
const oldRoot = fez.root.cloneNode(true)
|
|
167
|
+
|
|
168
|
+
if (fez.class.fezHtmlFunc) {
|
|
169
|
+
fez.render()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const slot = fez.root.querySelector('.fez-slot')
|
|
173
|
+
if (slot) {
|
|
174
|
+
if (fez.props.html) {
|
|
175
|
+
slot.innerHTML = fez.props.html
|
|
176
|
+
} else {
|
|
177
|
+
fez.slot(oldRoot, slot)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (fez.onSubmit) {
|
|
182
|
+
const form = fez.root.nodeName == 'FORM' ? fez.root : fez.find('form')
|
|
183
|
+
form.onsubmit = (e) => {
|
|
184
|
+
e.preventDefault()
|
|
185
|
+
fez.onSubmit(fez.formData())
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fez.onMount(fez.props)
|
|
190
|
+
|
|
191
|
+
// if onPropsChange method defined, add observer and trigger call on all attributes once component is loaded
|
|
192
|
+
if (fez.onPropsChange) {
|
|
193
|
+
observer.observe(newNode, {attributes:true})
|
|
194
|
+
for (const [key, value] of Object.entries(fez.props)) {
|
|
195
|
+
fez.onPropsChange(key, value)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//
|
|
202
|
+
|
|
203
|
+
const observer = new MutationObserver((mutationsList, _) => {
|
|
204
|
+
for (const mutation of mutationsList) {
|
|
205
|
+
if (mutation.type === 'attributes') {
|
|
206
|
+
const fez = mutation.target.fez
|
|
207
|
+
const name = mutation.attributeName
|
|
208
|
+
const value = mutation.target.getAttribute(name)
|
|
209
|
+
|
|
210
|
+
if (fez) {
|
|
211
|
+
fez.props[name] = value
|
|
212
|
+
fez.onPropsChange(name, value)
|
|
213
|
+
// console.log(`The [${name}] attribute was modified to [${value}].`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|