@lengkapp/edge 0.0.3 → 0.0.5
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 +27 -0
- package/README.md +514 -0
- package/edge-client.js +251 -243
- package/edge-server.d.ts +128 -63
- package/edge-server.js +122 -37
- package/package.json +3 -15
- package/client.min.d.ts +0 -3
- package/edge-client.min.js +0 -1
- package/edge-server.min.js +0 -12
- package/readme.md +0 -118
- package/server.min.d.ts +0 -83
package/edge-client.js
CHANGED
|
@@ -1,279 +1,287 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
|
|
2
|
+
'use strict';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const d = document,
|
|
5
|
+
body = d.body,
|
|
6
|
+
head = d.head;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
// ---------- Constants and caches ----------
|
|
9
|
+
const VALID_EVENTS = new Set([
|
|
10
|
+
'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'
|
|
18
|
+
]);
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
24
|
+
|
|
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
|
+
}
|
|
45
|
+
100% {
|
|
46
|
+
background-position: 200% 0;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
24
49
|
|
|
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 { display:flex; flex-direction:column; gap:8px; padding:10px; }
|
|
32
|
-
.df-skeleton .df-bar {
|
|
33
|
-
height:12px; background:linear-gradient(90deg,#e0e0e0 25%,#f0f0f0 50%,#e0e0e0 75%);
|
|
34
|
-
background-size:200% 100%; animation:df-shimmer 1.5s infinite; border-radius:4px;
|
|
35
|
-
}
|
|
36
|
-
@keyframes df-shimmer { 0%{background-position:-200% 0} 100%{background-position:200% 0} }
|
|
37
50
|
.df-retry {
|
|
38
51
|
display:inline-block; padding:8px 16px; background:#007bff; color:#fff;
|
|
39
52
|
border-radius:4px; cursor:pointer; text-decoration:none; font-size:14px;
|
|
40
53
|
}
|
|
41
54
|
.df-retry:hover { background:#0056b3; }
|
|
42
55
|
`;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
head.appendChild(style);
|
|
57
|
+
};
|
|
58
|
+
injectBaseStyles();
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<div class="df-skeleton"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<div class="df-bar"></div>
|
|
54
|
-
</div>`;
|
|
55
|
-
const retryTemplate = d.createElement('template');
|
|
56
|
-
retryTemplate.innerHTML = `<span class="df-retry">Retry</span>`;
|
|
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>`;
|
|
57
66
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
};
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
};
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
};
|
|
89
98
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
};
|
|
107
116
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
// ---------- Core run function ----------
|
|
118
|
+
const run = async (el) => {
|
|
119
|
+
if (el.dataset.running === 'true') return;
|
|
120
|
+
el.dataset.running = 'true';
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
const post = el.hasAttribute('_post');
|
|
123
|
+
const url = el.getAttribute('_post') || el.getAttribute('_get');
|
|
124
|
+
let bodyData, headers = {};
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|
|
146
155
|
|
|
147
|
-
|
|
148
|
-
container.replaceChildren(skeletonTemplate.content.cloneNode(true));
|
|
156
|
+
container.replaceChildren(skeletonTemplate.content.cloneNode(true));
|
|
149
157
|
|
|
150
|
-
|
|
151
|
-
|
|
158
|
+
try {
|
|
159
|
+
const res = await fetch(url, { method: post ? 'POST' : 'GET', body: bodyData, headers });
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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');
|
|
158
166
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
};
|
|
175
183
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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');
|
|
199
|
+
}
|
|
200
|
+
triggerCache.set(el, events);
|
|
201
|
+
return events;
|
|
202
|
+
};
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
// ---------- Element initialisation (no per‑element listeners) ----------
|
|
205
|
+
const initElement = (el) => {
|
|
206
|
+
if (el.dataset.initialized === 'true') return;
|
|
207
|
+
el.dataset.initialized = 'true';
|
|
200
208
|
|
|
201
|
-
|
|
209
|
+
const events = getTriggerEvents(el);
|
|
202
210
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
};
|
|
214
222
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
+
]);
|
|
224
232
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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);
|
|
245
|
+
}
|
|
246
|
+
}, isPassive ? { passive: true } : false);
|
|
247
|
+
});
|
|
240
248
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
// ---------- MutationObserver (batch processing) ----------
|
|
250
|
+
let mutationQueue = [];
|
|
251
|
+
let mutationScheduled = false;
|
|
244
252
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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);
|
|
261
|
+
});
|
|
262
|
+
};
|
|
255
263
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
if (!mutationScheduled) {
|
|
263
|
-
mutationScheduled = true;
|
|
264
|
-
Promise.resolve().then(processMutations);
|
|
265
|
-
}
|
|
264
|
+
const mo = new MutationObserver((mutations) => {
|
|
265
|
+
mutations.forEach(m => {
|
|
266
|
+
m.addedNodes.forEach(node => {
|
|
267
|
+
if (node.nodeType === 1) mutationQueue.push(node);
|
|
266
268
|
});
|
|
267
|
-
|
|
269
|
+
});
|
|
270
|
+
if (!mutationScheduled) {
|
|
271
|
+
mutationScheduled = true;
|
|
272
|
+
Promise.resolve().then(processMutations);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
mo.observe(body, { childList: true, subtree: true });
|
|
268
276
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
// ---------- Initialisation on DOM ready ----------
|
|
278
|
+
const initAll = () => {
|
|
279
|
+
d.querySelectorAll('[_get], [_post]').forEach(initElement);
|
|
280
|
+
};
|
|
273
281
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
282
|
+
if (d.readyState === 'loading') {
|
|
283
|
+
d.addEventListener('DOMContentLoaded', initAll, { once: true });
|
|
284
|
+
} else {
|
|
285
|
+
initAll();
|
|
286
|
+
}
|
|
287
|
+
})();
|