@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,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('`', '&#x60;')
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
+ });