dolphin-client 1.0.4 → 1.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.
- package/README.md +25 -2
- package/dist/api.d.ts +20 -0
- package/dist/core.d.ts +3 -1
- package/dist/dolphin-client.js +566 -43
- package/dist/dolphin-client.min.js +29 -18
- package/dist/index.cjs +566 -43
- package/dist/index.js +566 -43
- package/dist/store.d.ts +7 -0
- package/fulltutorial.md +235 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,7 +22,21 @@ var APIHandler = class {
|
|
|
22
22
|
target.requestDirect = (method, path, body, options) => {
|
|
23
23
|
return this.requestDirect(method, path, body, options);
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
target._findCSRFToken = () => this._findCSRFToken();
|
|
26
|
+
target._resolveBaseUrl = (path) => this._resolveBaseUrl(path);
|
|
27
|
+
target._normalizeValidationErrors = (errData) => this._normalizeValidationErrors(errData);
|
|
28
|
+
const methods = [
|
|
29
|
+
"get",
|
|
30
|
+
"post",
|
|
31
|
+
"put",
|
|
32
|
+
"patch",
|
|
33
|
+
"del",
|
|
34
|
+
"request",
|
|
35
|
+
"requestDirect",
|
|
36
|
+
"_findCSRFToken",
|
|
37
|
+
"_resolveBaseUrl",
|
|
38
|
+
"_normalizeValidationErrors"
|
|
39
|
+
];
|
|
26
40
|
return new Proxy(target, {
|
|
27
41
|
get: (t, prop) => {
|
|
28
42
|
if (typeof prop === "string" && !methods.includes(prop)) {
|
|
@@ -32,6 +46,108 @@ var APIHandler = class {
|
|
|
32
46
|
}
|
|
33
47
|
});
|
|
34
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Attempts to find a CSRF token in the document (meta tags, forms, or cookies).
|
|
51
|
+
* Works for Laravel, CakePHP, WordPress, Express, NestJS, etc.
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
_findCSRFToken() {
|
|
55
|
+
if (typeof document === "undefined") return null;
|
|
56
|
+
const metaNames = ["csrf-token", "_csrf", "xsrf-token", "csrf_token"];
|
|
57
|
+
for (const name of metaNames) {
|
|
58
|
+
const metaEl = document.querySelector(`meta[name="${name}"], meta[content][name$="${name}"]`);
|
|
59
|
+
if (metaEl) {
|
|
60
|
+
const token = metaEl.getAttribute("content");
|
|
61
|
+
if (token) return token;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const inputNames = ["_csrfToken", "_token", "_csrf", "csrf_token"];
|
|
65
|
+
for (const name of inputNames) {
|
|
66
|
+
const inputEl = document.querySelector(`input[type="hidden"][name="${name}"]`);
|
|
67
|
+
if (inputEl && inputEl.value) return inputEl.value;
|
|
68
|
+
}
|
|
69
|
+
const cookies = ["csrfToken", "XSRF-TOKEN", "_csrf", "csrf_token"];
|
|
70
|
+
for (const name of cookies) {
|
|
71
|
+
const match = document.cookie.match(new RegExp("(^|;\\s*)" + name + "=([^;]*)"));
|
|
72
|
+
if (match) return decodeURIComponent(match[2]);
|
|
73
|
+
}
|
|
74
|
+
const wpNonce = typeof window !== "undefined" && window.wpApiSettings?.nonce;
|
|
75
|
+
if (wpNonce) return wpNonce;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Dynamically resolves the Base Path/URL from `<base href="...">` or subfolders.
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
_resolveBaseUrl(path) {
|
|
83
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
84
|
+
return path;
|
|
85
|
+
}
|
|
86
|
+
let baseUrl = this.client.httpUrl;
|
|
87
|
+
if (typeof document !== "undefined") {
|
|
88
|
+
const baseEl = document.querySelector("base[href]");
|
|
89
|
+
if (baseEl) {
|
|
90
|
+
const href = baseEl.getAttribute("href") || "";
|
|
91
|
+
if (href && href !== "/") {
|
|
92
|
+
const cleanHref = href.endsWith("/") ? href.slice(0, -1) : href;
|
|
93
|
+
baseUrl = `${this.client.httpUrl}${cleanHref.startsWith("/") ? cleanHref : "/" + cleanHref}`;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const metaBase = document.querySelector('meta[name="base-path"]');
|
|
97
|
+
if (metaBase) {
|
|
98
|
+
const content = metaBase.getAttribute("content") || "";
|
|
99
|
+
if (content && content !== "/") {
|
|
100
|
+
const cleanContent = content.endsWith("/") ? content.slice(0, -1) : content;
|
|
101
|
+
baseUrl = `${this.client.httpUrl}${cleanContent.startsWith("/") ? cleanContent : "/" + cleanContent}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const cleanPath = path.startsWith("/") ? path : "/" + path;
|
|
107
|
+
return `${baseUrl}${cleanPath}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalizes backend validation errors from major PHP and Node.js frameworks
|
|
111
|
+
* into a unified { [field]: message } object.
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
_normalizeValidationErrors(errData) {
|
|
115
|
+
const normalized = {};
|
|
116
|
+
if (!errData || typeof errData !== "object") return normalized;
|
|
117
|
+
const errors = errData.errors || errData.validationErrors || errData;
|
|
118
|
+
if (Array.isArray(errors)) {
|
|
119
|
+
for (const err of errors) {
|
|
120
|
+
if (err && typeof err === "object") {
|
|
121
|
+
const field = err.path || err.param || err.field || err.property;
|
|
122
|
+
const msg = err.msg || err.message || err.error;
|
|
123
|
+
if (field && msg) {
|
|
124
|
+
normalized[field] = Array.isArray(msg) ? msg[0] : msg;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
if (typeof errors === "object" && errors !== null) {
|
|
131
|
+
for (const key in errors) {
|
|
132
|
+
const val = errors[key];
|
|
133
|
+
if (!val) continue;
|
|
134
|
+
if (Array.isArray(val)) {
|
|
135
|
+
if (val.length > 0) {
|
|
136
|
+
normalized[key] = String(val[0]);
|
|
137
|
+
}
|
|
138
|
+
} else if (typeof val === "object") {
|
|
139
|
+
const innerKeys = Object.keys(val);
|
|
140
|
+
if (innerKeys.length > 0) {
|
|
141
|
+
const firstInnerKey = innerKeys[0];
|
|
142
|
+
normalized[key] = String(val[firstInnerKey]);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
normalized[key] = String(val);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
35
151
|
/**
|
|
36
152
|
* Intercept request for offline-first caching and queuing.
|
|
37
153
|
*/
|
|
@@ -82,30 +198,68 @@ var APIHandler = class {
|
|
|
82
198
|
*/
|
|
83
199
|
async requestDirect(method, path, body = null, options = {}) {
|
|
84
200
|
const _isRetry = options._isRetry === true;
|
|
85
|
-
|
|
201
|
+
let finalMethod = method.toUpperCase();
|
|
202
|
+
let finalBody = body;
|
|
203
|
+
const headers = {
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
...options.headers || {}
|
|
206
|
+
};
|
|
207
|
+
if (["PUT", "PATCH", "DELETE"].includes(finalMethod)) {
|
|
208
|
+
if (this.client.options.methodSpoofing || options.methodSpoofing) {
|
|
209
|
+
headers["X-HTTP-Method-Override"] = finalMethod;
|
|
210
|
+
if (finalBody instanceof FormData) {
|
|
211
|
+
finalBody.append("_method", finalMethod);
|
|
212
|
+
} else if (finalBody && typeof finalBody === "object") {
|
|
213
|
+
finalBody = {
|
|
214
|
+
...finalBody,
|
|
215
|
+
_method: finalMethod
|
|
216
|
+
};
|
|
217
|
+
} else if (!finalBody) {
|
|
218
|
+
finalBody = { _method: finalMethod };
|
|
219
|
+
}
|
|
220
|
+
finalMethod = "POST";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const url = this._resolveBaseUrl(path);
|
|
86
224
|
if (this.client.options.debug) {
|
|
87
|
-
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path,
|
|
225
|
+
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path, finalBody || "");
|
|
88
226
|
}
|
|
89
227
|
const controller = new AbortController();
|
|
90
228
|
const timeoutId = setTimeout(
|
|
91
229
|
() => controller.abort(),
|
|
92
230
|
this.client.options.timeout
|
|
93
231
|
);
|
|
94
|
-
const headers = {
|
|
95
|
-
"Content-Type": "application/json",
|
|
96
|
-
...options.headers || {}
|
|
97
|
-
};
|
|
98
232
|
if (this.client.accessToken) {
|
|
99
233
|
headers["Authorization"] = `Bearer ${this.client.accessToken}`;
|
|
100
234
|
}
|
|
235
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase())) {
|
|
236
|
+
const csrfToken = this._findCSRFToken();
|
|
237
|
+
if (csrfToken) {
|
|
238
|
+
headers["X-CSRF-Token"] = csrfToken;
|
|
239
|
+
headers["X-XSRF-TOKEN"] = csrfToken;
|
|
240
|
+
headers["X-CSRFToken"] = csrfToken;
|
|
241
|
+
headers["X-WP-Nonce"] = csrfToken;
|
|
242
|
+
if (finalBody && typeof finalBody === "object") {
|
|
243
|
+
if (!finalBody._csrfToken && !finalBody._token && !finalBody._csrf) {
|
|
244
|
+
finalBody = {
|
|
245
|
+
...finalBody,
|
|
246
|
+
_csrfToken: csrfToken,
|
|
247
|
+
_token: csrfToken,
|
|
248
|
+
_csrf: csrfToken
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
101
254
|
const fetchOptions = { ...options };
|
|
102
255
|
delete fetchOptions._isRetry;
|
|
256
|
+
delete fetchOptions.methodSpoofing;
|
|
103
257
|
try {
|
|
104
258
|
const response = await fetch(url, {
|
|
105
|
-
method,
|
|
259
|
+
method: finalMethod,
|
|
106
260
|
headers,
|
|
107
261
|
signal: controller.signal,
|
|
108
|
-
...
|
|
262
|
+
...finalBody ? { body: JSON.stringify(finalBody) } : {},
|
|
109
263
|
...fetchOptions
|
|
110
264
|
});
|
|
111
265
|
clearTimeout(timeoutId);
|
|
@@ -137,6 +291,14 @@ var APIHandler = class {
|
|
|
137
291
|
if (this.client.options.debug) {
|
|
138
292
|
console.error(`%c\u274C [Dolphin API Error]:`, "color: #ef4444; font-weight: bold;", method.toUpperCase(), path, err);
|
|
139
293
|
}
|
|
294
|
+
if (err && typeof err === "object" && err.data) {
|
|
295
|
+
const normErrors = this._normalizeValidationErrors(err.data);
|
|
296
|
+
if (Object.keys(normErrors).length > 0) {
|
|
297
|
+
for (const field in normErrors) {
|
|
298
|
+
this.client.publish(`errors/${field}`, normErrors[field]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
140
302
|
if (err.name === "AbortError") {
|
|
141
303
|
throw { status: 408, data: { error: "Request timed out" } };
|
|
142
304
|
}
|
|
@@ -270,12 +432,15 @@ var DolphinStore = class {
|
|
|
270
432
|
data;
|
|
271
433
|
listeners;
|
|
272
434
|
subscribed;
|
|
435
|
+
/** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions (was: subscriptions never removed) */
|
|
436
|
+
_unsubscribers;
|
|
273
437
|
/** @param {DolphinClient} client */
|
|
274
438
|
constructor(client) {
|
|
275
439
|
this.client = client;
|
|
276
440
|
this.data = /* @__PURE__ */ new Map();
|
|
277
441
|
this.listeners = /* @__PURE__ */ new Set();
|
|
278
442
|
this.subscribed = /* @__PURE__ */ new Set();
|
|
443
|
+
this._unsubscribers = /* @__PURE__ */ new Map();
|
|
279
444
|
return new Proxy(this, {
|
|
280
445
|
get: (target, prop) => {
|
|
281
446
|
if (prop in target) return target[prop];
|
|
@@ -327,9 +492,12 @@ var DolphinStore = class {
|
|
|
327
492
|
state.error = null;
|
|
328
493
|
this._applyTransform(state);
|
|
329
494
|
if (!this.subscribed.has(name)) {
|
|
330
|
-
this.client.
|
|
495
|
+
const unsubscribe = () => this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
496
|
+
const updateHandler = (update) => {
|
|
331
497
|
this._handleRemoteUpdate(name, update);
|
|
332
|
-
}
|
|
498
|
+
};
|
|
499
|
+
this.client.subscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
500
|
+
this._unsubscribers.set(name, unsubscribe);
|
|
333
501
|
this.subscribed.add(name);
|
|
334
502
|
}
|
|
335
503
|
} catch (e) {
|
|
@@ -386,6 +554,22 @@ var DolphinStore = class {
|
|
|
386
554
|
_notify() {
|
|
387
555
|
this.listeners.forEach((l) => l());
|
|
388
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Clean up all WebSocket subscriptions and listeners.
|
|
559
|
+
* Call this when the store is no longer needed to prevent resource leaks.
|
|
560
|
+
*/
|
|
561
|
+
destroy() {
|
|
562
|
+
this._unsubscribers.forEach((unsub) => {
|
|
563
|
+
try {
|
|
564
|
+
unsub();
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
this._unsubscribers.clear();
|
|
569
|
+
this.subscribed.clear();
|
|
570
|
+
this.listeners.clear();
|
|
571
|
+
this.data.clear();
|
|
572
|
+
}
|
|
389
573
|
};
|
|
390
574
|
|
|
391
575
|
// src/core.ts
|
|
@@ -405,6 +589,8 @@ var DolphinClient = class {
|
|
|
405
589
|
fileHandlers;
|
|
406
590
|
_offlineQueue;
|
|
407
591
|
reconnectAttempts;
|
|
592
|
+
/** @fix: Store timer ID so disconnect() can cancel pending reconnects (was: memory/logic leak) */
|
|
593
|
+
_reconnectTimer;
|
|
408
594
|
_attachedListeners;
|
|
409
595
|
constructor(url = "", deviceId = "", options = {}) {
|
|
410
596
|
if (!url && typeof window !== "undefined") url = window.location.host;
|
|
@@ -414,7 +600,7 @@ var DolphinClient = class {
|
|
|
414
600
|
else if (typeof window !== "undefined") protocol = window.location.protocol;
|
|
415
601
|
this.host = (url || "localhost").replace(/\/$/, "").replace(/^https?:\/\//, "");
|
|
416
602
|
this.httpUrl = `${protocol}//${this.host}`;
|
|
417
|
-
this.deviceId = deviceId || "web_" + Math.random().toString(36).
|
|
603
|
+
this.deviceId = deviceId || (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? "web_" + crypto.randomUUID().replace(/-/g, "").slice(0, 8) : "web_" + Math.random().toString(36).slice(2, 10));
|
|
418
604
|
this.options = {
|
|
419
605
|
timeout: 15e3,
|
|
420
606
|
chunkSize: 65536,
|
|
@@ -422,6 +608,9 @@ var DolphinClient = class {
|
|
|
422
608
|
maxReconnect: 5,
|
|
423
609
|
autoRefreshToken: true,
|
|
424
610
|
debug: false,
|
|
611
|
+
methodSpoofing: false,
|
|
612
|
+
routerViewport: "main, #viewport, body",
|
|
613
|
+
routerTransitions: true,
|
|
425
614
|
...options
|
|
426
615
|
};
|
|
427
616
|
this.socket = null;
|
|
@@ -441,6 +630,7 @@ var DolphinClient = class {
|
|
|
441
630
|
this.fileHandlers = /* @__PURE__ */ new Set();
|
|
442
631
|
this._offlineQueue = [];
|
|
443
632
|
this.reconnectAttempts = 0;
|
|
633
|
+
this._reconnectTimer = null;
|
|
444
634
|
this._attachedListeners = [];
|
|
445
635
|
if (typeof window !== "undefined" && typeof this._initDOMBinding === "function") {
|
|
446
636
|
this._initDOMBinding();
|
|
@@ -469,6 +659,9 @@ var DolphinClient = class {
|
|
|
469
659
|
// ── WebSocket ─────────────────────────────────────────────────────────────
|
|
470
660
|
/** Connect to the Dolphin realtime server */
|
|
471
661
|
async connect() {
|
|
662
|
+
if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) {
|
|
663
|
+
return Promise.resolve();
|
|
664
|
+
}
|
|
472
665
|
return new Promise((resolve, reject) => {
|
|
473
666
|
const protocol = this.httpUrl.startsWith("https") ? "wss:" : "ws:";
|
|
474
667
|
const wsUrl = `${protocol}//${this.host}/realtime?deviceId=${this.deviceId}`;
|
|
@@ -493,11 +686,18 @@ var DolphinClient = class {
|
|
|
493
686
|
}
|
|
494
687
|
/** Disconnect cleanly */
|
|
495
688
|
disconnect() {
|
|
689
|
+
if (this._reconnectTimer !== null) {
|
|
690
|
+
clearTimeout(this._reconnectTimer);
|
|
691
|
+
this._reconnectTimer = null;
|
|
692
|
+
}
|
|
496
693
|
if (this.socket) {
|
|
497
694
|
this.socket.onclose = null;
|
|
498
695
|
this.socket.close();
|
|
499
696
|
this.socket = null;
|
|
500
697
|
}
|
|
698
|
+
if (typeof this._collabCleanup === "function") {
|
|
699
|
+
this._collabCleanup();
|
|
700
|
+
}
|
|
501
701
|
this.cleanupDomListeners();
|
|
502
702
|
}
|
|
503
703
|
/** @private */
|
|
@@ -585,8 +785,11 @@ var DolphinClient = class {
|
|
|
585
785
|
this.reconnectAttempts++;
|
|
586
786
|
const delay = Math.pow(2, this.reconnectAttempts) * 1e3;
|
|
587
787
|
console.log(`[Dolphin] Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts})...`);
|
|
588
|
-
|
|
589
|
-
|
|
788
|
+
this._reconnectTimer = setTimeout(() => {
|
|
789
|
+
this._reconnectTimer = null;
|
|
790
|
+
this.connect().catch(() => {
|
|
791
|
+
});
|
|
792
|
+
}, delay);
|
|
590
793
|
} else {
|
|
591
794
|
console.error("[Dolphin] Max reconnect attempts reached.");
|
|
592
795
|
}
|
|
@@ -764,6 +967,7 @@ var DolphinClient = class {
|
|
|
764
967
|
|
|
765
968
|
// src/dom.ts
|
|
766
969
|
function attachDOMBinding(clientProto) {
|
|
970
|
+
const componentPromiseCache = /* @__PURE__ */ new Map();
|
|
767
971
|
function escapeRegExp(str) {
|
|
768
972
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
769
973
|
}
|
|
@@ -1010,7 +1214,9 @@ function attachDOMBinding(clientProto) {
|
|
|
1010
1214
|
const scheduleFn = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 0);
|
|
1011
1215
|
scheduleFn(() => {
|
|
1012
1216
|
pendingUpdates.forEach((html, el) => {
|
|
1013
|
-
|
|
1217
|
+
if (el.isConnected !== false) {
|
|
1218
|
+
patchDOM(el, html);
|
|
1219
|
+
}
|
|
1014
1220
|
});
|
|
1015
1221
|
pendingUpdates.clear();
|
|
1016
1222
|
rafScheduled = false;
|
|
@@ -1149,7 +1355,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1149
1355
|
if (this._domInitialized) return;
|
|
1150
1356
|
this._domInitialized = true;
|
|
1151
1357
|
const PUSH_EVENTS = ["input", "change", "keyup", "paste", "blur"];
|
|
1152
|
-
const debounceTimers = /* @__PURE__ */ new
|
|
1358
|
+
const debounceTimers = /* @__PURE__ */ new WeakMap();
|
|
1153
1359
|
PUSH_EVENTS.forEach((evtName) => {
|
|
1154
1360
|
this.addDomListener(document, evtName, (e) => {
|
|
1155
1361
|
if (!e.target || !e.target.getAttribute) return;
|
|
@@ -1202,6 +1408,14 @@ function attachDOMBinding(clientProto) {
|
|
|
1202
1408
|
const rtTopic = e.target.getAttribute("data-rt-submit");
|
|
1203
1409
|
const apiTarget = e.target.getAttribute("data-api-submit");
|
|
1204
1410
|
if (rtTopic || apiTarget) {
|
|
1411
|
+
const formInputs = e.target.querySelectorAll("[name]");
|
|
1412
|
+
formInputs.forEach((inputEl) => {
|
|
1413
|
+
const name = inputEl.name;
|
|
1414
|
+
if (name) {
|
|
1415
|
+
this.publish(`errors/${name}`, "");
|
|
1416
|
+
inputEl.classList.remove("invalid");
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1205
1419
|
const validatedInputs = e.target.querySelectorAll("[data-rt-validate]");
|
|
1206
1420
|
let formIsValid = true;
|
|
1207
1421
|
if (validatedInputs.length > 0 && typeof this.validateField === "function") {
|
|
@@ -1215,9 +1429,6 @@ function attachDOMBinding(clientProto) {
|
|
|
1215
1429
|
formIsValid = false;
|
|
1216
1430
|
inputEl.classList.add("invalid");
|
|
1217
1431
|
this.publish(`errors/${name}`, errorMsg);
|
|
1218
|
-
} else {
|
|
1219
|
-
inputEl.classList.remove("invalid");
|
|
1220
|
-
this.publish(`errors/${name}`, "");
|
|
1221
1432
|
}
|
|
1222
1433
|
}
|
|
1223
1434
|
});
|
|
@@ -1245,8 +1456,11 @@ function attachDOMBinding(clientProto) {
|
|
|
1245
1456
|
resolvedTarget = resolvedTarget.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1246
1457
|
}
|
|
1247
1458
|
const parts = resolvedTarget.trim().split(" ");
|
|
1248
|
-
|
|
1459
|
+
let method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
|
|
1249
1460
|
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
1461
|
+
if (data._method) {
|
|
1462
|
+
method = String(data._method).toUpperCase();
|
|
1463
|
+
}
|
|
1250
1464
|
try {
|
|
1251
1465
|
const result = await this.api.request(method, path, data);
|
|
1252
1466
|
const resultBind = e.target.getAttribute("data-api-result");
|
|
@@ -1333,6 +1547,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1333
1547
|
});
|
|
1334
1548
|
this._scanAndFetchAPIBinds();
|
|
1335
1549
|
this._scanStoreBinds();
|
|
1550
|
+
this._resolveImports();
|
|
1551
|
+
this._initSPARouter();
|
|
1336
1552
|
};
|
|
1337
1553
|
clientProto._scanAndFetchAPIBinds = async function() {
|
|
1338
1554
|
if (typeof document === "undefined") return;
|
|
@@ -1340,6 +1556,10 @@ function attachDOMBinding(clientProto) {
|
|
|
1340
1556
|
for (const el of Array.from(elements)) {
|
|
1341
1557
|
const path = el.getAttribute("data-api-get");
|
|
1342
1558
|
if (!path) continue;
|
|
1559
|
+
if (typeof el.hasAttribute === "function" && el.hasAttribute("data-api-initialized")) continue;
|
|
1560
|
+
if (typeof el.setAttribute === "function") {
|
|
1561
|
+
el.setAttribute("data-api-initialized", "true");
|
|
1562
|
+
}
|
|
1343
1563
|
try {
|
|
1344
1564
|
const result = await this.api.get(path);
|
|
1345
1565
|
const apiStore = el.getAttribute("data-api-store");
|
|
@@ -1355,20 +1575,22 @@ function attachDOMBinding(clientProto) {
|
|
|
1355
1575
|
} else if (!apiStore) {
|
|
1356
1576
|
const template = resolveTemplate(el);
|
|
1357
1577
|
if (template && typeof result === "object" && result !== null) {
|
|
1358
|
-
|
|
1578
|
+
const processedResult = this._applyDeclarativeDirectives(el, result);
|
|
1579
|
+
if (Array.isArray(processedResult)) {
|
|
1359
1580
|
let combinedHTML = "";
|
|
1360
|
-
for (const item of
|
|
1581
|
+
for (const item of processedResult) {
|
|
1361
1582
|
combinedHTML += renderTemplate(template, item);
|
|
1362
1583
|
}
|
|
1363
1584
|
scheduleDOMUpdate(el, combinedHTML);
|
|
1364
1585
|
} else {
|
|
1365
|
-
scheduleDOMUpdate(el, renderTemplate(template,
|
|
1586
|
+
scheduleDOMUpdate(el, renderTemplate(template, processedResult));
|
|
1366
1587
|
}
|
|
1367
1588
|
} else {
|
|
1368
1589
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1369
1590
|
el.value = typeof result === "object" ? result.value !== void 0 ? result.value : "" : result;
|
|
1370
1591
|
} else {
|
|
1371
|
-
|
|
1592
|
+
const rawHTML = typeof result === "object" ? result.html || result.text || JSON.stringify(result) : String(result);
|
|
1593
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1372
1594
|
}
|
|
1373
1595
|
}
|
|
1374
1596
|
}
|
|
@@ -1377,21 +1599,139 @@ function attachDOMBinding(clientProto) {
|
|
|
1377
1599
|
}
|
|
1378
1600
|
}
|
|
1379
1601
|
};
|
|
1602
|
+
clientProto._applyDeclarativeDirectives = function(el, payload) {
|
|
1603
|
+
let processedPayload = payload;
|
|
1604
|
+
if (typeof payload === "object" && payload !== null) {
|
|
1605
|
+
const applyFilterSearchSort = (arr) => {
|
|
1606
|
+
let result = [...arr];
|
|
1607
|
+
const filterAttr = el.getAttribute("data-rt-filter");
|
|
1608
|
+
if (filterAttr) {
|
|
1609
|
+
const parts = filterAttr.split("==");
|
|
1610
|
+
if (parts.length === 2) {
|
|
1611
|
+
const itemProp = parts[0].trim();
|
|
1612
|
+
const storeExpr = parts[1].trim();
|
|
1613
|
+
let filterVal = void 0;
|
|
1614
|
+
const storeParts = storeExpr.split(".");
|
|
1615
|
+
if (storeParts.length === 2) {
|
|
1616
|
+
filterVal = this.getStoreState(storeParts[0], storeParts[1]);
|
|
1617
|
+
} else {
|
|
1618
|
+
filterVal = payload[storeExpr] !== void 0 ? payload[storeExpr] : this.getStoreState("app", storeExpr);
|
|
1619
|
+
}
|
|
1620
|
+
if (filterVal !== void 0 && filterVal !== null && filterVal !== "") {
|
|
1621
|
+
result = result.filter((item) => item[itemProp] === filterVal);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const searchAttr = el.getAttribute("data-rt-search");
|
|
1626
|
+
if (searchAttr) {
|
|
1627
|
+
const parts = searchAttr.split("==");
|
|
1628
|
+
if (parts.length === 2) {
|
|
1629
|
+
const itemProp = parts[0].trim();
|
|
1630
|
+
const storeExpr = parts[1].trim();
|
|
1631
|
+
let searchVal = void 0;
|
|
1632
|
+
const storeParts = storeExpr.split(".");
|
|
1633
|
+
if (storeParts.length === 2) {
|
|
1634
|
+
searchVal = this.getStoreState(storeParts[0], storeParts[1]);
|
|
1635
|
+
} else {
|
|
1636
|
+
searchVal = payload[storeExpr] !== void 0 ? payload[storeExpr] : this.getStoreState("app", storeExpr);
|
|
1637
|
+
}
|
|
1638
|
+
if (searchVal !== void 0 && searchVal !== null && searchVal !== "") {
|
|
1639
|
+
const query = String(searchVal).toLowerCase();
|
|
1640
|
+
result = result.filter((item) => {
|
|
1641
|
+
const val = item[itemProp];
|
|
1642
|
+
return val !== void 0 && val !== null && String(val).toLowerCase().includes(query);
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
const sortAttr = el.getAttribute("data-rt-sort");
|
|
1648
|
+
if (sortAttr) {
|
|
1649
|
+
let sortByVal = void 0;
|
|
1650
|
+
const storeParts = sortAttr.split(".");
|
|
1651
|
+
if (storeParts.length === 2) {
|
|
1652
|
+
sortByVal = this.getStoreState(storeParts[0], storeParts[1]);
|
|
1653
|
+
} else {
|
|
1654
|
+
sortByVal = payload[sortAttr] !== void 0 ? payload[sortAttr] : this.getStoreState("app", sortAttr);
|
|
1655
|
+
}
|
|
1656
|
+
if (sortByVal && sortByVal !== "") {
|
|
1657
|
+
if (sortByVal === "popular") {
|
|
1658
|
+
result.sort((a, b) => {
|
|
1659
|
+
const rateA = a.rating?.rate || a.rate || 0;
|
|
1660
|
+
const rateB = b.rating?.rate || b.rate || 0;
|
|
1661
|
+
return rateB - rateA;
|
|
1662
|
+
});
|
|
1663
|
+
} else {
|
|
1664
|
+
let field = "";
|
|
1665
|
+
let direction = "asc";
|
|
1666
|
+
if (sortByVal.endsWith("-low") || sortByVal.endsWith("-asc")) {
|
|
1667
|
+
field = sortByVal.replace("-low", "").replace("-asc", "");
|
|
1668
|
+
direction = "asc";
|
|
1669
|
+
} else if (sortByVal.endsWith("-high") || sortByVal.endsWith("-desc")) {
|
|
1670
|
+
field = sortByVal.replace("-high", "").replace("-desc", "");
|
|
1671
|
+
direction = "desc";
|
|
1672
|
+
}
|
|
1673
|
+
if (field) {
|
|
1674
|
+
result.sort((a, b) => {
|
|
1675
|
+
const resolveVal = (obj, path) => {
|
|
1676
|
+
return path.split(".").reduce((acc, part) => acc && acc[part], obj);
|
|
1677
|
+
};
|
|
1678
|
+
let valA = resolveVal(a, field);
|
|
1679
|
+
let valB = resolveVal(b, field);
|
|
1680
|
+
if (valA === void 0) valA = a[field];
|
|
1681
|
+
if (valB === void 0) valB = b[field];
|
|
1682
|
+
if (typeof valA === "string" && typeof valB === "string") {
|
|
1683
|
+
return direction === "asc" ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
1684
|
+
}
|
|
1685
|
+
const numA = Number(valA);
|
|
1686
|
+
const numB = Number(valB);
|
|
1687
|
+
if (!isNaN(numA) && !isNaN(numB)) {
|
|
1688
|
+
return direction === "asc" ? numA - numB : numB - numA;
|
|
1689
|
+
}
|
|
1690
|
+
return 0;
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return result;
|
|
1697
|
+
};
|
|
1698
|
+
if (Array.isArray(payload)) {
|
|
1699
|
+
processedPayload = applyFilterSearchSort(payload);
|
|
1700
|
+
} else {
|
|
1701
|
+
let foundArrayKey = "";
|
|
1702
|
+
for (const key in payload) {
|
|
1703
|
+
if (Array.isArray(payload[key])) {
|
|
1704
|
+
foundArrayKey = key;
|
|
1705
|
+
break;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
if (foundArrayKey) {
|
|
1709
|
+
const processedArray = applyFilterSearchSort(payload[foundArrayKey]);
|
|
1710
|
+
processedPayload = {
|
|
1711
|
+
...payload,
|
|
1712
|
+
[foundArrayKey]: processedArray
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return processedPayload;
|
|
1718
|
+
};
|
|
1380
1719
|
clientProto._updateDOM = function(topic, payload) {
|
|
1381
1720
|
if (typeof document === "undefined") return;
|
|
1382
1721
|
const elements = document.querySelectorAll(`[data-rt-bind="${topic}"]`);
|
|
1383
1722
|
elements.forEach((el) => {
|
|
1384
|
-
|
|
1385
|
-
|
|
1723
|
+
const processedPayload = this._applyDeclarativeDirectives(el, payload);
|
|
1724
|
+
if (el.getAttribute("data-rt-type") === "context" && typeof processedPayload === "object" && processedPayload !== null) {
|
|
1725
|
+
el._rtContext = processedPayload;
|
|
1386
1726
|
const processNode = (node) => {
|
|
1387
1727
|
if (node.hasAttribute("data-rt-text")) {
|
|
1388
1728
|
const key = node.getAttribute("data-rt-text");
|
|
1389
|
-
if (key &&
|
|
1729
|
+
if (key && processedPayload[key] !== void 0 && processedPayload[key] !== null) node.textContent = processedPayload[key];
|
|
1390
1730
|
}
|
|
1391
1731
|
if (node.hasAttribute("data-rt-html")) {
|
|
1392
1732
|
const key = node.getAttribute("data-rt-html");
|
|
1393
|
-
if (key &&
|
|
1394
|
-
node.innerHTML = sanitizeHTML(
|
|
1733
|
+
if (key && processedPayload[key] !== void 0 && processedPayload[key] !== null) {
|
|
1734
|
+
node.innerHTML = sanitizeHTML(processedPayload[key]);
|
|
1395
1735
|
}
|
|
1396
1736
|
}
|
|
1397
1737
|
if (node.hasAttribute("data-rt-attr")) {
|
|
@@ -1402,8 +1742,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1402
1742
|
if (parts.length === 2) {
|
|
1403
1743
|
const attrName = parts[0].trim();
|
|
1404
1744
|
const key = parts[1].trim();
|
|
1405
|
-
if (attrName && key &&
|
|
1406
|
-
node.setAttribute(attrName,
|
|
1745
|
+
if (attrName && key && processedPayload[key] !== void 0 && processedPayload[key] !== null) {
|
|
1746
|
+
node.setAttribute(attrName, processedPayload[key]);
|
|
1407
1747
|
}
|
|
1408
1748
|
}
|
|
1409
1749
|
});
|
|
@@ -1418,7 +1758,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1418
1758
|
const className = parts[0].trim();
|
|
1419
1759
|
const key = parts[1].trim();
|
|
1420
1760
|
const classNames = className.split(/\s+/).filter(Boolean);
|
|
1421
|
-
if (
|
|
1761
|
+
if (processedPayload[key]) {
|
|
1422
1762
|
classNames.forEach((c) => node.classList.add(c));
|
|
1423
1763
|
} else {
|
|
1424
1764
|
classNames.forEach((c) => node.classList.remove(c));
|
|
@@ -1430,7 +1770,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1430
1770
|
if (node.hasAttribute("data-rt-if")) {
|
|
1431
1771
|
const key = node.getAttribute("data-rt-if");
|
|
1432
1772
|
if (key) {
|
|
1433
|
-
if (
|
|
1773
|
+
if (processedPayload[key]) {
|
|
1434
1774
|
node.style.display = "";
|
|
1435
1775
|
} else {
|
|
1436
1776
|
node.style.display = "none";
|
|
@@ -1440,7 +1780,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1440
1780
|
if (node.hasAttribute("data-rt-hide")) {
|
|
1441
1781
|
const key = node.getAttribute("data-rt-hide");
|
|
1442
1782
|
if (key) {
|
|
1443
|
-
if (
|
|
1783
|
+
if (processedPayload[key]) {
|
|
1444
1784
|
node.style.display = "none";
|
|
1445
1785
|
} else {
|
|
1446
1786
|
node.style.display = "";
|
|
@@ -1453,25 +1793,172 @@ function attachDOMBinding(clientProto) {
|
|
|
1453
1793
|
return;
|
|
1454
1794
|
}
|
|
1455
1795
|
const template = resolveTemplate(el);
|
|
1456
|
-
if (template && typeof
|
|
1457
|
-
if (Array.isArray(
|
|
1796
|
+
if (template && typeof processedPayload === "object" && processedPayload !== null) {
|
|
1797
|
+
if (Array.isArray(processedPayload)) {
|
|
1458
1798
|
let combinedHTML = "";
|
|
1459
|
-
for (const item of
|
|
1799
|
+
for (const item of processedPayload) {
|
|
1460
1800
|
combinedHTML += renderTemplate(template, item);
|
|
1461
1801
|
}
|
|
1462
1802
|
scheduleDOMUpdate(el, combinedHTML);
|
|
1463
1803
|
} else {
|
|
1464
|
-
scheduleDOMUpdate(el, renderTemplate(template,
|
|
1804
|
+
scheduleDOMUpdate(el, renderTemplate(template, processedPayload));
|
|
1465
1805
|
}
|
|
1466
1806
|
return;
|
|
1467
1807
|
}
|
|
1468
1808
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1469
|
-
el.value = typeof
|
|
1809
|
+
el.value = typeof processedPayload === "object" ? processedPayload.value !== void 0 ? processedPayload.value : "" : processedPayload;
|
|
1810
|
+
} else if (template) {
|
|
1811
|
+
el.innerHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1470
1812
|
} else {
|
|
1471
|
-
|
|
1813
|
+
const rawHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1814
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1472
1815
|
}
|
|
1473
1816
|
});
|
|
1474
1817
|
};
|
|
1818
|
+
clientProto._resolveImports = async function(container) {
|
|
1819
|
+
if (typeof document === "undefined") return;
|
|
1820
|
+
const root = container || document.body || document;
|
|
1821
|
+
if (!root || typeof root.querySelectorAll !== "function") return;
|
|
1822
|
+
const elements = root.querySelectorAll("[data-import]");
|
|
1823
|
+
if (elements.length === 0) return;
|
|
1824
|
+
const resolveNode = async (el, resolvingSet) => {
|
|
1825
|
+
const src = el.getAttribute("data-import");
|
|
1826
|
+
if (!src) return;
|
|
1827
|
+
if (resolvingSet.has(src)) {
|
|
1828
|
+
console.warn(`[Dolphin Component Warning]: Circular import detected for "${src}". Skipping resolving.`);
|
|
1829
|
+
el.innerHTML = `<span style="color:red;font-weight:bold;">Circular import: ${src}</span>`;
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
resolvingSet.add(src);
|
|
1833
|
+
let promise = componentPromiseCache.get(src);
|
|
1834
|
+
if (!promise) {
|
|
1835
|
+
promise = fetch(src).then((res) => {
|
|
1836
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1837
|
+
return res.text();
|
|
1838
|
+
});
|
|
1839
|
+
promise.catch(() => componentPromiseCache.delete(src));
|
|
1840
|
+
componentPromiseCache.set(src, promise);
|
|
1841
|
+
}
|
|
1842
|
+
let content = "";
|
|
1843
|
+
try {
|
|
1844
|
+
content = await promise;
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
console.error(`[Dolphin Component Error]: Failed to fetch component "${src}":`, err);
|
|
1847
|
+
content = `<span style="color:red;font-weight:bold;">Failed to import ${src}</span>`;
|
|
1848
|
+
}
|
|
1849
|
+
el.innerHTML = sanitizeHTML(content);
|
|
1850
|
+
el.removeAttribute("data-import");
|
|
1851
|
+
const nestedElements = el.querySelectorAll("[data-import]");
|
|
1852
|
+
if (nestedElements.length > 0) {
|
|
1853
|
+
const subPromises = Array.from(nestedElements).map((child) => resolveNode(child, new Set(resolvingSet)));
|
|
1854
|
+
await Promise.all(subPromises);
|
|
1855
|
+
}
|
|
1856
|
+
this._scanStoreBinds();
|
|
1857
|
+
this._scanAndFetchAPIBinds();
|
|
1858
|
+
};
|
|
1859
|
+
const promises = Array.from(elements).map((el) => resolveNode(el, /* @__PURE__ */ new Set()));
|
|
1860
|
+
await Promise.all(promises);
|
|
1861
|
+
};
|
|
1862
|
+
clientProto._initSPARouter = function() {
|
|
1863
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1864
|
+
if (this._routerInitialized) return;
|
|
1865
|
+
this._routerInitialized = true;
|
|
1866
|
+
let _spaAbortController = null;
|
|
1867
|
+
const findViewport = () => {
|
|
1868
|
+
const selector = this.options.routerViewport || "main, #viewport, body";
|
|
1869
|
+
const selectors = selector.split(",").map((s) => s.trim());
|
|
1870
|
+
for (const sel of selectors) {
|
|
1871
|
+
const el = document.querySelector(sel);
|
|
1872
|
+
if (el) return el;
|
|
1873
|
+
}
|
|
1874
|
+
return document.body;
|
|
1875
|
+
};
|
|
1876
|
+
const loadPage = async (url, pushState = true) => {
|
|
1877
|
+
try {
|
|
1878
|
+
if (this.options.debug) {
|
|
1879
|
+
console.log(`%c\u{1F6E3}\uFE0F [Dolphin Router]: Navigating to ${url}...`, "color: #3b82f6; font-weight: bold;");
|
|
1880
|
+
}
|
|
1881
|
+
if (_spaAbortController) {
|
|
1882
|
+
_spaAbortController.abort();
|
|
1883
|
+
}
|
|
1884
|
+
_spaAbortController = new AbortController();
|
|
1885
|
+
const signal = _spaAbortController.signal;
|
|
1886
|
+
const viewport = findViewport();
|
|
1887
|
+
if (this.options.routerTransitions && viewport) {
|
|
1888
|
+
viewport.classList.add("dolphin-fade-out");
|
|
1889
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1890
|
+
}
|
|
1891
|
+
const response = await fetch(url, { signal });
|
|
1892
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1893
|
+
const html = await response.text();
|
|
1894
|
+
_spaAbortController = null;
|
|
1895
|
+
const parser = new DOMParser();
|
|
1896
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
1897
|
+
if (doc.title) {
|
|
1898
|
+
document.title = doc.title;
|
|
1899
|
+
}
|
|
1900
|
+
const newViewport = doc.querySelector(this.options.routerViewport || "main, #viewport, body");
|
|
1901
|
+
const currentViewport = findViewport();
|
|
1902
|
+
if (newViewport && currentViewport) {
|
|
1903
|
+
currentViewport.innerHTML = newViewport.innerHTML;
|
|
1904
|
+
Array.from(newViewport.attributes).forEach((attr) => {
|
|
1905
|
+
currentViewport.setAttribute(attr.name, attr.value);
|
|
1906
|
+
});
|
|
1907
|
+
} else if (currentViewport) {
|
|
1908
|
+
currentViewport.innerHTML = doc.body.innerHTML;
|
|
1909
|
+
}
|
|
1910
|
+
if (pushState) {
|
|
1911
|
+
window.history.pushState({ dolphinSpa: true, url }, "", url);
|
|
1912
|
+
}
|
|
1913
|
+
if (this.options.routerTransitions && currentViewport) {
|
|
1914
|
+
currentViewport.classList.remove("dolphin-fade-out");
|
|
1915
|
+
currentViewport.classList.add("dolphin-fade-in");
|
|
1916
|
+
setTimeout(() => currentViewport.classList.remove("dolphin-fade-in"), 300);
|
|
1917
|
+
}
|
|
1918
|
+
await this._resolveImports(currentViewport);
|
|
1919
|
+
this._scanStoreBinds();
|
|
1920
|
+
this._scanAndFetchAPIBinds();
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
if (err && err.name === "AbortError") return;
|
|
1923
|
+
console.error("[Dolphin Router Error]: Failed to route page:", err);
|
|
1924
|
+
window.location.href = url;
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
this.addDomListener(document, "click", (e) => {
|
|
1928
|
+
const anchor = e.target.closest("a");
|
|
1929
|
+
if (!anchor) return;
|
|
1930
|
+
if (!anchor.hasAttribute("data-spa") && anchor.getAttribute("data-spa") !== "true") return;
|
|
1931
|
+
const href = anchor.getAttribute("href");
|
|
1932
|
+
if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
|
|
1933
|
+
const url = new URL(href, window.location.href);
|
|
1934
|
+
if (url.origin !== window.location.origin) return;
|
|
1935
|
+
e.preventDefault();
|
|
1936
|
+
loadPage(href);
|
|
1937
|
+
});
|
|
1938
|
+
this.addDomListener(window, "popstate", (e) => {
|
|
1939
|
+
if (e.state && e.state.dolphinSpa) {
|
|
1940
|
+
loadPage(e.state.url, false);
|
|
1941
|
+
} else if (e.state === null) {
|
|
1942
|
+
loadPage(window.location.pathname, false);
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
if (this.options.routerTransitions) {
|
|
1946
|
+
const style = document.createElement("style");
|
|
1947
|
+
style.innerHTML = `
|
|
1948
|
+
.dolphin-fade-out {
|
|
1949
|
+
opacity: 0;
|
|
1950
|
+
transition: opacity 0.15s ease-in-out;
|
|
1951
|
+
}
|
|
1952
|
+
.dolphin-fade-in {
|
|
1953
|
+
opacity: 0;
|
|
1954
|
+
}
|
|
1955
|
+
main, #viewport, body {
|
|
1956
|
+
transition: opacity 0.15s ease-in-out;
|
|
1957
|
+
}
|
|
1958
|
+
`;
|
|
1959
|
+
document.head.appendChild(style);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1475
1962
|
}
|
|
1476
1963
|
|
|
1477
1964
|
// src/offline.ts
|
|
@@ -1549,6 +2036,10 @@ var DolphinOffline = class {
|
|
|
1549
2036
|
const store = transaction.objectStore("cache");
|
|
1550
2037
|
store.put({ data, timestamp: Date.now() }, key);
|
|
1551
2038
|
transaction.oncomplete = () => resolve();
|
|
2039
|
+
transaction.onerror = () => {
|
|
2040
|
+
console.warn("[Dolphin Offline] setCache write failed for key:", key);
|
|
2041
|
+
resolve();
|
|
2042
|
+
};
|
|
1552
2043
|
} catch {
|
|
1553
2044
|
resolve();
|
|
1554
2045
|
}
|
|
@@ -1571,6 +2062,11 @@ var DolphinOffline = class {
|
|
|
1571
2062
|
const store = transaction.objectStore("mutations");
|
|
1572
2063
|
store.add(mutation);
|
|
1573
2064
|
transaction.oncomplete = () => resolve();
|
|
2065
|
+
transaction.onerror = () => {
|
|
2066
|
+
console.warn("[Dolphin Offline] queueMutation write failed:", method, path);
|
|
2067
|
+
this.memoryMutations.push(mutation);
|
|
2068
|
+
resolve();
|
|
2069
|
+
};
|
|
1574
2070
|
} catch {
|
|
1575
2071
|
resolve();
|
|
1576
2072
|
}
|
|
@@ -1603,6 +2099,10 @@ var DolphinOffline = class {
|
|
|
1603
2099
|
const store = transaction.objectStore("mutations");
|
|
1604
2100
|
store.delete(id);
|
|
1605
2101
|
transaction.oncomplete = () => resolve();
|
|
2102
|
+
transaction.onerror = () => {
|
|
2103
|
+
console.warn("[Dolphin Offline] removeMutation failed for id:", id);
|
|
2104
|
+
resolve();
|
|
2105
|
+
};
|
|
1606
2106
|
} catch {
|
|
1607
2107
|
resolve();
|
|
1608
2108
|
}
|
|
@@ -1948,6 +2448,9 @@ function attachDragDrop(clientProto) {
|
|
|
1948
2448
|
function attachCollab(clientProto) {
|
|
1949
2449
|
clientProto._initCollab = function() {
|
|
1950
2450
|
if (typeof document === "undefined") return;
|
|
2451
|
+
const remoteCursors = /* @__PURE__ */ new Map();
|
|
2452
|
+
const cursorStaleTimers = /* @__PURE__ */ new Map();
|
|
2453
|
+
const CURSOR_STALE_MS = 5e3;
|
|
1951
2454
|
this.addDomListener(document, "mousemove", (e) => {
|
|
1952
2455
|
const shareContainers = document.querySelectorAll("[data-rt-cursor-share]");
|
|
1953
2456
|
shareContainers.forEach((container) => {
|
|
@@ -1984,6 +2487,7 @@ function attachCollab(clientProto) {
|
|
|
1984
2487
|
if (e.target._typingTimer) clearTimeout(e.target._typingTimer);
|
|
1985
2488
|
e.target._typingTimer = setTimeout(() => {
|
|
1986
2489
|
e.target._isTyping = false;
|
|
2490
|
+
e.target._typingTimer = null;
|
|
1987
2491
|
publishTyping(false);
|
|
1988
2492
|
}, 2e3);
|
|
1989
2493
|
});
|
|
@@ -2007,21 +2511,32 @@ function attachCollab(clientProto) {
|
|
|
2007
2511
|
if (remoteDeviceId === this.deviceId) return;
|
|
2008
2512
|
const container = document.querySelector(`[data-rt-cursor-share="${room}"]`);
|
|
2009
2513
|
if (!container) return;
|
|
2010
|
-
|
|
2011
|
-
|
|
2514
|
+
const cursorKey = `${room}::${remoteDeviceId}`;
|
|
2515
|
+
let cursorEl = remoteCursors.get(cursorKey);
|
|
2516
|
+
if (!cursorEl || !document.contains(cursorEl)) {
|
|
2012
2517
|
cursorEl = document.createElement("div");
|
|
2013
2518
|
cursorEl.className = `rt-cursor rt-cursor-${remoteDeviceId}`;
|
|
2014
2519
|
cursorEl.style.position = "absolute";
|
|
2015
2520
|
cursorEl.style.width = "10px";
|
|
2016
2521
|
cursorEl.style.height = "10px";
|
|
2017
2522
|
cursorEl.style.borderRadius = "50%";
|
|
2018
|
-
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
|
|
2523
|
+
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
|
|
2019
2524
|
cursorEl.style.pointerEvents = "none";
|
|
2020
2525
|
container.appendChild(cursorEl);
|
|
2526
|
+
remoteCursors.set(cursorKey, cursorEl);
|
|
2021
2527
|
}
|
|
2022
2528
|
const box = container.getBoundingClientRect();
|
|
2023
2529
|
cursorEl.style.left = payload.x * box.width + "px";
|
|
2024
2530
|
cursorEl.style.top = payload.y * box.height + "px";
|
|
2531
|
+
if (cursorStaleTimers.has(cursorKey)) {
|
|
2532
|
+
clearTimeout(cursorStaleTimers.get(cursorKey));
|
|
2533
|
+
}
|
|
2534
|
+
cursorStaleTimers.set(cursorKey, setTimeout(() => {
|
|
2535
|
+
const el = remoteCursors.get(cursorKey);
|
|
2536
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2537
|
+
remoteCursors.delete(cursorKey);
|
|
2538
|
+
cursorStaleTimers.delete(cursorKey);
|
|
2539
|
+
}, CURSOR_STALE_MS));
|
|
2025
2540
|
});
|
|
2026
2541
|
this.subscribe("collab/+/crdt", (payload, topic) => {
|
|
2027
2542
|
if (payload.deviceId === this.deviceId) return;
|
|
@@ -2039,6 +2554,14 @@ function attachCollab(clientProto) {
|
|
|
2039
2554
|
}
|
|
2040
2555
|
});
|
|
2041
2556
|
});
|
|
2557
|
+
this._collabCleanup = () => {
|
|
2558
|
+
cursorStaleTimers.forEach((t) => clearTimeout(t));
|
|
2559
|
+
cursorStaleTimers.clear();
|
|
2560
|
+
remoteCursors.forEach((el) => {
|
|
2561
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2562
|
+
});
|
|
2563
|
+
remoteCursors.clear();
|
|
2564
|
+
};
|
|
2042
2565
|
};
|
|
2043
2566
|
}
|
|
2044
2567
|
|