@lengkapp/edge 0.0.5 → 0.0.6

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.
Files changed (2) hide show
  1. package/edge-client.js +265 -246
  2. package/package.json +1 -1
package/edge-client.js CHANGED
@@ -1,284 +1,303 @@
1
- (() => {
2
- 'use strict';
1
+ (function () {
2
+ if (window.__edge) return;
3
+ window.__edge = true;
3
4
 
4
- const d = document,
5
- body = d.body,
6
- head = d.head;
5
+ var d = document, head = d.head, root = d.documentElement;
7
6
 
8
- // ---------- Constants and caches ----------
9
- const VALID_EVENTS = new Set([
7
+ var METHODS = ['get', 'post', 'put', 'patch', 'delete'];
8
+ var METHOD_ATTRS = METHODS.map(function (m) { return '_' + m; });
9
+ var HAS_BODY = { post: 1, put: 1, patch: 1 };
10
+ var SEL = METHOD_ATTRS.map(function (a) { return '[' + a + ']'; }).join(',');
11
+
12
+ var DELEGATABLE = new Set([
10
13
  'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
11
- 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu',
12
- 'focus', 'blur', 'focusin', 'focusout',
13
- 'keydown', 'keyup', 'keypress',
14
- 'change', 'input', 'submit', 'reset',
15
- 'load', 'DOMContentLoaded', 'ready',
16
- 'scroll', 'resize', 'wheel', 'touchstart', 'touchend', 'touchmove',
17
- 'visible', 'intersect'
14
+ 'mousemove', 'contextmenu', 'focusin', 'focusout', 'keydown', 'keyup',
15
+ 'keypress', 'change', 'input', 'submit', 'reset', 'wheel',
16
+ 'touchstart', 'touchend', 'touchmove'
18
17
  ]);
19
18
 
20
- // WeakMaps for per‑element caches
21
- const triggerCache = new WeakMap(); // element -> Set of event names (lowercase)
22
- const targetCache = new WeakMap(); // element -> target DOM node (resolved)
23
- const resourceCache = new Set(); // already injected CSS/JS URLs
19
+ var bound = new WeakSet();
20
+ var running = new WeakSet();
21
+ var targetCache = new WeakMap();
22
+ var wasVisible = new WeakMap();
23
+ var ioObservers = new Map();
24
+ var injected = new Set();
25
+ var executedSrc = new Set();
26
+
27
+ var style = d.createElement('style');
28
+ style.textContent =
29
+ '.aj-skel-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;' +
30
+ 'max-width:100%;max-height:600px;display:flex;align-items:center;justify-content:center}' +
31
+ '.aj-skel-wrap svg{width:1.75em;height:1.75em;color:#c7c7c7;' +
32
+ 'animation:aj-pulse 1.4s ease-in-out infinite}' +
33
+ '@keyframes aj-pulse{0%,100%{opacity:.35;transform:scale(.92)}50%{opacity:1;transform:scale(1)}}' +
34
+ '.aj-retry-wrap{box-sizing:border-box;width:100%;height:100%;min-width:80px;min-height:48px;' +
35
+ 'max-height:600px;display:flex;align-items:center;justify-content:center}' +
36
+ '.aj-retry{width:2.5em;height:2.5em;padding:0;cursor:pointer;background:#007bff;color:#fff;' +
37
+ 'border:0;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;' +
38
+ 'transition:background .15s ease}' +
39
+ '.aj-retry:hover{background:#0056b3}' +
40
+ '.aj-retry:hover svg{transform:rotate(-90deg)}' +
41
+ '.aj-retry svg{width:1.25em;height:1.25em;transition:transform .3s ease}';
42
+ head.appendChild(style);
24
43
 
25
- // ---------- Injected base styles ----------
26
- const injectBaseStyles = () => {
27
- if (d.getElementById('df-style')) return;
28
- const style = d.createElement('style');
29
- style.id = 'df-style';
30
- style.textContent = `
31
- .df-skeleton {
32
- position: absolute;
33
- inset: 0;
34
- background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
35
- background-size: 200% 100%;
36
- animation: df-shimmer 1.5s ease-in-out infinite;
37
- border-radius: inherit;
38
- pointer-events: none;
39
- box-sizing: border-box;
40
- }
41
- @keyframes df-shimmer {
42
- 0% {
43
- background-position: -200% 0;
44
+ function cfg(el) {
45
+ var attr = null;
46
+ for (var i = 0; i < METHOD_ATTRS.length; i++) {
47
+ if (el.hasAttribute(METHOD_ATTRS[i])) { attr = METHOD_ATTRS[i]; break; }
48
+ }
49
+ if (!attr) return null;
50
+ var method = attr.slice(1);
51
+ return {
52
+ url: el.getAttribute(attr),
53
+ method: method.toUpperCase(),
54
+ hasBody: !!HAS_BODY[method],
55
+ target: el.getAttribute('_target'),
56
+ trigger: el.getAttribute('_trigger'),
57
+ form: el.getAttribute('_form'),
58
+ json: el.getAttribute('_json'),
59
+ skeleton: el.getAttribute('_skeleton') !== 'false',
60
+ retry: el.getAttribute('_retry') !== 'false'
61
+ };
44
62
  }
45
- 100% {
46
- background-position: 200% 0;
63
+
64
+ function resolveTarget(el, c) {
65
+ if (!c.target || c.target === 'this') return el;
66
+ var cached = targetCache.get(el);
67
+ if (cached && cached.isConnected) return cached;
68
+ var found = d.querySelector(c.target);
69
+ if (found) targetCache.set(el, found);
70
+ return found;
47
71
  }
48
- }
49
72
 
50
- .df-retry {
51
- display:inline-block; padding:8px 16px; background:#007bff; color:#fff;
52
- border-radius:4px; cursor:pointer; text-decoration:none; font-size:14px;
53
- }
54
- .df-retry:hover { background:#0056b3; }
55
- `;
56
- head.appendChild(style);
57
- };
58
- injectBaseStyles();
73
+ function triggersFor(c) {
74
+ return (c.trigger || (c.target === 'this' ? 'load' : 'click'))
75
+ .split(',').map(function (s) { return s.trim().toLowerCase(); });
76
+ }
59
77
 
60
- // ---------- Pre‑built templates (cloned on use) ----------
61
- const skeletonTemplate = d.createElement('template');
62
- skeletonTemplate.innerHTML = `
63
- <div class="df-skeleton"></div>`;
64
- const retryTemplate = d.createElement('template');
65
- retryTemplate.innerHTML = `<span class="df-retry">Retry</span>`;
78
+ function isActuallyVisible(el) {
79
+ var s = getComputedStyle(el);
80
+ if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
81
+ var r = el.getBoundingClientRect();
82
+ return r.width > 0 && r.height > 0;
83
+ }
66
84
 
67
- // ---------- Resource helpers ----------
68
- const parseHeaderList = (str) => {
69
- if (!str) return [];
70
- return str.replace(/^\[|\]$/g, '').split(',')
71
- .map(s => s.trim().replace(/^['"]|['"]$/g, ''))
72
- .filter(Boolean);
73
- };
85
+ var SKELETON_ICON =
86
+ '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" ' +
87
+ 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
88
+ 'stroke-linejoin="round" aria-hidden="true">' +
89
+ '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>' +
90
+ '</svg>';
74
91
 
75
- const injectResources = (resources, type) => {
76
- resources.forEach(url => {
77
- if (resourceCache.has(url)) return;
78
- resourceCache.add(url);
79
- if (type === 'css') {
80
- const link = d.createElement('link');
81
- link.rel = 'stylesheet'; link.href = url;
82
- head.appendChild(link);
83
- } else if (type === 'js') {
84
- const script = d.createElement('script');
85
- script.src = url;
86
- body.appendChild(script);
87
- }
88
- });
89
- };
92
+ function showSkeleton(el) {
93
+ el.innerHTML = '<div class="aj-skel-wrap">' + SKELETON_ICON + '</div>';
94
+ }
90
95
 
91
- // ---------- Visibility helpers ----------
92
- const isElementActuallyVisible = (el) => {
93
- const style = getComputedStyle(el);
94
- if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
95
- const rect = el.getBoundingClientRect();
96
- return rect.width > 0 && rect.height > 0;
97
- };
96
+ var RETRY_ICON =
97
+ '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" ' +
98
+ 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
99
+ 'stroke-linejoin="round" aria-hidden="true">' +
100
+ '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>' +
101
+ '</svg>';
98
102
 
99
- // Shared IntersectionObserver
100
- let intersectionObserver = null;
101
- const getIO = () => {
102
- if (!intersectionObserver) {
103
- intersectionObserver = new IntersectionObserver((entries) => {
104
- entries.forEach(entry => {
105
- const el = entry.target;
106
- const visible = entry.isIntersecting && isElementActuallyVisible(el);
107
- if (visible && el.dataset.wasVisible !== 'true') {
108
- run(el);
109
- }
110
- el.dataset.wasVisible = visible ? 'true' : 'false';
111
- });
112
- }, { threshold: 0 });
113
- }
114
- return intersectionObserver;
115
- };
103
+ function showRetry(el, fn) {
104
+ el.innerHTML = '';
105
+ var wrap = d.createElement('div');
106
+ wrap.className = 'aj-retry-wrap';
107
+ var b = d.createElement('button');
108
+ b.type = 'button';
109
+ b.className = 'aj-retry';
110
+ b.setAttribute('aria-label', 'Retry');
111
+ b.innerHTML = RETRY_ICON;
112
+ b.addEventListener('click', fn, { once: true });
113
+ wrap.appendChild(b);
114
+ el.appendChild(wrap);
115
+ }
116
116
 
117
- // ---------- Core run function ----------
118
- const run = async (el) => {
119
- if (el.dataset.running === 'true') return;
120
- el.dataset.running = 'true';
117
+ function parseList(raw) {
118
+ if (!raw) return [];
119
+ return raw.replace(/^\[|\]$/g, '').split(',')
120
+ .map(function (s) { return s.trim().replace(/^['"]|['"]$/g, ''); })
121
+ .filter(Boolean);
122
+ }
121
123
 
122
- const post = el.hasAttribute('_post');
123
- const url = el.getAttribute('_post') || el.getAttribute('_get');
124
- let bodyData, headers = {};
124
+ function injectResources(headers) {
125
+ parseList(headers.get('x-css-required')).forEach(function (url) {
126
+ if (injected.has(url)) return;
127
+ injected.add(url);
128
+ var link = d.createElement('link');
129
+ link.rel = 'stylesheet'; link.href = url;
130
+ head.appendChild(link);
131
+ });
132
+ parseList(headers.get('x-js-required')).forEach(function (url) {
133
+ if (injected.has(url)) return;
134
+ injected.add(url);
135
+ var s = d.createElement('script');
136
+ s.src = url;
137
+ head.appendChild(s);
138
+ });
139
+ }
125
140
 
126
- if (post) {
127
- const formId = el.getAttribute('_form');
128
- const json = el.getAttribute('_json');
129
- if (formId) {
130
- const form = d.getElementById(formId);
131
- if (form) bodyData = new URLSearchParams(new FormData(form));
132
- } else if (json) {
133
- bodyData = JSON.stringify(Object.fromEntries(
134
- json.split(',').map(name => {
135
- const input = d.querySelector(`[name="${name}"]`);
136
- return [name, input ? input.value : ''];
137
- })
138
- ));
139
- headers['Content-Type'] = 'application/json';
141
+ function execScripts(container) {
142
+ var scripts = Array.prototype.slice.call(container.querySelectorAll('script'));
143
+ var i = 0;
144
+ function next() {
145
+ if (i >= scripts.length) return;
146
+ var old = scripts[i];
147
+ var s = d.createElement('script');
148
+ for (var a = 0; a < old.attributes.length; a++) {
149
+ s.setAttribute(old.attributes[a].name, old.attributes[a].value);
150
+ }
151
+ if (old.nonce) s.nonce = old.nonce;
152
+ if (old.src) {
153
+ if (executedSrc.has(old.src)) { old.remove(); i++; next(); return; }
154
+ executedSrc.add(old.src);
155
+ s.onload = s.onerror = function () { i++; next(); };
156
+ old.replaceWith(s);
157
+ } else {
158
+ s.textContent = '{\n' + old.textContent + '\n}';
159
+ old.replaceWith(s);
160
+ i++; next();
140
161
  }
141
162
  }
163
+ next();
164
+ }
142
165
 
143
- // Resolve target (cached)
144
- const targetSelector = el.getAttribute('_target');
145
- let container = targetCache.get(el);
146
- if (!container) {
147
- container = targetSelector === 'this' ? el : d.querySelector(targetSelector);
148
- if (container) targetCache.set(el, container);
149
- }
150
- if (!container) {
151
- console.warn('Target not found:', targetSelector);
152
- el.dataset.running = 'false';
153
- return;
154
- }
155
-
156
- container.replaceChildren(skeletonTemplate.content.cloneNode(true));
157
-
158
- try {
159
- const res = await fetch(url, { method: post ? 'POST' : 'GET', body: bodyData, headers });
166
+ function setContent(el, html) {
167
+ el.innerHTML = html;
168
+ execScripts(el);
169
+ bind(el);
170
+ }
160
171
 
161
- // Inject resources from headers
162
- const css = res.headers.get('x-css-required') || res.headers.get('x-css-requiered');
163
- const js = res.headers.get('x-js-required');
164
- if (css) injectResources(parseHeaderList(css), 'css');
165
- if (js) injectResources(parseHeaderList(js), 'js');
172
+ function doFetch(el, c) {
173
+ if (running.has(el)) return;
174
+ running.add(el);
166
175
 
167
- const text = await res.text();
168
- container.innerHTML = text; // MutationObserver picks up new declarative elements
169
- } catch (err) {
170
- console.error(err);
171
- // Show retry (clone template)
172
- const retry = retryTemplate.content.firstElementChild.cloneNode(true);
173
- retry.addEventListener('click', (e) => {
174
- e.preventDefault();
175
- e.stopPropagation();
176
- run(el);
177
- });
178
- container.replaceChildren(retry);
179
- } finally {
180
- el.dataset.running = 'false';
181
- }
182
- };
176
+ var target = resolveTarget(el, c);
177
+ if (!target) { running.delete(el); return; }
178
+ if (c.skeleton) showSkeleton(target);
183
179
 
184
- // ---------- Trigger parsing and caching ----------
185
- const getTriggerEvents = (el) => {
186
- let events = triggerCache.get(el);
187
- if (events) return events;
188
- events = new Set();
189
- const attr = el.getAttribute('_trigger');
190
- if (attr && attr.trim() !== '') {
191
- attr.split(',').forEach(s => {
192
- const evt = s.trim().toLowerCase();
193
- if (VALID_EVENTS.has(evt)) events.add(evt);
194
- });
195
- } else {
196
- // Default: click (plus load if _target="this")
197
- events.add('click');
198
- if (el.getAttribute('_target') === 'this') events.add('load');
180
+ var opts = { method: c.method };
181
+ if (c.hasBody) {
182
+ if (c.json) {
183
+ var obj = {};
184
+ c.json.split(',').forEach(function (name) {
185
+ name = name.trim();
186
+ var input = d.querySelector('[name="' + name + '"]');
187
+ obj[name] = input ? input.value : undefined;
188
+ });
189
+ opts.headers = { 'Content-Type': 'application/json' };
190
+ opts.body = JSON.stringify(obj);
191
+ } else if (c.form) {
192
+ var form = d.getElementById(c.form);
193
+ opts.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
194
+ opts.body = form ? new URLSearchParams(new FormData(form)).toString() : '';
195
+ }
199
196
  }
200
- triggerCache.set(el, events);
201
- return events;
202
- };
203
-
204
- // ---------- Element initialisation (no per‑element listeners) ----------
205
- const initElement = (el) => {
206
- if (el.dataset.initialized === 'true') return;
207
- el.dataset.initialized = 'true';
208
197
 
209
- const events = getTriggerEvents(el);
198
+ fetch(c.url, opts)
199
+ .then(function (res) {
200
+ if (!res.ok) throw new Error('HTTP ' + res.status);
201
+ injectResources(res.headers);
202
+ return res.text();
203
+ })
204
+ .then(function (html) { setContent(target, html); })
205
+ .catch(function (err) {
206
+ console.error(err);
207
+ if (c.retry) showRetry(target, function () { doFetch(el, c); });
208
+ })
209
+ .finally(function () { running.delete(el); });
210
+ }
210
211
 
211
- // Handle load / ready immediately
212
- if (events.has('load') || events.has('domcontentloaded') || events.has('ready')) {
213
- run(el);
214
- }
215
- // Set up visibility observation
216
- if (events.has('visible') || events.has('intersect')) {
217
- el.dataset.wasVisible = 'false';
218
- getIO().observe(el);
219
- }
220
- // All other events are handled by the global delegated listener
221
- };
212
+ var delegationInit = false;
213
+ function initDelegation() {
214
+ if (delegationInit) return;
215
+ delegationInit = true;
216
+ DELEGATABLE.forEach(function (evt) {
217
+ var isClickOrSubmit = evt === 'click' || evt === 'submit';
218
+ d.addEventListener(evt, function (e) {
219
+ var t = e.target;
220
+ if (!(t instanceof Element)) return;
221
+ var el = t.closest(SEL);
222
+ if (!el) return;
223
+ var c = cfg(el);
224
+ if (!c || triggersFor(c).indexOf(evt) === -1) return;
225
+ if (isClickOrSubmit) e.preventDefault();
226
+ doFetch(el, c);
227
+ }, isClickOrSubmit ? false : { passive: true });
228
+ });
229
+ }
222
230
 
223
- // ---------- Global event delegation ----------
224
- const delegatedEvents = new Set([
225
- 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
226
- 'mouseenter', 'mouseleave', 'mousemove', 'contextmenu',
227
- 'focus', 'blur', 'focusin', 'focusout',
228
- 'keydown', 'keyup', 'keypress',
229
- 'change', 'input', 'submit', 'reset',
230
- 'scroll', 'resize', 'wheel', 'touchstart', 'touchend', 'touchmove'
231
- ]);
231
+ function bindOne(el) {
232
+ if (bound.has(el)) return;
233
+ bound.add(el);
234
+ var c = cfg(el);
235
+ if (!c) return;
232
236
 
233
- // Attach one listener per event type, passive for scroll‑like events
234
- delegatedEvents.forEach(evt => {
235
- const isPassive = ['scroll', 'touchstart', 'touchmove', 'touchend', 'wheel'].includes(evt);
236
- d.addEventListener(evt, (e) => {
237
- const target = e.target;
238
- if (!(target instanceof Element)) return;
239
- const el = target.closest('[_get], [_post]');
240
- if (!el) return;
241
- const events = getTriggerEvents(el);
242
- if (events.has(evt)) {
243
- if (evt === 'click') e.preventDefault();
244
- run(el);
237
+ triggersFor(c).forEach(function (trig) {
238
+ if (trig === 'load' || trig === 'domcontentloaded' || trig === 'ready') {
239
+ queueMicrotask(function () { doFetch(el, c); });
240
+ } else if (trig === 'visible' || trig === 'intersect') {
241
+ wasVisible.set(el, false);
242
+ var obs = new IntersectionObserver(function (entries) {
243
+ entries.forEach(function (entry) {
244
+ if (!entry.target.isConnected) { obs.disconnect(); ioObservers.delete(el); return; }
245
+ var vis = entry.isIntersecting && isActuallyVisible(el);
246
+ if (vis && !wasVisible.get(el)) doFetch(el, c);
247
+ wasVisible.set(el, vis);
248
+ });
249
+ });
250
+ obs.observe(el);
251
+ ioObservers.set(el, obs);
252
+ } else if (!DELEGATABLE.has(trig)) {
253
+ el.addEventListener(trig, function () { doFetch(el, c); });
245
254
  }
246
- }, isPassive ? { passive: true } : false);
247
- });
255
+ });
256
+ }
248
257
 
249
- // ---------- MutationObserver (batch processing) ----------
250
- let mutationQueue = [];
251
- let mutationScheduled = false;
258
+ function bind(subtreeRoot) {
259
+ if (subtreeRoot.nodeType !== 1) return;
260
+ if (subtreeRoot.matches && subtreeRoot.matches(SEL)) bindOne(subtreeRoot);
261
+ if (subtreeRoot.querySelectorAll) subtreeRoot.querySelectorAll(SEL).forEach(bindOne);
262
+ }
252
263
 
253
- const processMutations = () => {
254
- mutationScheduled = false;
255
- const nodes = mutationQueue;
256
- mutationQueue = [];
257
- nodes.forEach(node => {
258
- if (node.nodeType !== 1) return;
259
- if (node.matches('[_get], [_post]')) initElement(node);
260
- node.querySelectorAll('[_get], [_post]').forEach(initElement);
264
+ function cleanup(removedRoot) {
265
+ if (!ioObservers.size) return;
266
+ ioObservers.forEach(function (obs, el) {
267
+ if (removedRoot === el || removedRoot.contains(el)) {
268
+ obs.disconnect();
269
+ ioObservers.delete(el);
270
+ bound.delete(el);
271
+ }
261
272
  });
262
- };
273
+ }
263
274
 
264
- const mo = new MutationObserver((mutations) => {
265
- mutations.forEach(m => {
266
- m.addedNodes.forEach(node => {
267
- if (node.nodeType === 1) mutationQueue.push(node);
268
- });
269
- });
270
- if (!mutationScheduled) {
271
- mutationScheduled = true;
272
- Promise.resolve().then(processMutations);
273
- }
274
- });
275
- mo.observe(body, { childList: true, subtree: true });
275
+ var addQueue = new Set(), removeQueue = new Set(), flushScheduled = false;
276
+ function flush() {
277
+ flushScheduled = false;
278
+ var adds = Array.from(addQueue); addQueue.clear();
279
+ var rems = Array.from(removeQueue); removeQueue.clear();
280
+ adds.forEach(bind);
281
+ rems.forEach(cleanup);
282
+ }
283
+ function scheduleFlush() {
284
+ if (flushScheduled) return;
285
+ flushScheduled = true;
286
+ queueMicrotask(flush);
287
+ }
276
288
 
277
- // ---------- Initialisation on DOM ready ----------
278
- const initAll = () => {
279
- d.querySelectorAll('[_get], [_post]').forEach(initElement);
280
- };
289
+ new MutationObserver(function (mutations) {
290
+ mutations.forEach(function (m) {
291
+ m.addedNodes.forEach(function (n) { if (n.nodeType === 1) addQueue.add(n); });
292
+ m.removedNodes.forEach(function (n) { if (n.nodeType === 1) removeQueue.add(n); });
293
+ });
294
+ scheduleFlush();
295
+ }).observe(root, { childList: true, subtree: true });
281
296
 
297
+ function initAll() {
298
+ initDelegation();
299
+ bind(root);
300
+ }
282
301
  if (d.readyState === 'loading') {
283
302
  d.addEventListener('DOMContentLoaded', initAll, { once: true });
284
303
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lengkapp/edge",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Edge framework used by Lengkapp",
5
5
  "main": "edge-server.js",
6
6
  "types": "./edge-server.d.ts",