@shamar/wire 0.3.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.
package/client/wire.js ADDED
@@ -0,0 +1,722 @@
1
+ /**
2
+ * @shamar/wire browser runtime.
3
+ *
4
+ * Additive with Alpine. Existing `x-data` islands keep working. A component
5
+ * opts in with `wire:*` and `$wire`. The server returns HTML and this runtime
6
+ * morphs the current island so focus, cursor, and nested Alpine trees survive.
7
+ *
8
+ * Model timing matches Livewire 3:
9
+ * wire:model deferred until the next action
10
+ * wire:model.live send on input
11
+ * wire:model.blur send on blur
12
+ * wire:model.debounce.500ms send after the delay
13
+ */
14
+ (function () {
15
+ const queued = new WeakMap();
16
+ const timers = new WeakMap();
17
+ const generation = new WeakMap();
18
+
19
+ function csrfToken() {
20
+ return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
21
+ }
22
+
23
+ function rootOf(node) {
24
+ return node && node.closest ? node.closest('[wire\\:id]') : null;
25
+ }
26
+
27
+ function snapshotOf(root) {
28
+ const raw = root.getAttribute('wire:snapshot');
29
+ if (!raw) throw new Error('Missing wire snapshot');
30
+ return JSON.parse(raw);
31
+ }
32
+
33
+ function attrNamed(el, prefix) {
34
+ return [...el.attributes].find((attr) => attr.name === prefix || attr.name.startsWith(`${prefix}.`));
35
+ }
36
+
37
+ function parseArgs(raw) {
38
+ if (!raw.trim()) return [];
39
+ return raw.split(',').map((part) => {
40
+ const token = part.trim();
41
+ if (
42
+ (token.startsWith("'") && token.endsWith("'")) ||
43
+ (token.startsWith('"') && token.endsWith('"'))
44
+ ) {
45
+ return token.slice(1, -1);
46
+ }
47
+ if (token === 'true') return true;
48
+ if (token === 'false') return false;
49
+ if (token === 'null') return null;
50
+ if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
51
+ return token;
52
+ });
53
+ }
54
+
55
+ function parseCall(expression) {
56
+ const text = String(expression || '').trim();
57
+ const match = text.match(/^([A-Za-z_][A-Za-z0-9_]*)\((.*)\)$/);
58
+ if (!match) return { method: text, params: [] };
59
+ return { method: match[1], params: parseArgs(match[2]) };
60
+ }
61
+
62
+ function queueUpdate(root, key, value) {
63
+ const bag = queued.get(root) || {};
64
+ bag[key] = value;
65
+ queued.set(root, bag);
66
+ }
67
+
68
+ function takeQueued(root, extra) {
69
+ const updates = { ...(queued.get(root) || {}), ...(extra || {}) };
70
+ queued.delete(root);
71
+ return Object.keys(updates).length ? updates : undefined;
72
+ }
73
+
74
+ function setLoading(root, on) {
75
+ root.classList.toggle('wire-loading', on);
76
+ }
77
+
78
+ function sameElement(from, to) {
79
+ return from.nodeType === 1 && to.nodeType === 1 && from.tagName === to.tagName;
80
+ }
81
+
82
+ function morph(from, to, origin, skipIslands = true) {
83
+ if (from.nodeType === 3 && to.nodeType === 3) {
84
+ if (from.nodeValue !== to.nodeValue) from.nodeValue = to.nodeValue;
85
+ return;
86
+ }
87
+ if (!sameElement(from, to)) {
88
+ from.replaceWith(to.cloneNode(true));
89
+ return;
90
+ }
91
+ if (from.hasAttribute('wire:ignore')) return;
92
+ if (skipIslands && from !== origin && from.hasAttribute('wire:id')) return;
93
+
94
+ const nextNames = new Set([...to.attributes].map((attr) => attr.name));
95
+ for (const attr of [...from.attributes]) {
96
+ if (!nextNames.has(attr.name)) from.removeAttribute(attr.name);
97
+ }
98
+ for (const attr of [...to.attributes]) {
99
+ if (from.getAttribute(attr.name) !== attr.value) from.setAttribute(attr.name, attr.value);
100
+ }
101
+
102
+ if (from instanceof HTMLInputElement || from instanceof HTMLTextAreaElement) {
103
+ const nextValue = 'value' in to ? to.value : '';
104
+ if (document.activeElement !== from && from.value !== nextValue) from.value = nextValue;
105
+ }
106
+ if (from instanceof HTMLSelectElement && to instanceof HTMLSelectElement && document.activeElement !== from) {
107
+ from.value = to.value;
108
+ }
109
+ if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type === 'checkbox') {
110
+ from.checked = to.checked;
111
+ }
112
+
113
+ morphChildren(from, to, origin, skipIslands);
114
+ }
115
+
116
+ function morphChildren(from, to, origin, skipIslands = true) {
117
+ const current = [...from.childNodes];
118
+ const incoming = [...to.childNodes];
119
+ const used = new Set();
120
+ const keyed = new Map();
121
+ for (const node of current) {
122
+ if (node.nodeType === 1 && node.hasAttribute('wire:key')) {
123
+ keyed.set(node.getAttribute('wire:key'), node);
124
+ }
125
+ }
126
+
127
+ const ordered = [];
128
+ for (const target of incoming) {
129
+ let match = null;
130
+ if (target.nodeType === 1 && target.hasAttribute('wire:key')) {
131
+ match = keyed.get(target.getAttribute('wire:key')) || null;
132
+ }
133
+ if (!match) {
134
+ match =
135
+ current.find(
136
+ (node) =>
137
+ !used.has(node) &&
138
+ node.nodeType === target.nodeType &&
139
+ (node.nodeType !== 1 || node.tagName === target.tagName),
140
+ ) || null;
141
+ }
142
+ if (match) {
143
+ used.add(match);
144
+ morph(match, target, origin, skipIslands);
145
+ ordered.push(match);
146
+ } else {
147
+ ordered.push(target.cloneNode(true));
148
+ }
149
+ }
150
+ for (const node of current) {
151
+ if (!used.has(node)) node.remove();
152
+ }
153
+ ordered.forEach((node, index) => {
154
+ if (from.childNodes[index] !== node) from.insertBefore(node, from.childNodes[index] || null);
155
+ });
156
+ if (window.Alpine?.initTree) {
157
+ for (const node of ordered) {
158
+ if (!used.has(node) && node.nodeType === 1 && !node.hasAttribute('wire:persist')) {
159
+ window.Alpine.initTree(node);
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ async function commit(root, payload) {
166
+ const endpoint = root.getAttribute('wire:endpoint');
167
+ if (!endpoint) return;
168
+ const gen = (generation.get(root) || 0) + 1;
169
+ generation.set(root, gen);
170
+ setLoading(root, true);
171
+ const timer = timers.get(root);
172
+ if (timer) clearTimeout(timer);
173
+ let res;
174
+ try {
175
+ res = await fetch(endpoint, {
176
+ method: 'POST',
177
+ credentials: 'same-origin',
178
+ headers: {
179
+ Accept: 'application/json',
180
+ 'Content-Type': 'application/json',
181
+ 'X-CSRF-Token': csrfToken(),
182
+ },
183
+ body: JSON.stringify({
184
+ snapshot: snapshotOf(root),
185
+ updates: takeQueued(root, payload.updates),
186
+ calls: payload.calls,
187
+ }),
188
+ });
189
+ } catch {
190
+ if (generation.get(root) === gen) setLoading(root, false);
191
+ return;
192
+ }
193
+ if (generation.get(root) !== gen) return;
194
+ setLoading(root, false);
195
+ if (!res.ok) return;
196
+ const body = await res.json();
197
+ if (body?.effects?.redirect) {
198
+ prefetchCache.clear();
199
+ followRedirect(body.effects.redirect);
200
+ return;
201
+ }
202
+ prefetchCache.clear();
203
+ if (!body || typeof body.html !== 'string') return;
204
+ const template = document.createElement('template');
205
+ template.innerHTML = body.html.trim();
206
+ const next = template.content.firstElementChild;
207
+ if (!next || next.getAttribute('wire:id') !== root.getAttribute('wire:id')) return;
208
+ morph(root, next, root);
209
+ }
210
+
211
+ function schedule(root, key, value, delay) {
212
+ queueUpdate(root, key, value);
213
+ const previous = timers.get(root);
214
+ if (previous) clearTimeout(previous);
215
+ timers.set(
216
+ root,
217
+ setTimeout(() => {
218
+ timers.delete(root);
219
+ commit(root, {}).catch(() => {});
220
+ }, delay),
221
+ );
222
+ }
223
+
224
+ document.addEventListener('input', (event) => {
225
+ const target = event.target;
226
+ if (!(target instanceof HTMLElement)) return;
227
+ const binding = attrNamed(target, 'wire:model');
228
+ if (!binding) return;
229
+ const root = rootOf(target);
230
+ if (!root) return;
231
+ const value = 'value' in target ? target.value : '';
232
+ const name = binding.name;
233
+ if (name.includes('.blur')) return;
234
+ if (name.includes('.live')) {
235
+ schedule(root, binding.value, value, 0);
236
+ return;
237
+ }
238
+ const debounce = name.match(/\.debounce\.(\d+)ms/);
239
+ if (debounce) {
240
+ schedule(root, binding.value, value, Number(debounce[1]));
241
+ return;
242
+ }
243
+ queueUpdate(root, binding.value, value);
244
+ });
245
+
246
+ document.addEventListener(
247
+ 'blur',
248
+ (event) => {
249
+ const target = event.target;
250
+ if (!(target instanceof HTMLElement)) return;
251
+ const binding = attrNamed(target, 'wire:model');
252
+ if (!binding || !binding.name.includes('.blur')) return;
253
+ const root = rootOf(target);
254
+ if (!root || !('value' in target)) return;
255
+ commit(root, { updates: { [binding.value]: target.value } }).catch(() => {});
256
+ },
257
+ true,
258
+ );
259
+
260
+ document.addEventListener('click', (event) => {
261
+ const target = event.target instanceof Element ? event.target.closest('[wire\\:click]') : null;
262
+ if (!target) return;
263
+ const root = rootOf(target);
264
+ if (!root) return;
265
+ event.preventDefault();
266
+ const call = parseCall(target.getAttribute('wire:click') || '');
267
+ commit(root, { calls: [call] }).catch(() => {});
268
+ });
269
+
270
+ document.addEventListener('submit', (event) => {
271
+ const form = event.target;
272
+ if (!(form instanceof HTMLFormElement)) return;
273
+ const binding = attrNamed(form, 'wire:submit');
274
+ if (!binding) return;
275
+ event.preventDefault();
276
+ const root = rootOf(form);
277
+ if (!root) return;
278
+ commit(root, { calls: [parseCall(binding.value)] }).catch(() => {});
279
+ });
280
+
281
+ document.addEventListener('keydown', (event) => {
282
+ const target = event.target instanceof Element ? event.target : null;
283
+ if (!target) return;
284
+ const host = target.closest('[wire\\:keydown\\.enter], [wire\\:keydown]');
285
+ if (!host) return;
286
+ const enter = host.getAttribute('wire:keydown.enter');
287
+ const any = host.getAttribute('wire:keydown');
288
+ const expression = event.key === 'Enter' ? enter || any : any && !enter ? any : '';
289
+ if (!expression) return;
290
+ if (enter && event.key !== 'Enter') return;
291
+ const root = rootOf(host);
292
+ if (!root) return;
293
+ event.preventDefault();
294
+ commit(root, { calls: [parseCall(expression)] }).catch(() => {});
295
+ });
296
+
297
+ const style = document.createElement('style');
298
+ style.textContent =
299
+ '[wire\\:loading]{display:none !important}.wire-loading [wire\\:loading]{display:revert !important}.wire-loading [wire\\:loading\\.remove]{display:none !important}' +
300
+ '.wire-progress{position:fixed;top:0;left:0;height:2px;width:0;opacity:0;z-index:2147483646;pointer-events:none;background:#f59e0b;transition:width .2s ease,opacity .2s ease}' +
301
+ '.wire-progress.is-active{opacity:1}';
302
+ document.head.appendChild(style);
303
+
304
+ document.addEventListener('change', (event) => {
305
+ const target = event.target;
306
+ if (!(target instanceof HTMLInputElement) || target.type !== 'file') return;
307
+ const binding = attrNamed(target, 'wire:model');
308
+ if (!binding || !target.files?.[0]) return;
309
+ const root = rootOf(target);
310
+ if (!root) return;
311
+ const endpoint = root.getAttribute('wire:endpoint');
312
+ if (!endpoint) return;
313
+ const body = new FormData();
314
+ body.append('file', target.files[0]);
315
+ fetch(`${endpoint.replace(/\/$/, '')}/upload`, {
316
+ method: 'POST',
317
+ credentials: 'same-origin',
318
+ headers: { Accept: 'application/json', 'X-CSRF-Token': csrfToken() },
319
+ body,
320
+ })
321
+ .then((res) => (res.ok ? res.json() : null))
322
+ .then((saved) => {
323
+ if (!saved) return;
324
+ return commit(root, { updates: { [binding.value]: saved } });
325
+ })
326
+ .catch(() => {});
327
+ });
328
+
329
+ const prefetchCache = new Map();
330
+ const prefetchTimers = new WeakMap();
331
+ const ranScripts = new Set();
332
+ for (const existing of document.scripts) {
333
+ if (existing.src || existing.hasAttribute('data-navigate-once')) {
334
+ ranScripts.add(existing.src || existing.textContent || '');
335
+ }
336
+ }
337
+ let navigationId = 0;
338
+ const PREFETCH_TTL = 30000;
339
+
340
+ document.addEventListener('click', (event) => {
341
+ const found = navigateLink(event.target);
342
+ if (!found) return;
343
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return;
344
+ if (found.link.hasAttribute('download')) return;
345
+ const target = found.link.getAttribute('target');
346
+ if (target && target !== '_self') return;
347
+ const url = new URL(found.link.href, window.location.href);
348
+ if (url.origin !== window.location.origin) return;
349
+ event.preventDefault();
350
+ const preserveScroll = found.name.includes('.preserve-scroll');
351
+ if (sameDocument(url)) {
352
+ visitHash(url, true);
353
+ return;
354
+ }
355
+ navigate(url, { push: true, scroll: preserveScroll ? 'preserve' : 'top' }).catch(() => {
356
+ window.location.assign(url);
357
+ });
358
+ });
359
+
360
+ document.addEventListener('pointerenter', (event) => {
361
+ schedulePrefetch(event.target);
362
+ }, true);
363
+
364
+ document.addEventListener('focusin', (event) => {
365
+ schedulePrefetch(event.target);
366
+ });
367
+
368
+ window.addEventListener('popstate', () => {
369
+ const url = new URL(window.location.href);
370
+ const saved = history.state && typeof history.state.wireScroll === 'number' ? history.state.wireScroll : 0;
371
+ navigate(url, { push: false, scroll: 'restore', scrollY: saved }).catch(() => window.location.reload());
372
+ });
373
+
374
+ window.Wire = {
375
+ navigate(href) {
376
+ const url = new URL(href, window.location.href);
377
+ if (url.origin !== window.location.origin) {
378
+ window.location.assign(url);
379
+ return Promise.resolve();
380
+ }
381
+ return navigate(url, { push: true, scroll: 'top' });
382
+ },
383
+ prefetch(href) {
384
+ const url = new URL(href, window.location.href);
385
+ return prefetch(url);
386
+ },
387
+ };
388
+
389
+ function navigateLink(node) {
390
+ const link = node instanceof Element ? node.closest('a') : null;
391
+ if (!(link instanceof HTMLAnchorElement)) return null;
392
+ const attr = [...link.attributes].find(
393
+ (item) => item.name === 'wire:navigate' || item.name.startsWith('wire:navigate.'),
394
+ );
395
+ if (!attr) return null;
396
+ return { link, name: attr.name };
397
+ }
398
+
399
+ function sameDocument(url) {
400
+ return url.pathname === window.location.pathname && url.search === window.location.search;
401
+ }
402
+
403
+ function visitHash(url, push) {
404
+ rememberScroll();
405
+ if (push) history.pushState({ wireScroll: window.scrollY }, '', url);
406
+ const id = decodeURIComponent(url.hash.slice(1));
407
+ const target = id ? document.getElementById(id) : null;
408
+ if (target) target.scrollIntoView();
409
+ }
410
+
411
+ function rememberScroll() {
412
+ const state = history.state && typeof history.state === 'object' ? history.state : {};
413
+ history.replaceState({ ...state, wireScroll: window.scrollY }, '');
414
+ }
415
+
416
+ function schedulePrefetch(node) {
417
+ const found = navigateLink(node);
418
+ if (!found || found.name.includes('.no-prefetch')) return;
419
+ const previous = prefetchTimers.get(found.link);
420
+ if (previous) clearTimeout(previous);
421
+ prefetchTimers.set(
422
+ found.link,
423
+ setTimeout(() => {
424
+ prefetchTimers.delete(found.link);
425
+ const url = new URL(found.link.href, window.location.href);
426
+ if (url.origin !== window.location.origin || sameDocument(url)) return;
427
+ prefetch(url).catch(() => {});
428
+ }, 60),
429
+ );
430
+ }
431
+
432
+ function cacheKey(url) {
433
+ return `${url.origin}${url.pathname}${url.search}`;
434
+ }
435
+
436
+ function prefetch(url) {
437
+ const key = cacheKey(url);
438
+ const hit = prefetchCache.get(key);
439
+ if (hit && Date.now() - hit.time < PREFETCH_TTL) return hit.promise;
440
+ const promise = fetch(url, {
441
+ credentials: 'same-origin',
442
+ headers: {
443
+ Accept: 'text/html',
444
+ 'X-Requested-With': 'XMLHttpRequest',
445
+ 'X-Wire-Prefetch': '1',
446
+ },
447
+ }).then(async (res) => {
448
+ const type = res.headers.get('content-type') || '';
449
+ if (!res.ok || !type.includes('text/html')) return null;
450
+ return { html: await res.text(), url: res.url || url.href };
451
+ });
452
+ prefetchCache.set(key, { promise, time: Date.now() });
453
+ return promise;
454
+ }
455
+
456
+ function followRedirect(value) {
457
+ let url;
458
+ try {
459
+ url = new URL(value, window.location.href);
460
+ } catch {
461
+ window.location.assign(value);
462
+ return;
463
+ }
464
+ if (url.origin !== window.location.origin) {
465
+ window.location.assign(url);
466
+ return;
467
+ }
468
+ navigate(url, { push: true, scroll: 'top' }).catch(() => window.location.assign(url));
469
+ }
470
+
471
+ function startProgress() {
472
+ let bar = document.getElementById('wire-progress');
473
+ if (!bar) {
474
+ bar = document.createElement('div');
475
+ bar.id = 'wire-progress';
476
+ bar.className = 'wire-progress';
477
+ bar.setAttribute('wire:ignore', '');
478
+ document.documentElement.appendChild(bar);
479
+ }
480
+ bar.style.width = '0%';
481
+ const show = setTimeout(() => {
482
+ bar.classList.add('is-active');
483
+ bar.style.width = '35%';
484
+ }, 150);
485
+ const tick = setInterval(() => {
486
+ const width = Number.parseFloat(bar.style.width) || 0;
487
+ if (width < 90) bar.style.width = `${width + 8}%`;
488
+ }, 200);
489
+ return () => {
490
+ clearTimeout(show);
491
+ clearInterval(tick);
492
+ bar.classList.add('is-active');
493
+ bar.style.width = '100%';
494
+ setTimeout(() => {
495
+ bar.classList.remove('is-active');
496
+ bar.style.width = '0%';
497
+ }, 220);
498
+ };
499
+ }
500
+
501
+ function persistKey(el) {
502
+ const named = el.getAttribute('wire:persist');
503
+ if (named) return named;
504
+ if (el.id) return `#${el.id}`;
505
+ return '';
506
+ }
507
+
508
+ function liftPersisted(root) {
509
+ const bag = new Map();
510
+ for (const el of [...root.querySelectorAll('[wire\\:persist]')]) {
511
+ const key = persistKey(el);
512
+ if (!key || bag.has(key)) continue;
513
+ const marker = document.createComment(`wire-persist:${key}`);
514
+ el.replaceWith(marker);
515
+ bag.set(key, el);
516
+ }
517
+ return bag;
518
+ }
519
+
520
+ function restorePersisted(root, bag) {
521
+ const restored = [];
522
+ for (const el of [...root.querySelectorAll('[wire\\:persist]')]) {
523
+ const key = persistKey(el);
524
+ const saved = bag.get(key);
525
+ if (!saved || saved === el) continue;
526
+ el.replaceWith(saved);
527
+ restored.push(saved);
528
+ bag.delete(key);
529
+ }
530
+ return restored;
531
+ }
532
+
533
+ function assetIdentity(node) {
534
+ if (node instanceof HTMLLinkElement && node.rel === 'stylesheet') return `link:${node.href}`;
535
+ if (node instanceof HTMLScriptElement && node.src) return `script:${node.src}`;
536
+ if (node.tagName === 'STYLE') return `style:${node.id || node.textContent}`;
537
+ return '';
538
+ }
539
+
540
+ function mergeHead(nextDoc) {
541
+ document.title = nextDoc.title;
542
+ for (const node of [...nextDoc.head.children]) {
543
+ if (node.tagName === 'TITLE') continue;
544
+ if (node instanceof HTMLLinkElement && node.rel === 'stylesheet') {
545
+ if (![...document.querySelectorAll('link[rel="stylesheet"]')].some((link) => link.href === node.href)) {
546
+ document.head.appendChild(node.cloneNode(true));
547
+ }
548
+ continue;
549
+ }
550
+ if (node instanceof HTMLScriptElement && node.src) {
551
+ if (![...document.scripts].some((script) => script.src === node.src)) {
552
+ document.head.appendChild(node.cloneNode(true));
553
+ }
554
+ continue;
555
+ }
556
+ if (node instanceof HTMLScriptElement) {
557
+ reviveScript(node, document.head);
558
+ continue;
559
+ }
560
+ if (node.tagName === 'META') {
561
+ const name = node.getAttribute('name') || node.getAttribute('property');
562
+ if (!name) continue;
563
+ const selector = `meta[name="${CSS.escape(name)}"], meta[property="${CSS.escape(name)}"]`;
564
+ const current = document.head.querySelector(selector);
565
+ const copy = node.cloneNode(true);
566
+ if (current) current.replaceWith(copy);
567
+ else document.head.appendChild(copy);
568
+ continue;
569
+ }
570
+ if (node.tagName === 'STYLE') {
571
+ const key = assetIdentity(node);
572
+ const exists = [...document.head.querySelectorAll('style')].some((styleNode) => assetIdentity(styleNode) === key);
573
+ if (!exists) document.head.appendChild(node.cloneNode(true));
574
+ }
575
+ }
576
+ }
577
+
578
+ function reviveScripts(root, restored) {
579
+ for (const script of [...root.querySelectorAll('script')]) {
580
+ if (restored.some((node) => node === script || node.contains(script))) continue;
581
+ reviveScript(script, script.parentNode);
582
+ }
583
+ }
584
+
585
+ function reviveScript(script, parent) {
586
+ if (!parent) return;
587
+ const key = script.src || script.textContent || '';
588
+ const once = Boolean(script.src) || script.hasAttribute('data-navigate-once');
589
+ const live = script.isConnected && script.ownerDocument === document;
590
+ if (once && ranScripts.has(key)) {
591
+ if (live) script.remove();
592
+ return;
593
+ }
594
+ ranScripts.add(key);
595
+ const copy = document.createElement('script');
596
+ for (const attr of script.attributes) copy.setAttribute(attr.name, attr.value);
597
+ if (!script.src) copy.textContent = script.textContent;
598
+ if (live) script.replaceWith(copy);
599
+ else parent.appendChild(copy);
600
+ }
601
+
602
+ function sharedRoot(nextDoc) {
603
+ const selectors = ['[data-shamar-scroll-root]', '[data-wire-navigate-root]'];
604
+ for (const selector of selectors) {
605
+ const current = document.querySelector(selector);
606
+ const next = nextDoc.querySelector(selector);
607
+ if (current instanceof HTMLElement && next instanceof HTMLElement) return { current, next };
608
+ }
609
+ return null;
610
+ }
611
+
612
+ function applyScroll(mode, scrollY, url) {
613
+ if (mode === 'preserve') return;
614
+ if (mode === 'restore') {
615
+ window.scrollTo(0, scrollY || 0);
616
+ return;
617
+ }
618
+ const id = decodeURIComponent(url.hash.replace(/^#/, ''));
619
+ if (id) {
620
+ const target = document.getElementById(id);
621
+ if (target) {
622
+ target.scrollIntoView();
623
+ return;
624
+ }
625
+ }
626
+ window.scrollTo(0, 0);
627
+ const root = document.querySelector('[data-shamar-scroll-root], [data-wire-navigate-root]');
628
+ if (root instanceof HTMLElement && root !== document.body) root.scrollTop = 0;
629
+ }
630
+
631
+ async function navigate(url, options) {
632
+ const id = ++navigationId;
633
+ const navigating = new CustomEvent('wire:navigating', {
634
+ cancelable: true,
635
+ detail: { url: url.href },
636
+ });
637
+ if (!document.dispatchEvent(navigating)) return;
638
+ const finish = startProgress();
639
+ try {
640
+ const cached = prefetchCache.get(cacheKey(url));
641
+ let html = null;
642
+ let finalUrl = url.href;
643
+ if (cached && Date.now() - cached.time < PREFETCH_TTL) {
644
+ const payload = await cached.promise;
645
+ if (payload) {
646
+ html = payload.html;
647
+ finalUrl = payload.url;
648
+ }
649
+ }
650
+ if (html === null) {
651
+ const res = await fetch(url, {
652
+ credentials: 'same-origin',
653
+ headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
654
+ });
655
+ const type = res.headers.get('content-type') || '';
656
+ if (!res.ok || !type.includes('text/html')) {
657
+ window.location.assign(res.url || url);
658
+ return;
659
+ }
660
+ html = await res.text();
661
+ finalUrl = res.url || url.href;
662
+ }
663
+ if (id !== navigationId) return;
664
+ const doc = new DOMParser().parseFromString(html, 'text/html');
665
+ if (!doc.body) {
666
+ window.location.assign(finalUrl);
667
+ return;
668
+ }
669
+ if (options.push !== false) {
670
+ rememberScroll();
671
+ history.pushState({ wireScroll: 0 }, '', finalUrl);
672
+ }
673
+ mergeHead(doc);
674
+ const region = sharedRoot(doc);
675
+ const from = region ? region.current : document.body;
676
+ const to = region ? region.next : doc.body;
677
+ const kept = liftPersisted(from);
678
+ morph(from, to, from, false);
679
+ const restored = restorePersisted(from, kept);
680
+ if (window.Alpine?.initTree) {
681
+ for (const el of from.querySelectorAll('[wire\\:persist]')) {
682
+ if (!restored.includes(el)) window.Alpine.initTree(el);
683
+ }
684
+ }
685
+ reviveScripts(from, restored);
686
+ applyScroll(options.scroll, options.scrollY, new URL(finalUrl, window.location.href));
687
+ document.dispatchEvent(new CustomEvent('wire:navigated', { detail: { url: window.location.href } }));
688
+ } finally {
689
+ if (id === navigationId) finish();
690
+ }
691
+ }
692
+
693
+ document.addEventListener('alpine:init', () => {
694
+ if (!window.Alpine?.magic) return;
695
+ window.Alpine.magic('wire', (el) => {
696
+ const root = rootOf(el);
697
+ if (!root) return {};
698
+ return new Proxy(
699
+ {},
700
+ {
701
+ get(_target, prop) {
702
+ if (prop === '$set') {
703
+ return (key, value) => commit(root, { updates: { [key]: value } });
704
+ }
705
+ if (prop === '$call') {
706
+ return (method, ...params) => commit(root, { calls: [{ method, params }] });
707
+ }
708
+ const data = snapshotOf(root).data || {};
709
+ if (Object.prototype.hasOwnProperty.call(data, prop)) return data[prop];
710
+ if (typeof prop !== 'string') return undefined;
711
+ return (...params) => commit(root, { calls: [{ method: prop, params }] });
712
+ },
713
+ set(_target, prop, value) {
714
+ if (typeof prop !== 'string') return false;
715
+ commit(root, { updates: { [prop]: value } }).catch(() => {});
716
+ return true;
717
+ },
718
+ },
719
+ );
720
+ });
721
+ });
722
+ })();