@supersoniks/concorde 4.9.7 → 4.9.91
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/build-infos.json +1 -1
- package/concorde-core.bundle.js +265 -265
- package/concorde-core.es.js +4600 -4385
- package/dist/concorde-core.bundle.js +265 -265
- package/dist/concorde-core.es.js +4600 -4385
- package/dist/js/core/components/functional/queue/queue.js +25 -153
- package/dist/js/core/components/functional/queue/queue.js.map +1 -1
- package/dist/js/core/components/ui/_css/scroll.js +2 -0
- package/dist/js/core/components/ui/_css/scroll.js.map +1 -1
- package/dist/js/core/decorators/api.js +92 -2
- package/dist/js/core/decorators/api.js.map +1 -1
- package/dist/js/core/utils/SafariScrollGuard.js +384 -0
- package/dist/js/core/utils/SafariScrollGuard.js.map +1 -0
- package/dist/js/decorators.js +4 -0
- package/dist/js/decorators.js.map +1 -1
- package/dist/js/utils.js +3 -0
- package/dist/js/utils.js.map +1 -1
- package/dist/types/core/components/functional/queue/queue.d.ts +8 -24
- package/dist/types/core/components/functional/queue/queue.d.ts.map +1 -1
- package/dist/types/core/components/ui/_css/scroll.d.ts.map +1 -1
- package/dist/types/core/decorators/api.d.ts +82 -60
- package/dist/types/core/decorators/api.d.ts.map +1 -1
- package/dist/types/core/utils/SafariScrollGuard.d.ts +36 -0
- package/dist/types/core/utils/SafariScrollGuard.d.ts.map +1 -0
- package/dist/types/decorators.d.ts +3 -1
- package/dist/types/decorators.d.ts.map +1 -1
- package/dist/types/utils.d.ts +1 -0
- package/dist/types/utils.d.ts.map +1 -1
- package/docs/assets/index-BL6WVF7g.css +1 -0
- package/docs/assets/{index-hElUgV9_.js → index-DVn8AeyA.js} +572 -555
- package/docs/crawl/docs/_decorators/get.html +64 -81
- package/docs/crawl/docs/_decorators/patch.html +35 -23
- package/docs/crawl/docs/_decorators/post.html +72 -72
- package/docs/crawl/docs/_decorators/put.html +36 -21
- package/docs/crawl/docs/_getting-started/ai-agents.html +45 -6
- package/docs/crawl/hubs/decorators.html +141 -69
- package/docs/crawl/hubs/remote-data.html +141 -69
- package/docs/index.html +2 -2
- package/docs/llms-full.txt +203 -149
- package/docs/llms.txt +4 -4
- package/docs/src/core/components/functional/queue/queue.md +0 -0
- package/docs/src/core/components/ui/button/button.md +0 -0
- package/docs/src/docs/_decorators/get.md +55 -81
- package/docs/src/docs/_decorators/patch.md +36 -25
- package/docs/src/docs/_decorators/post.md +71 -62
- package/docs/src/docs/_decorators/put.md +37 -24
- package/docs/src/docs/_getting-started/ai-agents.md +24 -4
- package/docs/src/docs/search/docs-search.json +151 -1
- package/docs/src/tsconfig.json +3 -0
- package/package.json +6 -2
- package/src/core/components/functional/queue/queue.ts +33 -164
- package/src/core/components/ui/_css/scroll.ts +3 -0
- package/src/core/decorators/api.post.spec.ts +79 -1
- package/src/core/decorators/api.spec.ts +100 -1
- package/src/core/decorators/api.ts +252 -138
- package/src/core/utils/SafariScrollGuard.ts +429 -0
- package/src/decorators.ts +7 -0
- package/src/docs/_decorators/get.md +81 -80
- package/src/docs/_decorators/patch.md +36 -25
- package/src/docs/_decorators/post.md +99 -62
- package/src/docs/_decorators/put.md +37 -24
- package/src/docs/components/docs-demo-sources.ts +1 -6
- package/src/docs/example/decorators-demo-geo.ts +0 -21
- package/src/docs/example/decorators-demo-subscribe-publish-get-demos.ts +41 -132
- package/src/docs/search/docs-search.json +156 -241
- package/src/docs/tailwind/css/tailwind.css +0 -0
- package/src/tsconfig.json +3 -0
- package/src/tsconfig.tsbuildinfo +1 -1
- package/src/utils.ts +13 -0
- package/vite.config.mts +4 -0
- package/vitest.setup.ts +22 -0
- package/docs/assets/index-Bnp7gVaA.css +0 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garde Safari / WebKit : empêche les resets fantômes de scroll.
|
|
3
|
+
*
|
|
4
|
+
* - stock = dernière position user (intent + settle inertie)
|
|
5
|
+
* - jump brutal vers ~0 → JAMAIS capturé (même mid-gesture), restore
|
|
6
|
+
* - collapse Lit / slot light-DOM : observe aussi le host + sticky restore multi-frames
|
|
7
|
+
* - nested : intent uniquement sur le scroller le plus proche (composedPath)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ScrollRoot = HTMLElement | Window;
|
|
11
|
+
|
|
12
|
+
const SCROLL_SETTLE_MS = 150;
|
|
13
|
+
const RESTORE_EPS = 10;
|
|
14
|
+
/** Jump vers le haut considéré fantôme (pas une remontée progressive trackpad). */
|
|
15
|
+
const JUMP_EPS = 200;
|
|
16
|
+
/** Combien de frames on re-applique le stock après un fantôme / mutation. */
|
|
17
|
+
const STICKY_RESTORE_FRAMES = 16;
|
|
18
|
+
|
|
19
|
+
type RootState = {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
restoring: boolean;
|
|
23
|
+
watching: boolean;
|
|
24
|
+
userEngaged: boolean;
|
|
25
|
+
settleTimer?: ReturnType<typeof setTimeout>;
|
|
26
|
+
raf: number;
|
|
27
|
+
stickyFrames: number;
|
|
28
|
+
resizeObserver?: ResizeObserver;
|
|
29
|
+
mutationObserver?: MutationObserver;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const states = new WeakMap<object, RootState>();
|
|
33
|
+
let installed = false;
|
|
34
|
+
|
|
35
|
+
export function isSafariScrollEngine(): boolean {
|
|
36
|
+
if (typeof navigator === "undefined") return false;
|
|
37
|
+
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function read(root: ScrollRoot): { x: number; y: number } {
|
|
41
|
+
if (root === window) {
|
|
42
|
+
return { x: window.scrollX, y: window.scrollY };
|
|
43
|
+
}
|
|
44
|
+
const el = root as HTMLElement;
|
|
45
|
+
return { x: el.scrollLeft, y: el.scrollTop };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function write(root: ScrollRoot, x: number, y: number): void {
|
|
49
|
+
if (root === window) {
|
|
50
|
+
window.scrollTo(x, y);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const el = root as HTMLElement;
|
|
54
|
+
// scrollTo coupe mieux l’inertie iOS que le setter seul
|
|
55
|
+
if (typeof el.scrollTo === "function") {
|
|
56
|
+
el.scrollTo(x, y);
|
|
57
|
+
} else {
|
|
58
|
+
el.scrollLeft = x;
|
|
59
|
+
el.scrollTop = y;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function maxScrollY(root: ScrollRoot): number {
|
|
64
|
+
if (root === window) {
|
|
65
|
+
const doc = document.documentElement;
|
|
66
|
+
return Math.max(0, doc.scrollHeight - window.innerHeight);
|
|
67
|
+
}
|
|
68
|
+
const el = root as HTMLElement;
|
|
69
|
+
return Math.max(0, el.scrollHeight - el.clientHeight);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getState(root: ScrollRoot): RootState {
|
|
73
|
+
let s = states.get(root);
|
|
74
|
+
if (!s) {
|
|
75
|
+
const pos = read(root);
|
|
76
|
+
s = {
|
|
77
|
+
x: pos.x,
|
|
78
|
+
y: pos.y,
|
|
79
|
+
restoring: false,
|
|
80
|
+
watching: false,
|
|
81
|
+
userEngaged: false,
|
|
82
|
+
raf: 0,
|
|
83
|
+
stickyFrames: 0,
|
|
84
|
+
};
|
|
85
|
+
states.set(root, s);
|
|
86
|
+
}
|
|
87
|
+
return s;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function markUserEngaged(root: ScrollRoot): void {
|
|
91
|
+
getState(root).userEngaged = true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function armScrollSettle(root: ScrollRoot): void {
|
|
95
|
+
const s = getState(root);
|
|
96
|
+
clearTimeout(s.settleTimer);
|
|
97
|
+
s.settleTimer = setTimeout(() => {
|
|
98
|
+
s.userEngaged = false;
|
|
99
|
+
s.settleTimer = undefined;
|
|
100
|
+
}, SCROLL_SETTLE_MS);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Saut brutal vers le haut (pas seulement ~0) : Safari clampe souvent à une
|
|
105
|
+
* valeur intermédiaire (ex. 1716→225). Un `cap` sans `rst` = on avait accepté
|
|
106
|
+
* ce drop comme scroll user.
|
|
107
|
+
*/
|
|
108
|
+
function looksLikePhantomDrop(currentY: number, storedY: number): boolean {
|
|
109
|
+
return storedY - currentY >= JUMP_EPS;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Mémorise la position courante — refuse tout gros drop d’un coup.
|
|
114
|
+
*/
|
|
115
|
+
export function captureScrollRoot(root: ScrollRoot): void {
|
|
116
|
+
if (!isSafariScrollEngine()) return;
|
|
117
|
+
const s = getState(root);
|
|
118
|
+
if (s.restoring) return;
|
|
119
|
+
const pos = read(root);
|
|
120
|
+
if (looksLikePhantomDrop(pos.y, s.y)) {
|
|
121
|
+
stickyRestore(root);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
s.x = pos.x;
|
|
125
|
+
s.y = pos.y;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Ré-applique le stock. Ne pollue jamais le stock avec le scroll courant.
|
|
130
|
+
*
|
|
131
|
+
* Important : on peut clamper la *cible d’écriture* à maxScroll (le navigateur
|
|
132
|
+
* le ferait de toute façon), mais on ne doit JAMAIS abaisser `s.y` pour autant —
|
|
133
|
+
* pendant un re-render Lit maxScroll plonge souvent un instant (ex. 215→75) puis
|
|
134
|
+
* revient ; baisser le stock là, c’est perdre la position pour de bon.
|
|
135
|
+
*/
|
|
136
|
+
export function restoreScrollRoot(root: ScrollRoot): void {
|
|
137
|
+
if (!isSafariScrollEngine()) return;
|
|
138
|
+
const s = getState(root);
|
|
139
|
+
const maxY = maxScrollY(root);
|
|
140
|
+
const targetX = s.x;
|
|
141
|
+
// maxY===0 : collapse total transitoire → tenter quand même s.y (no-op clamp)
|
|
142
|
+
const targetY = maxY > 0 ? Math.min(s.y, maxY) : s.y;
|
|
143
|
+
|
|
144
|
+
const { x, y } = read(root);
|
|
145
|
+
if (
|
|
146
|
+
Math.abs(targetX - x) <= RESTORE_EPS &&
|
|
147
|
+
Math.abs(targetY - y) <= RESTORE_EPS
|
|
148
|
+
) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
s.restoring = true;
|
|
153
|
+
try {
|
|
154
|
+
write(root, targetX, targetY);
|
|
155
|
+
} finally {
|
|
156
|
+
requestAnimationFrame(() => {
|
|
157
|
+
s.restoring = false;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** @deprecated alias */
|
|
163
|
+
export function syncScrollRoot(root: ScrollRoot): void {
|
|
164
|
+
restoreScrollRoot(root);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function resetScrollRoot(
|
|
168
|
+
root: ScrollRoot,
|
|
169
|
+
position: { x?: number; y?: number } | "current" = { x: 0, y: 0 },
|
|
170
|
+
): void {
|
|
171
|
+
if (!isSafariScrollEngine()) return;
|
|
172
|
+
const s = getState(root);
|
|
173
|
+
if (position === "current") {
|
|
174
|
+
const pos = read(root);
|
|
175
|
+
s.x = pos.x;
|
|
176
|
+
s.y = pos.y;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
s.x = position.x ?? 0;
|
|
180
|
+
s.y = position.y ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function stickyRestore(root: ScrollRoot): void {
|
|
184
|
+
const s = getState(root);
|
|
185
|
+
s.stickyFrames = STICKY_RESTORE_FRAMES;
|
|
186
|
+
if (s.raf) return;
|
|
187
|
+
const tick = () => {
|
|
188
|
+
s.raf = 0;
|
|
189
|
+
restoreScrollRoot(root);
|
|
190
|
+
if (s.stickyFrames > 0) {
|
|
191
|
+
s.stickyFrames--;
|
|
192
|
+
s.raf = requestAnimationFrame(tick);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
s.raf = requestAnimationFrame(tick);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function scheduleRestore(root: ScrollRoot): void {
|
|
199
|
+
stickyRestore(root);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Branche écoute intent + scroll + resize (+ mutations).
|
|
204
|
+
* Idempotent.
|
|
205
|
+
*/
|
|
206
|
+
export function watchScrollRoot(root: ScrollRoot): void {
|
|
207
|
+
if (!isSafariScrollEngine()) return;
|
|
208
|
+
const s = getState(root);
|
|
209
|
+
if (s.watching) return;
|
|
210
|
+
s.watching = true;
|
|
211
|
+
|
|
212
|
+
const target: EventTarget = root === window ? window : root;
|
|
213
|
+
|
|
214
|
+
const onScroll = () => {
|
|
215
|
+
if (s.restoring) return;
|
|
216
|
+
const { y } = read(root);
|
|
217
|
+
|
|
218
|
+
// Gros drop d’un coup (y compris vers une valeur intermédiaire) → restore
|
|
219
|
+
if (looksLikePhantomDrop(y, s.y)) {
|
|
220
|
+
stickyRestore(root);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (s.userEngaged) {
|
|
225
|
+
captureScrollRoot(root);
|
|
226
|
+
armScrollSettle(root);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Pas d’intent taggé : adopter la position courante (pas un gros drop).
|
|
231
|
+
captureScrollRoot(root);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const isGestureForThisRoot = (e: Event) =>
|
|
235
|
+
nearestScrollRootFromEvent(e) === root;
|
|
236
|
+
|
|
237
|
+
const onUserIntent = (e: Event) => {
|
|
238
|
+
if (!isGestureForThisRoot(e)) return;
|
|
239
|
+
// Snapshot immédiat avant mutation DOM déclenchée par le clic.
|
|
240
|
+
captureScrollRoot(root);
|
|
241
|
+
markUserEngaged(root);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const onUserIntentEnd = (e: Event) => {
|
|
245
|
+
if (!isGestureForThisRoot(e)) return;
|
|
246
|
+
armScrollSettle(root);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const onWheel = (e: Event) => {
|
|
250
|
+
if (!isGestureForThisRoot(e)) return;
|
|
251
|
+
captureScrollRoot(root);
|
|
252
|
+
markUserEngaged(root);
|
|
253
|
+
armScrollSettle(root);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
target.addEventListener("scroll", onScroll, { passive: true });
|
|
257
|
+
target.addEventListener("touchstart", onUserIntent, { passive: true });
|
|
258
|
+
target.addEventListener("touchend", onUserIntentEnd, { passive: true });
|
|
259
|
+
target.addEventListener("touchcancel", onUserIntentEnd, { passive: true });
|
|
260
|
+
target.addEventListener("wheel", onWheel, { passive: true });
|
|
261
|
+
target.addEventListener("pointerdown", onUserIntent, { passive: true });
|
|
262
|
+
target.addEventListener("pointerup", onUserIntentEnd, { passive: true });
|
|
263
|
+
target.addEventListener("pointercancel", onUserIntentEnd, { passive: true });
|
|
264
|
+
|
|
265
|
+
const pos = read(root);
|
|
266
|
+
s.x = pos.x;
|
|
267
|
+
s.y = pos.y;
|
|
268
|
+
|
|
269
|
+
if (root === window) {
|
|
270
|
+
const ro = new ResizeObserver(() => scheduleRestore(window));
|
|
271
|
+
ro.observe(document.documentElement);
|
|
272
|
+
s.resizeObserver = ro;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const el = root as HTMLElement;
|
|
277
|
+
const ro = new ResizeObserver(() => scheduleRestore(el));
|
|
278
|
+
ro.observe(el);
|
|
279
|
+
for (const child of Array.from(el.children)) {
|
|
280
|
+
ro.observe(child);
|
|
281
|
+
}
|
|
282
|
+
s.resizeObserver = ro;
|
|
283
|
+
|
|
284
|
+
const onDomChange = () => scheduleRestore(el);
|
|
285
|
+
const mo = new MutationObserver((mutations) => {
|
|
286
|
+
for (const mutation of mutations) {
|
|
287
|
+
if (mutation.type !== "childList") continue;
|
|
288
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
289
|
+
if (node instanceof Element) ro.observe(node);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
onDomChange();
|
|
293
|
+
});
|
|
294
|
+
// Shadow children
|
|
295
|
+
mo.observe(el, { childList: true, subtree: true, characterData: true });
|
|
296
|
+
// Light DOM du host (contenu <slot>) — sinon on rate suppressions / updates
|
|
297
|
+
const rootNode = el.getRootNode();
|
|
298
|
+
if (rootNode instanceof ShadowRoot && rootNode.host) {
|
|
299
|
+
mo.observe(rootNode.host, {
|
|
300
|
+
childList: true,
|
|
301
|
+
subtree: true,
|
|
302
|
+
characterData: true,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
s.mutationObserver = mo;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function nearestScrollRootFromEvent(e: Event): ScrollRoot {
|
|
309
|
+
for (const node of e.composedPath()) {
|
|
310
|
+
if (node === window || node === document) continue;
|
|
311
|
+
if (node instanceof HTMLElement && isScrollContainer(node)) {
|
|
312
|
+
return node;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return window;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isScrollContainer(el: Element): boolean {
|
|
319
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
320
|
+
if (el === document.documentElement || el === document.body) return false;
|
|
321
|
+
const oy = getComputedStyle(el).overflowY;
|
|
322
|
+
return oy === "auto" || oy === "scroll" || oy === "overlay";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function discoverFromEvent(e: Event): void {
|
|
326
|
+
for (const node of e.composedPath()) {
|
|
327
|
+
if (node === window || node === document) continue;
|
|
328
|
+
if (node instanceof HTMLElement && isScrollContainer(node)) {
|
|
329
|
+
watchScrollRoot(node);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const engageRoot = nearestScrollRootFromEvent(e);
|
|
334
|
+
watchScrollRoot(engageRoot);
|
|
335
|
+
|
|
336
|
+
if (e.type === "wheel") {
|
|
337
|
+
captureScrollRoot(engageRoot);
|
|
338
|
+
markUserEngaged(engageRoot);
|
|
339
|
+
armScrollSettle(engageRoot);
|
|
340
|
+
} else if (e.type === "touchstart" || e.type === "pointerdown") {
|
|
341
|
+
captureScrollRoot(engageRoot);
|
|
342
|
+
markUserEngaged(engageRoot);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function watchScrollContainersInTree(root: ParentNode): void {
|
|
347
|
+
if (root instanceof Element && root.classList?.contains("custom-scroll")) {
|
|
348
|
+
watchScrollRoot(root as HTMLElement);
|
|
349
|
+
}
|
|
350
|
+
root.querySelectorAll?.(".custom-scroll").forEach((el) => {
|
|
351
|
+
if (el instanceof HTMLElement) watchScrollRoot(el);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function observeShadowRoot(shadow: ShadowRoot): void {
|
|
356
|
+
watchScrollContainersInTree(shadow);
|
|
357
|
+
const mo = new MutationObserver((mutations) => {
|
|
358
|
+
for (const mutation of mutations) {
|
|
359
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
360
|
+
if (node instanceof Element) watchScrollContainersInTree(node);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
mo.observe(shadow, { childList: true, subtree: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function walkExistingShadows(node: ParentNode): void {
|
|
368
|
+
node.querySelectorAll?.("*").forEach((el) => {
|
|
369
|
+
if (el.shadowRoot) {
|
|
370
|
+
observeShadowRoot(el.shadowRoot);
|
|
371
|
+
walkExistingShadows(el.shadowRoot);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function patchAttachShadow(): void {
|
|
377
|
+
const proto = Element.prototype as Element & {
|
|
378
|
+
__safariScrollGuardPatched?: boolean;
|
|
379
|
+
};
|
|
380
|
+
if (proto.__safariScrollGuardPatched) return;
|
|
381
|
+
proto.__safariScrollGuardPatched = true;
|
|
382
|
+
const nativeAttachShadow = proto.attachShadow;
|
|
383
|
+
proto.attachShadow = function attachShadowPatched(
|
|
384
|
+
this: Element,
|
|
385
|
+
init: ShadowRootInit,
|
|
386
|
+
): ShadowRoot {
|
|
387
|
+
const shadow = nativeAttachShadow.call(this, init);
|
|
388
|
+
observeShadowRoot(shadow);
|
|
389
|
+
return shadow;
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function installSafariScrollGuard(): void {
|
|
394
|
+
if (typeof window === "undefined" || installed) return;
|
|
395
|
+
installed = true;
|
|
396
|
+
|
|
397
|
+
if (!isSafariScrollEngine()) return;
|
|
398
|
+
|
|
399
|
+
watchScrollRoot(window);
|
|
400
|
+
patchAttachShadow();
|
|
401
|
+
|
|
402
|
+
const opts: AddEventListenerOptions = { capture: true, passive: true };
|
|
403
|
+
window.addEventListener("wheel", discoverFromEvent, opts);
|
|
404
|
+
window.addEventListener("touchstart", discoverFromEvent, opts);
|
|
405
|
+
window.addEventListener("pointerdown", discoverFromEvent, opts);
|
|
406
|
+
|
|
407
|
+
const scan = () => {
|
|
408
|
+
watchScrollContainersInTree(document);
|
|
409
|
+
walkExistingShadows(document);
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
if (document.readyState === "loading") {
|
|
413
|
+
document.addEventListener("DOMContentLoaded", scan, { once: true });
|
|
414
|
+
} else {
|
|
415
|
+
scan();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const mo = new MutationObserver((mutations) => {
|
|
419
|
+
for (const mutation of mutations) {
|
|
420
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
421
|
+
if (!(node instanceof Element)) continue;
|
|
422
|
+
watchScrollContainersInTree(node);
|
|
423
|
+
if (node.shadowRoot) observeShadowRoot(node.shadowRoot);
|
|
424
|
+
walkExistingShadows(node);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
429
|
+
}
|
package/src/decorators.ts
CHANGED
|
@@ -19,6 +19,8 @@ export const get = api.get;
|
|
|
19
19
|
export const post = api.post;
|
|
20
20
|
export const put = api.put;
|
|
21
21
|
export const patch = api.patch;
|
|
22
|
+
export const refetch = api.refetch;
|
|
23
|
+
export const send = api.send;
|
|
22
24
|
export {
|
|
23
25
|
DataProviderKey,
|
|
24
26
|
type DataProviderKeyHost,
|
|
@@ -27,6 +29,9 @@ export { Endpoint };
|
|
|
27
29
|
export type { ApiResult, ApiGetResult } from "./core/utils/api";
|
|
28
30
|
export type {
|
|
29
31
|
ApiSendOptions,
|
|
32
|
+
ApiSendDecoratorHandle,
|
|
33
|
+
ApiMethodActionOptions,
|
|
34
|
+
GetDecoratorHandle,
|
|
30
35
|
GetOptions,
|
|
31
36
|
PostOptions,
|
|
32
37
|
PutOptions,
|
|
@@ -57,4 +62,6 @@ window["concorde-decorator-subscriber"] = {
|
|
|
57
62
|
post: api.post,
|
|
58
63
|
put: api.put,
|
|
59
64
|
patch: api.patch,
|
|
65
|
+
refetch: api.refetch,
|
|
66
|
+
send: api.send,
|
|
60
67
|
};
|
|
@@ -1,123 +1,124 @@
|
|
|
1
1
|
# @get
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`@get` loads data and stores the result in an optional `ApiResult<T>` property.
|
|
4
|
+
The property is `undefined` until a result is available.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
- `payload.result`: typed data;
|
|
7
|
+
- `payload.response`: the HTTP response;
|
|
8
|
+
- `payload.request`: the request that was sent.
|
|
6
9
|
|
|
7
|
-
##
|
|
10
|
+
## Minimal usage
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
```typescript
|
|
13
|
+
const usersEndpoint = new Endpoint<User[]>("users");
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
@get(usersEndpoint)
|
|
16
|
+
@state()
|
|
17
|
+
payload?: ApiResult<User[]>;
|
|
18
|
+
```
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
The first argument is always an
|
|
21
|
+
[`Endpoint<T>`](#docs/_misc/endpoint.md/endpoint). API configuration is read
|
|
22
|
+
from the HTML scope (`serviceURL`, token, etc.), so the component is usually
|
|
23
|
+
placed below an element that provides this configuration.
|
|
15
24
|
|
|
16
|
-
##
|
|
25
|
+
## Refetching a GET
|
|
17
26
|
|
|
18
|
-
|
|
27
|
+
### From the component
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
| `refetchEveryMs` | Automatic polling interval (ms). `0` or omitted = no polling. |
|
|
24
|
-
| `triggerKey` | `DataProviderKey` — `PublisherManager.get(...).invalidate()` on that publisher re-runs the GET. |
|
|
29
|
+
Keep the decorator in a constant to access `refetch(component)`. Refetch only
|
|
30
|
+
targets the instance passed as the argument, even before it has received a
|
|
31
|
+
result.
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
```typescript
|
|
34
|
+
const usersGet = get(new Endpoint<User[]>("users"));
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
@usersGet
|
|
37
|
+
@state()
|
|
38
|
+
payload?: ApiResult<User[]>;
|
|
39
|
+
|
|
40
|
+
refresh() {
|
|
41
|
+
usersGet.refetch(this);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### From a method decorator
|
|
46
|
+
|
|
47
|
+
The refetch can decorate a local method. The default is `after`; if the
|
|
48
|
+
method returns a Promise, the GET waits for its resolution.
|
|
29
49
|
|
|
30
50
|
```typescript
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
const geoCommunesRefreshKey = new DataProviderKey<void>(
|
|
40
|
-
"docsDemoGeoCommunesRefresh",
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
@get(geoCommunesEndpoint, apiConfigurationKey, {
|
|
44
|
-
triggerKey: geoCommunesRefreshKey,
|
|
45
|
-
})
|
|
46
|
-
@state()
|
|
47
|
-
payload: ApiResult<GeoCommuneRow[]> | null = null;
|
|
51
|
+
import { refetch } from "@supersoniks/concorde/decorators";
|
|
52
|
+
|
|
53
|
+
@refetch(usersGet)
|
|
54
|
+
async reload() {
|
|
55
|
+
this.userId = "42";
|
|
56
|
+
}
|
|
57
|
+
```
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
Use `{ when: "before" }` when the GET must be scheduled before the method:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
@refetch(usersGet, { when: "before" })
|
|
63
|
+
reloadBefore() {
|
|
64
|
+
// The refetch is scheduled before this method runs.
|
|
65
|
+
}
|
|
51
66
|
```
|
|
52
67
|
|
|
53
|
-
|
|
68
|
+
The method decorator adds an explicit trigger; changes to dynamic endpoint
|
|
69
|
+
properties and configuration publishers keep their existing automatic behavior.
|
|
70
|
+
|
|
71
|
+
## Options (`GetOptions`)
|
|
72
|
+
|
|
73
|
+
Options are the second argument, or the third argument when an API
|
|
74
|
+
configuration key is provided.
|
|
75
|
+
|
|
76
|
+
- `skipEmptyPlaceholder`: blocks the GET when `${prop}` is `""`.
|
|
77
|
+
- `refetchEveryMs`: automatically runs the GET again after this interval
|
|
78
|
+
(milliseconds). Omit the option, or use `0`, to disable polling.
|
|
54
79
|
|
|
55
|
-
|
|
80
|
+
With a configuration key, use:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
@get(usersEndpoint, apiConfigurationKey)
|
|
84
|
+
```
|
|
56
85
|
|
|
57
86
|
## When the GET runs again
|
|
58
87
|
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
- `refetchEveryMs`
|
|
88
|
+
- when the component connects and configuration is available;
|
|
89
|
+
- when a property used in `${...}` changes;
|
|
90
|
+
- when a configuration publisher changes;
|
|
91
|
+
- after each response when `refetchEveryMs` is set.
|
|
92
|
+
|
|
93
|
+
See [Dynamic path placeholders](#docs/_misc/dynamic-path.md/dynamic-path) for
|
|
94
|
+
dynamic path details.
|
|
63
95
|
|
|
64
96
|
## Import
|
|
65
97
|
|
|
66
98
|
<sonic-code language="typescript">
|
|
67
99
|
<template>
|
|
68
100
|
import { get, type ApiResult } from "@supersoniks/concorde/decorators";
|
|
69
|
-
import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
|
|
70
101
|
import { DataProviderKey } from "@supersoniks/concorde/dataProviderKey";
|
|
102
|
+
import { Endpoint } from "@supersoniks/concorde/utils/endpoint";
|
|
71
103
|
</template>
|
|
72
104
|
</sonic-code>
|
|
73
105
|
|
|
74
|
-
##
|
|
75
|
-
|
|
76
|
-
Same demo service as [`sonic-queue`](../../core/components/functional/queue/queue.demo.ts) (`/docs-mock-api/geo/`). Publisher setup lives in `decorators-demo-geo.ts` and `decorators-demo-subscribe-publish-get-demos.ts`.
|
|
77
|
-
|
|
78
|
-
<sonic-code language="typescript">
|
|
79
|
-
<template>
|
|
80
|
-
@get(new Endpoint<User>("users/${userId}"))
|
|
81
|
-
@state()
|
|
82
|
-
payload: ApiResult<User> | null = null;
|
|
83
|
-
</template>
|
|
84
|
-
</sonic-code>
|
|
85
|
-
|
|
86
|
-
## Live demos
|
|
87
|
-
|
|
88
|
-
`@get` with **`triggerKey`** — refresh button + `X-Fetched-At` timestamp in the UI:
|
|
106
|
+
## Demos
|
|
89
107
|
|
|
90
108
|
<sonic-code>
|
|
91
109
|
<template>
|
|
92
110
|
<docs-demo-sources for="demo-api-get"></docs-demo-sources>
|
|
93
111
|
<demo-api-get></demo-api-get>
|
|
94
|
-
</template>
|
|
95
|
-
</sonic-code>
|
|
96
|
-
|
|
97
|
-
Dynamic config and endpoint path. Manual refetch is triggered **from a separate component** (same `triggerKey` publisher):
|
|
98
|
-
|
|
99
|
-
<sonic-code>
|
|
100
|
-
<template>
|
|
101
112
|
<docs-demo-sources for="demo-api-get-configuration-key"></docs-demo-sources>
|
|
102
113
|
<demo-api-get-configuration-key></demo-api-get-configuration-key>
|
|
103
|
-
<demo-api-get-refresh-remote></demo-api-get-refresh-remote>
|
|
104
|
-
</template>
|
|
105
|
-
</sonic-code>
|
|
106
|
-
|
|
107
|
-
Scoped `@get` with `@publish` / `@subscribe` on the payload (see [@publish](#docs/_decorators/publish.md/publish) and [@subscribe](#docs/_decorators/subscribe.md/subscribe)) — wrap under an ancestor with `serviceURL="/docs-mock-api/geo/"`:
|
|
108
|
-
|
|
109
|
-
<sonic-code>
|
|
110
|
-
<template>
|
|
111
|
-
<div serviceURL="/docs-mock-api/geo/">
|
|
112
|
-
<docs-demo-sources for="demo-api-get-publish-subscribe"></docs-demo-sources>
|
|
113
|
-
<demo-api-get-publish-subscribe></demo-api-get-publish-subscribe>
|
|
114
|
-
</div>
|
|
115
114
|
</template>
|
|
116
115
|
</sonic-code>
|
|
117
116
|
|
|
118
|
-
|
|
117
|
+
See [@post](#docs/_decorators/post.md/post),
|
|
118
|
+
[@put](#docs/_decorators/put.md/put), and
|
|
119
|
+
[@patch](#docs/_decorators/patch.md/patch) for write operations.
|
|
119
120
|
|
|
120
|
-
##
|
|
121
|
+
## Advanced
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
External invalidation is currently available through `triggerKey`; it is kept
|
|
124
|
+
out of the main usage examples.
|