dolphin-client 1.0.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/LICENSE +15 -0
- package/README.md +100 -0
- package/dist/a11y.d.ts +1 -0
- package/dist/animation.d.ts +1 -0
- package/dist/api.d.ts +30 -0
- package/dist/api.js +112 -0
- package/dist/auth.d.ts +56 -0
- package/dist/auth.js +120 -0
- package/dist/collab.d.ts +1 -0
- package/dist/core.d.ts +119 -0
- package/dist/core.js +345 -0
- package/dist/dolphin-client.js +2042 -0
- package/dist/dom.d.ts +1 -0
- package/dist/dom.js +316 -0
- package/dist/dragdrop.d.ts +1 -0
- package/dist/i18n.d.ts +1 -0
- package/dist/index.cjs +2040 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2018 -0
- package/dist/offline.d.ts +24 -0
- package/dist/pwa.d.ts +1 -0
- package/dist/store.d.ts +22 -0
- package/dist/store.js +131 -0
- package/dist/testing.d.ts +20 -0
- package/dist/validation.d.ts +2 -0
- package/package.json +53 -0
|
@@ -0,0 +1,2042 @@
|
|
|
1
|
+
var DolphinModule = (() => {
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DolphinClient: () => DolphinClient
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// src/api.ts
|
|
27
|
+
var APIHandler = class {
|
|
28
|
+
client;
|
|
29
|
+
/** @param {DolphinClient} client */
|
|
30
|
+
constructor(client) {
|
|
31
|
+
this.client = client;
|
|
32
|
+
return this._createProxy([]);
|
|
33
|
+
}
|
|
34
|
+
/** @private */
|
|
35
|
+
_createProxy(pathParts) {
|
|
36
|
+
const joined = pathParts.join("/");
|
|
37
|
+
const target = (options) => this.request("GET", joined, null, options);
|
|
38
|
+
target.get = (pathOrOptions, options) => typeof pathOrOptions === "string" ? this.request("GET", pathOrOptions, null, options) : this.request("GET", joined, null, pathOrOptions);
|
|
39
|
+
target.post = (pathOrBody, bodyOrOptions, options) => typeof pathOrBody === "string" ? this.request("POST", pathOrBody, bodyOrOptions, options) : this.request("POST", joined, pathOrBody, bodyOrOptions);
|
|
40
|
+
target.put = (pathOrBody, bodyOrOptions, options) => typeof pathOrBody === "string" ? this.request("PUT", pathOrBody, bodyOrOptions, options) : this.request("PUT", joined, pathOrBody, bodyOrOptions);
|
|
41
|
+
target.patch = (pathOrBody, bodyOrOptions, options) => typeof pathOrBody === "string" ? this.request("PATCH", pathOrBody, bodyOrOptions, options) : this.request("PATCH", joined, pathOrBody, bodyOrOptions);
|
|
42
|
+
target.del = (pathOrOptions, options) => typeof pathOrOptions === "string" ? this.request("DELETE", pathOrOptions, null, options) : this.request("DELETE", joined, null, pathOrOptions);
|
|
43
|
+
target.request = (method, subPath, body, options) => {
|
|
44
|
+
const finalPath = subPath ? `${joined}/${subPath.startsWith("/") ? subPath.slice(1) : subPath}` : joined;
|
|
45
|
+
return this.request(method, finalPath, body, options);
|
|
46
|
+
};
|
|
47
|
+
target.requestDirect = (method, path, body, options) => {
|
|
48
|
+
return this.requestDirect(method, path, body, options);
|
|
49
|
+
};
|
|
50
|
+
const methods = ["get", "post", "put", "patch", "del", "request", "requestDirect"];
|
|
51
|
+
return new Proxy(target, {
|
|
52
|
+
get: (t, prop) => {
|
|
53
|
+
if (typeof prop === "string" && !methods.includes(prop)) {
|
|
54
|
+
return this._createProxy([...pathParts, prop]);
|
|
55
|
+
}
|
|
56
|
+
return t[prop];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Intercept request for offline-first caching and queuing.
|
|
62
|
+
*/
|
|
63
|
+
async request(method, path, body = null, options = {}) {
|
|
64
|
+
if (this.client.offline) {
|
|
65
|
+
const isOnline = this.client.offline.isOnline;
|
|
66
|
+
const cacheKey = `${method.toUpperCase()}:${path}`;
|
|
67
|
+
if (method.toUpperCase() === "GET") {
|
|
68
|
+
if (isOnline) {
|
|
69
|
+
try {
|
|
70
|
+
const result = await this.requestDirect(method, path, body, options);
|
|
71
|
+
await this.client.offline.setCache(cacheKey, result);
|
|
72
|
+
return result;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const cached = await this.client.offline.getCache(cacheKey);
|
|
75
|
+
if (cached !== void 0 && cached !== null) {
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const cached = await this.client.offline.getCache(cacheKey);
|
|
82
|
+
if (cached !== void 0 && cached !== null) {
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
85
|
+
throw { status: 503, data: { error: "Offline, no cache available" } };
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
if (isOnline) {
|
|
89
|
+
return this.requestDirect(method, path, body, options);
|
|
90
|
+
} else {
|
|
91
|
+
await this.client.offline.queueMutation(method, path, body);
|
|
92
|
+
this.client._dispatch("offline:queued", { method, path, body });
|
|
93
|
+
return { success: true, offline: true, message: "Mutation queued offline" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return this.requestDirect(method, path, body, options);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Make an HTTP request with timeout + auto token refresh.
|
|
101
|
+
* @param {string} method
|
|
102
|
+
* @param {string} path
|
|
103
|
+
* @param {any} [body]
|
|
104
|
+
* @param {RequestInit} [options]
|
|
105
|
+
* @param {boolean} [_isRetry=false] — internal: prevent infinite refresh loop
|
|
106
|
+
* @returns {Promise<any>}
|
|
107
|
+
*/
|
|
108
|
+
async requestDirect(method, path, body = null, options = {}) {
|
|
109
|
+
const _isRetry = options._isRetry === true;
|
|
110
|
+
const url = `${this.client.httpUrl}${path.startsWith("/") ? path : "/" + path}`;
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const timeoutId = setTimeout(
|
|
113
|
+
() => controller.abort(),
|
|
114
|
+
this.client.options.timeout
|
|
115
|
+
);
|
|
116
|
+
const headers = {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
...options.headers || {}
|
|
119
|
+
};
|
|
120
|
+
if (this.client.accessToken) {
|
|
121
|
+
headers["Authorization"] = `Bearer ${this.client.accessToken}`;
|
|
122
|
+
}
|
|
123
|
+
const fetchOptions = { ...options };
|
|
124
|
+
delete fetchOptions._isRetry;
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(url, {
|
|
127
|
+
method,
|
|
128
|
+
headers,
|
|
129
|
+
signal: controller.signal,
|
|
130
|
+
...body ? { body: JSON.stringify(body) } : {},
|
|
131
|
+
...fetchOptions
|
|
132
|
+
});
|
|
133
|
+
clearTimeout(timeoutId);
|
|
134
|
+
if (response.status === 401 && !_isRetry && this.client.options.autoRefreshToken) {
|
|
135
|
+
const refreshed = await this.client.auth._silentRefresh();
|
|
136
|
+
if (refreshed) {
|
|
137
|
+
return this.request(method, path, body, { ...options, _isRetry: true });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const contentType = response.headers.get("content-type") || "";
|
|
141
|
+
const data = contentType.includes("application/json") ? await response.json() : await response.text();
|
|
142
|
+
if (!response.ok) throw { status: response.status, data };
|
|
143
|
+
if (data && typeof data === "object") {
|
|
144
|
+
if (data.accessToken) {
|
|
145
|
+
this.client.setToken(data.accessToken);
|
|
146
|
+
if (data.user) this.client.auth.user = data.user;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (this.client.options.autoBroadcast && ["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase())) {
|
|
150
|
+
const cleanPath = path.startsWith("/") ? path.substring(1) : path;
|
|
151
|
+
this.client.publish(cleanPath, { method: method.toUpperCase(), payload: body, result: data });
|
|
152
|
+
}
|
|
153
|
+
return data;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
clearTimeout(timeoutId);
|
|
156
|
+
if (err.name === "AbortError") {
|
|
157
|
+
throw { status: 408, data: { error: "Request timed out" } };
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/auth.ts
|
|
165
|
+
var AuthHandler = class {
|
|
166
|
+
client;
|
|
167
|
+
user;
|
|
168
|
+
_refreshing;
|
|
169
|
+
/** @param {DolphinClient} client */
|
|
170
|
+
constructor(client) {
|
|
171
|
+
this.client = client;
|
|
172
|
+
this.user = null;
|
|
173
|
+
this._refreshing = false;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Login with email + password.
|
|
177
|
+
* @param {string} email
|
|
178
|
+
* @param {string} password
|
|
179
|
+
*/
|
|
180
|
+
async login(email, password) {
|
|
181
|
+
const res = await this.client.api.post("/api/auth/login", { email, password });
|
|
182
|
+
if (res.accessToken) {
|
|
183
|
+
this.client.setToken(res.accessToken);
|
|
184
|
+
this.user = res.user || null;
|
|
185
|
+
}
|
|
186
|
+
return res;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Register a new account.
|
|
190
|
+
* @param {{ email: string, password: string, [key: string]: any }} data
|
|
191
|
+
*/
|
|
192
|
+
async register(data) {
|
|
193
|
+
return this.client.api.post("/api/auth/register", data);
|
|
194
|
+
}
|
|
195
|
+
/** Get current user profile. */
|
|
196
|
+
async me() {
|
|
197
|
+
const res = await this.client.api.get("/api/auth/me");
|
|
198
|
+
if (res.success) this.user = res.data;
|
|
199
|
+
return res;
|
|
200
|
+
}
|
|
201
|
+
/** Logout and clear token. */
|
|
202
|
+
async logout() {
|
|
203
|
+
try {
|
|
204
|
+
await this.client.api.post("/api/auth/logout");
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
this.client.setToken(null);
|
|
208
|
+
this.user = null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Manually refresh the access token using the httpOnly refresh-token cookie.
|
|
212
|
+
* Called automatically on 401 if autoRefreshToken is enabled.
|
|
213
|
+
* @returns {Promise<boolean>} — true if refresh succeeded
|
|
214
|
+
*/
|
|
215
|
+
async refresh() {
|
|
216
|
+
return this._silentRefresh();
|
|
217
|
+
}
|
|
218
|
+
/** @private */
|
|
219
|
+
async _silentRefresh() {
|
|
220
|
+
if (this._refreshing) return false;
|
|
221
|
+
this._refreshing = true;
|
|
222
|
+
try {
|
|
223
|
+
const res = await this.client.api.post("/api/auth/refresh", null, { _isRetry: true });
|
|
224
|
+
if (res.accessToken) {
|
|
225
|
+
this.client.setToken(res.accessToken);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
} catch {
|
|
230
|
+
this.client.setToken(null);
|
|
231
|
+
return false;
|
|
232
|
+
} finally {
|
|
233
|
+
this._refreshing = false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Verify a 2FA TOTP code after login.
|
|
238
|
+
* @param {string} code — 6-digit TOTP code
|
|
239
|
+
* @param {string} [email] — email (if not already in user)
|
|
240
|
+
*/
|
|
241
|
+
async verify2FA(code, email) {
|
|
242
|
+
const payload = {
|
|
243
|
+
code,
|
|
244
|
+
email: email || this.user?.email
|
|
245
|
+
};
|
|
246
|
+
const res = await this.client.api.post("/api/auth/2fa/verify", payload);
|
|
247
|
+
if (res.accessToken) {
|
|
248
|
+
this.client.setToken(res.accessToken);
|
|
249
|
+
if (res.user) this.user = res.user;
|
|
250
|
+
}
|
|
251
|
+
return res;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Enable 2FA — returns QR code URL and secret.
|
|
255
|
+
*/
|
|
256
|
+
async enable2FA() {
|
|
257
|
+
return this.client.api.post("/api/auth/2fa/enable");
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Disable 2FA.
|
|
261
|
+
* @param {string} code — current TOTP code to confirm
|
|
262
|
+
*/
|
|
263
|
+
async disable2FA(code) {
|
|
264
|
+
return this.client.api.post("/api/auth/2fa/disable", { code });
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Request a password reset email.
|
|
268
|
+
* @param {string} email
|
|
269
|
+
*/
|
|
270
|
+
async forgotPassword(email) {
|
|
271
|
+
return this.client.api.post("/api/auth/forgot-password", { email });
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Reset password using the token from email.
|
|
275
|
+
* @param {string} token
|
|
276
|
+
* @param {string} newPassword
|
|
277
|
+
*/
|
|
278
|
+
async resetPassword(token, newPassword) {
|
|
279
|
+
return this.client.api.post("/api/auth/reset-password", { token, newPassword });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// src/store.ts
|
|
284
|
+
var DolphinStore = class {
|
|
285
|
+
client;
|
|
286
|
+
data;
|
|
287
|
+
listeners;
|
|
288
|
+
subscribed;
|
|
289
|
+
/** @param {DolphinClient} client */
|
|
290
|
+
constructor(client) {
|
|
291
|
+
this.client = client;
|
|
292
|
+
this.data = /* @__PURE__ */ new Map();
|
|
293
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
294
|
+
this.subscribed = /* @__PURE__ */ new Set();
|
|
295
|
+
return new Proxy(this, {
|
|
296
|
+
get: (target, prop) => {
|
|
297
|
+
if (prop in target) return target[prop];
|
|
298
|
+
if (typeof prop === "string") return this._getCollection(prop);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/** @private */
|
|
303
|
+
_getCollection(name) {
|
|
304
|
+
if (!this.data.has(name)) {
|
|
305
|
+
const collection = {
|
|
306
|
+
_rawItems: [],
|
|
307
|
+
items: [],
|
|
308
|
+
loading: true,
|
|
309
|
+
error: null,
|
|
310
|
+
success: false,
|
|
311
|
+
_filter: null,
|
|
312
|
+
_sort: null,
|
|
313
|
+
where: (fn) => {
|
|
314
|
+
collection._filter = fn;
|
|
315
|
+
this._applyTransform(collection);
|
|
316
|
+
return collection;
|
|
317
|
+
},
|
|
318
|
+
orderBy: (key, direction = "asc") => {
|
|
319
|
+
collection._sort = { key, direction };
|
|
320
|
+
this._applyTransform(collection);
|
|
321
|
+
return collection;
|
|
322
|
+
},
|
|
323
|
+
reset: () => {
|
|
324
|
+
collection._filter = null;
|
|
325
|
+
collection._sort = null;
|
|
326
|
+
this._applyTransform(collection);
|
|
327
|
+
return collection;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
this.data.set(name, collection);
|
|
331
|
+
this._fetchAndSync(name);
|
|
332
|
+
}
|
|
333
|
+
return this.data.get(name);
|
|
334
|
+
}
|
|
335
|
+
/** @private */
|
|
336
|
+
async _fetchAndSync(name) {
|
|
337
|
+
const state = this.data.get(name);
|
|
338
|
+
try {
|
|
339
|
+
const res = await this.client.api.get(`/${name.toLowerCase()}`);
|
|
340
|
+
state._rawItems = Array.isArray(res) ? res : res.data || [];
|
|
341
|
+
state.loading = false;
|
|
342
|
+
state.success = true;
|
|
343
|
+
state.error = null;
|
|
344
|
+
this._applyTransform(state);
|
|
345
|
+
if (!this.subscribed.has(name)) {
|
|
346
|
+
this.client.subscribe(`db:sync/${name.toLowerCase()}`, (update) => {
|
|
347
|
+
this._handleRemoteUpdate(name, update);
|
|
348
|
+
});
|
|
349
|
+
this.subscribed.add(name);
|
|
350
|
+
}
|
|
351
|
+
} catch (e) {
|
|
352
|
+
state.loading = false;
|
|
353
|
+
state.success = false;
|
|
354
|
+
state.error = e.data?.error || e.message || "Fetch failed";
|
|
355
|
+
this._notify();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** @private */
|
|
359
|
+
_applyTransform(state) {
|
|
360
|
+
let result = [...state._rawItems];
|
|
361
|
+
if (state._filter) result = result.filter(state._filter);
|
|
362
|
+
if (state._sort) {
|
|
363
|
+
const { key, direction } = state._sort;
|
|
364
|
+
result.sort((a, b) => {
|
|
365
|
+
if (a[key] === b[key]) return 0;
|
|
366
|
+
return (a[key] > b[key] ? 1 : -1) * (direction === "asc" ? 1 : -1);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
state.items = result;
|
|
370
|
+
this._notify();
|
|
371
|
+
}
|
|
372
|
+
/** @private */
|
|
373
|
+
_handleRemoteUpdate(collection, update) {
|
|
374
|
+
const state = this.data.get(collection);
|
|
375
|
+
if (!state) return;
|
|
376
|
+
const { type, data } = update;
|
|
377
|
+
let items = state._rawItems;
|
|
378
|
+
if (type === "create") {
|
|
379
|
+
items = [...items, data];
|
|
380
|
+
} else if (type === "update") {
|
|
381
|
+
items = items.map((i) => i.id === data.id || i._id === data._id ? { ...i, ...data } : i);
|
|
382
|
+
} else if (type === "delete") {
|
|
383
|
+
items = items.filter((i) => {
|
|
384
|
+
if (data.id != null && i.id === data.id) return false;
|
|
385
|
+
if (data._id != null && i._id === data._id) return false;
|
|
386
|
+
return true;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
state._rawItems = items;
|
|
390
|
+
this._applyTransform(state);
|
|
391
|
+
}
|
|
392
|
+
/** Subscribe for React useSyncExternalStore */
|
|
393
|
+
subscribe(listener) {
|
|
394
|
+
this.listeners.add(listener);
|
|
395
|
+
return () => this.listeners.delete(listener);
|
|
396
|
+
}
|
|
397
|
+
/** @param {string} collection */
|
|
398
|
+
getSnapshot(collection) {
|
|
399
|
+
return this.data.get(collection) || { items: [], loading: false, error: null, success: false };
|
|
400
|
+
}
|
|
401
|
+
/** @private */
|
|
402
|
+
_notify() {
|
|
403
|
+
this.listeners.forEach((l) => l());
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// src/core.ts
|
|
408
|
+
var DolphinClient = class {
|
|
409
|
+
host;
|
|
410
|
+
httpUrl;
|
|
411
|
+
deviceId;
|
|
412
|
+
options;
|
|
413
|
+
socket;
|
|
414
|
+
storage;
|
|
415
|
+
accessToken;
|
|
416
|
+
api;
|
|
417
|
+
auth;
|
|
418
|
+
store;
|
|
419
|
+
handlers;
|
|
420
|
+
signalHandlers;
|
|
421
|
+
fileHandlers;
|
|
422
|
+
_offlineQueue;
|
|
423
|
+
reconnectAttempts;
|
|
424
|
+
_attachedListeners;
|
|
425
|
+
constructor(url = "", deviceId = "", options = {}) {
|
|
426
|
+
if (!url && typeof window !== "undefined") url = window.location.host;
|
|
427
|
+
let protocol = "http:";
|
|
428
|
+
if (url.startsWith("https://")) protocol = "https:";
|
|
429
|
+
else if (url.startsWith("http://")) protocol = "http:";
|
|
430
|
+
else if (typeof window !== "undefined") protocol = window.location.protocol;
|
|
431
|
+
this.host = (url || "localhost").replace(/\/$/, "").replace(/^https?:\/\//, "");
|
|
432
|
+
this.httpUrl = `${protocol}//${this.host}`;
|
|
433
|
+
this.deviceId = deviceId || "web_" + Math.random().toString(36).substr(2, 8);
|
|
434
|
+
this.options = {
|
|
435
|
+
timeout: 15e3,
|
|
436
|
+
chunkSize: 65536,
|
|
437
|
+
// 64 KB
|
|
438
|
+
maxReconnect: 5,
|
|
439
|
+
autoRefreshToken: true,
|
|
440
|
+
...options
|
|
441
|
+
};
|
|
442
|
+
this.socket = null;
|
|
443
|
+
this.storage = typeof localStorage !== "undefined" ? localStorage : {
|
|
444
|
+
getItem: () => null,
|
|
445
|
+
setItem: () => {
|
|
446
|
+
},
|
|
447
|
+
removeItem: () => {
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
this.accessToken = this.storage.getItem("dolphin_token");
|
|
451
|
+
this.api = new APIHandler(this);
|
|
452
|
+
this.auth = new AuthHandler(this);
|
|
453
|
+
this.store = new DolphinStore(this);
|
|
454
|
+
this.handlers = /* @__PURE__ */ new Map();
|
|
455
|
+
this.signalHandlers = /* @__PURE__ */ new Set();
|
|
456
|
+
this.fileHandlers = /* @__PURE__ */ new Set();
|
|
457
|
+
this._offlineQueue = [];
|
|
458
|
+
this.reconnectAttempts = 0;
|
|
459
|
+
this._attachedListeners = [];
|
|
460
|
+
if (typeof window !== "undefined" && typeof this._initDOMBinding === "function") {
|
|
461
|
+
this._initDOMBinding();
|
|
462
|
+
}
|
|
463
|
+
if (typeof this._initOffline === "function") {
|
|
464
|
+
this._initOffline();
|
|
465
|
+
}
|
|
466
|
+
if (typeof this._initA11y === "function") {
|
|
467
|
+
this._initA11y();
|
|
468
|
+
}
|
|
469
|
+
if (typeof this._initI18n === "function") {
|
|
470
|
+
this._initI18n();
|
|
471
|
+
}
|
|
472
|
+
if (typeof this._initDragDrop === "function") {
|
|
473
|
+
this._initDragDrop();
|
|
474
|
+
}
|
|
475
|
+
if (typeof this._initCollab === "function") {
|
|
476
|
+
this._initCollab();
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** Save or clear the access token */
|
|
480
|
+
setToken(token) {
|
|
481
|
+
this.accessToken = token;
|
|
482
|
+
token ? this.storage.setItem("dolphin_token", token) : this.storage.removeItem("dolphin_token");
|
|
483
|
+
}
|
|
484
|
+
// ── WebSocket ─────────────────────────────────────────────────────────────
|
|
485
|
+
/** Connect to the Dolphin realtime server */
|
|
486
|
+
async connect() {
|
|
487
|
+
return new Promise((resolve, reject) => {
|
|
488
|
+
const protocol = this.httpUrl.startsWith("https") ? "wss:" : "ws:";
|
|
489
|
+
const wsUrl = `${protocol}//${this.host}/realtime?deviceId=${this.deviceId}`;
|
|
490
|
+
console.log(`[Dolphin] Connecting to ${wsUrl}...`);
|
|
491
|
+
this.socket = new WebSocket(wsUrl);
|
|
492
|
+
this.socket.onopen = () => {
|
|
493
|
+
console.log(`[Dolphin] Connected as "${this.deviceId}" \u{1F42C}`);
|
|
494
|
+
this.reconnectAttempts = 0;
|
|
495
|
+
this._flushOfflineQueue();
|
|
496
|
+
resolve();
|
|
497
|
+
};
|
|
498
|
+
this.socket.onmessage = (ev) => this._handleMessage(ev.data);
|
|
499
|
+
this.socket.onclose = () => {
|
|
500
|
+
console.warn("[Dolphin] Connection closed");
|
|
501
|
+
this._maybeReconnect();
|
|
502
|
+
};
|
|
503
|
+
this.socket.onerror = (err) => {
|
|
504
|
+
console.error("[Dolphin] WebSocket error:", err);
|
|
505
|
+
reject(err);
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/** Disconnect cleanly */
|
|
510
|
+
disconnect() {
|
|
511
|
+
if (this.socket) {
|
|
512
|
+
this.socket.onclose = null;
|
|
513
|
+
this.socket.close();
|
|
514
|
+
this.socket = null;
|
|
515
|
+
}
|
|
516
|
+
this.cleanupDomListeners();
|
|
517
|
+
}
|
|
518
|
+
/** @private */
|
|
519
|
+
_handleMessage(data) {
|
|
520
|
+
try {
|
|
521
|
+
const msg = JSON.parse(data);
|
|
522
|
+
if (msg.type && msg.from && (msg.to === this.deviceId || msg.to === "all")) {
|
|
523
|
+
if (msg.msgId && msg.type !== "ACK") this._sendAck(msg.from, msg.msgId);
|
|
524
|
+
this.signalHandlers.forEach((h) => h(msg));
|
|
525
|
+
}
|
|
526
|
+
if (msg.type === "FILE_AVAILABLE") {
|
|
527
|
+
this.fileHandlers.forEach((h) => h(msg));
|
|
528
|
+
}
|
|
529
|
+
if (msg.type === "FILE_CHUNK") {
|
|
530
|
+
this.saveFileProgress(msg.fileId, msg.chunkIndex);
|
|
531
|
+
this._dispatch("file:chunk", msg);
|
|
532
|
+
this._dispatch(`file:chunk/${msg.fileId}`, msg);
|
|
533
|
+
}
|
|
534
|
+
if (msg.type === "FILE_UPLOAD_ACK") {
|
|
535
|
+
this._dispatch(`file:upload:ack/${msg.fileId}`, msg);
|
|
536
|
+
}
|
|
537
|
+
if (msg.type === "PULL_RESPONSE") {
|
|
538
|
+
this._dispatch("pull:response", msg.payload, msg.topic);
|
|
539
|
+
this._dispatch(`pull:response/${msg.topic}`, msg.payload, msg.topic);
|
|
540
|
+
}
|
|
541
|
+
if (msg.topic && msg.payload !== void 0) {
|
|
542
|
+
this.handlers.forEach((cbs, pattern) => {
|
|
543
|
+
if (this._matchTopic(pattern, msg.topic)) {
|
|
544
|
+
cbs.forEach((cb) => cb(msg.payload, msg.topic));
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
this._dispatch("raw", data);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/** @private */
|
|
553
|
+
_dispatch(pattern, payload, topic) {
|
|
554
|
+
const cbs = this.handlers.get(pattern);
|
|
555
|
+
if (cbs) cbs.forEach((cb) => cb(payload, topic || pattern));
|
|
556
|
+
}
|
|
557
|
+
/** @private */
|
|
558
|
+
_sendRaw(msg) {
|
|
559
|
+
const str = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
560
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
561
|
+
this.socket.send(str);
|
|
562
|
+
} else {
|
|
563
|
+
if (this._offlineQueue.length < 100) this._offlineQueue.push(str);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/** Flush buffered messages after reconnect @private */
|
|
567
|
+
_flushOfflineQueue() {
|
|
568
|
+
while (this._offlineQueue.length > 0) {
|
|
569
|
+
const msg = this._offlineQueue.shift();
|
|
570
|
+
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
571
|
+
this.socket.send(msg);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/** @private */
|
|
576
|
+
_sendAck(to, msgId) {
|
|
577
|
+
this._sendRaw({ type: "ACK", from: this.deviceId, to, data: { ackId: msgId }, timestamp: Date.now() });
|
|
578
|
+
}
|
|
579
|
+
/** MQTT wildcard topic matching @private */
|
|
580
|
+
_matchTopic(pattern, topic) {
|
|
581
|
+
if (pattern === topic || pattern === "#") return true;
|
|
582
|
+
const pp = pattern.split("/");
|
|
583
|
+
const tp = topic.split("/");
|
|
584
|
+
if (pp.length !== tp.length && !pattern.includes("#")) return false;
|
|
585
|
+
for (let i = 0; i < pp.length; i++) {
|
|
586
|
+
if (pp[i] === "#") return true;
|
|
587
|
+
if (pp[i] !== "+" && pp[i] !== tp[i]) return false;
|
|
588
|
+
}
|
|
589
|
+
return pp.length === tp.length;
|
|
590
|
+
}
|
|
591
|
+
/** @private */
|
|
592
|
+
_maybeReconnect() {
|
|
593
|
+
if (this.reconnectAttempts < this.options.maxReconnect) {
|
|
594
|
+
this.reconnectAttempts++;
|
|
595
|
+
const delay = Math.pow(2, this.reconnectAttempts) * 1e3;
|
|
596
|
+
console.log(`[Dolphin] Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts})...`);
|
|
597
|
+
setTimeout(() => this.connect().catch(() => {
|
|
598
|
+
}), delay);
|
|
599
|
+
} else {
|
|
600
|
+
console.error("[Dolphin] Max reconnect attempts reached.");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// ── Pub/Sub ───────────────────────────────────────────────────────────────
|
|
604
|
+
/**
|
|
605
|
+
* Subscribe to a topic (MQTT wildcards supported: + and #).
|
|
606
|
+
* @param {string} topic
|
|
607
|
+
* @param {TopicCallback} callback
|
|
608
|
+
*/
|
|
609
|
+
subscribe(topic, callback) {
|
|
610
|
+
if (!this.handlers.has(topic)) {
|
|
611
|
+
this.handlers.set(topic, /* @__PURE__ */ new Set());
|
|
612
|
+
this._sendRaw({ type: "sub", topic });
|
|
613
|
+
}
|
|
614
|
+
this.handlers.get(topic).add(callback);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Unsubscribe from a topic.
|
|
618
|
+
* @param {string} topic
|
|
619
|
+
* @param {TopicCallback} callback
|
|
620
|
+
*/
|
|
621
|
+
unsubscribe(topic, callback) {
|
|
622
|
+
if (this.handlers.has(topic)) {
|
|
623
|
+
const cbs = this.handlers.get(topic);
|
|
624
|
+
cbs.delete(callback);
|
|
625
|
+
if (cbs.size === 0) {
|
|
626
|
+
this.handlers.delete(topic);
|
|
627
|
+
this._sendRaw({ type: "unsub", topic });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Publish a message to a topic. Queued if offline.
|
|
633
|
+
* @param {string} topic
|
|
634
|
+
* @param {any} payload
|
|
635
|
+
*/
|
|
636
|
+
publish(topic, payload) {
|
|
637
|
+
this._sendRaw({ topic, payload });
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* High-frequency data push (IoT sensors).
|
|
641
|
+
* @param {string} topic
|
|
642
|
+
* @param {any} payload
|
|
643
|
+
*/
|
|
644
|
+
pubPush(topic, payload) {
|
|
645
|
+
this._sendRaw({ type: "pub", topic, payload });
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Request historical data from a topic.
|
|
649
|
+
* @param {string} topic
|
|
650
|
+
* @param {number} [count=10]
|
|
651
|
+
*/
|
|
652
|
+
subPull(topic, count = 10) {
|
|
653
|
+
this._sendRaw({ type: "PULL_REQUEST", topic, count });
|
|
654
|
+
}
|
|
655
|
+
// ── File Transfer ─────────────────────────────────────────────────────────
|
|
656
|
+
/**
|
|
657
|
+
* Upload a file to the server in chunks.
|
|
658
|
+
* @param {string} fileId
|
|
659
|
+
* @param {Blob|ArrayBuffer|Uint8Array} fileData
|
|
660
|
+
* @param {string} [filename]
|
|
661
|
+
* @param {function(number): void} [onProgress] — progress callback (0-100)
|
|
662
|
+
* @returns {Promise<void>}
|
|
663
|
+
*/
|
|
664
|
+
async pubFile(fileId, fileData, filename = "", onProgress) {
|
|
665
|
+
let buffer;
|
|
666
|
+
if (fileData instanceof Blob) {
|
|
667
|
+
buffer = await fileData.arrayBuffer();
|
|
668
|
+
} else if (fileData instanceof ArrayBuffer) {
|
|
669
|
+
buffer = fileData;
|
|
670
|
+
} else {
|
|
671
|
+
buffer = fileData.buffer || fileData;
|
|
672
|
+
}
|
|
673
|
+
const bytes = new Uint8Array(buffer);
|
|
674
|
+
const chunkSize = this.options.chunkSize;
|
|
675
|
+
const totalChunks = Math.ceil(bytes.length / chunkSize);
|
|
676
|
+
this._sendRaw({
|
|
677
|
+
type: "FILE_UPLOAD_START",
|
|
678
|
+
fileId,
|
|
679
|
+
name: filename,
|
|
680
|
+
size: bytes.length,
|
|
681
|
+
totalChunks,
|
|
682
|
+
chunkSize
|
|
683
|
+
});
|
|
684
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
685
|
+
const chunk = bytes.slice(i * chunkSize, (i + 1) * chunkSize);
|
|
686
|
+
const b64 = this._uint8ToBase64(chunk);
|
|
687
|
+
this._sendRaw({
|
|
688
|
+
type: "FILE_UPLOAD_CHUNK",
|
|
689
|
+
fileId,
|
|
690
|
+
chunkIndex: i,
|
|
691
|
+
totalChunks,
|
|
692
|
+
data: b64
|
|
693
|
+
});
|
|
694
|
+
if (onProgress) onProgress(Math.round((i + 1) / totalChunks * 100));
|
|
695
|
+
if (i % 10 === 0) await new Promise((r) => setTimeout(r, 0));
|
|
696
|
+
}
|
|
697
|
+
this._sendRaw({ type: "FILE_UPLOAD_DONE", fileId });
|
|
698
|
+
}
|
|
699
|
+
/** @private */
|
|
700
|
+
_uint8ToBase64(uint8) {
|
|
701
|
+
let binary = "";
|
|
702
|
+
for (let i = 0; i < uint8.length; i++) binary += String.fromCharCode(uint8[i]);
|
|
703
|
+
if (typeof btoa !== "undefined") return btoa(binary);
|
|
704
|
+
return Buffer.from(binary, "binary").toString("base64");
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Download a file from the server by chunks.
|
|
708
|
+
* @param {string} fileId
|
|
709
|
+
* @param {number} [startChunk=0]
|
|
710
|
+
*/
|
|
711
|
+
subFile(fileId, startChunk = 0) {
|
|
712
|
+
this._sendRaw({ type: "FILE_REQUEST", fileId, startChunk });
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* Resume a file download from saved progress.
|
|
716
|
+
* @param {string} fileId
|
|
717
|
+
*/
|
|
718
|
+
resumeFile(fileId) {
|
|
719
|
+
const last = parseInt(this.storage.getItem(`dolphin_file_${fileId}`) || "-1");
|
|
720
|
+
this.subFile(fileId, last + 1);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Save download chunk progress.
|
|
724
|
+
* @param {string} fileId
|
|
725
|
+
* @param {number} chunkIndex
|
|
726
|
+
*/
|
|
727
|
+
saveFileProgress(fileId, chunkIndex) {
|
|
728
|
+
this.storage.setItem(`dolphin_file_${fileId}`, chunkIndex.toString());
|
|
729
|
+
}
|
|
730
|
+
// ── Signaling ─────────────────────────────────────────────────────────────
|
|
731
|
+
/**
|
|
732
|
+
* @param {function(SignalMessage): void} handler
|
|
733
|
+
*/
|
|
734
|
+
onSignal(handler) {
|
|
735
|
+
this.signalHandlers.add(handler);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* @param {function(SignalMessage): void} handler
|
|
739
|
+
*/
|
|
740
|
+
offSignal(handler) {
|
|
741
|
+
this.signalHandlers.delete(handler);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* @param {function(FileMetadata): void} handler
|
|
745
|
+
*/
|
|
746
|
+
onFileAvailable(handler) {
|
|
747
|
+
this.fileHandlers.add(handler);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* @param {function(FileMetadata): void} handler
|
|
751
|
+
*/
|
|
752
|
+
offFileAvailable(handler) {
|
|
753
|
+
this.fileHandlers.delete(handler);
|
|
754
|
+
}
|
|
755
|
+
addDomListener(target, event, cb) {
|
|
756
|
+
if (!target) return;
|
|
757
|
+
target.addEventListener(event, cb);
|
|
758
|
+
this._attachedListeners = this._attachedListeners || [];
|
|
759
|
+
this._attachedListeners.push({ target, event, cb });
|
|
760
|
+
}
|
|
761
|
+
cleanupDomListeners() {
|
|
762
|
+
if (this._attachedListeners) {
|
|
763
|
+
this._attachedListeners.forEach(({ target, event, cb }) => {
|
|
764
|
+
try {
|
|
765
|
+
target.removeEventListener(event, cb);
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
this._attachedListeners = [];
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// src/dom.ts
|
|
775
|
+
function attachDOMBinding(clientProto) {
|
|
776
|
+
function escapeRegExp(str) {
|
|
777
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
778
|
+
}
|
|
779
|
+
function resolveTemplate(el) {
|
|
780
|
+
const template = el.getAttribute("data-rt-template");
|
|
781
|
+
if (!template) return null;
|
|
782
|
+
if (typeof document !== "undefined" && !template.includes("<")) {
|
|
783
|
+
try {
|
|
784
|
+
const tempEl = document.querySelector(template);
|
|
785
|
+
if (tempEl) return tempEl.innerHTML;
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return template;
|
|
790
|
+
}
|
|
791
|
+
function sanitizeHTML(html) {
|
|
792
|
+
if (typeof document === "undefined") return html;
|
|
793
|
+
try {
|
|
794
|
+
const parser = new DOMParser();
|
|
795
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
796
|
+
const body = doc.body;
|
|
797
|
+
const sanitizeNode = (el) => {
|
|
798
|
+
const tag = el.tagName.toLowerCase();
|
|
799
|
+
if (["script", "iframe", "object", "embed", "link", "style", "meta", "applet", "svg"].includes(tag)) {
|
|
800
|
+
el.parentNode?.removeChild(el);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const attrs = el.attributes;
|
|
804
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
805
|
+
const attrName = attrs[i].name.toLowerCase();
|
|
806
|
+
const attrVal = attrs[i].value.toLowerCase();
|
|
807
|
+
if (attrName.startsWith("on")) {
|
|
808
|
+
el.removeAttribute(attrs[i].name);
|
|
809
|
+
} else if (["src", "href", "data"].includes(attrName) && (attrVal.includes("javascript:") || attrVal.includes("data:text/html"))) {
|
|
810
|
+
el.removeAttribute(attrs[i].name);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
Array.from(el.children).forEach(sanitizeNode);
|
|
814
|
+
};
|
|
815
|
+
Array.from(body.children).forEach(sanitizeNode);
|
|
816
|
+
return body.innerHTML;
|
|
817
|
+
} catch {
|
|
818
|
+
return html;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
function diffDOM(existingNode, newNode) {
|
|
822
|
+
if (existingNode.nodeType !== newNode.nodeType) {
|
|
823
|
+
existingNode.parentNode?.replaceChild(newNode.cloneNode(true), existingNode);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (existingNode.nodeType === Node.TEXT_NODE) {
|
|
827
|
+
if (existingNode.textContent !== newNode.textContent) {
|
|
828
|
+
existingNode.textContent = newNode.textContent;
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (existingNode.nodeType === Node.ELEMENT_NODE) {
|
|
833
|
+
const el1 = existingNode;
|
|
834
|
+
const el2 = newNode;
|
|
835
|
+
if (el1.tagName !== el2.tagName) {
|
|
836
|
+
el1.parentNode?.replaceChild(el2.cloneNode(true), el1);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const attr1 = el1.attributes;
|
|
840
|
+
const attr2 = el2.attributes;
|
|
841
|
+
for (let i = attr1.length - 1; i >= 0; i--) {
|
|
842
|
+
const name = attr1[i].name;
|
|
843
|
+
if (!el2.hasAttribute(name)) el1.removeAttribute(name);
|
|
844
|
+
}
|
|
845
|
+
for (let i = 0; i < attr2.length; i++) {
|
|
846
|
+
const name = attr2[i].name;
|
|
847
|
+
const val = attr2[i].value;
|
|
848
|
+
if (el1.getAttribute(name) !== val) el1.setAttribute(name, val);
|
|
849
|
+
}
|
|
850
|
+
if (el1.tagName === "INPUT" || el1.tagName === "TEXTAREA") {
|
|
851
|
+
if (el1.value !== el2.value) el1.value = el2.value;
|
|
852
|
+
if (el1.checked !== el2.checked) el1.checked = el2.checked;
|
|
853
|
+
} else if (el1.tagName === "SELECT") {
|
|
854
|
+
if (el1.value !== el2.value) el1.value = el2.value;
|
|
855
|
+
}
|
|
856
|
+
const childs1 = Array.from(el1.childNodes);
|
|
857
|
+
const childs2 = Array.from(el2.childNodes);
|
|
858
|
+
const len1 = childs1.length;
|
|
859
|
+
const len2 = childs2.length;
|
|
860
|
+
const maxLen = Math.max(len1, len2);
|
|
861
|
+
for (let i = 0; i < maxLen; i++) {
|
|
862
|
+
if (i >= len1) {
|
|
863
|
+
el1.appendChild(childs2[i].cloneNode(true));
|
|
864
|
+
} else if (i >= len2) {
|
|
865
|
+
el1.removeChild(childs1[i]);
|
|
866
|
+
} else {
|
|
867
|
+
diffDOM(childs1[i], childs2[i]);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function patchDOM(parentElement, newHTML) {
|
|
873
|
+
if (typeof document === "undefined") return;
|
|
874
|
+
const temp = document.createElement(parentElement.tagName);
|
|
875
|
+
temp.innerHTML = newHTML;
|
|
876
|
+
const childs1 = Array.from(parentElement.childNodes);
|
|
877
|
+
const childs2 = Array.from(temp.childNodes);
|
|
878
|
+
const len1 = childs1.length;
|
|
879
|
+
const len2 = childs2.length;
|
|
880
|
+
const maxLen = Math.max(len1, len2);
|
|
881
|
+
for (let i = 0; i < maxLen; i++) {
|
|
882
|
+
if (i >= len1) {
|
|
883
|
+
parentElement.appendChild(childs2[i].cloneNode(true));
|
|
884
|
+
} else if (i >= len2) {
|
|
885
|
+
parentElement.removeChild(childs1[i]);
|
|
886
|
+
} else {
|
|
887
|
+
diffDOM(childs1[i], childs2[i]);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
892
|
+
let rafScheduled = false;
|
|
893
|
+
function scheduleDOMUpdate(element, newHTML) {
|
|
894
|
+
pendingUpdates.set(element, newHTML);
|
|
895
|
+
if (!rafScheduled) {
|
|
896
|
+
rafScheduled = true;
|
|
897
|
+
const scheduleFn = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 0);
|
|
898
|
+
scheduleFn(() => {
|
|
899
|
+
pendingUpdates.forEach((html, el) => {
|
|
900
|
+
patchDOM(el, html);
|
|
901
|
+
});
|
|
902
|
+
pendingUpdates.clear();
|
|
903
|
+
rafScheduled = false;
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
clientProto.setStoreState = function(storeName, key, val) {
|
|
908
|
+
this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
|
|
909
|
+
if (!this.uiStores.has(storeName)) {
|
|
910
|
+
this.uiStores.set(storeName, {});
|
|
911
|
+
}
|
|
912
|
+
const store = this.uiStores.get(storeName);
|
|
913
|
+
store[key] = val;
|
|
914
|
+
if (typeof document !== "undefined") {
|
|
915
|
+
const readElements = document.querySelectorAll(`[data-store-read="${storeName}.${key}"]`);
|
|
916
|
+
readElements.forEach((el) => {
|
|
917
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
918
|
+
if (el.type === "checkbox") {
|
|
919
|
+
el.checked = !!val;
|
|
920
|
+
} else {
|
|
921
|
+
el.value = val !== void 0 && val !== null ? val : "";
|
|
922
|
+
}
|
|
923
|
+
} else {
|
|
924
|
+
el.textContent = val !== void 0 && val !== null ? val : "";
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
this.publish(`store/${storeName}`, store);
|
|
929
|
+
};
|
|
930
|
+
clientProto.getStoreState = function(storeName, key) {
|
|
931
|
+
this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
|
|
932
|
+
const store = this.uiStores.get(storeName);
|
|
933
|
+
return store ? store[key] : void 0;
|
|
934
|
+
};
|
|
935
|
+
clientProto._scanStoreBinds = function() {
|
|
936
|
+
if (typeof document === "undefined") return;
|
|
937
|
+
const writeEls = document.querySelectorAll("[data-store-write]");
|
|
938
|
+
writeEls.forEach((el) => {
|
|
939
|
+
const writeBind = el.getAttribute("data-store-write");
|
|
940
|
+
if (writeBind) {
|
|
941
|
+
const parts = writeBind.split(".");
|
|
942
|
+
if (parts.length === 2) {
|
|
943
|
+
const storeName = parts[0];
|
|
944
|
+
const key = parts[1];
|
|
945
|
+
const val = el.type === "checkbox" ? el.checked : el.value;
|
|
946
|
+
this.uiStores = this.uiStores || /* @__PURE__ */ new Map();
|
|
947
|
+
if (!this.uiStores.has(storeName)) {
|
|
948
|
+
this.uiStores.set(storeName, {});
|
|
949
|
+
}
|
|
950
|
+
const store = this.uiStores.get(storeName);
|
|
951
|
+
if (store[key] === void 0) {
|
|
952
|
+
store[key] = val;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
const readEls = document.querySelectorAll("[data-store-read]");
|
|
958
|
+
readEls.forEach((el) => {
|
|
959
|
+
const readBind = el.getAttribute("data-store-read");
|
|
960
|
+
if (readBind) {
|
|
961
|
+
const parts = readBind.split(".");
|
|
962
|
+
if (parts.length === 2) {
|
|
963
|
+
const storeName = parts[0];
|
|
964
|
+
const key = parts[1];
|
|
965
|
+
const val = this.getStoreState(storeName, key);
|
|
966
|
+
if (val !== void 0 && val !== null) {
|
|
967
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
968
|
+
if (el.type === "checkbox") {
|
|
969
|
+
el.checked = !!val;
|
|
970
|
+
} else {
|
|
971
|
+
el.value = val;
|
|
972
|
+
}
|
|
973
|
+
} else {
|
|
974
|
+
el.textContent = val;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
};
|
|
981
|
+
clientProto.getClosestContext = function(element, key) {
|
|
982
|
+
let current = element;
|
|
983
|
+
while (current) {
|
|
984
|
+
if (current._rtContext) {
|
|
985
|
+
const ctx = current._rtContext;
|
|
986
|
+
if (key) return ctx[key];
|
|
987
|
+
return ctx;
|
|
988
|
+
}
|
|
989
|
+
current = current.parentElement;
|
|
990
|
+
}
|
|
991
|
+
return null;
|
|
992
|
+
};
|
|
993
|
+
clientProto._initDOMBinding = function() {
|
|
994
|
+
if (this._domInitialized) return;
|
|
995
|
+
this._domInitialized = true;
|
|
996
|
+
const PUSH_EVENTS = ["input", "change", "keyup", "paste", "blur"];
|
|
997
|
+
const debounceTimers = /* @__PURE__ */ new Map();
|
|
998
|
+
PUSH_EVENTS.forEach((evtName) => {
|
|
999
|
+
this.addDomListener(document, evtName, (e) => {
|
|
1000
|
+
if (!e.target || !e.target.getAttribute) return;
|
|
1001
|
+
const writeBind = e.target.getAttribute("data-store-write");
|
|
1002
|
+
if (writeBind) {
|
|
1003
|
+
const parts = writeBind.split(".");
|
|
1004
|
+
if (parts.length === 2) {
|
|
1005
|
+
const storeName = parts[0];
|
|
1006
|
+
const key = parts[1];
|
|
1007
|
+
const val = e.target.type === "checkbox" ? e.target.checked : e.target.value;
|
|
1008
|
+
this.setStoreState(storeName, key, val);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const rules = e.target.getAttribute("data-rt-validate");
|
|
1012
|
+
const name = e.target.name;
|
|
1013
|
+
if (rules && name && typeof this.validateField === "function") {
|
|
1014
|
+
const form = e.target.closest("form");
|
|
1015
|
+
const formValues = form ? Object.fromEntries(new FormData(form).entries()) : {};
|
|
1016
|
+
const errorMsg = this.validateField(e.target.value, rules, formValues);
|
|
1017
|
+
if (errorMsg) {
|
|
1018
|
+
e.target.classList.add("invalid");
|
|
1019
|
+
this.publish(`errors/${name}`, errorMsg);
|
|
1020
|
+
} else {
|
|
1021
|
+
e.target.classList.remove("invalid");
|
|
1022
|
+
this.publish(`errors/${name}`, "");
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const topic = e.target.getAttribute("data-rt-push");
|
|
1026
|
+
if (topic) {
|
|
1027
|
+
const debounceVal = e.target.getAttribute("data-rt-debounce");
|
|
1028
|
+
const waitMs = debounceVal ? parseInt(debounceVal, 10) : 0;
|
|
1029
|
+
const triggerPush = () => {
|
|
1030
|
+
const payload = { name: e.target.name, value: e.target.value };
|
|
1031
|
+
this.pubPush(topic, payload);
|
|
1032
|
+
};
|
|
1033
|
+
if (waitMs > 0) {
|
|
1034
|
+
if (debounceTimers.has(e.target)) {
|
|
1035
|
+
clearTimeout(debounceTimers.get(e.target));
|
|
1036
|
+
}
|
|
1037
|
+
const timer = setTimeout(triggerPush, waitMs);
|
|
1038
|
+
debounceTimers.set(e.target, timer);
|
|
1039
|
+
} else {
|
|
1040
|
+
triggerPush();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
this.addDomListener(document, "submit", async (e) => {
|
|
1046
|
+
if (!e.target || !e.target.getAttribute) return;
|
|
1047
|
+
const rtTopic = e.target.getAttribute("data-rt-submit");
|
|
1048
|
+
const apiTarget = e.target.getAttribute("data-api-submit");
|
|
1049
|
+
if (rtTopic || apiTarget) {
|
|
1050
|
+
const validatedInputs = e.target.querySelectorAll("[data-rt-validate]");
|
|
1051
|
+
let formIsValid = true;
|
|
1052
|
+
if (validatedInputs.length > 0 && typeof this.validateField === "function") {
|
|
1053
|
+
const formValues = Object.fromEntries(new FormData(e.target).entries());
|
|
1054
|
+
validatedInputs.forEach((inputEl) => {
|
|
1055
|
+
const rules = inputEl.getAttribute("data-rt-validate");
|
|
1056
|
+
const name = inputEl.name;
|
|
1057
|
+
if (rules && name) {
|
|
1058
|
+
const errorMsg = this.validateField(inputEl.value, rules, formValues);
|
|
1059
|
+
if (errorMsg) {
|
|
1060
|
+
formIsValid = false;
|
|
1061
|
+
inputEl.classList.add("invalid");
|
|
1062
|
+
this.publish(`errors/${name}`, errorMsg);
|
|
1063
|
+
} else {
|
|
1064
|
+
inputEl.classList.remove("invalid");
|
|
1065
|
+
this.publish(`errors/${name}`, "");
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
if (!formIsValid) {
|
|
1071
|
+
e.preventDefault();
|
|
1072
|
+
e.stopPropagation();
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
e.preventDefault();
|
|
1076
|
+
const parentCtx = this.getClosestContext(e.target) || {};
|
|
1077
|
+
const formData = new FormData(e.target);
|
|
1078
|
+
const data = Object.fromEntries(formData.entries());
|
|
1079
|
+
if (rtTopic) {
|
|
1080
|
+
let resolvedTopic = rtTopic;
|
|
1081
|
+
for (const k in parentCtx) {
|
|
1082
|
+
const escapedK = escapeRegExp(k);
|
|
1083
|
+
resolvedTopic = resolvedTopic.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1084
|
+
}
|
|
1085
|
+
this.publish(resolvedTopic, data);
|
|
1086
|
+
} else if (apiTarget) {
|
|
1087
|
+
let resolvedTarget = apiTarget;
|
|
1088
|
+
for (const k in parentCtx) {
|
|
1089
|
+
const escapedK = escapeRegExp(k);
|
|
1090
|
+
resolvedTarget = resolvedTarget.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1091
|
+
}
|
|
1092
|
+
const parts = resolvedTarget.trim().split(" ");
|
|
1093
|
+
const method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
|
|
1094
|
+
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
1095
|
+
try {
|
|
1096
|
+
const result = await this.api.request(method, path, data);
|
|
1097
|
+
const resultBind = e.target.getAttribute("data-api-result");
|
|
1098
|
+
if (resultBind) this._updateDOM(resultBind, result);
|
|
1099
|
+
const redirect = e.target.getAttribute("data-api-redirect");
|
|
1100
|
+
if (redirect) window.location.href = redirect;
|
|
1101
|
+
if (e.target.hasAttribute("data-api-reload")) window.location.reload();
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
console.error("[Dolphin] API Submit Error:", err);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
const INTERACTION_EVENTS = ["click", "change", "submit", "input", "keydown", "keyup", "dblclick", "focus", "blur", "mouseenter", "mouseleave"];
|
|
1109
|
+
INTERACTION_EVENTS.forEach((evtName) => {
|
|
1110
|
+
this.addDomListener(document, evtName, async (e) => {
|
|
1111
|
+
if (!e.target || !e.target.closest) return;
|
|
1112
|
+
const rtBtn = e.target.closest(`[data-rt-${evtName}]`);
|
|
1113
|
+
const apiBtn = e.target.closest(`[data-api-${evtName}]`);
|
|
1114
|
+
if (rtBtn) {
|
|
1115
|
+
if (evtName === "submit") e.preventDefault();
|
|
1116
|
+
const topic = rtBtn.getAttribute(`data-rt-${evtName}`);
|
|
1117
|
+
const actionData = rtBtn.getAttribute("data-rt-payload");
|
|
1118
|
+
const parentCtx = this.getClosestContext(rtBtn) || {};
|
|
1119
|
+
let payload = {};
|
|
1120
|
+
if (actionData) {
|
|
1121
|
+
let resolvedDataStr = actionData;
|
|
1122
|
+
for (const k in parentCtx) {
|
|
1123
|
+
const escapedK = escapeRegExp(k);
|
|
1124
|
+
resolvedDataStr = resolvedDataStr.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
payload = JSON.parse(resolvedDataStr);
|
|
1128
|
+
} catch {
|
|
1129
|
+
payload = {};
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
this.publish(topic, payload);
|
|
1133
|
+
}
|
|
1134
|
+
if (apiBtn) {
|
|
1135
|
+
if (evtName === "submit") e.preventDefault();
|
|
1136
|
+
const apiTarget = apiBtn.getAttribute(`data-api-${evtName}`);
|
|
1137
|
+
const actionData = apiBtn.getAttribute("data-api-payload");
|
|
1138
|
+
const parentCtx = this.getClosestContext(apiBtn) || {};
|
|
1139
|
+
const parts = apiTarget.trim().split(" ");
|
|
1140
|
+
const method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
|
|
1141
|
+
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
1142
|
+
let payload = null;
|
|
1143
|
+
if (actionData) {
|
|
1144
|
+
let resolvedDataStr = actionData;
|
|
1145
|
+
for (const k in parentCtx) {
|
|
1146
|
+
const escapedK = escapeRegExp(k);
|
|
1147
|
+
resolvedDataStr = resolvedDataStr.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
payload = JSON.parse(resolvedDataStr);
|
|
1151
|
+
} catch {
|
|
1152
|
+
payload = null;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
const result = await this.api.request(method, path, payload);
|
|
1157
|
+
const resultBind = apiBtn.getAttribute("data-api-result");
|
|
1158
|
+
if (resultBind) this._updateDOM(resultBind, result);
|
|
1159
|
+
const redirect = apiBtn.getAttribute("data-api-redirect");
|
|
1160
|
+
if (redirect) window.location.href = redirect;
|
|
1161
|
+
if (apiBtn.hasAttribute("data-api-reload")) window.location.reload();
|
|
1162
|
+
} catch (err) {
|
|
1163
|
+
console.error(`[Dolphin] API ${evtName} Error:`, err);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
this.subscribe("#", (payload, topic) => {
|
|
1169
|
+
this._updateDOM(topic, payload);
|
|
1170
|
+
});
|
|
1171
|
+
this._scanAndFetchAPIBinds();
|
|
1172
|
+
this._scanStoreBinds();
|
|
1173
|
+
};
|
|
1174
|
+
clientProto._scanAndFetchAPIBinds = async function() {
|
|
1175
|
+
if (typeof document === "undefined") return;
|
|
1176
|
+
const elements = document.querySelectorAll("[data-api-get]");
|
|
1177
|
+
for (const el of Array.from(elements)) {
|
|
1178
|
+
const path = el.getAttribute("data-api-get");
|
|
1179
|
+
if (!path) continue;
|
|
1180
|
+
try {
|
|
1181
|
+
const result = await this.api.get(path);
|
|
1182
|
+
const rtBind = el.getAttribute("data-rt-bind");
|
|
1183
|
+
if (rtBind) {
|
|
1184
|
+
this._updateDOM(rtBind, result);
|
|
1185
|
+
} else {
|
|
1186
|
+
const template = resolveTemplate(el);
|
|
1187
|
+
if (template && typeof result === "object" && result !== null) {
|
|
1188
|
+
if (Array.isArray(result)) {
|
|
1189
|
+
let combinedHTML = "";
|
|
1190
|
+
for (const item of result) {
|
|
1191
|
+
let finalItemHTML = template;
|
|
1192
|
+
for (let key in item) {
|
|
1193
|
+
const escapedKey = escapeRegExp(key);
|
|
1194
|
+
finalItemHTML = finalItemHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), item[key] !== void 0 && item[key] !== null ? item[key] : "");
|
|
1195
|
+
}
|
|
1196
|
+
combinedHTML += finalItemHTML;
|
|
1197
|
+
}
|
|
1198
|
+
scheduleDOMUpdate(el, combinedHTML);
|
|
1199
|
+
} else {
|
|
1200
|
+
let finalHTML = template;
|
|
1201
|
+
for (let key in result) {
|
|
1202
|
+
const escapedKey = escapeRegExp(key);
|
|
1203
|
+
finalHTML = finalHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), result[key] !== void 0 && result[key] !== null ? result[key] : "");
|
|
1204
|
+
}
|
|
1205
|
+
scheduleDOMUpdate(el, finalHTML);
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1209
|
+
el.value = typeof result === "object" ? result.value !== void 0 ? result.value : "" : result;
|
|
1210
|
+
} else {
|
|
1211
|
+
el.innerHTML = typeof result === "object" ? result.html || result.text || JSON.stringify(result) : result;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
} catch (e) {
|
|
1216
|
+
console.error("[Dolphin] API Get Error:", e);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
clientProto._updateDOM = function(topic, payload) {
|
|
1221
|
+
if (typeof document === "undefined") return;
|
|
1222
|
+
const elements = document.querySelectorAll(`[data-rt-bind="${topic}"]`);
|
|
1223
|
+
elements.forEach((el) => {
|
|
1224
|
+
if (el.getAttribute("data-rt-type") === "context" && typeof payload === "object" && payload !== null) {
|
|
1225
|
+
el._rtContext = payload;
|
|
1226
|
+
const processNode = (node) => {
|
|
1227
|
+
if (node.hasAttribute("data-rt-text")) {
|
|
1228
|
+
const key = node.getAttribute("data-rt-text");
|
|
1229
|
+
if (key && payload[key] !== void 0 && payload[key] !== null) node.textContent = payload[key];
|
|
1230
|
+
}
|
|
1231
|
+
if (node.hasAttribute("data-rt-html")) {
|
|
1232
|
+
const key = node.getAttribute("data-rt-html");
|
|
1233
|
+
if (key && payload[key] !== void 0 && payload[key] !== null) {
|
|
1234
|
+
node.innerHTML = sanitizeHTML(payload[key]);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (node.hasAttribute("data-rt-attr")) {
|
|
1238
|
+
const attrStr = node.getAttribute("data-rt-attr");
|
|
1239
|
+
if (attrStr) {
|
|
1240
|
+
attrStr.split(",").forEach((b) => {
|
|
1241
|
+
const parts = b.split(":");
|
|
1242
|
+
if (parts.length === 2) {
|
|
1243
|
+
const attrName = parts[0].trim();
|
|
1244
|
+
const key = parts[1].trim();
|
|
1245
|
+
if (attrName && key && payload[key] !== void 0 && payload[key] !== null) {
|
|
1246
|
+
node.setAttribute(attrName, payload[key]);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (node.hasAttribute("data-rt-class")) {
|
|
1253
|
+
const classStr = node.getAttribute("data-rt-class");
|
|
1254
|
+
if (classStr) {
|
|
1255
|
+
classStr.split(",").forEach((b) => {
|
|
1256
|
+
const parts = b.split(":");
|
|
1257
|
+
if (parts.length === 2) {
|
|
1258
|
+
const className = parts[0].trim();
|
|
1259
|
+
const key = parts[1].trim();
|
|
1260
|
+
if (payload[key]) {
|
|
1261
|
+
node.classList.add(className);
|
|
1262
|
+
} else {
|
|
1263
|
+
node.classList.remove(className);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (node.hasAttribute("data-rt-if")) {
|
|
1270
|
+
const key = node.getAttribute("data-rt-if");
|
|
1271
|
+
if (key) {
|
|
1272
|
+
if (payload[key]) {
|
|
1273
|
+
node.style.display = "";
|
|
1274
|
+
} else {
|
|
1275
|
+
node.style.display = "none";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
if (node.hasAttribute("data-rt-hide")) {
|
|
1280
|
+
const key = node.getAttribute("data-rt-hide");
|
|
1281
|
+
if (key) {
|
|
1282
|
+
if (payload[key]) {
|
|
1283
|
+
node.style.display = "none";
|
|
1284
|
+
} else {
|
|
1285
|
+
node.style.display = "";
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
processNode(el);
|
|
1291
|
+
el.querySelectorAll("[data-rt-text], [data-rt-html], [data-rt-attr], [data-rt-class], [data-rt-if], [data-rt-hide]").forEach(processNode);
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const template = resolveTemplate(el);
|
|
1295
|
+
if (template && typeof payload === "object" && payload !== null) {
|
|
1296
|
+
if (Array.isArray(payload)) {
|
|
1297
|
+
let combinedHTML = "";
|
|
1298
|
+
for (const item of payload) {
|
|
1299
|
+
let finalItemHTML = template;
|
|
1300
|
+
for (let key in item) {
|
|
1301
|
+
const escapedKey = escapeRegExp(key);
|
|
1302
|
+
finalItemHTML = finalItemHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), item[key] !== void 0 && item[key] !== null ? item[key] : "");
|
|
1303
|
+
}
|
|
1304
|
+
combinedHTML += finalItemHTML;
|
|
1305
|
+
}
|
|
1306
|
+
scheduleDOMUpdate(el, combinedHTML);
|
|
1307
|
+
} else {
|
|
1308
|
+
let finalHTML = template;
|
|
1309
|
+
for (let key in payload) {
|
|
1310
|
+
const escapedKey = escapeRegExp(key);
|
|
1311
|
+
finalHTML = finalHTML.replace(new RegExp(`\\{\\{${escapedKey}\\}\\}`, "g"), payload[key] !== void 0 && payload[key] !== null ? payload[key] : "");
|
|
1312
|
+
}
|
|
1313
|
+
scheduleDOMUpdate(el, finalHTML);
|
|
1314
|
+
}
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1318
|
+
el.value = typeof payload === "object" ? payload.value !== void 0 ? payload.value : "" : payload;
|
|
1319
|
+
} else {
|
|
1320
|
+
el.innerHTML = typeof payload === "object" ? payload.html || payload.text || JSON.stringify(payload) : payload;
|
|
1321
|
+
}
|
|
1322
|
+
});
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/offline.ts
|
|
1327
|
+
var DolphinOffline = class {
|
|
1328
|
+
client;
|
|
1329
|
+
db;
|
|
1330
|
+
isOnline;
|
|
1331
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
1332
|
+
memoryMutations = [];
|
|
1333
|
+
constructor(client) {
|
|
1334
|
+
this.client = client;
|
|
1335
|
+
this.isOnline = typeof window !== "undefined" && typeof navigator !== "undefined" ? navigator.onLine : true;
|
|
1336
|
+
this.initDB();
|
|
1337
|
+
this.setupNetworkListeners();
|
|
1338
|
+
}
|
|
1339
|
+
initDB() {
|
|
1340
|
+
if (typeof indexedDB === "undefined") return;
|
|
1341
|
+
try {
|
|
1342
|
+
const request = indexedDB.open("dolphin_offline", 1);
|
|
1343
|
+
request.onupgradeneeded = (e) => {
|
|
1344
|
+
const db = e.target.result;
|
|
1345
|
+
if (!db.objectStoreNames.contains("cache")) {
|
|
1346
|
+
db.createObjectStore("cache");
|
|
1347
|
+
}
|
|
1348
|
+
if (!db.objectStoreNames.contains("mutations")) {
|
|
1349
|
+
db.createObjectStore("mutations", { keyPath: "id", autoIncrement: true });
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
request.onsuccess = (e) => {
|
|
1353
|
+
this.db = e.target.result;
|
|
1354
|
+
if (this.isOnline) {
|
|
1355
|
+
this.syncMutations();
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
console.warn("[Dolphin Offline] Failed to initialize IndexedDB:", err);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
setupNetworkListeners() {
|
|
1363
|
+
if (typeof window === "undefined") return;
|
|
1364
|
+
this.client.addDomListener(window, "online", () => {
|
|
1365
|
+
this.isOnline = true;
|
|
1366
|
+
this.client._dispatch("network:status", { online: true });
|
|
1367
|
+
this.syncMutations();
|
|
1368
|
+
});
|
|
1369
|
+
this.client.addDomListener(window, "offline", () => {
|
|
1370
|
+
this.isOnline = false;
|
|
1371
|
+
this.client._dispatch("network:status", { online: false });
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
async getCache(key) {
|
|
1375
|
+
if (!this.db) {
|
|
1376
|
+
return this.memoryCache.get(key);
|
|
1377
|
+
}
|
|
1378
|
+
return new Promise((resolve) => {
|
|
1379
|
+
try {
|
|
1380
|
+
const transaction = this.db.transaction("cache", "readonly");
|
|
1381
|
+
const store = transaction.objectStore("cache");
|
|
1382
|
+
const req = store.get(key);
|
|
1383
|
+
req.onsuccess = () => resolve(req.result ? req.result.data : null);
|
|
1384
|
+
req.onerror = () => resolve(null);
|
|
1385
|
+
} catch {
|
|
1386
|
+
resolve(null);
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
async setCache(key, data) {
|
|
1391
|
+
if (!this.db) {
|
|
1392
|
+
this.memoryCache.set(key, data);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
return new Promise((resolve) => {
|
|
1396
|
+
try {
|
|
1397
|
+
const transaction = this.db.transaction("cache", "readwrite");
|
|
1398
|
+
const store = transaction.objectStore("cache");
|
|
1399
|
+
store.put({ data, timestamp: Date.now() }, key);
|
|
1400
|
+
transaction.oncomplete = () => resolve();
|
|
1401
|
+
} catch {
|
|
1402
|
+
resolve();
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
async queueMutation(method, path, payload) {
|
|
1407
|
+
const mutation = {
|
|
1408
|
+
method,
|
|
1409
|
+
path,
|
|
1410
|
+
payload,
|
|
1411
|
+
timestamp: Date.now()
|
|
1412
|
+
};
|
|
1413
|
+
if (!this.db) {
|
|
1414
|
+
this.memoryMutations.push(mutation);
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
return new Promise((resolve) => {
|
|
1418
|
+
try {
|
|
1419
|
+
const transaction = this.db.transaction("mutations", "readwrite");
|
|
1420
|
+
const store = transaction.objectStore("mutations");
|
|
1421
|
+
store.add(mutation);
|
|
1422
|
+
transaction.oncomplete = () => resolve();
|
|
1423
|
+
} catch {
|
|
1424
|
+
resolve();
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
async getMutations() {
|
|
1429
|
+
if (!this.db) {
|
|
1430
|
+
return [...this.memoryMutations];
|
|
1431
|
+
}
|
|
1432
|
+
return new Promise((resolve) => {
|
|
1433
|
+
try {
|
|
1434
|
+
const transaction = this.db.transaction("mutations", "readonly");
|
|
1435
|
+
const store = transaction.objectStore("mutations");
|
|
1436
|
+
const req = store.getAll();
|
|
1437
|
+
req.onsuccess = () => resolve(req.result || []);
|
|
1438
|
+
req.onerror = () => resolve([]);
|
|
1439
|
+
} catch {
|
|
1440
|
+
resolve([]);
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
async removeMutation(id) {
|
|
1445
|
+
if (!this.db) {
|
|
1446
|
+
this.memoryMutations = this.memoryMutations.filter((m) => m.id !== id);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
return new Promise((resolve) => {
|
|
1450
|
+
try {
|
|
1451
|
+
const transaction = this.db.transaction("mutations", "readwrite");
|
|
1452
|
+
const store = transaction.objectStore("mutations");
|
|
1453
|
+
store.delete(id);
|
|
1454
|
+
transaction.oncomplete = () => resolve();
|
|
1455
|
+
} catch {
|
|
1456
|
+
resolve();
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
async syncMutations() {
|
|
1461
|
+
const mutations = await this.getMutations();
|
|
1462
|
+
if (mutations.length === 0) return;
|
|
1463
|
+
console.log(`[Dolphin Offline] Syncing ${mutations.length} queued mutations...`);
|
|
1464
|
+
for (const mutation of mutations) {
|
|
1465
|
+
try {
|
|
1466
|
+
await this.client.api.requestDirect(mutation.method, mutation.path, mutation.payload);
|
|
1467
|
+
if (mutation.id !== void 0) {
|
|
1468
|
+
await this.removeMutation(mutation.id);
|
|
1469
|
+
} else {
|
|
1470
|
+
this.memoryMutations.shift();
|
|
1471
|
+
}
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
console.error(`[Dolphin Offline] Sync failed for mutation ${mutation.method} ${mutation.path}:`, err);
|
|
1474
|
+
if (err && err.status && err.status >= 400 && err.status < 500) {
|
|
1475
|
+
console.warn("[Dolphin Offline] Discarding invalid mutation.");
|
|
1476
|
+
if (mutation.id !== void 0) {
|
|
1477
|
+
await this.removeMutation(mutation.id);
|
|
1478
|
+
} else {
|
|
1479
|
+
this.memoryMutations.shift();
|
|
1480
|
+
}
|
|
1481
|
+
} else {
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
function attachOffline(clientProto) {
|
|
1489
|
+
clientProto._initOffline = function() {
|
|
1490
|
+
this.offline = new DolphinOffline(this);
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/validation.ts
|
|
1495
|
+
function validateField(value, rulesStr, allValues) {
|
|
1496
|
+
const rules = rulesStr.split(",");
|
|
1497
|
+
for (const rule of rules) {
|
|
1498
|
+
const parts = rule.trim().split(":");
|
|
1499
|
+
const ruleName = parts[0];
|
|
1500
|
+
const ruleArg = parts[1];
|
|
1501
|
+
if (ruleName === "required") {
|
|
1502
|
+
if (!value || value.trim() === "") {
|
|
1503
|
+
return "This field is required";
|
|
1504
|
+
}
|
|
1505
|
+
} else if (ruleName === "email") {
|
|
1506
|
+
if (value && value.trim() !== "") {
|
|
1507
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1508
|
+
if (!emailRegex.test(value)) {
|
|
1509
|
+
return "Please enter a valid email address";
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
} else if (ruleName === "min") {
|
|
1513
|
+
const minLen = parseInt(ruleArg, 10);
|
|
1514
|
+
if (!value || value.length < minLen) {
|
|
1515
|
+
return `Must be at least ${minLen} characters`;
|
|
1516
|
+
}
|
|
1517
|
+
} else if (ruleName === "match") {
|
|
1518
|
+
if (allValues && value !== allValues[ruleArg]) {
|
|
1519
|
+
return `Must match ${ruleArg}`;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
function attachValidation(clientProto) {
|
|
1526
|
+
clientProto.validateField = validateField;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/animation.ts
|
|
1530
|
+
function attachAnimations(clientProto) {
|
|
1531
|
+
clientProto.animateElement = function(el, animationClass, durationMs = 300) {
|
|
1532
|
+
if (typeof el.animate !== "function") {
|
|
1533
|
+
el.classList.add(animationClass);
|
|
1534
|
+
setTimeout(() => el.classList.remove(animationClass), durationMs);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (animationClass === "fade-in") {
|
|
1538
|
+
el.animate([
|
|
1539
|
+
{ opacity: 0, transform: "translateY(10px)" },
|
|
1540
|
+
{ opacity: 1, transform: "translateY(0)" }
|
|
1541
|
+
], { duration: durationMs, easing: "ease-out" });
|
|
1542
|
+
} else if (animationClass === "fade-out") {
|
|
1543
|
+
el.animate([
|
|
1544
|
+
{ opacity: 1, transform: "translateY(0)" },
|
|
1545
|
+
{ opacity: 0, transform: "translateY(10px)" }
|
|
1546
|
+
], { duration: durationMs, easing: "ease-in" });
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
clientProto.staggerListItems = function(container, itemSelector, delayMs = 50) {
|
|
1550
|
+
if (typeof document === "undefined") return;
|
|
1551
|
+
const items = container.querySelectorAll(itemSelector);
|
|
1552
|
+
items.forEach((item, idx) => {
|
|
1553
|
+
item.style.animationDelay = `${idx * delayMs}ms`;
|
|
1554
|
+
item.classList.add("staggered-item");
|
|
1555
|
+
});
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/a11y.ts
|
|
1560
|
+
function attachA11y(clientProto) {
|
|
1561
|
+
clientProto._initA11y = function() {
|
|
1562
|
+
if (typeof document === "undefined") return;
|
|
1563
|
+
this.addDomListener(document, "keydown", (e) => {
|
|
1564
|
+
if (e.key !== "Tab") return;
|
|
1565
|
+
const trappedContainers = document.querySelectorAll("[data-rt-a11y-focus-trap]");
|
|
1566
|
+
trappedContainers.forEach((container) => {
|
|
1567
|
+
if (container.style.display === "none" || container.hasAttribute("aria-hidden") && container.getAttribute("aria-hidden") === "true") {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const focusableSelectors = 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
|
|
1571
|
+
const focusableElements = Array.from(container.querySelectorAll(focusableSelectors));
|
|
1572
|
+
if (focusableElements.length === 0) return;
|
|
1573
|
+
const firstEl = focusableElements[0];
|
|
1574
|
+
const lastEl = focusableElements[focusableElements.length - 1];
|
|
1575
|
+
if (e.shiftKey) {
|
|
1576
|
+
if (document.activeElement === firstEl) {
|
|
1577
|
+
lastEl.focus();
|
|
1578
|
+
e.preventDefault();
|
|
1579
|
+
}
|
|
1580
|
+
} else {
|
|
1581
|
+
if (document.activeElement === lastEl) {
|
|
1582
|
+
firstEl.focus();
|
|
1583
|
+
e.preventDefault();
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
});
|
|
1588
|
+
this.addDomListener(document, "keydown", (e) => {
|
|
1589
|
+
if (!["ArrowUp", "ArrowDown", "Enter"].includes(e.key)) return;
|
|
1590
|
+
const keyNavLists = document.querySelectorAll("[data-rt-keynav]");
|
|
1591
|
+
keyNavLists.forEach((list) => {
|
|
1592
|
+
const items = Array.from(list.children);
|
|
1593
|
+
if (items.length === 0) return;
|
|
1594
|
+
let activeIdx = items.findIndex((el) => el.classList.contains("active") || document.activeElement === el);
|
|
1595
|
+
if (e.key === "ArrowDown") {
|
|
1596
|
+
activeIdx = (activeIdx + 1) % items.length;
|
|
1597
|
+
items[activeIdx].focus();
|
|
1598
|
+
items.forEach((item, idx) => {
|
|
1599
|
+
if (idx === activeIdx) item.classList.add("active");
|
|
1600
|
+
else item.classList.remove("active");
|
|
1601
|
+
});
|
|
1602
|
+
e.preventDefault();
|
|
1603
|
+
} else if (e.key === "ArrowUp") {
|
|
1604
|
+
activeIdx = (activeIdx - 1 + items.length) % items.length;
|
|
1605
|
+
items[activeIdx].focus();
|
|
1606
|
+
items.forEach((item, idx) => {
|
|
1607
|
+
if (idx === activeIdx) item.classList.add("active");
|
|
1608
|
+
else item.classList.remove("active");
|
|
1609
|
+
});
|
|
1610
|
+
e.preventDefault();
|
|
1611
|
+
} else if (e.key === "Enter" && activeIdx !== -1) {
|
|
1612
|
+
items[activeIdx].click();
|
|
1613
|
+
e.preventDefault();
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
});
|
|
1617
|
+
};
|
|
1618
|
+
clientProto.autoAriaModal = function(modalEl, isOpen) {
|
|
1619
|
+
if (isOpen) {
|
|
1620
|
+
modalEl.setAttribute("role", "dialog");
|
|
1621
|
+
modalEl.setAttribute("aria-modal", "true");
|
|
1622
|
+
modalEl.setAttribute("aria-hidden", "false");
|
|
1623
|
+
modalEl.focus();
|
|
1624
|
+
} else {
|
|
1625
|
+
modalEl.setAttribute("aria-hidden", "true");
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/i18n.ts
|
|
1631
|
+
function attachI18n(clientProto) {
|
|
1632
|
+
clientProto._initI18n = function() {
|
|
1633
|
+
this.i18n = this.i18n || {
|
|
1634
|
+
locale: "en",
|
|
1635
|
+
dicts: {}
|
|
1636
|
+
};
|
|
1637
|
+
if (typeof document === "undefined") return;
|
|
1638
|
+
const dictEls = document.querySelectorAll("[data-i18n-dict]");
|
|
1639
|
+
dictEls.forEach((el) => {
|
|
1640
|
+
const locale = el.getAttribute("data-i18n-dict");
|
|
1641
|
+
if (locale) {
|
|
1642
|
+
try {
|
|
1643
|
+
const dictData = JSON.parse(el.textContent || "{}");
|
|
1644
|
+
this.i18n.dicts[locale] = {
|
|
1645
|
+
...this.i18n.dicts[locale] || {},
|
|
1646
|
+
...dictData
|
|
1647
|
+
};
|
|
1648
|
+
} catch (e) {
|
|
1649
|
+
console.warn("[Dolphin i18n] Failed to parse dictionary for locale:", locale, e);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
if (!this.i18n.locale && typeof navigator !== "undefined") {
|
|
1654
|
+
const browserLang = navigator.language.split("-")[0];
|
|
1655
|
+
if (this.i18n.dicts[browserLang]) {
|
|
1656
|
+
this.i18n.locale = browserLang;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
this.addDomListener(document, "click", (e) => {
|
|
1660
|
+
const switcher = e.target.closest("[data-i18n-switch]");
|
|
1661
|
+
if (switcher) {
|
|
1662
|
+
const newLocale = switcher.getAttribute("data-i18n-switch");
|
|
1663
|
+
if (newLocale) {
|
|
1664
|
+
this.setLocale(newLocale);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
this.translateDOM();
|
|
1669
|
+
};
|
|
1670
|
+
clientProto.setLocale = function(locale) {
|
|
1671
|
+
this.i18n = this.i18n || { locale: "en", dicts: {} };
|
|
1672
|
+
this.i18n.locale = locale;
|
|
1673
|
+
this.translateDOM();
|
|
1674
|
+
this.publish("i18n/locale", locale);
|
|
1675
|
+
};
|
|
1676
|
+
clientProto.translateDOM = function() {
|
|
1677
|
+
if (typeof document === "undefined") return;
|
|
1678
|
+
this.i18n = this.i18n || { locale: "en", dicts: {} };
|
|
1679
|
+
const currentLocale = this.i18n.locale || "en";
|
|
1680
|
+
const dict = this.i18n.dicts[currentLocale] || {};
|
|
1681
|
+
const translateEls = document.querySelectorAll("[data-i18n-key]");
|
|
1682
|
+
translateEls.forEach((el) => {
|
|
1683
|
+
const key = el.getAttribute("data-i18n-key");
|
|
1684
|
+
if (!key) return;
|
|
1685
|
+
let translation = key.split(".").reduce((o, i) => o ? o[i] : null, dict);
|
|
1686
|
+
if (translation === void 0 || translation === null) {
|
|
1687
|
+
translation = key;
|
|
1688
|
+
}
|
|
1689
|
+
const paramsAttr = el.getAttribute("data-i18n-params");
|
|
1690
|
+
if (paramsAttr) {
|
|
1691
|
+
try {
|
|
1692
|
+
const params = JSON.parse(paramsAttr);
|
|
1693
|
+
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1694
|
+
for (const k in params) {
|
|
1695
|
+
const escapedK = escapeRegExp(k);
|
|
1696
|
+
translation = translation.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), params[k]);
|
|
1697
|
+
}
|
|
1698
|
+
} catch {
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1702
|
+
el.placeholder = translation;
|
|
1703
|
+
} else {
|
|
1704
|
+
el.textContent = translation;
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// src/dragdrop.ts
|
|
1711
|
+
function attachDragDrop(clientProto) {
|
|
1712
|
+
clientProto._initDragDrop = function() {
|
|
1713
|
+
if (typeof document === "undefined") return;
|
|
1714
|
+
this.addDomListener(document, "dragstart", (e) => {
|
|
1715
|
+
const dragEl = e.target.closest("[data-drag]");
|
|
1716
|
+
if (!dragEl) return;
|
|
1717
|
+
const payloadStr = dragEl.getAttribute("data-drag");
|
|
1718
|
+
if (payloadStr) {
|
|
1719
|
+
e.dataTransfer.setData("text/plain", payloadStr);
|
|
1720
|
+
e.dataTransfer.effectAllowed = "move";
|
|
1721
|
+
dragEl.classList.add("dragging");
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
this.addDomListener(document, "dragend", (e) => {
|
|
1725
|
+
const dragEl = e.target.closest("[data-drag]");
|
|
1726
|
+
if (dragEl) {
|
|
1727
|
+
dragEl.classList.remove("dragging");
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
this.addDomListener(document, "dragover", (e) => {
|
|
1731
|
+
const dropZone = e.target.closest("[data-drop]");
|
|
1732
|
+
if (dropZone) {
|
|
1733
|
+
e.preventDefault();
|
|
1734
|
+
dropZone.classList.add("drag-over");
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
this.addDomListener(document, "dragleave", (e) => {
|
|
1738
|
+
const dropZone = e.target.closest("[data-drop]");
|
|
1739
|
+
if (dropZone) {
|
|
1740
|
+
dropZone.classList.remove("drag-over");
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
this.addDomListener(document, "drop", (e) => {
|
|
1744
|
+
const dropZone = e.target.closest("[data-drop]");
|
|
1745
|
+
if (!dropZone) return;
|
|
1746
|
+
e.preventDefault();
|
|
1747
|
+
dropZone.classList.remove("drag-over");
|
|
1748
|
+
const topic = dropZone.getAttribute("data-drop");
|
|
1749
|
+
const dataStr = e.dataTransfer.getData("text/plain");
|
|
1750
|
+
if (topic && dataStr) {
|
|
1751
|
+
try {
|
|
1752
|
+
const payload = JSON.parse(dataStr);
|
|
1753
|
+
this.publish(topic, payload);
|
|
1754
|
+
} catch {
|
|
1755
|
+
this.publish(topic, { value: dataStr });
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
this.addDomListener(document, "dragover", (e) => {
|
|
1760
|
+
const sortableContainer = e.target.closest("[data-sortable]");
|
|
1761
|
+
if (!sortableContainer) return;
|
|
1762
|
+
e.preventDefault();
|
|
1763
|
+
const draggingEl = sortableContainer.querySelector(".dragging");
|
|
1764
|
+
if (!draggingEl) return;
|
|
1765
|
+
const siblings = Array.from(sortableContainer.querySelectorAll("[data-drag]:not(.dragging)"));
|
|
1766
|
+
const nextSibling = siblings.find((sibling) => {
|
|
1767
|
+
const box = sibling.getBoundingClientRect();
|
|
1768
|
+
const offset = e.clientY - box.top - box.height / 2;
|
|
1769
|
+
return offset < 0;
|
|
1770
|
+
});
|
|
1771
|
+
if (nextSibling) {
|
|
1772
|
+
sortableContainer.insertBefore(draggingEl, nextSibling);
|
|
1773
|
+
} else {
|
|
1774
|
+
sortableContainer.appendChild(draggingEl);
|
|
1775
|
+
}
|
|
1776
|
+
});
|
|
1777
|
+
this.addDomListener(document, "drop", (e) => {
|
|
1778
|
+
const sortableContainer = e.target.closest("[data-sortable]");
|
|
1779
|
+
if (!sortableContainer) return;
|
|
1780
|
+
const topic = sortableContainer.getAttribute("data-sortable");
|
|
1781
|
+
if (!topic) return;
|
|
1782
|
+
const elements = Array.from(sortableContainer.querySelectorAll("[data-drag]"));
|
|
1783
|
+
const newOrder = elements.map((el, index) => {
|
|
1784
|
+
const payloadStr = el.getAttribute("data-drag");
|
|
1785
|
+
try {
|
|
1786
|
+
return { index, payload: JSON.parse(payloadStr || "{}") };
|
|
1787
|
+
} catch {
|
|
1788
|
+
return { index, payload: payloadStr };
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
this.publish(topic, newOrder);
|
|
1792
|
+
});
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// src/collab.ts
|
|
1797
|
+
function attachCollab(clientProto) {
|
|
1798
|
+
clientProto._initCollab = function() {
|
|
1799
|
+
if (typeof document === "undefined") return;
|
|
1800
|
+
this.addDomListener(document, "mousemove", (e) => {
|
|
1801
|
+
const shareContainers = document.querySelectorAll("[data-rt-cursor-share]");
|
|
1802
|
+
shareContainers.forEach((container) => {
|
|
1803
|
+
const room = container.getAttribute("data-rt-cursor-share");
|
|
1804
|
+
if (!room) return;
|
|
1805
|
+
const box = container.getBoundingClientRect();
|
|
1806
|
+
const xRatio = (e.clientX - box.left) / box.width;
|
|
1807
|
+
const yRatio = (e.clientY - box.top) / box.height;
|
|
1808
|
+
const now = Date.now();
|
|
1809
|
+
if (!container._lastSent || now - container._lastSent > 50) {
|
|
1810
|
+
container._lastSent = now;
|
|
1811
|
+
this.pubPush(`collab/${room}/cursor/${this.deviceId}`, {
|
|
1812
|
+
deviceId: this.deviceId,
|
|
1813
|
+
x: xRatio,
|
|
1814
|
+
y: yRatio
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
this.addDomListener(document, "input", (e) => {
|
|
1820
|
+
const typingBind = e.target.getAttribute("data-rt-typing");
|
|
1821
|
+
if (!typingBind) return;
|
|
1822
|
+
const room = typingBind;
|
|
1823
|
+
const publishTyping = (isTyping) => {
|
|
1824
|
+
this.pubPush(`collab/${room}/typing/${this.deviceId}`, {
|
|
1825
|
+
deviceId: this.deviceId,
|
|
1826
|
+
typing: isTyping
|
|
1827
|
+
});
|
|
1828
|
+
};
|
|
1829
|
+
if (!e.target._isTyping) {
|
|
1830
|
+
e.target._isTyping = true;
|
|
1831
|
+
publishTyping(true);
|
|
1832
|
+
}
|
|
1833
|
+
if (e.target._typingTimer) clearTimeout(e.target._typingTimer);
|
|
1834
|
+
e.target._typingTimer = setTimeout(() => {
|
|
1835
|
+
e.target._isTyping = false;
|
|
1836
|
+
publishTyping(false);
|
|
1837
|
+
}, 2e3);
|
|
1838
|
+
});
|
|
1839
|
+
this.addDomListener(document, "input", (e) => {
|
|
1840
|
+
const crdtBind = e.target.getAttribute("data-rt-crdt");
|
|
1841
|
+
if (!crdtBind) return;
|
|
1842
|
+
const docName = crdtBind;
|
|
1843
|
+
const value = e.target.value;
|
|
1844
|
+
const now = Date.now();
|
|
1845
|
+
this.publish(`collab/${docName}/crdt`, {
|
|
1846
|
+
deviceId: this.deviceId,
|
|
1847
|
+
value,
|
|
1848
|
+
timestamp: now,
|
|
1849
|
+
cursorPos: e.target.selectionStart
|
|
1850
|
+
});
|
|
1851
|
+
});
|
|
1852
|
+
this.subscribe("collab/+/cursor/+", (payload, topic) => {
|
|
1853
|
+
const parts = topic.split("/");
|
|
1854
|
+
const room = parts[1];
|
|
1855
|
+
const remoteDeviceId = parts[3];
|
|
1856
|
+
if (remoteDeviceId === this.deviceId) return;
|
|
1857
|
+
const container = document.querySelector(`[data-rt-cursor-share="${room}"]`);
|
|
1858
|
+
if (!container) return;
|
|
1859
|
+
let cursorEl = container.querySelector(`.rt-cursor-${remoteDeviceId}`);
|
|
1860
|
+
if (!cursorEl) {
|
|
1861
|
+
cursorEl = document.createElement("div");
|
|
1862
|
+
cursorEl.className = `rt-cursor rt-cursor-${remoteDeviceId}`;
|
|
1863
|
+
cursorEl.style.position = "absolute";
|
|
1864
|
+
cursorEl.style.width = "10px";
|
|
1865
|
+
cursorEl.style.height = "10px";
|
|
1866
|
+
cursorEl.style.borderRadius = "50%";
|
|
1867
|
+
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
|
|
1868
|
+
cursorEl.style.pointerEvents = "none";
|
|
1869
|
+
container.appendChild(cursorEl);
|
|
1870
|
+
}
|
|
1871
|
+
const box = container.getBoundingClientRect();
|
|
1872
|
+
cursorEl.style.left = payload.x * box.width + "px";
|
|
1873
|
+
cursorEl.style.top = payload.y * box.height + "px";
|
|
1874
|
+
});
|
|
1875
|
+
this.subscribe("collab/+/crdt", (payload, topic) => {
|
|
1876
|
+
if (payload.deviceId === this.deviceId) return;
|
|
1877
|
+
const parts = topic.split("/");
|
|
1878
|
+
const docName = parts[1];
|
|
1879
|
+
const crdtInputs = document.querySelectorAll(`[data-rt-crdt="${docName}"]`);
|
|
1880
|
+
crdtInputs.forEach((input) => {
|
|
1881
|
+
if (!input._lastUpdate || payload.timestamp > input._lastUpdate) {
|
|
1882
|
+
input._lastUpdate = payload.timestamp;
|
|
1883
|
+
const originalPos = input.selectionStart;
|
|
1884
|
+
input.value = payload.value;
|
|
1885
|
+
if (document.activeElement === input) {
|
|
1886
|
+
input.setSelectionRange(originalPos, originalPos);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
});
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// src/pwa.ts
|
|
1895
|
+
function attachPwa(clientProto) {
|
|
1896
|
+
clientProto.registerServiceWorker = async function(swPath = "/sw.js") {
|
|
1897
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
|
|
1898
|
+
console.warn("[Dolphin PWA] Service Workers are not supported in this browser.");
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
try {
|
|
1902
|
+
const registration = await navigator.serviceWorker.register(swPath);
|
|
1903
|
+
console.log("[Dolphin PWA] Service Worker registered successfully with scope:", registration.scope);
|
|
1904
|
+
return registration;
|
|
1905
|
+
} catch (e) {
|
|
1906
|
+
console.error("[Dolphin PWA] Service Worker registration failed:", e);
|
|
1907
|
+
return null;
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
clientProto.subscribePushNotifications = async function(vapidPublicKey) {
|
|
1911
|
+
if (typeof window === "undefined" || !("serviceWorker" in navigator) || !("PushManager" in window)) {
|
|
1912
|
+
console.warn("[Dolphin PWA] Push notifications are not supported in this browser.");
|
|
1913
|
+
return null;
|
|
1914
|
+
}
|
|
1915
|
+
try {
|
|
1916
|
+
const registration = await navigator.serviceWorker.ready;
|
|
1917
|
+
let subscription = await registration.pushManager.getSubscription();
|
|
1918
|
+
if (!subscription) {
|
|
1919
|
+
const padding = "=".repeat((4 - vapidPublicKey.length % 4) % 4);
|
|
1920
|
+
const base64 = (vapidPublicKey + padding).replace(/\-/g, "+").replace(/_/g, "/");
|
|
1921
|
+
const rawData = window.atob(base64);
|
|
1922
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
1923
|
+
for (let i = 0; i < rawData.length; ++i) {
|
|
1924
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
1925
|
+
}
|
|
1926
|
+
subscription = await registration.pushManager.subscribe({
|
|
1927
|
+
userVisibleOnly: true,
|
|
1928
|
+
applicationServerKey: outputArray
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
console.log("[Dolphin PWA] Subscribed to push notifications:", subscription);
|
|
1932
|
+
return subscription;
|
|
1933
|
+
} catch (e) {
|
|
1934
|
+
console.error("[Dolphin PWA] Push notification subscription failed:", e);
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/testing.ts
|
|
1941
|
+
var DolphinTestUtils = class {
|
|
1942
|
+
static render(html) {
|
|
1943
|
+
if (typeof document === "undefined") {
|
|
1944
|
+
throw new Error("DolphinTestUtils.render requires a DOM document environment to execute.");
|
|
1945
|
+
}
|
|
1946
|
+
const container = document.createElement("div");
|
|
1947
|
+
container.innerHTML = html;
|
|
1948
|
+
document.body.appendChild(container);
|
|
1949
|
+
return {
|
|
1950
|
+
container,
|
|
1951
|
+
find: (sel) => container.querySelector(sel),
|
|
1952
|
+
fireEvent: (el, eventType) => {
|
|
1953
|
+
const evt = document.createEvent("Event");
|
|
1954
|
+
evt.initEvent(eventType, true, true);
|
|
1955
|
+
el.dispatchEvent(evt);
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
static mockWebSocket() {
|
|
1960
|
+
const sentMessages = [];
|
|
1961
|
+
const mockWS = {
|
|
1962
|
+
readyState: 1,
|
|
1963
|
+
// OPEN
|
|
1964
|
+
send: (data) => {
|
|
1965
|
+
sentMessages.push(data);
|
|
1966
|
+
},
|
|
1967
|
+
close: jest.fn(),
|
|
1968
|
+
onopen: jest.fn(),
|
|
1969
|
+
onmessage: jest.fn(),
|
|
1970
|
+
onclose: jest.fn(),
|
|
1971
|
+
onerror: jest.fn(),
|
|
1972
|
+
sentMessages
|
|
1973
|
+
};
|
|
1974
|
+
global.WebSocket = class {
|
|
1975
|
+
static OPEN = 1;
|
|
1976
|
+
readyState = mockWS.readyState;
|
|
1977
|
+
send = mockWS.send;
|
|
1978
|
+
close = mockWS.close;
|
|
1979
|
+
set onopen(v) {
|
|
1980
|
+
mockWS.onopen = v;
|
|
1981
|
+
}
|
|
1982
|
+
get onopen() {
|
|
1983
|
+
return mockWS.onopen;
|
|
1984
|
+
}
|
|
1985
|
+
set onmessage(v) {
|
|
1986
|
+
mockWS.onmessage = v;
|
|
1987
|
+
}
|
|
1988
|
+
get onmessage() {
|
|
1989
|
+
return mockWS.onmessage;
|
|
1990
|
+
}
|
|
1991
|
+
set onclose(v) {
|
|
1992
|
+
mockWS.onclose = v;
|
|
1993
|
+
}
|
|
1994
|
+
get getonclose() {
|
|
1995
|
+
return mockWS.onclose;
|
|
1996
|
+
}
|
|
1997
|
+
constructor() {
|
|
1998
|
+
setTimeout(() => mockWS.onopen && mockWS.onopen(), 0);
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
return mockWS;
|
|
2002
|
+
}
|
|
2003
|
+
static simulateClick(el) {
|
|
2004
|
+
const clickEvt = {
|
|
2005
|
+
target: el,
|
|
2006
|
+
preventDefault: jest.fn(),
|
|
2007
|
+
stopPropagation: jest.fn()
|
|
2008
|
+
};
|
|
2009
|
+
const clickListeners = global.document._listeners?.["click"] || [];
|
|
2010
|
+
clickListeners.forEach((listener) => listener(clickEvt));
|
|
2011
|
+
}
|
|
2012
|
+
static simulateChange(el, value) {
|
|
2013
|
+
el.value = value;
|
|
2014
|
+
const changeEvt = {
|
|
2015
|
+
target: el,
|
|
2016
|
+
preventDefault: jest.fn(),
|
|
2017
|
+
stopPropagation: jest.fn()
|
|
2018
|
+
};
|
|
2019
|
+
const changeListeners = global.document._listeners?.["change"] || [];
|
|
2020
|
+
changeListeners.forEach((listener) => listener(changeEvt));
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
2023
|
+
function attachTesting(clientProto) {
|
|
2024
|
+
clientProto.testing = DolphinTestUtils;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// src/index.ts
|
|
2028
|
+
attachDOMBinding(DolphinClient.prototype);
|
|
2029
|
+
attachOffline(DolphinClient.prototype);
|
|
2030
|
+
attachValidation(DolphinClient.prototype);
|
|
2031
|
+
attachAnimations(DolphinClient.prototype);
|
|
2032
|
+
attachA11y(DolphinClient.prototype);
|
|
2033
|
+
attachI18n(DolphinClient.prototype);
|
|
2034
|
+
attachDragDrop(DolphinClient.prototype);
|
|
2035
|
+
attachCollab(DolphinClient.prototype);
|
|
2036
|
+
attachPwa(DolphinClient.prototype);
|
|
2037
|
+
attachTesting(DolphinClient.prototype);
|
|
2038
|
+
if (typeof window !== "undefined") {
|
|
2039
|
+
window.DolphinClient = DolphinClient;
|
|
2040
|
+
}
|
|
2041
|
+
return __toCommonJS(index_exports);
|
|
2042
|
+
})();
|