@dinoreic/fez 0.3.2 → 0.4.1

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.
@@ -46,7 +46,7 @@ export default (Fez) => {
46
46
  script.type = 'module';
47
47
  script.textContent = config.script;
48
48
  document.head.appendChild(script);
49
- setTimeout(()=>script.remove(), 100)
49
+ requestAnimationFrame(()=>script.remove())
50
50
  } else {
51
51
  try {
52
52
  new Function(config.script)();
@@ -121,6 +121,110 @@ export default (Fez) => {
121
121
  return element;
122
122
  }
123
123
 
124
+ // Fetch wrapper with automatic caching and data handling
125
+ // Usage:
126
+ // Fez.fetch(url) - GET request (default)
127
+ // Fez.fetch(url, callback) - GET with callback
128
+ // Fez.fetch(url, data) - GET with query params (?foo=bar&baz=qux)
129
+ // Fez.fetch(url, data, callback) - GET with query params and callback
130
+ // Fez.fetch('POST', url, data) - POST with FormData body (multipart/form-data)
131
+ // Fez.fetch('POST', url, data, callback) - POST with FormData and callback
132
+ // Data object is automatically converted:
133
+ // - GET: appended as URL query parameters
134
+ // - POST: sent as FormData (multipart/form-data) without custom headers
135
+ Fez.fetch = function(...args) {
136
+ // Initialize cache if not exists
137
+ Fez._fetchCache ||= {};
138
+
139
+ let method = 'GET';
140
+ let url;
141
+ let callback;
142
+
143
+ // Check if first arg is HTTP method (uppercase letters)
144
+ if (typeof args[0] === 'string' && /^[A-Z]+$/.test(args[0])) {
145
+ method = args.shift();
146
+ }
147
+
148
+ // URL is required
149
+ url = args.shift();
150
+
151
+ // Check for data/options object
152
+ let opts = {};
153
+ let data = null;
154
+ if (typeof args[0] === 'object') {
155
+ data = args.shift();
156
+ }
157
+
158
+ // Check for callback function
159
+ if (typeof args[0] === 'function') {
160
+ callback = args.shift();
161
+ }
162
+
163
+ // Handle data based on method
164
+ if (data) {
165
+ if (method === 'GET') {
166
+ // For GET, append data as query parameters
167
+ const params = new URLSearchParams(data);
168
+ url += (url.includes('?') ? '&' : '?') + params.toString();
169
+ } else if (method === 'POST') {
170
+ // For POST, convert to FormData
171
+ const formData = new FormData();
172
+ for (const [key, value] of Object.entries(data)) {
173
+ formData.append(key, value);
174
+ }
175
+ opts.body = formData;
176
+ }
177
+ }
178
+
179
+ // Set method
180
+ opts.method = method;
181
+
182
+ // Create cache key from method, url, and stringified opts
183
+ const cacheKey = `${method}:${url}:${JSON.stringify(opts)}`;
184
+
185
+ // Check cache first
186
+ if (Fez._fetchCache[cacheKey]) {
187
+ const cachedData = Fez._fetchCache[cacheKey];
188
+ Fez.log(`fetch cache hit: ${method} ${url}`);
189
+ if (callback) {
190
+ callback(cachedData);
191
+ return;
192
+ }
193
+ return Promise.resolve(cachedData);
194
+ }
195
+
196
+ // Log live fetch
197
+ Fez.log(`fetch live: ${method} ${url}`);
198
+
199
+ // Helper to process and cache response
200
+ const processResponse = (response) => {
201
+ if (response.headers.get('content-type')?.includes('application/json')) {
202
+ return response.json();
203
+ }
204
+ return response.text();
205
+ };
206
+
207
+ // If callback provided, execute and handle
208
+ if (callback) {
209
+ fetch(url, opts)
210
+ .then(processResponse)
211
+ .then(data => {
212
+ Fez._fetchCache[cacheKey] = data;
213
+ callback(data);
214
+ })
215
+ .catch(error => Fez.onError('fetch', error));
216
+ return;
217
+ }
218
+
219
+ // Return promise with automatic JSON parsing
220
+ return fetch(url, opts)
221
+ .then(processResponse)
222
+ .then(data => {
223
+ Fez._fetchCache[cacheKey] = data;
224
+ return data;
225
+ });
226
+ }
227
+
124
228
  Fez.darkenColor = (color, percent = 20) => {
125
229
  // Convert hex to RGB
126
230
  const num = parseInt(color.replace("#", ""), 16)
@@ -191,7 +295,99 @@ export default (Fez) => {
191
295
  return pointer;
192
296
  }
193
297
  else if (typeof pointer === 'string') {
194
- return new Function(pointer);
298
+ // Check if it's a function expression (arrow function or function keyword)
299
+ // Arrow function: (args) => or args =>
300
+ const arrowFuncPattern = /^\s*\(?\s*\w+(\s*,\s*\w+)*\s*\)?\s*=>/;
301
+ const functionPattern = /^\s*function\s*\(/;
302
+
303
+ if (arrowFuncPattern.test(pointer) || functionPattern.test(pointer)) {
304
+ return new Function('return ' + pointer)();
305
+ } else if (pointer.includes('.') && !pointer.includes('(')) {
306
+ // It's a property access like "this.focus" - return a function that calls it
307
+ return new Function(`return function() { return ${pointer}(); }`);
308
+ } else {
309
+ // It's a function body
310
+ return new Function(pointer);
311
+ }
312
+ }
313
+ }
314
+
315
+ // Execute a function when DOM is ready or immediately if already loaded
316
+ Fez.onReady = (callback) => {
317
+ if (document.readyState === 'loading') {
318
+ document.addEventListener('DOMContentLoaded', ()=>{
319
+ callback()
320
+ }, { once: true })
321
+ } else {
322
+ callback()
323
+ }
324
+ }
325
+
326
+ // get unique id from string
327
+ Fez.fnv1 = (str) => {
328
+ var FNV_OFFSET_BASIS, FNV_PRIME, hash, i, j, ref;
329
+ FNV_OFFSET_BASIS = 2166136261;
330
+ FNV_PRIME = 16777619;
331
+ hash = FNV_OFFSET_BASIS;
332
+ for (i = j = 0, ref = str.length - 1; (0 <= ref ? j <= ref : j >= ref); i = 0 <= ref ? ++j : --j) {
333
+ hash ^= str.charCodeAt(i);
334
+ hash *= FNV_PRIME;
335
+ }
336
+ return hash.toString(36).replaceAll('-', '');
337
+ }
338
+
339
+ Fez.tag = (tag, opts = {}, html = '') => {
340
+ const json = encodeURIComponent(JSON.stringify(opts))
341
+ return `<${tag} data-props="${json}">${html}</${tag}>`
342
+ // const json = JSON.stringify(opts, null, 2)
343
+ // const data = `<script type="text/template">${json}</script><${tag} data-json-template="true">${html}</${tag}>`
344
+ // return data
345
+ }
346
+
347
+ // execute function until it returns true
348
+ Fez.untilTrue = (func, pingRate) => {
349
+ pingRate ||= 200
350
+
351
+ if (!func()) {
352
+ setTimeout(()=>{
353
+ Fez.untilTrue(func, pingRate)
354
+ } ,pingRate)
195
355
  }
196
356
  }
357
+
358
+ // throttle function calls
359
+ Fez.throttle = (func, delay = 200) => {
360
+ let lastRun = 0;
361
+ let timeout;
362
+
363
+ return function(...args) {
364
+ const now = Date.now();
365
+
366
+ if (now - lastRun >= delay) {
367
+ func.apply(this, args);
368
+ lastRun = now;
369
+ } else {
370
+ clearTimeout(timeout);
371
+ timeout = setTimeout(() => {
372
+ func.apply(this, args);
373
+ lastRun = Date.now();
374
+ }, delay - (now - lastRun));
375
+ }
376
+ };
377
+ }
378
+
379
+ // const firstTimeHash = new Map()
380
+ // Fez.firstTime = (key, func) => {
381
+ // if ( !firstTimeHash.get(key) ) {
382
+ // firstTimeHash.set(key, true)
383
+
384
+ // if (func) {
385
+ // func()
386
+ // }
387
+
388
+ // return true
389
+ // }
390
+
391
+ // return false
392
+ // }
197
393
  }
@@ -0,0 +1,25 @@
1
+ // define custom style macro - simple scss mixin
2
+ // :mobile { ... } -> @media (max-width: 768px) { ... }
3
+ // @include mobile { ... } -> @media (max-width: 768px) { ... }
4
+ // demo/fez/ui-style.fez
5
+
6
+ const CssMixins = {}
7
+
8
+ export default (Fez) => {
9
+ Fez.cssMixin = (name, content) => {
10
+ if (content) {
11
+ CssMixins[name] = content
12
+ } else {
13
+ Object.entries(CssMixins).forEach(([key, val])=>{
14
+ name = name.replaceAll(`:${key} `, `${val} `)
15
+ name = name.replaceAll(`@include ${key} `, `${val} `)
16
+ })
17
+
18
+ return name
19
+ }
20
+ }
21
+
22
+ Fez.cssMixin('mobile', '@media (max-width: 767px)')
23
+ Fez.cssMixin('tablet', '@media (min-width: 768px) and (max-width: 1023px)')
24
+ Fez.cssMixin('desktop', '@media (min-width: 1200px)')
25
+ }
@@ -54,8 +54,9 @@ const LOG = (() => {
54
54
  const logs = [];
55
55
  const logTypes = []; // Track the original type of each log
56
56
  let currentIndex = 0;
57
+ let renderContent = null; // Will hold the render function
57
58
 
58
- // Add ESC key handler
59
+ // Add ESC key handler and arrow key navigation
59
60
  document.addEventListener('keydown', (e) => {
60
61
  if (e.key === 'Escape') {
61
62
  e.preventDefault();
@@ -65,14 +66,34 @@ const LOG = (() => {
65
66
  if (dialog) {
66
67
  // Close dialog
67
68
  dialog.remove();
68
- localStorage.setItem('_LOG_CLOSED', 'true');
69
69
  createLogButton();
70
70
  } else if (button) {
71
71
  // Open dialog
72
72
  button.remove();
73
- localStorage.setItem('_LOG_CLOSED', 'false');
74
73
  showLogDialog();
75
74
  }
75
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') {
76
+ const dialog = document.getElementById('dump-dialog');
77
+ if (dialog && logs.length > 0) {
78
+ e.preventDefault();
79
+ if (e.key === 'ArrowLeft' && currentIndex > 0) {
80
+ currentIndex--;
81
+ localStorage.setItem('_LOG_INDEX', currentIndex);
82
+ renderContent();
83
+ } else if (e.key === 'ArrowRight' && currentIndex < logs.length - 1) {
84
+ currentIndex++;
85
+ localStorage.setItem('_LOG_INDEX', currentIndex);
86
+ renderContent();
87
+ } else if (e.key === 'ArrowUp' && currentIndex > 0) {
88
+ currentIndex = Math.max(0, currentIndex - 5);
89
+ localStorage.setItem('_LOG_INDEX', currentIndex);
90
+ renderContent();
91
+ } else if (e.key === 'ArrowDown' && currentIndex < logs.length - 1) {
92
+ currentIndex = Math.min(logs.length - 1, currentIndex + 5);
93
+ localStorage.setItem('_LOG_INDEX', currentIndex);
94
+ renderContent();
95
+ }
96
+ }
76
97
  }
77
98
  });
78
99
 
@@ -90,7 +111,6 @@ const LOG = (() => {
90
111
  'opacity:1;visibility:visible;box-shadow:0 4px 12px rgba(255,51,51,0.3)';
91
112
  btn.onclick = () => {
92
113
  btn.remove();
93
- localStorage.setItem('_LOG_CLOSED', 'false');
94
114
  showLogDialog();
95
115
  };
96
116
  }
@@ -117,7 +137,7 @@ const LOG = (() => {
117
137
  currentIndex = logs.length - 1;
118
138
  }
119
139
 
120
- const renderContent = () => {
140
+ renderContent = () => {
121
141
  const buttons = logs.map((_, i) => {
122
142
  let bgColor = '#f0f0f0'; // default
123
143
  if (i !== currentIndex) {
@@ -141,7 +161,6 @@ const LOG = (() => {
141
161
 
142
162
  d.querySelector('button[style*="flex-shrink:0"]').onclick = () => {
143
163
  d.remove();
144
- localStorage.setItem('_LOG_CLOSED', 'true');
145
164
  createLogButton();
146
165
  };
147
166
 
@@ -163,13 +182,17 @@ const LOG = (() => {
163
182
  return
164
183
  }
165
184
 
166
- if (o instanceof Node) {
167
- o = log_pretty_print(o.outerHTML)
168
- }
169
-
170
185
  // Store the original type
171
186
  let originalType = typeof o;
172
187
 
188
+ if (o instanceof Node) {
189
+ if (o.nodeType === Node.TEXT_NODE) {
190
+ o = o.textContent || String(o)
191
+ } else {
192
+ o = log_pretty_print(o.outerHTML)
193
+ }
194
+ }
195
+
173
196
  if (o === undefined) { o = 'undefined' }
174
197
  if (o === null) { o = 'null' }
175
198
 
@@ -193,13 +216,19 @@ const LOG = (() => {
193
216
  logs.push(o + `\n\ntype: ${originalType}`);
194
217
  logTypes.push(originalType);
195
218
 
196
- // Check if log was previously closed
197
- const isClosed = localStorage.getItem('_LOG_CLOSED') === 'true';
219
+ // Check if log dialog is open by checking for element
220
+ const isOpen = !!document.getElementById('dump-dialog');
198
221
 
199
- if (isClosed) {
200
- createLogButton();
201
- } else {
222
+ if (!isOpen) {
223
+ // Show log dialog by default
202
224
  showLogDialog();
225
+ } else {
226
+ // Update current index to the new log and refresh
227
+ currentIndex = logs.length - 1;
228
+ localStorage.setItem('_LOG_INDEX', currentIndex);
229
+ if (renderContent) {
230
+ renderContent();
231
+ }
203
232
  }
204
233
  };
205
234
  })();
@@ -0,0 +1,98 @@
1
+ // Highlight all Fez elements with their names
2
+ const highlightAll = () => {
3
+ // Only work if Fez.DEV is true OR (port is above 2999 and Fez.DEV is not false)
4
+ const port = parseInt(window.location.port) || 80;
5
+ if (!(Fez.DEV === true || (port > 2999 && Fez.DEV !== false))) return;
6
+
7
+ // Check if highlights already exist
8
+ const existingHighlights = document.querySelectorAll('.fez-highlight-overlay');
9
+
10
+ if (existingHighlights.length > 0) {
11
+ // Remove existing highlights
12
+ existingHighlights.forEach(el => el.remove());
13
+ return;
14
+ }
15
+
16
+ // Find all Fez and Svelte elements
17
+ const allElements = document.querySelectorAll('.fez, .svelte');
18
+
19
+ allElements.forEach(el => {
20
+ let componentName = null;
21
+ let componentType = null;
22
+
23
+ // Check for Fez component
24
+ if (el.classList.contains('fez') && el.fez && el.fez.fezName) {
25
+ componentName = el.fez.fezName;
26
+ componentType = 'fez';
27
+ }
28
+ // Check for Svelte component
29
+ else if (el.classList.contains('svelte') && el.svelte && el.svelte.svelteName) {
30
+ componentName = el.svelte.svelteName;
31
+ componentType = 'svelte';
32
+ }
33
+
34
+ if (componentName) {
35
+ // Create overlay div
36
+ const overlay = document.createElement('div');
37
+ overlay.className = 'fez-highlight-overlay';
38
+
39
+ // Get element position
40
+ const rect = el.getBoundingClientRect();
41
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
42
+ const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
43
+
44
+ // Style the overlay
45
+ overlay.style.cssText = `
46
+ position: absolute;
47
+ top: ${rect.top + scrollTop}px;
48
+ left: ${rect.left + scrollLeft}px;
49
+ width: ${rect.width}px;
50
+ height: ${rect.height}px;
51
+ border: 1px solid ${componentType === 'svelte' ? 'blue' : 'red'};
52
+ pointer-events: none;
53
+ z-index: 9999;
54
+ `;
55
+
56
+ // Create label for component name
57
+ const label = document.createElement('div');
58
+ label.textContent = componentName;
59
+ label.style.cssText = `
60
+ position: absolute;
61
+ top: -20px;
62
+ left: 0;
63
+ background: ${componentType === 'svelte' ? 'blue' : 'red'};
64
+ color: white;
65
+ padding: 4px 6px 2px 6px;
66
+ font-size: 14px;
67
+ font-family: monospace;
68
+ line-height: 1;
69
+ white-space: nowrap;
70
+ cursor: pointer;
71
+ pointer-events: auto;
72
+ text-transform: uppercase;
73
+ `;
74
+
75
+ // Add click handler to dump the node
76
+ label.addEventListener('click', (e) => {
77
+ e.stopPropagation();
78
+ Fez.dump(el);
79
+ });
80
+
81
+ overlay.appendChild(label);
82
+ document.body.appendChild(overlay);
83
+ }
84
+ });
85
+ }
86
+
87
+ // Bind Ctrl+E to highlightAll
88
+ document.addEventListener('keydown', (event) => {
89
+ if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
90
+ // Check if target is not inside a form
91
+ if (!event.target.closest('form')) {
92
+ event.preventDefault();
93
+ highlightAll();
94
+ }
95
+ }
96
+ });
97
+
98
+ export default highlightAll
package/src/rollup.js CHANGED
@@ -13,7 +13,7 @@ const transformFez = (code, filePath) => {
13
13
  const baseName = filePath.split('/').pop().split('.');
14
14
 
15
15
  if (baseName[1] === 'fez') {
16
- code = code.replace(/`/g, '\\`').replace(/\$/g, '\\$');
16
+ code = code.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
17
17
  return `Fez.compile('${baseName[0]}', \`\n${code}\`)`;
18
18
  }
19
19
  }
@@ -0,0 +1,123 @@
1
+ # get node attributes
2
+ getAttributes = (node) ->
3
+ attrs = {}
4
+
5
+ for el in node.attributes
6
+ if el.name.startsWith('on') && el.value[0] == '('
7
+ el.value = new Function('arg1, arg2', "(#{el.value})(arg1, arg2)")
8
+ if el.name.startsWith(':')
9
+ el.value = new Function("return (#{el.value})").bind(node)()
10
+ el.name = el.name.replace(':', '')
11
+ attrs[el.name] = el.value
12
+
13
+ if attrs['data-props']
14
+ attrs = JSON.parse(attrs['data-props'])
15
+
16
+ # pass props as json template
17
+ # <script type="text/template">{...}</script>
18
+ # <foo-bar data-json-template="true"></foo-bar>
19
+ if attrs['data-json-template']
20
+ prev = node.previousSibling
21
+ if prev?.textContent
22
+ attrs = JSON.parse(prev.textContent)
23
+ prev.remove()
24
+
25
+ # data = node.previousSibling?.textContent
26
+ # if data
27
+ # attrs = JSON.parse(data)
28
+
29
+ attrs
30
+
31
+ # passes root, use `Svelte(this.root.id)` or `this.root.svelte` to get node pointer
32
+ # export onMount(instance, list, node) if you want to do stuff on mount
33
+ # connect DOM node to Svelte class instance
34
+ connect = (node, name, klass) ->
35
+ return unless node.isConnected
36
+
37
+ # TODO: get node name
38
+ # (new klass(target: document.createElement('div'))).nodeName
39
+ exported = Object.getOwnPropertyNames(klass.prototype)
40
+ newNode = document.createElement(if exported.includes('nodeNameSpan') then 'span' else 'div')
41
+ newNode.classList.add('svelte')
42
+ newNode.classList.add("svelte-#{name}")
43
+ newNode.id = node.id || "svelte_#{++Svelte.count}"
44
+
45
+ # get attributes
46
+ props = getAttributes(node)
47
+ props.root ||= newNode
48
+ props.oldRoot ||= node
49
+ props.html ||= node.innerHTML
50
+
51
+ # has to come after getAttributes
52
+ node.parentNode.replaceChild(newNode, node);
53
+
54
+ # bind node and pass all props as single props attribute
55
+ exported = Reflect.ownKeys klass.prototype
56
+ svelteProps = {}
57
+ svelteProps.fast = true if exported.includes('fast') || exported.includes('FAST')
58
+ svelteProps.props = props if exported.includes('props')
59
+ svelteProps.root = newNode if exported.includes('root')
60
+ svelteProps.self = "Svelte('#{newNode.id}')" if exported.includes('self')
61
+
62
+ instance = newNode.svelte = new klass({target: newNode, props: svelteProps})
63
+ instance.svelteName = name
64
+
65
+ # fill slots
66
+ # slot has to be named sslot (not slot)
67
+ if slot = newNode.querySelector('sslot')
68
+ while node.firstChild
69
+ slot.parentNode.insertBefore(node.lastChild, slot.nextSibling);
70
+ slot.parentNode.removeChild(slot)
71
+
72
+ # in the end, call onmount
73
+ if instance.onMount
74
+ list = Array.from node.querySelectorAll(':scope > *')
75
+ instance.onMount(instance, list, node)
76
+
77
+ # # #
78
+
79
+ # passes root, use `Svelte(this.root.id)` or `this.root.svelte` to get node pointer
80
+ # export onMount(instance, list, node) if you want to do stuff on mount
81
+ # Svelte(node || node_id).toogle()
82
+ Svelte = (name, func) ->
83
+ if name.nodeName
84
+ # Svelte(this).close() -> return first parent svelte node
85
+ while name = name.parentNode
86
+ return name.svelte if name.svelte
87
+ else
88
+ name = '#' + name if name[0] != '#' && name[0] != '.'
89
+ document.querySelector(name)?.svelte
90
+
91
+ Svelte.count = 0
92
+
93
+ # Creates custom DOM element
94
+ Svelte.connect = (name, klass) ->
95
+ customElements.define name, class extends HTMLElement
96
+ connectedCallback: ->
97
+ # no not optimize requestAnimationFrame (try to avoid it)
98
+ # because events in nested components are sometimes not propagated at all
99
+ # export let fast
100
+ # %s-menu-vertical{ fast_connect: true }
101
+
102
+ # connect @, name, klass
103
+
104
+ if this.childNodes[0] || this.nextSibling || klass.prototype.hasOwnProperty('fast') || klass.prototype.hasOwnProperty('FAST') || @getAttribute('fast_connect') || @getAttribute('data-props') || @getAttribute('data-json-template')
105
+ connect @, name, klass
106
+ else
107
+ requestAnimationFrame =>
108
+ connect @, name, klass
109
+
110
+ # if document.readyState == 'loading'
111
+ # document.addEventListener 'DOMContentLoaded',
112
+ # => connect(@, name, klass)
113
+ # , once: true
114
+ # else
115
+ # connect(@, name, klass)
116
+
117
+ # Creates HTML tag
118
+ Svelte.tag = (tag, opts = {}, html = '') ->
119
+ json = JSON.stringify(opts).replaceAll("'", '&apos;')
120
+ "<#{tag} data-props='#{json}'>#{html}</#{tag}>"
121
+
122
+ Svelte.bind = Svelte.connect
123
+ window.Svelte = Svelte