juxscript 1.0.102 → 1.0.103
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/package.json +7 -2
- package/lib/componentsv2/base/BaseEngine.ts +0 -332
- package/lib/componentsv2/base/BaseSkin.ts +0 -124
- package/lib/componentsv2/base/GlobalBus.ts +0 -60
- package/lib/componentsv2/base/OptionsContract.ts +0 -139
- package/lib/componentsv2/base/State.ts +0 -62
- package/lib/componentsv2/element/component.ts +0 -54
- package/lib/componentsv2/element/engine.ts +0 -145
- package/lib/componentsv2/element/skin.ts +0 -64
- package/lib/componentsv2/grid/component.ts +0 -57
- package/lib/componentsv2/grid/engine.ts +0 -187
- package/lib/componentsv2/grid/skin.ts +0 -106
- package/lib/componentsv2/input/component.ts +0 -28
- package/lib/componentsv2/input/engine.ts +0 -179
- package/lib/componentsv2/input/skin.ts +0 -89
- package/lib/componentsv2/list/component.ts +0 -119
- package/lib/componentsv2/list/engine.ts +0 -412
- package/lib/componentsv2/list/skin.ts +0 -369
- package/lib/componentsv2/plugins/ClientSQLitePlugin.ts +0 -154
- package/lib/componentsv2/plugins/IndexedDBPlugin.ts +0 -96
- package/lib/componentsv2/plugins/LocalStoragePlugin.ts +0 -86
- package/lib/componentsv2/plugins/ServerSQLitePlugin.ts +0 -99
- package/lib/utils/fetch.ts +0 -553
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
import { BaseSkin } from '../base/BaseSkin.js';
|
|
2
|
-
import { ListEngine, ListState, ListItem } from './engine.js';
|
|
3
|
-
import structureCss from './structure.css';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export class ListSkin extends BaseSkin<ListState, ListEngine> {
|
|
7
|
-
// UI Feature Flags
|
|
8
|
-
#showSearch = false;
|
|
9
|
-
#showAdd = false;
|
|
10
|
-
#showDelete = false;
|
|
11
|
-
#showEdit = false;
|
|
12
|
-
#showMove = false;
|
|
13
|
-
#showSort = false;
|
|
14
|
-
|
|
15
|
-
// Internal Drag State
|
|
16
|
-
#dragSourceIndex: number | null = null;
|
|
17
|
-
|
|
18
|
-
// DOM References (Stable elements to prevent re-render thrashing)
|
|
19
|
-
#toolbar: HTMLElement | null = null;
|
|
20
|
-
#listContainer: HTMLElement | null = null;
|
|
21
|
-
|
|
22
|
-
// Toolbar Componenets
|
|
23
|
-
#searchInput: HTMLInputElement | null = null;
|
|
24
|
-
#addBtn: HTMLButtonElement | null = null;
|
|
25
|
-
#sortBtn: HTMLButtonElement | null = null;
|
|
26
|
-
|
|
27
|
-
constructor(engine: ListEngine) {
|
|
28
|
-
super(engine);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// --- Public UI API ---
|
|
32
|
-
enableSearch(enabled: boolean): void { this.#showSearch = enabled; this.#renderToolbar(); }
|
|
33
|
-
enableAdd(enabled: boolean): void { this.#showAdd = enabled; this.#renderToolbar(); }
|
|
34
|
-
enableDelete(enabled: boolean): void { this.#showDelete = enabled; this.updateSkin(this.engine.state); }
|
|
35
|
-
enableMove(enabled: boolean): void { this.#showMove = enabled; this.updateSkin(this.engine.state); }
|
|
36
|
-
enableSort(enabled: boolean): void { this.#showSort = enabled; this.#renderToolbar(); }
|
|
37
|
-
enableNoItems(message: string = 'No items found.'): void { this.engine.enableNoItems(message); }
|
|
38
|
-
|
|
39
|
-
enableEdit(enabled: boolean): void {
|
|
40
|
-
this.#showEdit = enabled;
|
|
41
|
-
this.updateSkin(this.engine.state);
|
|
42
|
-
}
|
|
43
|
-
protected get structureCss(): string {
|
|
44
|
-
return structureCss;
|
|
45
|
-
}
|
|
46
|
-
// --- Template Skin Implementation ---
|
|
47
|
-
|
|
48
|
-
protected bindEvents(root: HTMLElement): void {
|
|
49
|
-
// 1. Delegation: Toolbar Events (Handled via direct element ref where possible now, or delegation fallback)
|
|
50
|
-
|
|
51
|
-
// We attach the input listener globally on root for delegation,
|
|
52
|
-
// but now that we hold the #searchInput ref, we could bind directly.
|
|
53
|
-
// Delegation is still safer if elements are recreated.
|
|
54
|
-
root.addEventListener('input', (e: Event) => {
|
|
55
|
-
const target = e.target as HTMLInputElement;
|
|
56
|
-
if (target.matches('.jux-search-input')) {
|
|
57
|
-
this.engine.filter(target.value);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
root.addEventListener('click', (e: MouseEvent) => {
|
|
62
|
-
const target = e.target as HTMLElement;
|
|
63
|
-
|
|
64
|
-
// Toolbar: Add
|
|
65
|
-
if (target.closest('.jux-action-add')) {
|
|
66
|
-
this.engine.addItem('New Item');
|
|
67
|
-
const newIndex = this.engine.state.items.length - 1;
|
|
68
|
-
if (newIndex >= 0) this.#openEditModal(newIndex);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Toolbar: Sort
|
|
73
|
-
if (target.closest('.jux-action-sort')) {
|
|
74
|
-
this.engine.toggleSort();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// List Item Actions
|
|
79
|
-
const li = target.closest('li');
|
|
80
|
-
if (!li) return;
|
|
81
|
-
|
|
82
|
-
// ✅ USE ID for robust targeting
|
|
83
|
-
const id = li.dataset.id;
|
|
84
|
-
const indexStr = li.dataset.index;
|
|
85
|
-
if (!indexStr || !id) return;
|
|
86
|
-
|
|
87
|
-
const index = parseInt(indexStr, 10);
|
|
88
|
-
|
|
89
|
-
// Action: Delete
|
|
90
|
-
if (target.closest('.jux-action-delete')) {
|
|
91
|
-
this.engine.removeItem(id); // ✅ Valid per new engine signature
|
|
92
|
-
e.stopImmediatePropagation();
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Action: Edit
|
|
97
|
-
if (target.closest('.jux-action-edit')) {
|
|
98
|
-
this.#openEditModal(index);
|
|
99
|
-
e.stopImmediatePropagation();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// List Item: Selection
|
|
104
|
-
if (!target.closest('.jux-control')) {
|
|
105
|
-
this.engine.toggleSelection(index);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// 2. Drag & Drop Logic
|
|
110
|
-
root.addEventListener('dragstart', (e: DragEvent) => {
|
|
111
|
-
if (!this.#showMove) return;
|
|
112
|
-
const li = (e.target as HTMLElement).closest('li');
|
|
113
|
-
if (li) {
|
|
114
|
-
this.#dragSourceIndex = parseInt(li.dataset.index || '-1', 10);
|
|
115
|
-
e.dataTransfer!.effectAllowed = 'move';
|
|
116
|
-
e.dataTransfer!.setData('text/plain', li.dataset.index || '');
|
|
117
|
-
requestAnimationFrame(() => li.classList.add('jux-dragging'));
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
root.addEventListener('dragend', (e: DragEvent) => {
|
|
122
|
-
const li = (e.target as HTMLElement).closest('li');
|
|
123
|
-
if (li) li.classList.remove('jux-dragging');
|
|
124
|
-
root.querySelectorAll('.jux-item-drop-target').forEach(el => el.classList.remove('jux-item-drop-target'));
|
|
125
|
-
this.#dragSourceIndex = null;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
root.addEventListener('dragover', (e: DragEvent) => {
|
|
129
|
-
if (!this.#showMove) return;
|
|
130
|
-
e.preventDefault();
|
|
131
|
-
const li = (e.target as HTMLElement).closest('li');
|
|
132
|
-
if (li && !li.classList.contains('jux-dragging')) {
|
|
133
|
-
e.dataTransfer!.dropEffect = 'move';
|
|
134
|
-
root.querySelectorAll('.jux-item-drop-target').forEach(el => el.classList.remove('jux-item-drop-target'));
|
|
135
|
-
li.classList.add('jux-item-drop-target');
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
root.addEventListener('drop', (e: DragEvent) => {
|
|
140
|
-
if (!this.#showMove) return;
|
|
141
|
-
e.preventDefault();
|
|
142
|
-
const li = (e.target as HTMLElement).closest('li');
|
|
143
|
-
if (li && this.#dragSourceIndex !== null) {
|
|
144
|
-
const toIndex = parseInt(li.dataset.index || '-1', 10);
|
|
145
|
-
if (toIndex !== -1 && this.#dragSourceIndex !== toIndex) {
|
|
146
|
-
this.engine.moveItem(this.#dragSourceIndex, toIndex);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// --- Internals: Modal Editing ---
|
|
153
|
-
#openEditModal(index: number): void {
|
|
154
|
-
const item = this.engine.state.items[index];
|
|
155
|
-
if (!item) return;
|
|
156
|
-
|
|
157
|
-
const overlay = document.createElement('div');
|
|
158
|
-
overlay.className = 'jux-modal-overlay';
|
|
159
|
-
overlay.innerHTML = `
|
|
160
|
-
<div class="jux-modal">
|
|
161
|
-
<h3>Edit Item</h3>
|
|
162
|
-
<input type="text" id="jux-edit-input" value="${item.text.replace(/"/g, '"')}" />
|
|
163
|
-
<div class="jux-modal-actions">
|
|
164
|
-
<button class="jux-modal-btn jux-btn-cancel" id="jux-edit-cancel">Cancel</button>
|
|
165
|
-
<button class="jux-modal-btn jux-btn-save" id="jux-edit-save">Save</button>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
`;
|
|
169
|
-
document.body.appendChild(overlay);
|
|
170
|
-
const input = overlay.querySelector('#jux-edit-input') as HTMLInputElement;
|
|
171
|
-
const close = () => document.body.removeChild(overlay);
|
|
172
|
-
const save = () => {
|
|
173
|
-
if (input.value.trim()) this.engine.updateItem(index, input.value);
|
|
174
|
-
close();
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
overlay.querySelector('#jux-edit-cancel')?.addEventListener('click', close);
|
|
178
|
-
overlay.querySelector('#jux-edit-save')?.addEventListener('click', save);
|
|
179
|
-
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
|
180
|
-
input.addEventListener('keydown', (e) => {
|
|
181
|
-
if (e.key === 'Enter') save();
|
|
182
|
-
if (e.key === 'Escape') close();
|
|
183
|
-
});
|
|
184
|
-
setTimeout(() => { input.focus(); input.select(); }, 0);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Skin Contract: Update/Changes
|
|
189
|
-
*/
|
|
190
|
-
protected updateSkin(state: ListState): void {
|
|
191
|
-
if (!this.root) return;
|
|
192
|
-
|
|
193
|
-
// ✅ Ensure standard ID is set
|
|
194
|
-
this.root.id = state.id;
|
|
195
|
-
|
|
196
|
-
// Perform the DOM Update
|
|
197
|
-
const performUpdate = () => {
|
|
198
|
-
this.applySkinAttributes(this.root!, state);
|
|
199
|
-
|
|
200
|
-
// Lazy Creation
|
|
201
|
-
if (!this.#listContainer) {
|
|
202
|
-
this.root!.innerHTML = '';
|
|
203
|
-
this.#toolbar = document.createElement('div');
|
|
204
|
-
this.#toolbar.className = 'jux-list-toolbar';
|
|
205
|
-
this.root!.appendChild(this.#toolbar);
|
|
206
|
-
|
|
207
|
-
this.#listContainer = document.createElement('div');
|
|
208
|
-
this.root!.appendChild(this.#listContainer);
|
|
209
|
-
this.#renderToolbar(); // Initial render
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Always update toolbar state (e.g. icon direction) without destroying inputs
|
|
213
|
-
this.#renderToolbar();
|
|
214
|
-
|
|
215
|
-
// Container Classes
|
|
216
|
-
const classes = [...state.classes];
|
|
217
|
-
if (state.selectionMode !== 'none') classes.push('jux-list-selectable');
|
|
218
|
-
this.#listContainer!.className = classes.join(' ');
|
|
219
|
-
|
|
220
|
-
// Filtering
|
|
221
|
-
const visibleItems = state.items
|
|
222
|
-
.map((item, originalIndex) => ({ item, originalIndex }))
|
|
223
|
-
.filter(({ item }) =>
|
|
224
|
-
state.filterText
|
|
225
|
-
? item.text.toLowerCase().includes(state.filterText.toLowerCase())
|
|
226
|
-
: true
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
// Rendering Content
|
|
230
|
-
if (visibleItems.length === 0 && state.noItemsMessage) {
|
|
231
|
-
this.#listContainer!.innerHTML = `<div class="jux-list-empty-state">${state.noItemsMessage}</div>`;
|
|
232
|
-
} else {
|
|
233
|
-
const tag = state.listType === 'ordered' ? 'ol' : 'ul';
|
|
234
|
-
const html = visibleItems
|
|
235
|
-
.map(({ item, originalIndex }) => this.#renderListItem(item, originalIndex, state))
|
|
236
|
-
.join('');
|
|
237
|
-
|
|
238
|
-
this.#listContainer!.innerHTML = `<${tag} class="jux-list-element jux-list-type-${tag}">${html}</${tag}>`;
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
// ✨ View Transitions Logic
|
|
243
|
-
// We SKIP transition if the search input is focused, otherwise typing janks the UI.
|
|
244
|
-
const isSearching = document.activeElement &&
|
|
245
|
-
(document.activeElement === this.#searchInput);
|
|
246
|
-
|
|
247
|
-
if (!isSearching && typeof document !== 'undefined' && 'startViewTransition' in document) {
|
|
248
|
-
// @ts-ignore
|
|
249
|
-
document.startViewTransition(performUpdate);
|
|
250
|
-
} else {
|
|
251
|
-
performUpdate();
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// --- DOM-Based Toolbar Rendering ---
|
|
256
|
-
#renderToolbar(): void {
|
|
257
|
-
if (!this.#toolbar) return;
|
|
258
|
-
|
|
259
|
-
// 1. Search Logic
|
|
260
|
-
if (this.#showSearch) {
|
|
261
|
-
if (!this.#searchInput) {
|
|
262
|
-
this.#searchInput = document.createElement('input');
|
|
263
|
-
this.#searchInput.type = 'text';
|
|
264
|
-
this.#searchInput.className = 'jux-search-input';
|
|
265
|
-
this.#searchInput.placeholder = 'Filter items...';
|
|
266
|
-
// Insert at start
|
|
267
|
-
this.#toolbar.prepend(this.#searchInput);
|
|
268
|
-
}
|
|
269
|
-
} else if (this.#searchInput) {
|
|
270
|
-
this.#searchInput.remove();
|
|
271
|
-
this.#searchInput = null;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// 2. Sort Logic
|
|
275
|
-
if (this.#showSort) {
|
|
276
|
-
if (!this.#sortBtn) {
|
|
277
|
-
this.#sortBtn = document.createElement('button');
|
|
278
|
-
this.#sortBtn.className = 'jux-action-sort';
|
|
279
|
-
this.#sortBtn.type = 'button';
|
|
280
|
-
this.#sortBtn.ariaLabel = 'Sort';
|
|
281
|
-
this.#toolbar.appendChild(this.#sortBtn);
|
|
282
|
-
}
|
|
283
|
-
// Update Icon state
|
|
284
|
-
const s = this.engine.state.sorted;
|
|
285
|
-
const icon = s === 'asc' ? '↑' : (s === 'desc' ? '↓' : '↕');
|
|
286
|
-
if (this.#sortBtn.textContent !== icon) {
|
|
287
|
-
this.#sortBtn.textContent = icon;
|
|
288
|
-
}
|
|
289
|
-
} else if (this.#sortBtn) {
|
|
290
|
-
this.#sortBtn.remove();
|
|
291
|
-
this.#sortBtn = null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// 3. Add Logic
|
|
295
|
-
if (this.#showAdd) {
|
|
296
|
-
if (!this.#addBtn) {
|
|
297
|
-
this.#addBtn = document.createElement('button');
|
|
298
|
-
this.#addBtn.className = 'jux-action-add';
|
|
299
|
-
this.#addBtn.type = 'button';
|
|
300
|
-
this.#addBtn.ariaLabel = 'Add Item';
|
|
301
|
-
this.#addBtn.textContent = '+';
|
|
302
|
-
this.#toolbar.appendChild(this.#addBtn);
|
|
303
|
-
}
|
|
304
|
-
} else if (this.#addBtn) {
|
|
305
|
-
this.#addBtn.remove();
|
|
306
|
-
this.#addBtn = null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Parent Visibility
|
|
310
|
-
const hasChildren = this.#showSearch || this.#showSort || this.#showAdd;
|
|
311
|
-
this.#toolbar.style.display = hasChildren ? 'flex' : 'none';
|
|
312
|
-
|
|
313
|
-
// ensure Search input is first by re-prepending if it exists
|
|
314
|
-
if (this.#searchInput && this.#toolbar.firstChild !== this.#searchInput) {
|
|
315
|
-
this.#toolbar.prepend(this.#searchInput);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
#renderListItem(item: ListItem, index: number, state: ListState): string {
|
|
320
|
-
const css = [
|
|
321
|
-
'jux-list-item',
|
|
322
|
-
item.selected ? 'jux-selected' : '',
|
|
323
|
-
...(item.classes || [])
|
|
324
|
-
].filter(Boolean).join(' ');
|
|
325
|
-
|
|
326
|
-
// ✅ Determine Content based on Columns configuration
|
|
327
|
-
let content = item.text;
|
|
328
|
-
|
|
329
|
-
if (state.columns && state.columns.length > 0) {
|
|
330
|
-
content = state.columns.map(col => {
|
|
331
|
-
const key = typeof col === 'string' ? col : col.key;
|
|
332
|
-
const cssClass = (typeof col === 'object' && col.label) ? `jux-col-${key}` : ''; // Basic class hook
|
|
333
|
-
const value = item[key] ?? '';
|
|
334
|
-
// If complex object, stringify or fallback
|
|
335
|
-
const displayVal = (typeof value === 'object') ? JSON.stringify(value) : String(value);
|
|
336
|
-
|
|
337
|
-
return `<span class="jux-col ${cssClass}">${displayVal}</span>`;
|
|
338
|
-
}).join(' <span class="jux-col-sep"> </span> ');
|
|
339
|
-
} else {
|
|
340
|
-
// Default Fallback logic: If no columns but item.text is boring ("[object Object]"), try to find keys
|
|
341
|
-
if (content === '[object Object]') {
|
|
342
|
-
const keys = Object.keys(item).filter(k =>
|
|
343
|
-
k !== 'id' && k !== 'selected' && k !== 'classes' && k !== 'text' && k !== 'value'
|
|
344
|
-
);
|
|
345
|
-
if (keys.length > 0) {
|
|
346
|
-
content = `${item[keys[0]]}`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const moveBtn = this.#showMove ? `<span class="jux-action-drag jux-control" title="Drag to reorder">≡</span>` : '';
|
|
352
|
-
const editBtn = this.#showEdit ? `<button class="jux-action-edit jux-control" aria-label="Edit">✎</button>` : '';
|
|
353
|
-
const deleteBtn = this.#showDelete ? `<button class="jux-action-delete jux-control" aria-label="Delete">×</button>` : '';
|
|
354
|
-
const draggableAttr = this.#showMove ? 'draggable="true"' : '';
|
|
355
|
-
const transitionName = `jux-item-${item.id.replace(/[^a-zA-Z0-9-_]/g, '-')}`;
|
|
356
|
-
|
|
357
|
-
return `
|
|
358
|
-
<li data-id="${item.id}" data-index="${index}" class="${css}" ${draggableAttr} style="view-transition-name: ${transitionName}">
|
|
359
|
-
<div class="jux-list-item-content">
|
|
360
|
-
${content}
|
|
361
|
-
</div>
|
|
362
|
-
<div class="jux-item-controls">
|
|
363
|
-
${moveBtn}
|
|
364
|
-
${editBtn}
|
|
365
|
-
${deleteBtn}
|
|
366
|
-
</div>
|
|
367
|
-
</li>`;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { JuxServiceContract, BaseEngine } from '../base/BaseEngine.js';
|
|
2
|
-
|
|
3
|
-
export interface ClientSQLiteConfig {
|
|
4
|
-
/** Key used for IndexedDB storage (the "filename") */
|
|
5
|
-
dbName: string;
|
|
6
|
-
|
|
7
|
-
/** SQL to run if creating a brand new DB (Schema) */
|
|
8
|
-
initSql?: string;
|
|
9
|
-
|
|
10
|
-
/** Main Query to bind to the Engine State */
|
|
11
|
-
query?: string;
|
|
12
|
-
|
|
13
|
-
/** State property to bind results to (e.g. 'items') */
|
|
14
|
-
bindTo?: string;
|
|
15
|
-
|
|
16
|
-
/** Transform row object to State Item shape */
|
|
17
|
-
mapRow?: (row: any) => any;
|
|
18
|
-
|
|
19
|
-
/** If true, persists to IDB after every write operation */
|
|
20
|
-
autoSave?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Client-Side SQLite Plugin
|
|
25
|
-
* Runs a real SQLite engine via WebAssembly (WASM) in the browser, persisted to IndexedDB.
|
|
26
|
-
*/
|
|
27
|
-
export const ClientSQLitePlugin = (config: ClientSQLiteConfig): JuxServiceContract<BaseEngine<any>> => ({
|
|
28
|
-
name: 'client-sqlite-wasm',
|
|
29
|
-
version: '1.0.0',
|
|
30
|
-
targetEnv: 'client',
|
|
31
|
-
|
|
32
|
-
install: (engine) => {
|
|
33
|
-
let db: any = null;
|
|
34
|
-
const IDB_NAME = 'JuxSqliteStorage';
|
|
35
|
-
const IDB_STORE = 'files';
|
|
36
|
-
|
|
37
|
-
// --- 1. IndexedDB HELPER (The "Hard Drive") ---
|
|
38
|
-
const getFile = (key: string): Promise<Uint8Array | null> => {
|
|
39
|
-
return new Promise((resolve) => {
|
|
40
|
-
const r = indexedDB.open(IDB_NAME, 1);
|
|
41
|
-
r.onupgradeneeded = (e: any) => e.target.result.createObjectStore(IDB_STORE);
|
|
42
|
-
r.onsuccess = (e: any) => {
|
|
43
|
-
const t = e.target.result.transaction([IDB_STORE], 'readonly').objectStore(IDB_STORE).get(key);
|
|
44
|
-
t.onsuccess = () => resolve(t.result);
|
|
45
|
-
t.onerror = () => resolve(null);
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const saveFile = (key: string, data: Uint8Array) => {
|
|
51
|
-
const r = indexedDB.open(IDB_NAME, 1);
|
|
52
|
-
r.onsuccess = (e: any) => {
|
|
53
|
-
const tx = e.target.result.transaction([IDB_STORE], 'readwrite');
|
|
54
|
-
tx.objectStore(IDB_STORE).put(data, key);
|
|
55
|
-
};
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// --- 2. SQL Helper (Result Transformation) ---
|
|
59
|
-
// sql.js return format: [{ columns: ['id', 'name'], values: [[1, 'Bob']] }]
|
|
60
|
-
// We convert this to: [{ id: 1, name: 'Bob' }]
|
|
61
|
-
const normalizeResults = (res: any[]) => {
|
|
62
|
-
if (!res || !res.length) return [];
|
|
63
|
-
const columns = res[0].columns;
|
|
64
|
-
const values = res[0].values;
|
|
65
|
-
return values.map((row: any[]) => {
|
|
66
|
-
const obj: any = {};
|
|
67
|
-
columns.forEach((col: string, i: number) => {
|
|
68
|
-
obj[col] = row[i];
|
|
69
|
-
});
|
|
70
|
-
return obj;
|
|
71
|
-
});
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// --- 3. REFRESH LOGIC ---
|
|
75
|
-
const refresh = () => {
|
|
76
|
-
if (!db || !config.query || !config.bindTo) return;
|
|
77
|
-
try {
|
|
78
|
-
// @ts-ignore
|
|
79
|
-
if (engine.loading) engine.loading(true);
|
|
80
|
-
|
|
81
|
-
// Run Query
|
|
82
|
-
const raw = db.exec(config.query);
|
|
83
|
-
const rows = normalizeResults(raw);
|
|
84
|
-
|
|
85
|
-
// Map & Update State
|
|
86
|
-
const items = config.mapRow ? rows.map(config.mapRow) : rows;
|
|
87
|
-
// @ts-ignore
|
|
88
|
-
engine.updateState({ [config.bindTo]: items });
|
|
89
|
-
|
|
90
|
-
// @ts-ignore
|
|
91
|
-
if (engine.loading) engine.loading(false);
|
|
92
|
-
// @ts-ignore
|
|
93
|
-
if (engine.emit) engine.emit('sql:refresh', { count: items.length });
|
|
94
|
-
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.error('[ClientSQLite] Query Error:', e);
|
|
97
|
-
// @ts-ignore
|
|
98
|
-
if (engine.loading) engine.loading(false);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// --- 4. INITIALIZATION ---
|
|
103
|
-
const loadSqlJs = async () => {
|
|
104
|
-
// Lazy load sql.js from CDN if not present
|
|
105
|
-
if (!(window as any).initSqlJs) {
|
|
106
|
-
await new Promise(resolve => {
|
|
107
|
-
const script = document.createElement('script');
|
|
108
|
-
// sql-wasm.js is the glue code that loads the .wasm binary
|
|
109
|
-
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/sql-wasm.js';
|
|
110
|
-
script.onload = resolve;
|
|
111
|
-
document.head.appendChild(script);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const SQL = await (window as any).initSqlJs({
|
|
116
|
-
locateFile: (file: string) => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.8.0/${file}`
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// "Mount" the file from IDB
|
|
120
|
-
const binary = await getFile(config.dbName);
|
|
121
|
-
|
|
122
|
-
if (binary) {
|
|
123
|
-
db = new SQL.Database(binary);
|
|
124
|
-
// @ts-ignore
|
|
125
|
-
if (engine.emit) engine.emit('sql:mounted', { size: binary.length });
|
|
126
|
-
} else {
|
|
127
|
-
db = new SQL.Database();
|
|
128
|
-
if (config.initSql) db.run(config.initSql);
|
|
129
|
-
if (config.autoSave) saveFile(config.dbName, db.export());
|
|
130
|
-
// @ts-ignore
|
|
131
|
-
if (engine.emit) engine.emit('sql:created');
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Expose SQL methods to Engine (Mixins)
|
|
135
|
-
Object.assign(engine, {
|
|
136
|
-
sqlRun: (sql: string) => {
|
|
137
|
-
db.run(sql);
|
|
138
|
-
if (config.autoSave) saveFile(config.dbName, db.export());
|
|
139
|
-
refresh();
|
|
140
|
-
},
|
|
141
|
-
sqlExec: (sql: string) => {
|
|
142
|
-
return normalizeResults(db.exec(sql));
|
|
143
|
-
},
|
|
144
|
-
sqlExport: () => {
|
|
145
|
-
return db.export();
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
refresh();
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
loadSqlJs();
|
|
153
|
-
}
|
|
154
|
-
});
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { JuxServiceContract, BaseEngine } from '../base/BaseEngine.js';
|
|
2
|
-
|
|
3
|
-
export interface IndexedDBConfig {
|
|
4
|
-
dbName: string;
|
|
5
|
-
storeName: string;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* State Binding: The property key on the Engine's state to persist.
|
|
9
|
-
* e.g., 'items'
|
|
10
|
-
*/
|
|
11
|
-
bindTo: string;
|
|
12
|
-
|
|
13
|
-
/** If true, loads data immediately on install */
|
|
14
|
-
autoLoad?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* IndexedDB Plugin
|
|
19
|
-
* Asynchronous persistence for larger datasets.
|
|
20
|
-
*/
|
|
21
|
-
export const IndexedDBPlugin = (config: IndexedDBConfig): JuxServiceContract<BaseEngine<any>> => ({
|
|
22
|
-
name: 'indexed-db-persist',
|
|
23
|
-
version: '1.0.0',
|
|
24
|
-
targetEnv: 'client',
|
|
25
|
-
|
|
26
|
-
install: (engine) => {
|
|
27
|
-
|
|
28
|
-
// @ts-ignore
|
|
29
|
-
const initialState = engine.state[config.bindTo];
|
|
30
|
-
if (initialState !== undefined && !Array.isArray(initialState)) {
|
|
31
|
-
console.error(`[IndexedDBPlugin] 🛑 Configuration Error: bindTo='${config.bindTo}' is not an Array. This plugin is designed for List/Collection data.`);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let db: IDBDatabase | null = null;
|
|
36
|
-
const request = indexedDB.open(config.dbName, 1);
|
|
37
|
-
|
|
38
|
-
// 1. Schema Setup
|
|
39
|
-
request.onupgradeneeded = (e: any) => {
|
|
40
|
-
const d = e.target.result;
|
|
41
|
-
if (!d.objectStoreNames.contains(config.storeName)) {
|
|
42
|
-
d.createObjectStore(config.storeName);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// 2. IO Operations
|
|
47
|
-
const save = (data: any) => {
|
|
48
|
-
if (!db) return;
|
|
49
|
-
const tx = db.transaction([config.storeName], 'readwrite');
|
|
50
|
-
const store = tx.objectStore(config.storeName);
|
|
51
|
-
store.put(data, 'state_snapshot'); // Storing entire bound state as one blob for simplicity
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const load = () => {
|
|
55
|
-
if (!db) return;
|
|
56
|
-
// @ts-ignore
|
|
57
|
-
if (typeof engine.loading === 'function') engine.loading(true);
|
|
58
|
-
|
|
59
|
-
const tx = db.transaction([config.storeName], 'readonly');
|
|
60
|
-
const store = tx.objectStore(config.storeName);
|
|
61
|
-
const req = store.get('state_snapshot');
|
|
62
|
-
|
|
63
|
-
req.onsuccess = () => {
|
|
64
|
-
if (req.result) {
|
|
65
|
-
// @ts-ignore
|
|
66
|
-
engine.updateState({ [config.bindTo]: req.result });
|
|
67
|
-
// @ts-ignore
|
|
68
|
-
if (engine.emit) engine.emit('plugin:hydrated', { source: 'IndexedDB' });
|
|
69
|
-
}
|
|
70
|
-
// @ts-ignore
|
|
71
|
-
if (typeof engine.loading === 'function') engine.loading(false);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
req.onerror = () => {
|
|
75
|
-
// @ts-ignore
|
|
76
|
-
if (typeof engine.loading === 'function') engine.loading(false);
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// 3. Initialization
|
|
81
|
-
request.onsuccess = (e: any) => {
|
|
82
|
-
db = e.target.result;
|
|
83
|
-
|
|
84
|
-
if (config.autoLoad) {
|
|
85
|
-
load();
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Auto-save subscription (debounced slightly in real world, direct here)
|
|
89
|
-
engine.subscribe((state) => {
|
|
90
|
-
if (db) save(state[config.bindTo]);
|
|
91
|
-
});
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
request.onerror = (e) => console.error('[IndexedDBPlugin] Open Error', e);
|
|
95
|
-
}
|
|
96
|
-
});
|