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.cjs
CHANGED
|
@@ -47,7 +47,21 @@ var APIHandler = class {
|
|
|
47
47
|
target.requestDirect = (method, path, body, options) => {
|
|
48
48
|
return this.requestDirect(method, path, body, options);
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
target._findCSRFToken = () => this._findCSRFToken();
|
|
51
|
+
target._resolveBaseUrl = (path) => this._resolveBaseUrl(path);
|
|
52
|
+
target._normalizeValidationErrors = (errData) => this._normalizeValidationErrors(errData);
|
|
53
|
+
const methods = [
|
|
54
|
+
"get",
|
|
55
|
+
"post",
|
|
56
|
+
"put",
|
|
57
|
+
"patch",
|
|
58
|
+
"del",
|
|
59
|
+
"request",
|
|
60
|
+
"requestDirect",
|
|
61
|
+
"_findCSRFToken",
|
|
62
|
+
"_resolveBaseUrl",
|
|
63
|
+
"_normalizeValidationErrors"
|
|
64
|
+
];
|
|
51
65
|
return new Proxy(target, {
|
|
52
66
|
get: (t, prop) => {
|
|
53
67
|
if (typeof prop === "string" && !methods.includes(prop)) {
|
|
@@ -57,6 +71,108 @@ var APIHandler = class {
|
|
|
57
71
|
}
|
|
58
72
|
});
|
|
59
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Attempts to find a CSRF token in the document (meta tags, forms, or cookies).
|
|
76
|
+
* Works for Laravel, CakePHP, WordPress, Express, NestJS, etc.
|
|
77
|
+
* @private
|
|
78
|
+
*/
|
|
79
|
+
_findCSRFToken() {
|
|
80
|
+
if (typeof document === "undefined") return null;
|
|
81
|
+
const metaNames = ["csrf-token", "_csrf", "xsrf-token", "csrf_token"];
|
|
82
|
+
for (const name of metaNames) {
|
|
83
|
+
const metaEl = document.querySelector(`meta[name="${name}"], meta[content][name$="${name}"]`);
|
|
84
|
+
if (metaEl) {
|
|
85
|
+
const token = metaEl.getAttribute("content");
|
|
86
|
+
if (token) return token;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const inputNames = ["_csrfToken", "_token", "_csrf", "csrf_token"];
|
|
90
|
+
for (const name of inputNames) {
|
|
91
|
+
const inputEl = document.querySelector(`input[type="hidden"][name="${name}"]`);
|
|
92
|
+
if (inputEl && inputEl.value) return inputEl.value;
|
|
93
|
+
}
|
|
94
|
+
const cookies = ["csrfToken", "XSRF-TOKEN", "_csrf", "csrf_token"];
|
|
95
|
+
for (const name of cookies) {
|
|
96
|
+
const match = document.cookie.match(new RegExp("(^|;\\s*)" + name + "=([^;]*)"));
|
|
97
|
+
if (match) return decodeURIComponent(match[2]);
|
|
98
|
+
}
|
|
99
|
+
const wpNonce = typeof window !== "undefined" && window.wpApiSettings?.nonce;
|
|
100
|
+
if (wpNonce) return wpNonce;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Dynamically resolves the Base Path/URL from `<base href="...">` or subfolders.
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
_resolveBaseUrl(path) {
|
|
108
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
let baseUrl = this.client.httpUrl;
|
|
112
|
+
if (typeof document !== "undefined") {
|
|
113
|
+
const baseEl = document.querySelector("base[href]");
|
|
114
|
+
if (baseEl) {
|
|
115
|
+
const href = baseEl.getAttribute("href") || "";
|
|
116
|
+
if (href && href !== "/") {
|
|
117
|
+
const cleanHref = href.endsWith("/") ? href.slice(0, -1) : href;
|
|
118
|
+
baseUrl = `${this.client.httpUrl}${cleanHref.startsWith("/") ? cleanHref : "/" + cleanHref}`;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const metaBase = document.querySelector('meta[name="base-path"]');
|
|
122
|
+
if (metaBase) {
|
|
123
|
+
const content = metaBase.getAttribute("content") || "";
|
|
124
|
+
if (content && content !== "/") {
|
|
125
|
+
const cleanContent = content.endsWith("/") ? content.slice(0, -1) : content;
|
|
126
|
+
baseUrl = `${this.client.httpUrl}${cleanContent.startsWith("/") ? cleanContent : "/" + cleanContent}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const cleanPath = path.startsWith("/") ? path : "/" + path;
|
|
132
|
+
return `${baseUrl}${cleanPath}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Normalizes backend validation errors from major PHP and Node.js frameworks
|
|
136
|
+
* into a unified { [field]: message } object.
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_normalizeValidationErrors(errData) {
|
|
140
|
+
const normalized = {};
|
|
141
|
+
if (!errData || typeof errData !== "object") return normalized;
|
|
142
|
+
const errors = errData.errors || errData.validationErrors || errData;
|
|
143
|
+
if (Array.isArray(errors)) {
|
|
144
|
+
for (const err of errors) {
|
|
145
|
+
if (err && typeof err === "object") {
|
|
146
|
+
const field = err.path || err.param || err.field || err.property;
|
|
147
|
+
const msg = err.msg || err.message || err.error;
|
|
148
|
+
if (field && msg) {
|
|
149
|
+
normalized[field] = Array.isArray(msg) ? msg[0] : msg;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
155
|
+
if (typeof errors === "object" && errors !== null) {
|
|
156
|
+
for (const key in errors) {
|
|
157
|
+
const val = errors[key];
|
|
158
|
+
if (!val) continue;
|
|
159
|
+
if (Array.isArray(val)) {
|
|
160
|
+
if (val.length > 0) {
|
|
161
|
+
normalized[key] = String(val[0]);
|
|
162
|
+
}
|
|
163
|
+
} else if (typeof val === "object") {
|
|
164
|
+
const innerKeys = Object.keys(val);
|
|
165
|
+
if (innerKeys.length > 0) {
|
|
166
|
+
const firstInnerKey = innerKeys[0];
|
|
167
|
+
normalized[key] = String(val[firstInnerKey]);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
normalized[key] = String(val);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
60
176
|
/**
|
|
61
177
|
* Intercept request for offline-first caching and queuing.
|
|
62
178
|
*/
|
|
@@ -107,30 +223,68 @@ var APIHandler = class {
|
|
|
107
223
|
*/
|
|
108
224
|
async requestDirect(method, path, body = null, options = {}) {
|
|
109
225
|
const _isRetry = options._isRetry === true;
|
|
110
|
-
|
|
226
|
+
let finalMethod = method.toUpperCase();
|
|
227
|
+
let finalBody = body;
|
|
228
|
+
const headers = {
|
|
229
|
+
"Content-Type": "application/json",
|
|
230
|
+
...options.headers || {}
|
|
231
|
+
};
|
|
232
|
+
if (["PUT", "PATCH", "DELETE"].includes(finalMethod)) {
|
|
233
|
+
if (this.client.options.methodSpoofing || options.methodSpoofing) {
|
|
234
|
+
headers["X-HTTP-Method-Override"] = finalMethod;
|
|
235
|
+
if (finalBody instanceof FormData) {
|
|
236
|
+
finalBody.append("_method", finalMethod);
|
|
237
|
+
} else if (finalBody && typeof finalBody === "object") {
|
|
238
|
+
finalBody = {
|
|
239
|
+
...finalBody,
|
|
240
|
+
_method: finalMethod
|
|
241
|
+
};
|
|
242
|
+
} else if (!finalBody) {
|
|
243
|
+
finalBody = { _method: finalMethod };
|
|
244
|
+
}
|
|
245
|
+
finalMethod = "POST";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const url = this._resolveBaseUrl(path);
|
|
111
249
|
if (this.client.options.debug) {
|
|
112
|
-
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path,
|
|
250
|
+
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path, finalBody || "");
|
|
113
251
|
}
|
|
114
252
|
const controller = new AbortController();
|
|
115
253
|
const timeoutId = setTimeout(
|
|
116
254
|
() => controller.abort(),
|
|
117
255
|
this.client.options.timeout
|
|
118
256
|
);
|
|
119
|
-
const headers = {
|
|
120
|
-
"Content-Type": "application/json",
|
|
121
|
-
...options.headers || {}
|
|
122
|
-
};
|
|
123
257
|
if (this.client.accessToken) {
|
|
124
258
|
headers["Authorization"] = `Bearer ${this.client.accessToken}`;
|
|
125
259
|
}
|
|
260
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase())) {
|
|
261
|
+
const csrfToken = this._findCSRFToken();
|
|
262
|
+
if (csrfToken) {
|
|
263
|
+
headers["X-CSRF-Token"] = csrfToken;
|
|
264
|
+
headers["X-XSRF-TOKEN"] = csrfToken;
|
|
265
|
+
headers["X-CSRFToken"] = csrfToken;
|
|
266
|
+
headers["X-WP-Nonce"] = csrfToken;
|
|
267
|
+
if (finalBody && typeof finalBody === "object") {
|
|
268
|
+
if (!finalBody._csrfToken && !finalBody._token && !finalBody._csrf) {
|
|
269
|
+
finalBody = {
|
|
270
|
+
...finalBody,
|
|
271
|
+
_csrfToken: csrfToken,
|
|
272
|
+
_token: csrfToken,
|
|
273
|
+
_csrf: csrfToken
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
126
279
|
const fetchOptions = { ...options };
|
|
127
280
|
delete fetchOptions._isRetry;
|
|
281
|
+
delete fetchOptions.methodSpoofing;
|
|
128
282
|
try {
|
|
129
283
|
const response = await fetch(url, {
|
|
130
|
-
method,
|
|
284
|
+
method: finalMethod,
|
|
131
285
|
headers,
|
|
132
286
|
signal: controller.signal,
|
|
133
|
-
...
|
|
287
|
+
...finalBody ? { body: JSON.stringify(finalBody) } : {},
|
|
134
288
|
...fetchOptions
|
|
135
289
|
});
|
|
136
290
|
clearTimeout(timeoutId);
|
|
@@ -162,6 +316,14 @@ var APIHandler = class {
|
|
|
162
316
|
if (this.client.options.debug) {
|
|
163
317
|
console.error(`%c\u274C [Dolphin API Error]:`, "color: #ef4444; font-weight: bold;", method.toUpperCase(), path, err);
|
|
164
318
|
}
|
|
319
|
+
if (err && typeof err === "object" && err.data) {
|
|
320
|
+
const normErrors = this._normalizeValidationErrors(err.data);
|
|
321
|
+
if (Object.keys(normErrors).length > 0) {
|
|
322
|
+
for (const field in normErrors) {
|
|
323
|
+
this.client.publish(`errors/${field}`, normErrors[field]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
165
327
|
if (err.name === "AbortError") {
|
|
166
328
|
throw { status: 408, data: { error: "Request timed out" } };
|
|
167
329
|
}
|
|
@@ -295,12 +457,15 @@ var DolphinStore = class {
|
|
|
295
457
|
data;
|
|
296
458
|
listeners;
|
|
297
459
|
subscribed;
|
|
460
|
+
/** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions (was: subscriptions never removed) */
|
|
461
|
+
_unsubscribers;
|
|
298
462
|
/** @param {DolphinClient} client */
|
|
299
463
|
constructor(client) {
|
|
300
464
|
this.client = client;
|
|
301
465
|
this.data = /* @__PURE__ */ new Map();
|
|
302
466
|
this.listeners = /* @__PURE__ */ new Set();
|
|
303
467
|
this.subscribed = /* @__PURE__ */ new Set();
|
|
468
|
+
this._unsubscribers = /* @__PURE__ */ new Map();
|
|
304
469
|
return new Proxy(this, {
|
|
305
470
|
get: (target, prop) => {
|
|
306
471
|
if (prop in target) return target[prop];
|
|
@@ -352,9 +517,12 @@ var DolphinStore = class {
|
|
|
352
517
|
state.error = null;
|
|
353
518
|
this._applyTransform(state);
|
|
354
519
|
if (!this.subscribed.has(name)) {
|
|
355
|
-
this.client.
|
|
520
|
+
const unsubscribe = () => this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
521
|
+
const updateHandler = (update) => {
|
|
356
522
|
this._handleRemoteUpdate(name, update);
|
|
357
|
-
}
|
|
523
|
+
};
|
|
524
|
+
this.client.subscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
525
|
+
this._unsubscribers.set(name, unsubscribe);
|
|
358
526
|
this.subscribed.add(name);
|
|
359
527
|
}
|
|
360
528
|
} catch (e) {
|
|
@@ -411,6 +579,22 @@ var DolphinStore = class {
|
|
|
411
579
|
_notify() {
|
|
412
580
|
this.listeners.forEach((l) => l());
|
|
413
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Clean up all WebSocket subscriptions and listeners.
|
|
584
|
+
* Call this when the store is no longer needed to prevent resource leaks.
|
|
585
|
+
*/
|
|
586
|
+
destroy() {
|
|
587
|
+
this._unsubscribers.forEach((unsub) => {
|
|
588
|
+
try {
|
|
589
|
+
unsub();
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
this._unsubscribers.clear();
|
|
594
|
+
this.subscribed.clear();
|
|
595
|
+
this.listeners.clear();
|
|
596
|
+
this.data.clear();
|
|
597
|
+
}
|
|
414
598
|
};
|
|
415
599
|
|
|
416
600
|
// src/core.ts
|
|
@@ -430,6 +614,8 @@ var DolphinClient = class {
|
|
|
430
614
|
fileHandlers;
|
|
431
615
|
_offlineQueue;
|
|
432
616
|
reconnectAttempts;
|
|
617
|
+
/** @fix: Store timer ID so disconnect() can cancel pending reconnects (was: memory/logic leak) */
|
|
618
|
+
_reconnectTimer;
|
|
433
619
|
_attachedListeners;
|
|
434
620
|
constructor(url = "", deviceId = "", options = {}) {
|
|
435
621
|
if (!url && typeof window !== "undefined") url = window.location.host;
|
|
@@ -439,7 +625,7 @@ var DolphinClient = class {
|
|
|
439
625
|
else if (typeof window !== "undefined") protocol = window.location.protocol;
|
|
440
626
|
this.host = (url || "localhost").replace(/\/$/, "").replace(/^https?:\/\//, "");
|
|
441
627
|
this.httpUrl = `${protocol}//${this.host}`;
|
|
442
|
-
this.deviceId = deviceId || "web_" + Math.random().toString(36).
|
|
628
|
+
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));
|
|
443
629
|
this.options = {
|
|
444
630
|
timeout: 15e3,
|
|
445
631
|
chunkSize: 65536,
|
|
@@ -447,6 +633,9 @@ var DolphinClient = class {
|
|
|
447
633
|
maxReconnect: 5,
|
|
448
634
|
autoRefreshToken: true,
|
|
449
635
|
debug: false,
|
|
636
|
+
methodSpoofing: false,
|
|
637
|
+
routerViewport: "main, #viewport, body",
|
|
638
|
+
routerTransitions: true,
|
|
450
639
|
...options
|
|
451
640
|
};
|
|
452
641
|
this.socket = null;
|
|
@@ -466,6 +655,7 @@ var DolphinClient = class {
|
|
|
466
655
|
this.fileHandlers = /* @__PURE__ */ new Set();
|
|
467
656
|
this._offlineQueue = [];
|
|
468
657
|
this.reconnectAttempts = 0;
|
|
658
|
+
this._reconnectTimer = null;
|
|
469
659
|
this._attachedListeners = [];
|
|
470
660
|
if (typeof window !== "undefined" && typeof this._initDOMBinding === "function") {
|
|
471
661
|
this._initDOMBinding();
|
|
@@ -494,6 +684,9 @@ var DolphinClient = class {
|
|
|
494
684
|
// ── WebSocket ─────────────────────────────────────────────────────────────
|
|
495
685
|
/** Connect to the Dolphin realtime server */
|
|
496
686
|
async connect() {
|
|
687
|
+
if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) {
|
|
688
|
+
return Promise.resolve();
|
|
689
|
+
}
|
|
497
690
|
return new Promise((resolve, reject) => {
|
|
498
691
|
const protocol = this.httpUrl.startsWith("https") ? "wss:" : "ws:";
|
|
499
692
|
const wsUrl = `${protocol}//${this.host}/realtime?deviceId=${this.deviceId}`;
|
|
@@ -518,11 +711,18 @@ var DolphinClient = class {
|
|
|
518
711
|
}
|
|
519
712
|
/** Disconnect cleanly */
|
|
520
713
|
disconnect() {
|
|
714
|
+
if (this._reconnectTimer !== null) {
|
|
715
|
+
clearTimeout(this._reconnectTimer);
|
|
716
|
+
this._reconnectTimer = null;
|
|
717
|
+
}
|
|
521
718
|
if (this.socket) {
|
|
522
719
|
this.socket.onclose = null;
|
|
523
720
|
this.socket.close();
|
|
524
721
|
this.socket = null;
|
|
525
722
|
}
|
|
723
|
+
if (typeof this._collabCleanup === "function") {
|
|
724
|
+
this._collabCleanup();
|
|
725
|
+
}
|
|
526
726
|
this.cleanupDomListeners();
|
|
527
727
|
}
|
|
528
728
|
/** @private */
|
|
@@ -610,8 +810,11 @@ var DolphinClient = class {
|
|
|
610
810
|
this.reconnectAttempts++;
|
|
611
811
|
const delay = Math.pow(2, this.reconnectAttempts) * 1e3;
|
|
612
812
|
console.log(`[Dolphin] Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts})...`);
|
|
613
|
-
|
|
614
|
-
|
|
813
|
+
this._reconnectTimer = setTimeout(() => {
|
|
814
|
+
this._reconnectTimer = null;
|
|
815
|
+
this.connect().catch(() => {
|
|
816
|
+
});
|
|
817
|
+
}, delay);
|
|
615
818
|
} else {
|
|
616
819
|
console.error("[Dolphin] Max reconnect attempts reached.");
|
|
617
820
|
}
|
|
@@ -789,6 +992,7 @@ var DolphinClient = class {
|
|
|
789
992
|
|
|
790
993
|
// src/dom.ts
|
|
791
994
|
function attachDOMBinding(clientProto) {
|
|
995
|
+
const componentPromiseCache = /* @__PURE__ */ new Map();
|
|
792
996
|
function escapeRegExp(str) {
|
|
793
997
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
794
998
|
}
|
|
@@ -1035,7 +1239,9 @@ function attachDOMBinding(clientProto) {
|
|
|
1035
1239
|
const scheduleFn = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 0);
|
|
1036
1240
|
scheduleFn(() => {
|
|
1037
1241
|
pendingUpdates.forEach((html, el) => {
|
|
1038
|
-
|
|
1242
|
+
if (el.isConnected !== false) {
|
|
1243
|
+
patchDOM(el, html);
|
|
1244
|
+
}
|
|
1039
1245
|
});
|
|
1040
1246
|
pendingUpdates.clear();
|
|
1041
1247
|
rafScheduled = false;
|
|
@@ -1174,7 +1380,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1174
1380
|
if (this._domInitialized) return;
|
|
1175
1381
|
this._domInitialized = true;
|
|
1176
1382
|
const PUSH_EVENTS = ["input", "change", "keyup", "paste", "blur"];
|
|
1177
|
-
const debounceTimers = /* @__PURE__ */ new
|
|
1383
|
+
const debounceTimers = /* @__PURE__ */ new WeakMap();
|
|
1178
1384
|
PUSH_EVENTS.forEach((evtName) => {
|
|
1179
1385
|
this.addDomListener(document, evtName, (e) => {
|
|
1180
1386
|
if (!e.target || !e.target.getAttribute) return;
|
|
@@ -1227,6 +1433,14 @@ function attachDOMBinding(clientProto) {
|
|
|
1227
1433
|
const rtTopic = e.target.getAttribute("data-rt-submit");
|
|
1228
1434
|
const apiTarget = e.target.getAttribute("data-api-submit");
|
|
1229
1435
|
if (rtTopic || apiTarget) {
|
|
1436
|
+
const formInputs = e.target.querySelectorAll("[name]");
|
|
1437
|
+
formInputs.forEach((inputEl) => {
|
|
1438
|
+
const name = inputEl.name;
|
|
1439
|
+
if (name) {
|
|
1440
|
+
this.publish(`errors/${name}`, "");
|
|
1441
|
+
inputEl.classList.remove("invalid");
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1230
1444
|
const validatedInputs = e.target.querySelectorAll("[data-rt-validate]");
|
|
1231
1445
|
let formIsValid = true;
|
|
1232
1446
|
if (validatedInputs.length > 0 && typeof this.validateField === "function") {
|
|
@@ -1240,9 +1454,6 @@ function attachDOMBinding(clientProto) {
|
|
|
1240
1454
|
formIsValid = false;
|
|
1241
1455
|
inputEl.classList.add("invalid");
|
|
1242
1456
|
this.publish(`errors/${name}`, errorMsg);
|
|
1243
|
-
} else {
|
|
1244
|
-
inputEl.classList.remove("invalid");
|
|
1245
|
-
this.publish(`errors/${name}`, "");
|
|
1246
1457
|
}
|
|
1247
1458
|
}
|
|
1248
1459
|
});
|
|
@@ -1270,8 +1481,11 @@ function attachDOMBinding(clientProto) {
|
|
|
1270
1481
|
resolvedTarget = resolvedTarget.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1271
1482
|
}
|
|
1272
1483
|
const parts = resolvedTarget.trim().split(" ");
|
|
1273
|
-
|
|
1484
|
+
let method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
|
|
1274
1485
|
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
1486
|
+
if (data._method) {
|
|
1487
|
+
method = String(data._method).toUpperCase();
|
|
1488
|
+
}
|
|
1275
1489
|
try {
|
|
1276
1490
|
const result = await this.api.request(method, path, data);
|
|
1277
1491
|
const resultBind = e.target.getAttribute("data-api-result");
|
|
@@ -1358,6 +1572,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1358
1572
|
});
|
|
1359
1573
|
this._scanAndFetchAPIBinds();
|
|
1360
1574
|
this._scanStoreBinds();
|
|
1575
|
+
this._resolveImports();
|
|
1576
|
+
this._initSPARouter();
|
|
1361
1577
|
};
|
|
1362
1578
|
clientProto._scanAndFetchAPIBinds = async function() {
|
|
1363
1579
|
if (typeof document === "undefined") return;
|
|
@@ -1365,6 +1581,10 @@ function attachDOMBinding(clientProto) {
|
|
|
1365
1581
|
for (const el of Array.from(elements)) {
|
|
1366
1582
|
const path = el.getAttribute("data-api-get");
|
|
1367
1583
|
if (!path) continue;
|
|
1584
|
+
if (typeof el.hasAttribute === "function" && el.hasAttribute("data-api-initialized")) continue;
|
|
1585
|
+
if (typeof el.setAttribute === "function") {
|
|
1586
|
+
el.setAttribute("data-api-initialized", "true");
|
|
1587
|
+
}
|
|
1368
1588
|
try {
|
|
1369
1589
|
const result = await this.api.get(path);
|
|
1370
1590
|
const apiStore = el.getAttribute("data-api-store");
|
|
@@ -1380,20 +1600,22 @@ function attachDOMBinding(clientProto) {
|
|
|
1380
1600
|
} else if (!apiStore) {
|
|
1381
1601
|
const template = resolveTemplate(el);
|
|
1382
1602
|
if (template && typeof result === "object" && result !== null) {
|
|
1383
|
-
|
|
1603
|
+
const processedResult = this._applyDeclarativeDirectives(el, result);
|
|
1604
|
+
if (Array.isArray(processedResult)) {
|
|
1384
1605
|
let combinedHTML = "";
|
|
1385
|
-
for (const item of
|
|
1606
|
+
for (const item of processedResult) {
|
|
1386
1607
|
combinedHTML += renderTemplate(template, item);
|
|
1387
1608
|
}
|
|
1388
1609
|
scheduleDOMUpdate(el, combinedHTML);
|
|
1389
1610
|
} else {
|
|
1390
|
-
scheduleDOMUpdate(el, renderTemplate(template,
|
|
1611
|
+
scheduleDOMUpdate(el, renderTemplate(template, processedResult));
|
|
1391
1612
|
}
|
|
1392
1613
|
} else {
|
|
1393
1614
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1394
1615
|
el.value = typeof result === "object" ? result.value !== void 0 ? result.value : "" : result;
|
|
1395
1616
|
} else {
|
|
1396
|
-
|
|
1617
|
+
const rawHTML = typeof result === "object" ? result.html || result.text || JSON.stringify(result) : String(result);
|
|
1618
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1397
1619
|
}
|
|
1398
1620
|
}
|
|
1399
1621
|
}
|
|
@@ -1402,21 +1624,139 @@ function attachDOMBinding(clientProto) {
|
|
|
1402
1624
|
}
|
|
1403
1625
|
}
|
|
1404
1626
|
};
|
|
1627
|
+
clientProto._applyDeclarativeDirectives = function(el, payload) {
|
|
1628
|
+
let processedPayload = payload;
|
|
1629
|
+
if (typeof payload === "object" && payload !== null) {
|
|
1630
|
+
const applyFilterSearchSort = (arr) => {
|
|
1631
|
+
let result = [...arr];
|
|
1632
|
+
const filterAttr = el.getAttribute("data-rt-filter");
|
|
1633
|
+
if (filterAttr) {
|
|
1634
|
+
const parts = filterAttr.split("==");
|
|
1635
|
+
if (parts.length === 2) {
|
|
1636
|
+
const itemProp = parts[0].trim();
|
|
1637
|
+
const storeExpr = parts[1].trim();
|
|
1638
|
+
let filterVal = void 0;
|
|
1639
|
+
const storeParts = storeExpr.split(".");
|
|
1640
|
+
if (storeParts.length === 2) {
|
|
1641
|
+
filterVal = this.getStoreState(storeParts[0], storeParts[1]);
|
|
1642
|
+
} else {
|
|
1643
|
+
filterVal = payload[storeExpr] !== void 0 ? payload[storeExpr] : this.getStoreState("app", storeExpr);
|
|
1644
|
+
}
|
|
1645
|
+
if (filterVal !== void 0 && filterVal !== null && filterVal !== "") {
|
|
1646
|
+
result = result.filter((item) => item[itemProp] === filterVal);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const searchAttr = el.getAttribute("data-rt-search");
|
|
1651
|
+
if (searchAttr) {
|
|
1652
|
+
const parts = searchAttr.split("==");
|
|
1653
|
+
if (parts.length === 2) {
|
|
1654
|
+
const itemProp = parts[0].trim();
|
|
1655
|
+
const storeExpr = parts[1].trim();
|
|
1656
|
+
let searchVal = void 0;
|
|
1657
|
+
const storeParts = storeExpr.split(".");
|
|
1658
|
+
if (storeParts.length === 2) {
|
|
1659
|
+
searchVal = this.getStoreState(storeParts[0], storeParts[1]);
|
|
1660
|
+
} else {
|
|
1661
|
+
searchVal = payload[storeExpr] !== void 0 ? payload[storeExpr] : this.getStoreState("app", storeExpr);
|
|
1662
|
+
}
|
|
1663
|
+
if (searchVal !== void 0 && searchVal !== null && searchVal !== "") {
|
|
1664
|
+
const query = String(searchVal).toLowerCase();
|
|
1665
|
+
result = result.filter((item) => {
|
|
1666
|
+
const val = item[itemProp];
|
|
1667
|
+
return val !== void 0 && val !== null && String(val).toLowerCase().includes(query);
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
const sortAttr = el.getAttribute("data-rt-sort");
|
|
1673
|
+
if (sortAttr) {
|
|
1674
|
+
let sortByVal = void 0;
|
|
1675
|
+
const storeParts = sortAttr.split(".");
|
|
1676
|
+
if (storeParts.length === 2) {
|
|
1677
|
+
sortByVal = this.getStoreState(storeParts[0], storeParts[1]);
|
|
1678
|
+
} else {
|
|
1679
|
+
sortByVal = payload[sortAttr] !== void 0 ? payload[sortAttr] : this.getStoreState("app", sortAttr);
|
|
1680
|
+
}
|
|
1681
|
+
if (sortByVal && sortByVal !== "") {
|
|
1682
|
+
if (sortByVal === "popular") {
|
|
1683
|
+
result.sort((a, b) => {
|
|
1684
|
+
const rateA = a.rating?.rate || a.rate || 0;
|
|
1685
|
+
const rateB = b.rating?.rate || b.rate || 0;
|
|
1686
|
+
return rateB - rateA;
|
|
1687
|
+
});
|
|
1688
|
+
} else {
|
|
1689
|
+
let field = "";
|
|
1690
|
+
let direction = "asc";
|
|
1691
|
+
if (sortByVal.endsWith("-low") || sortByVal.endsWith("-asc")) {
|
|
1692
|
+
field = sortByVal.replace("-low", "").replace("-asc", "");
|
|
1693
|
+
direction = "asc";
|
|
1694
|
+
} else if (sortByVal.endsWith("-high") || sortByVal.endsWith("-desc")) {
|
|
1695
|
+
field = sortByVal.replace("-high", "").replace("-desc", "");
|
|
1696
|
+
direction = "desc";
|
|
1697
|
+
}
|
|
1698
|
+
if (field) {
|
|
1699
|
+
result.sort((a, b) => {
|
|
1700
|
+
const resolveVal = (obj, path) => {
|
|
1701
|
+
return path.split(".").reduce((acc, part) => acc && acc[part], obj);
|
|
1702
|
+
};
|
|
1703
|
+
let valA = resolveVal(a, field);
|
|
1704
|
+
let valB = resolveVal(b, field);
|
|
1705
|
+
if (valA === void 0) valA = a[field];
|
|
1706
|
+
if (valB === void 0) valB = b[field];
|
|
1707
|
+
if (typeof valA === "string" && typeof valB === "string") {
|
|
1708
|
+
return direction === "asc" ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
1709
|
+
}
|
|
1710
|
+
const numA = Number(valA);
|
|
1711
|
+
const numB = Number(valB);
|
|
1712
|
+
if (!isNaN(numA) && !isNaN(numB)) {
|
|
1713
|
+
return direction === "asc" ? numA - numB : numB - numA;
|
|
1714
|
+
}
|
|
1715
|
+
return 0;
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return result;
|
|
1722
|
+
};
|
|
1723
|
+
if (Array.isArray(payload)) {
|
|
1724
|
+
processedPayload = applyFilterSearchSort(payload);
|
|
1725
|
+
} else {
|
|
1726
|
+
let foundArrayKey = "";
|
|
1727
|
+
for (const key in payload) {
|
|
1728
|
+
if (Array.isArray(payload[key])) {
|
|
1729
|
+
foundArrayKey = key;
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
if (foundArrayKey) {
|
|
1734
|
+
const processedArray = applyFilterSearchSort(payload[foundArrayKey]);
|
|
1735
|
+
processedPayload = {
|
|
1736
|
+
...payload,
|
|
1737
|
+
[foundArrayKey]: processedArray
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return processedPayload;
|
|
1743
|
+
};
|
|
1405
1744
|
clientProto._updateDOM = function(topic, payload) {
|
|
1406
1745
|
if (typeof document === "undefined") return;
|
|
1407
1746
|
const elements = document.querySelectorAll(`[data-rt-bind="${topic}"]`);
|
|
1408
1747
|
elements.forEach((el) => {
|
|
1409
|
-
|
|
1410
|
-
|
|
1748
|
+
const processedPayload = this._applyDeclarativeDirectives(el, payload);
|
|
1749
|
+
if (el.getAttribute("data-rt-type") === "context" && typeof processedPayload === "object" && processedPayload !== null) {
|
|
1750
|
+
el._rtContext = processedPayload;
|
|
1411
1751
|
const processNode = (node) => {
|
|
1412
1752
|
if (node.hasAttribute("data-rt-text")) {
|
|
1413
1753
|
const key = node.getAttribute("data-rt-text");
|
|
1414
|
-
if (key &&
|
|
1754
|
+
if (key && processedPayload[key] !== void 0 && processedPayload[key] !== null) node.textContent = processedPayload[key];
|
|
1415
1755
|
}
|
|
1416
1756
|
if (node.hasAttribute("data-rt-html")) {
|
|
1417
1757
|
const key = node.getAttribute("data-rt-html");
|
|
1418
|
-
if (key &&
|
|
1419
|
-
node.innerHTML = sanitizeHTML(
|
|
1758
|
+
if (key && processedPayload[key] !== void 0 && processedPayload[key] !== null) {
|
|
1759
|
+
node.innerHTML = sanitizeHTML(processedPayload[key]);
|
|
1420
1760
|
}
|
|
1421
1761
|
}
|
|
1422
1762
|
if (node.hasAttribute("data-rt-attr")) {
|
|
@@ -1427,8 +1767,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1427
1767
|
if (parts.length === 2) {
|
|
1428
1768
|
const attrName = parts[0].trim();
|
|
1429
1769
|
const key = parts[1].trim();
|
|
1430
|
-
if (attrName && key &&
|
|
1431
|
-
node.setAttribute(attrName,
|
|
1770
|
+
if (attrName && key && processedPayload[key] !== void 0 && processedPayload[key] !== null) {
|
|
1771
|
+
node.setAttribute(attrName, processedPayload[key]);
|
|
1432
1772
|
}
|
|
1433
1773
|
}
|
|
1434
1774
|
});
|
|
@@ -1443,7 +1783,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1443
1783
|
const className = parts[0].trim();
|
|
1444
1784
|
const key = parts[1].trim();
|
|
1445
1785
|
const classNames = className.split(/\s+/).filter(Boolean);
|
|
1446
|
-
if (
|
|
1786
|
+
if (processedPayload[key]) {
|
|
1447
1787
|
classNames.forEach((c) => node.classList.add(c));
|
|
1448
1788
|
} else {
|
|
1449
1789
|
classNames.forEach((c) => node.classList.remove(c));
|
|
@@ -1455,7 +1795,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1455
1795
|
if (node.hasAttribute("data-rt-if")) {
|
|
1456
1796
|
const key = node.getAttribute("data-rt-if");
|
|
1457
1797
|
if (key) {
|
|
1458
|
-
if (
|
|
1798
|
+
if (processedPayload[key]) {
|
|
1459
1799
|
node.style.display = "";
|
|
1460
1800
|
} else {
|
|
1461
1801
|
node.style.display = "none";
|
|
@@ -1465,7 +1805,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1465
1805
|
if (node.hasAttribute("data-rt-hide")) {
|
|
1466
1806
|
const key = node.getAttribute("data-rt-hide");
|
|
1467
1807
|
if (key) {
|
|
1468
|
-
if (
|
|
1808
|
+
if (processedPayload[key]) {
|
|
1469
1809
|
node.style.display = "none";
|
|
1470
1810
|
} else {
|
|
1471
1811
|
node.style.display = "";
|
|
@@ -1478,25 +1818,172 @@ function attachDOMBinding(clientProto) {
|
|
|
1478
1818
|
return;
|
|
1479
1819
|
}
|
|
1480
1820
|
const template = resolveTemplate(el);
|
|
1481
|
-
if (template && typeof
|
|
1482
|
-
if (Array.isArray(
|
|
1821
|
+
if (template && typeof processedPayload === "object" && processedPayload !== null) {
|
|
1822
|
+
if (Array.isArray(processedPayload)) {
|
|
1483
1823
|
let combinedHTML = "";
|
|
1484
|
-
for (const item of
|
|
1824
|
+
for (const item of processedPayload) {
|
|
1485
1825
|
combinedHTML += renderTemplate(template, item);
|
|
1486
1826
|
}
|
|
1487
1827
|
scheduleDOMUpdate(el, combinedHTML);
|
|
1488
1828
|
} else {
|
|
1489
|
-
scheduleDOMUpdate(el, renderTemplate(template,
|
|
1829
|
+
scheduleDOMUpdate(el, renderTemplate(template, processedPayload));
|
|
1490
1830
|
}
|
|
1491
1831
|
return;
|
|
1492
1832
|
}
|
|
1493
1833
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1494
|
-
el.value = typeof
|
|
1834
|
+
el.value = typeof processedPayload === "object" ? processedPayload.value !== void 0 ? processedPayload.value : "" : processedPayload;
|
|
1835
|
+
} else if (template) {
|
|
1836
|
+
el.innerHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1495
1837
|
} else {
|
|
1496
|
-
|
|
1838
|
+
const rawHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1839
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1497
1840
|
}
|
|
1498
1841
|
});
|
|
1499
1842
|
};
|
|
1843
|
+
clientProto._resolveImports = async function(container) {
|
|
1844
|
+
if (typeof document === "undefined") return;
|
|
1845
|
+
const root = container || document.body || document;
|
|
1846
|
+
if (!root || typeof root.querySelectorAll !== "function") return;
|
|
1847
|
+
const elements = root.querySelectorAll("[data-import]");
|
|
1848
|
+
if (elements.length === 0) return;
|
|
1849
|
+
const resolveNode = async (el, resolvingSet) => {
|
|
1850
|
+
const src = el.getAttribute("data-import");
|
|
1851
|
+
if (!src) return;
|
|
1852
|
+
if (resolvingSet.has(src)) {
|
|
1853
|
+
console.warn(`[Dolphin Component Warning]: Circular import detected for "${src}". Skipping resolving.`);
|
|
1854
|
+
el.innerHTML = `<span style="color:red;font-weight:bold;">Circular import: ${src}</span>`;
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
resolvingSet.add(src);
|
|
1858
|
+
let promise = componentPromiseCache.get(src);
|
|
1859
|
+
if (!promise) {
|
|
1860
|
+
promise = fetch(src).then((res) => {
|
|
1861
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1862
|
+
return res.text();
|
|
1863
|
+
});
|
|
1864
|
+
promise.catch(() => componentPromiseCache.delete(src));
|
|
1865
|
+
componentPromiseCache.set(src, promise);
|
|
1866
|
+
}
|
|
1867
|
+
let content = "";
|
|
1868
|
+
try {
|
|
1869
|
+
content = await promise;
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
console.error(`[Dolphin Component Error]: Failed to fetch component "${src}":`, err);
|
|
1872
|
+
content = `<span style="color:red;font-weight:bold;">Failed to import ${src}</span>`;
|
|
1873
|
+
}
|
|
1874
|
+
el.innerHTML = sanitizeHTML(content);
|
|
1875
|
+
el.removeAttribute("data-import");
|
|
1876
|
+
const nestedElements = el.querySelectorAll("[data-import]");
|
|
1877
|
+
if (nestedElements.length > 0) {
|
|
1878
|
+
const subPromises = Array.from(nestedElements).map((child) => resolveNode(child, new Set(resolvingSet)));
|
|
1879
|
+
await Promise.all(subPromises);
|
|
1880
|
+
}
|
|
1881
|
+
this._scanStoreBinds();
|
|
1882
|
+
this._scanAndFetchAPIBinds();
|
|
1883
|
+
};
|
|
1884
|
+
const promises = Array.from(elements).map((el) => resolveNode(el, /* @__PURE__ */ new Set()));
|
|
1885
|
+
await Promise.all(promises);
|
|
1886
|
+
};
|
|
1887
|
+
clientProto._initSPARouter = function() {
|
|
1888
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1889
|
+
if (this._routerInitialized) return;
|
|
1890
|
+
this._routerInitialized = true;
|
|
1891
|
+
let _spaAbortController = null;
|
|
1892
|
+
const findViewport = () => {
|
|
1893
|
+
const selector = this.options.routerViewport || "main, #viewport, body";
|
|
1894
|
+
const selectors = selector.split(",").map((s) => s.trim());
|
|
1895
|
+
for (const sel of selectors) {
|
|
1896
|
+
const el = document.querySelector(sel);
|
|
1897
|
+
if (el) return el;
|
|
1898
|
+
}
|
|
1899
|
+
return document.body;
|
|
1900
|
+
};
|
|
1901
|
+
const loadPage = async (url, pushState = true) => {
|
|
1902
|
+
try {
|
|
1903
|
+
if (this.options.debug) {
|
|
1904
|
+
console.log(`%c\u{1F6E3}\uFE0F [Dolphin Router]: Navigating to ${url}...`, "color: #3b82f6; font-weight: bold;");
|
|
1905
|
+
}
|
|
1906
|
+
if (_spaAbortController) {
|
|
1907
|
+
_spaAbortController.abort();
|
|
1908
|
+
}
|
|
1909
|
+
_spaAbortController = new AbortController();
|
|
1910
|
+
const signal = _spaAbortController.signal;
|
|
1911
|
+
const viewport = findViewport();
|
|
1912
|
+
if (this.options.routerTransitions && viewport) {
|
|
1913
|
+
viewport.classList.add("dolphin-fade-out");
|
|
1914
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1915
|
+
}
|
|
1916
|
+
const response = await fetch(url, { signal });
|
|
1917
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1918
|
+
const html = await response.text();
|
|
1919
|
+
_spaAbortController = null;
|
|
1920
|
+
const parser = new DOMParser();
|
|
1921
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
1922
|
+
if (doc.title) {
|
|
1923
|
+
document.title = doc.title;
|
|
1924
|
+
}
|
|
1925
|
+
const newViewport = doc.querySelector(this.options.routerViewport || "main, #viewport, body");
|
|
1926
|
+
const currentViewport = findViewport();
|
|
1927
|
+
if (newViewport && currentViewport) {
|
|
1928
|
+
currentViewport.innerHTML = newViewport.innerHTML;
|
|
1929
|
+
Array.from(newViewport.attributes).forEach((attr) => {
|
|
1930
|
+
currentViewport.setAttribute(attr.name, attr.value);
|
|
1931
|
+
});
|
|
1932
|
+
} else if (currentViewport) {
|
|
1933
|
+
currentViewport.innerHTML = doc.body.innerHTML;
|
|
1934
|
+
}
|
|
1935
|
+
if (pushState) {
|
|
1936
|
+
window.history.pushState({ dolphinSpa: true, url }, "", url);
|
|
1937
|
+
}
|
|
1938
|
+
if (this.options.routerTransitions && currentViewport) {
|
|
1939
|
+
currentViewport.classList.remove("dolphin-fade-out");
|
|
1940
|
+
currentViewport.classList.add("dolphin-fade-in");
|
|
1941
|
+
setTimeout(() => currentViewport.classList.remove("dolphin-fade-in"), 300);
|
|
1942
|
+
}
|
|
1943
|
+
await this._resolveImports(currentViewport);
|
|
1944
|
+
this._scanStoreBinds();
|
|
1945
|
+
this._scanAndFetchAPIBinds();
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
if (err && err.name === "AbortError") return;
|
|
1948
|
+
console.error("[Dolphin Router Error]: Failed to route page:", err);
|
|
1949
|
+
window.location.href = url;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
this.addDomListener(document, "click", (e) => {
|
|
1953
|
+
const anchor = e.target.closest("a");
|
|
1954
|
+
if (!anchor) return;
|
|
1955
|
+
if (!anchor.hasAttribute("data-spa") && anchor.getAttribute("data-spa") !== "true") return;
|
|
1956
|
+
const href = anchor.getAttribute("href");
|
|
1957
|
+
if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
|
|
1958
|
+
const url = new URL(href, window.location.href);
|
|
1959
|
+
if (url.origin !== window.location.origin) return;
|
|
1960
|
+
e.preventDefault();
|
|
1961
|
+
loadPage(href);
|
|
1962
|
+
});
|
|
1963
|
+
this.addDomListener(window, "popstate", (e) => {
|
|
1964
|
+
if (e.state && e.state.dolphinSpa) {
|
|
1965
|
+
loadPage(e.state.url, false);
|
|
1966
|
+
} else if (e.state === null) {
|
|
1967
|
+
loadPage(window.location.pathname, false);
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
if (this.options.routerTransitions) {
|
|
1971
|
+
const style = document.createElement("style");
|
|
1972
|
+
style.innerHTML = `
|
|
1973
|
+
.dolphin-fade-out {
|
|
1974
|
+
opacity: 0;
|
|
1975
|
+
transition: opacity 0.15s ease-in-out;
|
|
1976
|
+
}
|
|
1977
|
+
.dolphin-fade-in {
|
|
1978
|
+
opacity: 0;
|
|
1979
|
+
}
|
|
1980
|
+
main, #viewport, body {
|
|
1981
|
+
transition: opacity 0.15s ease-in-out;
|
|
1982
|
+
}
|
|
1983
|
+
`;
|
|
1984
|
+
document.head.appendChild(style);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1500
1987
|
}
|
|
1501
1988
|
|
|
1502
1989
|
// src/offline.ts
|
|
@@ -1574,6 +2061,10 @@ var DolphinOffline = class {
|
|
|
1574
2061
|
const store = transaction.objectStore("cache");
|
|
1575
2062
|
store.put({ data, timestamp: Date.now() }, key);
|
|
1576
2063
|
transaction.oncomplete = () => resolve();
|
|
2064
|
+
transaction.onerror = () => {
|
|
2065
|
+
console.warn("[Dolphin Offline] setCache write failed for key:", key);
|
|
2066
|
+
resolve();
|
|
2067
|
+
};
|
|
1577
2068
|
} catch {
|
|
1578
2069
|
resolve();
|
|
1579
2070
|
}
|
|
@@ -1596,6 +2087,11 @@ var DolphinOffline = class {
|
|
|
1596
2087
|
const store = transaction.objectStore("mutations");
|
|
1597
2088
|
store.add(mutation);
|
|
1598
2089
|
transaction.oncomplete = () => resolve();
|
|
2090
|
+
transaction.onerror = () => {
|
|
2091
|
+
console.warn("[Dolphin Offline] queueMutation write failed:", method, path);
|
|
2092
|
+
this.memoryMutations.push(mutation);
|
|
2093
|
+
resolve();
|
|
2094
|
+
};
|
|
1599
2095
|
} catch {
|
|
1600
2096
|
resolve();
|
|
1601
2097
|
}
|
|
@@ -1628,6 +2124,10 @@ var DolphinOffline = class {
|
|
|
1628
2124
|
const store = transaction.objectStore("mutations");
|
|
1629
2125
|
store.delete(id);
|
|
1630
2126
|
transaction.oncomplete = () => resolve();
|
|
2127
|
+
transaction.onerror = () => {
|
|
2128
|
+
console.warn("[Dolphin Offline] removeMutation failed for id:", id);
|
|
2129
|
+
resolve();
|
|
2130
|
+
};
|
|
1631
2131
|
} catch {
|
|
1632
2132
|
resolve();
|
|
1633
2133
|
}
|
|
@@ -1973,6 +2473,9 @@ function attachDragDrop(clientProto) {
|
|
|
1973
2473
|
function attachCollab(clientProto) {
|
|
1974
2474
|
clientProto._initCollab = function() {
|
|
1975
2475
|
if (typeof document === "undefined") return;
|
|
2476
|
+
const remoteCursors = /* @__PURE__ */ new Map();
|
|
2477
|
+
const cursorStaleTimers = /* @__PURE__ */ new Map();
|
|
2478
|
+
const CURSOR_STALE_MS = 5e3;
|
|
1976
2479
|
this.addDomListener(document, "mousemove", (e) => {
|
|
1977
2480
|
const shareContainers = document.querySelectorAll("[data-rt-cursor-share]");
|
|
1978
2481
|
shareContainers.forEach((container) => {
|
|
@@ -2009,6 +2512,7 @@ function attachCollab(clientProto) {
|
|
|
2009
2512
|
if (e.target._typingTimer) clearTimeout(e.target._typingTimer);
|
|
2010
2513
|
e.target._typingTimer = setTimeout(() => {
|
|
2011
2514
|
e.target._isTyping = false;
|
|
2515
|
+
e.target._typingTimer = null;
|
|
2012
2516
|
publishTyping(false);
|
|
2013
2517
|
}, 2e3);
|
|
2014
2518
|
});
|
|
@@ -2032,21 +2536,32 @@ function attachCollab(clientProto) {
|
|
|
2032
2536
|
if (remoteDeviceId === this.deviceId) return;
|
|
2033
2537
|
const container = document.querySelector(`[data-rt-cursor-share="${room}"]`);
|
|
2034
2538
|
if (!container) return;
|
|
2035
|
-
|
|
2036
|
-
|
|
2539
|
+
const cursorKey = `${room}::${remoteDeviceId}`;
|
|
2540
|
+
let cursorEl = remoteCursors.get(cursorKey);
|
|
2541
|
+
if (!cursorEl || !document.contains(cursorEl)) {
|
|
2037
2542
|
cursorEl = document.createElement("div");
|
|
2038
2543
|
cursorEl.className = `rt-cursor rt-cursor-${remoteDeviceId}`;
|
|
2039
2544
|
cursorEl.style.position = "absolute";
|
|
2040
2545
|
cursorEl.style.width = "10px";
|
|
2041
2546
|
cursorEl.style.height = "10px";
|
|
2042
2547
|
cursorEl.style.borderRadius = "50%";
|
|
2043
|
-
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
|
|
2548
|
+
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
|
|
2044
2549
|
cursorEl.style.pointerEvents = "none";
|
|
2045
2550
|
container.appendChild(cursorEl);
|
|
2551
|
+
remoteCursors.set(cursorKey, cursorEl);
|
|
2046
2552
|
}
|
|
2047
2553
|
const box = container.getBoundingClientRect();
|
|
2048
2554
|
cursorEl.style.left = payload.x * box.width + "px";
|
|
2049
2555
|
cursorEl.style.top = payload.y * box.height + "px";
|
|
2556
|
+
if (cursorStaleTimers.has(cursorKey)) {
|
|
2557
|
+
clearTimeout(cursorStaleTimers.get(cursorKey));
|
|
2558
|
+
}
|
|
2559
|
+
cursorStaleTimers.set(cursorKey, setTimeout(() => {
|
|
2560
|
+
const el = remoteCursors.get(cursorKey);
|
|
2561
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2562
|
+
remoteCursors.delete(cursorKey);
|
|
2563
|
+
cursorStaleTimers.delete(cursorKey);
|
|
2564
|
+
}, CURSOR_STALE_MS));
|
|
2050
2565
|
});
|
|
2051
2566
|
this.subscribe("collab/+/crdt", (payload, topic) => {
|
|
2052
2567
|
if (payload.deviceId === this.deviceId) return;
|
|
@@ -2064,6 +2579,14 @@ function attachCollab(clientProto) {
|
|
|
2064
2579
|
}
|
|
2065
2580
|
});
|
|
2066
2581
|
});
|
|
2582
|
+
this._collabCleanup = () => {
|
|
2583
|
+
cursorStaleTimers.forEach((t) => clearTimeout(t));
|
|
2584
|
+
cursorStaleTimers.clear();
|
|
2585
|
+
remoteCursors.forEach((el) => {
|
|
2586
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2587
|
+
});
|
|
2588
|
+
remoteCursors.clear();
|
|
2589
|
+
};
|
|
2067
2590
|
};
|
|
2068
2591
|
}
|
|
2069
2592
|
|