@livenetworks/ashlar 1.5.1 → 1.6.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/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +92 -2
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -56
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -25
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +13 -3
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +36 -23
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +11 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +3 -5
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +2 -0
- package/scss/config/mixins/_ln-table.scss +41 -2
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +85 -60
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +27 -9
- package/scss/ln-ashlar.scss +7 -1
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1087
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -156
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
const DEFAULT_DB_NAME = 'ln_api_queue';
|
|
2
|
+
const DB_VERSION = 2;
|
|
3
|
+
const OUTBOX = 'outbox';
|
|
4
|
+
const META = '_queue_meta';
|
|
5
|
+
|
|
6
|
+
function requestError(request, fallback) {
|
|
7
|
+
return request.error || new Error(fallback);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function scopeRange(keyRange, scope) {
|
|
11
|
+
return keyRange.bound([scope, -Infinity], [scope, Infinity]);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function seqKey(scope) {
|
|
15
|
+
return 'seq:' + scope;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pauseKey(scope) {
|
|
19
|
+
return 'paused:' + scope;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clearLease(entry) {
|
|
23
|
+
entry.leaseOwner = null;
|
|
24
|
+
entry.leaseUntil = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function remapAction(action, oldKey, newId) {
|
|
28
|
+
if (typeof action !== 'string' || action.indexOf(oldKey) === -1) return action;
|
|
29
|
+
return action.split(oldKey).join(newId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function claimQueueHeads(entries, ownerId, leaseMs, now) {
|
|
33
|
+
const groups = new Map();
|
|
34
|
+
const claimed = [];
|
|
35
|
+
const wakeups = [];
|
|
36
|
+
|
|
37
|
+
for (const entry of entries || []) {
|
|
38
|
+
if (!groups.has(entry.chainKey)) groups.set(entry.chainKey, []);
|
|
39
|
+
groups.get(entry.chainKey).push(entry);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
groups.forEach((group, chainKey) => {
|
|
43
|
+
group.sort((a, b) => a.seq - b.seq);
|
|
44
|
+
const head = group[0];
|
|
45
|
+
if (!head || head.status === 'failed') return;
|
|
46
|
+
|
|
47
|
+
if (head.status === 'inflight' && (head.leaseUntil || 0) > now) {
|
|
48
|
+
wakeups.push({ chainKey, at: head.leaseUntil });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if ((head.nextAttemptAt || 0) > now) {
|
|
52
|
+
wakeups.push({ chainKey, at: head.nextAttemptAt });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
head.status = 'inflight';
|
|
57
|
+
head.leaseOwner = ownerId;
|
|
58
|
+
head.leaseUntil = now + leaseMs;
|
|
59
|
+
head.updatedAt = now;
|
|
60
|
+
claimed.push(head);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return { entries: claimed, wakeups };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function remapQueueEntries(entries, acknowledgedEntryId, oldKey, newId, now) {
|
|
67
|
+
const changed = [];
|
|
68
|
+
const deleted = [];
|
|
69
|
+
|
|
70
|
+
for (const entry of entries || []) {
|
|
71
|
+
if (entry.entryId === acknowledgedEntryId) {
|
|
72
|
+
deleted.push(entry.entryId);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (entry.chainKey !== oldKey) continue;
|
|
76
|
+
|
|
77
|
+
entry.chainKey = newId;
|
|
78
|
+
if (entry.targetId === oldKey) entry.targetId = newId;
|
|
79
|
+
if (entry.meta && entry.meta.id === oldKey) entry.meta.id = newId;
|
|
80
|
+
if (entry.meta && typeof entry.meta.action === 'string') {
|
|
81
|
+
entry.meta.action = remapAction(entry.meta.action, oldKey, newId);
|
|
82
|
+
}
|
|
83
|
+
entry.updatedAt = now;
|
|
84
|
+
changed.push(entry);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { changed, deleted };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export class QueueStorage {
|
|
91
|
+
constructor(options) {
|
|
92
|
+
options = options || {};
|
|
93
|
+
this.indexedDB = options.indexedDB || globalThis.indexedDB;
|
|
94
|
+
this.keyRange = options.IDBKeyRange || globalThis.IDBKeyRange;
|
|
95
|
+
this.dbName = options.dbName || DEFAULT_DB_NAME;
|
|
96
|
+
this.now = options.now || (() => Date.now());
|
|
97
|
+
this.uuid = options.uuid || (() => crypto.randomUUID());
|
|
98
|
+
this._db = null;
|
|
99
|
+
this._ready = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
open() {
|
|
103
|
+
if (this._ready) return this._ready;
|
|
104
|
+
if (!this.indexedDB || !this.keyRange) return Promise.resolve(null);
|
|
105
|
+
|
|
106
|
+
this._ready = new Promise((resolve, reject) => {
|
|
107
|
+
const request = this.indexedDB.open(this.dbName, DB_VERSION);
|
|
108
|
+
|
|
109
|
+
request.onupgradeneeded = event => {
|
|
110
|
+
const db = event.target.result;
|
|
111
|
+
let outbox;
|
|
112
|
+
if (!db.objectStoreNames.contains(OUTBOX)) {
|
|
113
|
+
outbox = db.createObjectStore(OUTBOX, { keyPath: 'entryId' });
|
|
114
|
+
} else {
|
|
115
|
+
outbox = event.target.transaction.objectStore(OUTBOX);
|
|
116
|
+
}
|
|
117
|
+
if (!outbox.indexNames.contains('by_scope_chain')) {
|
|
118
|
+
outbox.createIndex('by_scope_chain', ['scope', 'chainKey'], { unique: false });
|
|
119
|
+
}
|
|
120
|
+
if (!outbox.indexNames.contains('by_scope_seq')) {
|
|
121
|
+
outbox.createIndex('by_scope_seq', ['scope', 'seq'], { unique: false });
|
|
122
|
+
}
|
|
123
|
+
if (!db.objectStoreNames.contains(META)) {
|
|
124
|
+
db.createObjectStore(META, { keyPath: 'key' });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
request.onerror = () => reject(requestError(request, 'Queue database open failed'));
|
|
129
|
+
request.onsuccess = event => {
|
|
130
|
+
this._db = event.target.result;
|
|
131
|
+
this._db.onversionchange = () => this.close();
|
|
132
|
+
resolve(this._db);
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return this._ready;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
close() {
|
|
140
|
+
if (this._db) this._db.close();
|
|
141
|
+
this._db = null;
|
|
142
|
+
this._ready = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
deleteDatabase() {
|
|
146
|
+
this.close();
|
|
147
|
+
if (!this.indexedDB) return Promise.resolve();
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const request = this.indexedDB.deleteDatabase(this.dbName);
|
|
150
|
+
request.onsuccess = () => resolve();
|
|
151
|
+
request.onerror = () => reject(requestError(request, 'Queue database delete failed'));
|
|
152
|
+
request.onblocked = () => reject(new Error('Queue database delete blocked'));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
allForScope(scope) {
|
|
157
|
+
return this.open().then(db => {
|
|
158
|
+
if (!db) return [];
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const tx = db.transaction(OUTBOX, 'readonly');
|
|
161
|
+
const request = tx.objectStore(OUTBOX).index('by_scope_seq').getAll(scopeRange(this.keyRange, scope));
|
|
162
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
163
|
+
request.onerror = () => reject(requestError(request, 'Queue scope read failed'));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
enqueue(scope, detail) {
|
|
169
|
+
detail = detail || {};
|
|
170
|
+
return this.open().then(db => {
|
|
171
|
+
if (!db) return null;
|
|
172
|
+
|
|
173
|
+
return new Promise((resolve, reject) => {
|
|
174
|
+
const tx = db.transaction([META, OUTBOX], 'readwrite');
|
|
175
|
+
const meta = tx.objectStore(META);
|
|
176
|
+
const outbox = tx.objectStore(OUTBOX);
|
|
177
|
+
const key = seqKey(scope);
|
|
178
|
+
let entry = null;
|
|
179
|
+
|
|
180
|
+
const allocate = current => {
|
|
181
|
+
const next = current + 1;
|
|
182
|
+
entry = {
|
|
183
|
+
entryId: this.uuid(),
|
|
184
|
+
scope,
|
|
185
|
+
chainKey: detail.chainKey,
|
|
186
|
+
seq: next,
|
|
187
|
+
op: detail.op,
|
|
188
|
+
targetId: detail.targetId !== undefined ? detail.targetId : null,
|
|
189
|
+
payload: detail.payload,
|
|
190
|
+
expectedVersion: detail.expectedVersion !== undefined ? detail.expectedVersion : null,
|
|
191
|
+
meta: detail.meta || {},
|
|
192
|
+
attempts: 0,
|
|
193
|
+
nextAttemptAt: 0,
|
|
194
|
+
status: 'pending',
|
|
195
|
+
leaseOwner: null,
|
|
196
|
+
leaseUntil: 0,
|
|
197
|
+
createdAt: this.now(),
|
|
198
|
+
updatedAt: this.now()
|
|
199
|
+
};
|
|
200
|
+
meta.put({ key, value: next });
|
|
201
|
+
outbox.put(entry);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const metaRequest = meta.get(key);
|
|
205
|
+
metaRequest.onerror = () => reject(requestError(metaRequest, 'Queue sequence read failed'));
|
|
206
|
+
metaRequest.onsuccess = () => {
|
|
207
|
+
const record = metaRequest.result;
|
|
208
|
+
if (record && typeof record.value === 'number') {
|
|
209
|
+
allocate(record.value);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Migration path from v1: initialize the per-scope counter above
|
|
214
|
+
// the largest already-persisted sequence in this scope.
|
|
215
|
+
const existingRequest = outbox.index('by_scope_seq').getAll(scopeRange(this.keyRange, scope));
|
|
216
|
+
existingRequest.onerror = () => reject(requestError(existingRequest, 'Queue sequence migration failed'));
|
|
217
|
+
existingRequest.onsuccess = () => {
|
|
218
|
+
const current = (existingRequest.result || []).reduce((max, item) => Math.max(max, item.seq || 0), 0);
|
|
219
|
+
allocate(current);
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
tx.oncomplete = () => resolve(entry);
|
|
224
|
+
tx.onerror = () => reject(tx.error || new Error('Queue enqueue transaction failed'));
|
|
225
|
+
tx.onabort = () => reject(tx.error || new Error('Queue enqueue transaction aborted'));
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
claimReady(scope, ownerId, leaseMs) {
|
|
231
|
+
return this.open().then(db => {
|
|
232
|
+
if (!db) return { entries: [], wakeups: [] };
|
|
233
|
+
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const tx = db.transaction(OUTBOX, 'readwrite');
|
|
236
|
+
const outbox = tx.objectStore(OUTBOX);
|
|
237
|
+
const request = outbox.index('by_scope_seq').getAll(scopeRange(this.keyRange, scope));
|
|
238
|
+
const now = this.now();
|
|
239
|
+
let result = { entries: [], wakeups: [] };
|
|
240
|
+
|
|
241
|
+
request.onerror = () => reject(requestError(request, 'Queue claim read failed'));
|
|
242
|
+
request.onsuccess = () => {
|
|
243
|
+
result = claimQueueHeads(request.result || [], ownerId, leaseMs, now);
|
|
244
|
+
for (const entry of result.entries) outbox.put(entry);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
tx.oncomplete = () => resolve(result);
|
|
248
|
+
tx.onerror = () => reject(tx.error || new Error('Queue claim transaction failed'));
|
|
249
|
+
tx.onabort = () => reject(tx.error || new Error('Queue claim transaction aborted'));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
ack(scope, entryId) {
|
|
255
|
+
return this._updateEntry(scope, entryId, (entry, outbox) => {
|
|
256
|
+
outbox.delete(entry.entryId);
|
|
257
|
+
return { status: 'acked', entry };
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
nack(scope, entryId, reason, options) {
|
|
262
|
+
options = options || {};
|
|
263
|
+
const maxAttempts = options.maxAttempts || 8;
|
|
264
|
+
const backoff = options.backoff || [2000, 5000, 15000, 60000, 300000];
|
|
265
|
+
|
|
266
|
+
return this.open().then(db => {
|
|
267
|
+
if (!db) return null;
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
269
|
+
const tx = db.transaction([OUTBOX, META], 'readwrite');
|
|
270
|
+
const outbox = tx.objectStore(OUTBOX);
|
|
271
|
+
const meta = tx.objectStore(META);
|
|
272
|
+
const request = outbox.get(entryId);
|
|
273
|
+
let result = null;
|
|
274
|
+
|
|
275
|
+
request.onerror = () => reject(requestError(request, 'Queue nack read failed'));
|
|
276
|
+
request.onsuccess = () => {
|
|
277
|
+
const entry = request.result;
|
|
278
|
+
if (!entry || entry.scope !== scope) return;
|
|
279
|
+
|
|
280
|
+
if (reason === 'drop') {
|
|
281
|
+
outbox.delete(entry.entryId);
|
|
282
|
+
result = { status: 'dropped', entry };
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
clearLease(entry);
|
|
287
|
+
entry.updatedAt = this.now();
|
|
288
|
+
|
|
289
|
+
if (reason === 'auth') {
|
|
290
|
+
entry.status = 'pending';
|
|
291
|
+
outbox.put(entry);
|
|
292
|
+
meta.put({ key: pauseKey(scope), value: true });
|
|
293
|
+
result = { status: 'auth', entry };
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (reason === 'retry') {
|
|
298
|
+
entry.attempts = (entry.attempts || 0) + 1;
|
|
299
|
+
if (entry.attempts >= maxAttempts) {
|
|
300
|
+
entry.status = 'failed';
|
|
301
|
+
entry.nextAttemptAt = 0;
|
|
302
|
+
outbox.put(entry);
|
|
303
|
+
result = { status: 'failed', entry };
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const delay = backoff[Math.min(entry.attempts - 1, backoff.length - 1)];
|
|
308
|
+
entry.status = 'pending';
|
|
309
|
+
entry.nextAttemptAt = this.now() + delay;
|
|
310
|
+
outbox.put(entry);
|
|
311
|
+
result = { status: 'retry', entry, delay };
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
tx.oncomplete = () => resolve(result);
|
|
316
|
+
tx.onerror = () => reject(tx.error || new Error('Queue nack transaction failed'));
|
|
317
|
+
tx.onabort = () => reject(tx.error || new Error('Queue nack transaction aborted'));
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
remap(scope, oldKey, newId) {
|
|
323
|
+
return this._remapTransaction(scope, null, oldKey, newId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
resolveCreate(scope, entryId, oldKey, newId) {
|
|
327
|
+
return this._remapTransaction(scope, entryId, oldKey, newId);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
_remapTransaction(scope, acknowledgedEntryId, oldKey, newId) {
|
|
331
|
+
return this.open().then(db => {
|
|
332
|
+
if (!db) return [];
|
|
333
|
+
return new Promise((resolve, reject) => {
|
|
334
|
+
const tx = db.transaction(OUTBOX, 'readwrite');
|
|
335
|
+
const outbox = tx.objectStore(OUTBOX);
|
|
336
|
+
const request = outbox.index('by_scope_seq').getAll(scopeRange(this.keyRange, scope));
|
|
337
|
+
let result = { changed: [], deleted: [] };
|
|
338
|
+
|
|
339
|
+
request.onerror = () => reject(requestError(request, 'Queue remap read failed'));
|
|
340
|
+
request.onsuccess = () => {
|
|
341
|
+
result = remapQueueEntries(request.result || [], acknowledgedEntryId, oldKey, newId, this.now());
|
|
342
|
+
for (const entryId of result.deleted) outbox.delete(entryId);
|
|
343
|
+
for (const entry of result.changed) outbox.put(entry);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
tx.oncomplete = () => resolve(result.changed);
|
|
347
|
+
tx.onerror = () => reject(tx.error || new Error('Queue remap transaction failed'));
|
|
348
|
+
tx.onabort = () => reject(tx.error || new Error('Queue remap transaction aborted'));
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
resetFailed(scope) {
|
|
354
|
+
return this.open().then(db => {
|
|
355
|
+
if (!db) return 0;
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
const tx = db.transaction(OUTBOX, 'readwrite');
|
|
358
|
+
const outbox = tx.objectStore(OUTBOX);
|
|
359
|
+
const request = outbox.index('by_scope_seq').getAll(scopeRange(this.keyRange, scope));
|
|
360
|
+
let count = 0;
|
|
361
|
+
|
|
362
|
+
request.onerror = () => reject(requestError(request, 'Queue failed-entry read failed'));
|
|
363
|
+
request.onsuccess = () => {
|
|
364
|
+
for (const entry of request.result || []) {
|
|
365
|
+
if (entry.status !== 'failed') continue;
|
|
366
|
+
entry.status = 'pending';
|
|
367
|
+
entry.attempts = 0;
|
|
368
|
+
entry.nextAttemptAt = 0;
|
|
369
|
+
entry.updatedAt = this.now();
|
|
370
|
+
clearLease(entry);
|
|
371
|
+
outbox.put(entry);
|
|
372
|
+
count++;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
tx.oncomplete = () => resolve(count);
|
|
377
|
+
tx.onerror = () => reject(tx.error || new Error('Queue failed-entry reset failed'));
|
|
378
|
+
tx.onabort = () => reject(tx.error || new Error('Queue failed-entry reset aborted'));
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
getPaused(scope) {
|
|
384
|
+
return this.open().then(db => {
|
|
385
|
+
if (!db) return false;
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const tx = db.transaction(META, 'readonly');
|
|
388
|
+
const request = tx.objectStore(META).get(pauseKey(scope));
|
|
389
|
+
request.onsuccess = () => resolve(!!(request.result && request.result.value));
|
|
390
|
+
request.onerror = () => reject(requestError(request, 'Queue pause-state read failed'));
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
setPaused(scope, paused) {
|
|
396
|
+
return this.open().then(db => {
|
|
397
|
+
if (!db) return;
|
|
398
|
+
return new Promise((resolve, reject) => {
|
|
399
|
+
const tx = db.transaction(META, 'readwrite');
|
|
400
|
+
tx.objectStore(META).put({ key: pauseKey(scope), value: !!paused });
|
|
401
|
+
tx.oncomplete = () => resolve();
|
|
402
|
+
tx.onerror = () => reject(tx.error || new Error('Queue pause-state write failed'));
|
|
403
|
+
tx.onabort = () => reject(tx.error || new Error('Queue pause-state write aborted'));
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
clear(scope) {
|
|
409
|
+
return this.open().then(db => {
|
|
410
|
+
if (!db) return;
|
|
411
|
+
return new Promise((resolve, reject) => {
|
|
412
|
+
const tx = db.transaction([OUTBOX, META], 'readwrite');
|
|
413
|
+
const outbox = tx.objectStore(OUTBOX);
|
|
414
|
+
const request = outbox.index('by_scope_seq').openCursor(scopeRange(this.keyRange, scope));
|
|
415
|
+
request.onsuccess = event => {
|
|
416
|
+
const cursor = event.target.result;
|
|
417
|
+
if (!cursor) return;
|
|
418
|
+
cursor.delete();
|
|
419
|
+
cursor.continue();
|
|
420
|
+
};
|
|
421
|
+
request.onerror = () => reject(requestError(request, 'Queue clear failed'));
|
|
422
|
+
tx.objectStore(META).delete(seqKey(scope));
|
|
423
|
+
tx.objectStore(META).delete(pauseKey(scope));
|
|
424
|
+
tx.oncomplete = () => resolve();
|
|
425
|
+
tx.onerror = () => reject(tx.error || new Error('Queue clear transaction failed'));
|
|
426
|
+
tx.onabort = () => reject(tx.error || new Error('Queue clear transaction aborted'));
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_updateEntry(scope, entryId, mutate) {
|
|
432
|
+
return this.open().then(db => {
|
|
433
|
+
if (!db) return null;
|
|
434
|
+
return new Promise((resolve, reject) => {
|
|
435
|
+
const tx = db.transaction(OUTBOX, 'readwrite');
|
|
436
|
+
const outbox = tx.objectStore(OUTBOX);
|
|
437
|
+
const request = outbox.get(entryId);
|
|
438
|
+
let result = null;
|
|
439
|
+
|
|
440
|
+
request.onerror = () => reject(requestError(request, 'Queue entry read failed'));
|
|
441
|
+
request.onsuccess = () => {
|
|
442
|
+
const entry = request.result;
|
|
443
|
+
if (!entry || entry.scope !== scope) return;
|
|
444
|
+
result = mutate(entry, outbox);
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
tx.oncomplete = () => resolve(result);
|
|
448
|
+
tx.onerror = () => reject(tx.error || new Error('Queue entry transaction failed'));
|
|
449
|
+
tx.onabort = () => reject(tx.error || new Error('Queue entry transaction aborted'));
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export const QUEUE_DB_NAME = DEFAULT_DB_NAME;
|
|
@@ -78,3 +78,50 @@ textarea.lnAutoresize.destroy();
|
|
|
78
78
|
textarea.lnAutoresize._resize();
|
|
79
79
|
```
|
|
80
80
|
- **Conflicting manual drag handles:** By default, textareas may have `resize: vertical` applied. This lets users manual resize the input, which is instantly overridden by the script on the next keystroke. Set `resize: none` to clean up the interface.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 🔧 Internals
|
|
85
|
+
|
|
86
|
+
Source: `js/ln-autoresize/ln-autoresize.js`. A pure presentation utility — owns no data, emits no custom events, cooperates with no other component outside the `input` platform event.
|
|
87
|
+
|
|
88
|
+
### Instance state
|
|
89
|
+
|
|
90
|
+
| Property | Type | Description |
|
|
91
|
+
|----------|------|-------------|
|
|
92
|
+
| `dom` | `HTMLTextAreaElement` | The textarea, constructor argument |
|
|
93
|
+
| `_onInput` | `Function` | Bound `input` handler, kept for `destroy()` unbind |
|
|
94
|
+
|
|
95
|
+
No cached "current height" — `_resize` always reads `scrollHeight` fresh, so external CSS changes (`max-height`, font-size, padding) are picked up on the next keystroke.
|
|
96
|
+
|
|
97
|
+
### Resize mechanism
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
_resize():
|
|
101
|
+
1. style.height = 'auto'
|
|
102
|
+
2. read scrollHeight
|
|
103
|
+
3. style.height = scrollHeight + 'px'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Step 1 is what makes it shrink, not just grow: `scrollHeight` reports the explicit `height` when content fits inside it, so without the reset every measurement would ratchet upward only. The reset + re-read happen synchronously in one tick, so the browser never paints the intermediate `auto` state — no flicker.
|
|
107
|
+
|
|
108
|
+
The constructor also calls `_resize()` once before returning, to size server-rendered pre-filled values and late-attached fields (`setAttribute` after the fact) on first paint rather than first keystroke. If the textarea is hidden at construction (`display: none` ancestor), `scrollHeight` reads `0` and there's no `ResizeObserver`/`IntersectionObserver` to catch a later reveal — call `_resize()` manually after showing it.
|
|
109
|
+
|
|
110
|
+
### Tag validation
|
|
111
|
+
|
|
112
|
+
The constructor checks `dom.tagName !== 'TEXTAREA'` and, if true, warns and returns `this` early with no `dom`/listener. The element is still marked initialized, so re-attaching the attribute after swapping in a real `<textarea>` needs `destroy()` first.
|
|
113
|
+
|
|
114
|
+
### MutationObserver via `registerComponent`
|
|
115
|
+
|
|
116
|
+
Standard shared observer on `document.body`: `childList/subtree` catches new `[data-ln-autoresize]` textareas anywhere; `attributes` (filtered to the one attribute) catches late `setAttribute` addition. Only attribute *presence* matters — no `onAttributeChange` callback, so value changes and attribute removal are no-ops.
|
|
117
|
+
|
|
118
|
+
### Destroy
|
|
119
|
+
|
|
120
|
+
Idempotent (bails if already torn down). Unbinds `_onInput`, clears `style.height` (reverting to the CSS-driven `rows`-based height), deletes the instance property. Does not remove the `data-ln-autoresize` attribute — the caller owns it, so a later rescan can re-create the instance.
|
|
121
|
+
|
|
122
|
+
### Cross-component coordination
|
|
123
|
+
|
|
124
|
+
No ln-prefixed import/listen/dispatch — only the platform `input` event. Two paths feed it synthetic `input` events:
|
|
125
|
+
|
|
126
|
+
- **`ln-form:fill`** — `ln-form`'s `fill()` writes `.value` then dispatches `input` on each populated field; autoresize catches it and re-measures. Direct `.value =` writes from project code skip this and will not resize.
|
|
127
|
+
- **`lnForm.reset()`** — walks fields dispatching synthetic `input`/`change` after `dom.reset()`, so cleared textareas shrink back. The bare native `<button type="reset">` path does NOT dispatch synthetic `input` (intentionally minimal); wire it manually or use the `lnForm.reset()` API if auto-shrink is needed there.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function x(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const w={};function q(t,e){w[t]||(w[t]=document.querySelector('[data-ln-template="'+t+'"]'));const n=w[t];return n?n.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function x(t,e){if(!t||!e)return t;const n=t.querySelectorAll("[data-ln-field]");for(let o=0;o<n.length;o++){const i=n[o],l=i.getAttribute("data-ln-field");e[l]!=null&&(i.textContent=e[l])}const c=t.querySelectorAll("[data-ln-attr]");for(let o=0;o<c.length;o++){const i=c[o],l=i.getAttribute("data-ln-attr").split(",");for(let s=0;s<l.length;s++){const d=l[s].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),f=d[1].trim();e[f]!=null&&i.setAttribute(a,e[f])}}const u=t.querySelectorAll("[data-ln-show]");for(let o=0;o<u.length;o++){const i=u[o],l=i.getAttribute("data-ln-show");l in e&&i.classList.toggle("hidden",!e[l])}const r=t.querySelectorAll("[data-ln-class]");for(let o=0;o<r.length;o++){const i=r[o],l=i.getAttribute("data-ln-class").split(",");for(let s=0;s<l.length;s++){const d=l[s].trim().split(":");if(d.length!==2)continue;const a=d[0].trim(),f=d[1].trim();f in e&&i.classList.toggle(a,!!e[f])}}return t}function D(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const n=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let c=0;c<n.length;c++)n[c].dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));return t}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(t){if(!(!t.target.matches||!t.target.matches("[data-ln-fillable]")))if(t.detail)x(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let n=0;n<e.length;n++)e[n].textContent=""}})));function L(t,e){if(!t||!e)return t;const n=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;n.nextNode();){const r=n.currentNode;r.textContent.indexOf("{{")!==-1&&(r.textContent=r.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(o,i){return e[i]!==void 0?e[i]:""}))}const c=function(r,o){return e[o]!==void 0?e[o]:""},u=Array.from(t.querySelectorAll("*"));t.nodeType===1&&u.push(t);for(let r=0;r<u.length;r++){const o=u[r],i=o.attributes;for(let l=0;l<i.length;l++){const s=i[l];s.value.indexOf("{{")!==-1&&o.setAttribute(s.name,s.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function N(t,e,n,c,u,r){const o={};for(let l=0;l<t.children.length;l++){const s=t.children[l],d=s.getAttribute("data-ln-key");d&&(o[d]=s)}const i=document.createDocumentFragment();for(let l=0;l<e.length;l++){const s=e[l],d=String(c(s));let a=o[d];if(a)u(a,s,l);else{const f=q(n,r);if(!f||(L(f,s),a=f.firstElementChild,!a))continue;a.setAttribute("data-ln-key",d),u(a,s,l)}i.appendChild(a)}t.textContent="",t.appendChild(i)}function S(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){S(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function g(t,e,n,c){if(t.nodeType!==1)return;const r=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",o=Array.from(t.querySelectorAll(r));t.matches&&t.matches(r)&&o.push(t);for(const i of o)i[n]||(i[n]=new c(i))}function M(t,e,n,c,u={}){const r=u.extraAttributes||[],o=u.onAttributeChange||null,i=u.onInit||null;function l(s){const d=s||document.body;g(d,t,e,n),i&&i(d)}return S(function(){const s=new MutationObserver(function(a){for(let f=0;f<a.length;f++){const h=a[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(g(p,t,e,n),i&&i(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const k=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",b=Array.from(p.querySelectorAll(k));p.matches&&p.matches(k)&&b.push(p);for(let C=0;C<b.length;C++){const T=b[C];if(!document.contains(T)){const A=T[e];A&&typeof A.destroy=="function"&&A.destroy()}}}}}else h.type==="attributes"&&(o&&h.target[e]?o(h.target,h.attributeName):(g(h.target,t,e,n),i&&i(h.target)))}});let d=[];if(t.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let f;for(;(f=a.exec(t))!==null;)d.push(f[1])}else d.push(t);s.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:d.concat(r)})},c),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const v={};function j(t,e){v[t]=e}function I(t){return v[t]||{ingress:e=>e,egress:e=>e}}const _={};function E(t,e){if(!t||typeof e!="object")return;const n=t.toLowerCase().split("-")[0];_[n]=e}function z(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return _[e]||null}E("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=j,window.lnCore.getDataMapper=I,window.lnCore.registerLocaleFallback=E,window.lnCore.getLocaleFallback=z,window.lnCore.fillTemplate=L,window.lnCore.fill=x,window.lnCore.lnFill=D,window.lnCore.renderList=N);function O(t){return(t||"").replace(/^#/,"")}function y(t){const e=t===void 0?location.hash:t,n={},c=O(e);if(!c)return n;const u=c.split("&");for(let r=0;r<u.length;r++){const o=u[r];if(!o)continue;const i=o.indexOf(":"),l=i>-1?o.slice(0,i):o,s=i>-1?o.slice(i+1):"";if(l)try{n[l]=decodeURIComponent(s)}catch{n[l]=s}}return n}function W(t){if(!t)return null;const e=y();return t in e?e[t]:null}function F(t,e){if(!t)return;const n=y();e==null?delete n[t]:n[t]=String(e);const u=Object.keys(n).map(function(r){const o=n[r];return o===""?r:r+":"+encodeURIComponent(o)}).join("&");O(location.hash)!==u&&(location.hash=u)}function B(t){return t.button===1||t.ctrlKey||t.metaKey||t.shiftKey?!1:(t.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=y,window.lnCore.hashGet=W,window.lnCore.hashSet=F,window.lnCore.hashLinkClick=B),(function(){const t="data-ln-autoresize",e="lnAutoresize";if(window[e]!==void 0)return;function n(c){if(c.tagName!=="TEXTAREA")return console.warn("[ln-autoresize] Can only be applied to <textarea>, got:",c.tagName),this;this.dom=c;const u=this;return this._onInput=function(){u._resize()},c.addEventListener("input",this._onInput),this._resize(),this}n.prototype._resize=function(){this.dom.style.height="auto",this.dom.style.height=this.dom.scrollHeight+"px"},n.prototype.destroy=function(){this.dom[e]&&(this.dom.removeEventListener("input",this._onInput),this.dom.style.height="",delete this.dom[e])},M(t,e,n,"ln-autoresize")})()})();
|
package/js/ln-autosave/README.md
CHANGED
|
@@ -45,7 +45,7 @@ A zero-dependency, localStorage-backed **Draft Buffer Primitive** that prevents
|
|
|
45
45
|
|
|
46
46
|
### JS API
|
|
47
47
|
|
|
48
|
-
Access the persistence instance directly via the `
|
|
48
|
+
Access the persistence instance directly via the `lnAutosave` property on the form element:
|
|
49
49
|
|
|
50
50
|
```javascript
|
|
51
51
|
const form = document.getElementById('profile-edit');
|
|
@@ -90,3 +90,35 @@ form.lnAutosave.destroy();
|
|
|
90
90
|
});
|
|
91
91
|
```
|
|
92
92
|
- **Nameless & File Inputs:** `ln-autosave` ignores disabled inputs, button elements, `type="file"`, and inputs missing a `name` attribute.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🔧 Internals
|
|
97
|
+
|
|
98
|
+
Source: `js/ln-autosave/ln-autosave.js` (~130 lines). No `.scss` file — the component has no visual surface, no class it sets, no CSS-driving attribute.
|
|
99
|
+
|
|
100
|
+
### State
|
|
101
|
+
|
|
102
|
+
Per-form instance (`form.lnAutosave`): `dom`, `key` (resolved once at construction — `ln-autosave:{pathname}:{identifier}`; mutating the attribute, `form.id`, or navigating via the History API afterwards does NOT re-key — destroy and re-mount to switch), and the bound handlers (`_onFocusout`, `_onChange`, `_onSubmit`, `_onReset`, `_onClearClick`, plus `_onInput` only when debounce is opted in) held so `destroy()` can pass matching references to `removeEventListener`.
|
|
103
|
+
|
|
104
|
+
### Storage key
|
|
105
|
+
|
|
106
|
+
`_getStorageKey` resolves `identifier` from the attribute value if non-empty, else `form.id`. Empty attribute (`data-ln-autosave=""`) falls through to `form.id`; missing both returns `null` and construction aborts with a console warning — `form.lnAutosave` is never set.
|
|
107
|
+
|
|
108
|
+
### Save / restore / clear
|
|
109
|
+
|
|
110
|
+
- **Save** (`focusout`, `change`, optional debounced `input`): `serializeForm` → `localStorage.setItem`, wrapped in a silent `try/catch` (`QuotaExceededError`, storage disabled). A failed write fires no event — there is no `:save-failed`.
|
|
111
|
+
- **Restore** (construction-time only): read + parse (silent try/catch) → cancelable `:before-restore` → `populateForm` → dispatch synthetic `input` **and** `change` on every restored field (covers both signal types regardless of field kind: text/textarea react to `input`, checkbox/radio/select to `change`) → `:restored`. Events fire after all fields are written, so cross-field validation sees the fully-restored form.
|
|
112
|
+
- **Clear**: `localStorage.removeItem` (idempotent) → `:cleared`. Triggered by native `submit` (not by `form.submit()` — that call bypasses the event; use `requestSubmit()`), native `reset`, or a click on `[data-ln-autosave-clear]`. Touches only the storage entry, never field values.
|
|
113
|
+
|
|
114
|
+
### Debounce is input-only, opt-in
|
|
115
|
+
|
|
116
|
+
`focusout`/`change` are naturally rate-limited by user behaviour (one event per field visit) — no debounce needed. `input` fires per keystroke, so `data-ln-autosave-debounce-input` exists as an explicit opt-in for compose-style fields the user never blurs. Default off. The interval is read once at construction (`_resolveDebounceMs`); mutating the attribute afterwards has no effect on a live instance.
|
|
117
|
+
|
|
118
|
+
### MutationObserver
|
|
119
|
+
|
|
120
|
+
`registerComponent` watches `childList` (new `[data-ln-autosave]` forms) and `attributes` (filtered to `data-ln-autosave` only — NOT `-clear` or `-debounce-input`). The clear-button delegate uses `closest()` per click, so `data-ln-autosave-clear` works even if added after construction; the debounce attribute does not, since it's read once.
|
|
121
|
+
|
|
122
|
+
### Destroy
|
|
123
|
+
|
|
124
|
+
Removes all five listeners (plus `input` and its pending timer if debounce was active), dispatches `:destroyed`, deletes the instance. Does **not** call `_clear()` — the draft persists after destroy; destroy means "stop autosaving," not "discard the draft."
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function E(n,e,u){n.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:u||{}}))}function O(n,e,u){const s=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:u||{}});return n.dispatchEvent(s),s}function L(n,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){L(n,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}n()}function C(n,e,u,s){if(n.nodeType!==1)return;const o=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",f=Array.from(n.querySelectorAll(o));n.matches&&n.matches(o)&&f.push(n);for(const l of f)l[u]||(l[u]=new s(l))}function S(n){const e={},u=n.elements;for(let s=0;s<u.length;s++){const i=u[s];if(!(!i.name||i.disabled||i.type==="file"||i.type==="submit"||i.type==="button"))if(i.type==="checkbox")e[i.name]||(e[i.name]=[]),i.checked&&e[i.name].push(i.value);else if(i.type==="radio")i.checked&&(e[i.name]=i.value);else if(i.type==="select-multiple"){e[i.name]=[];for(let o=0;o<i.options.length;o++)i.options[o].selected&&e[i.name].push(i.options[o].value)}else e[i.name]=i.value}return e}function T(n,e){const u=n.elements,s=[];for(let i=0;i<u.length;i++){const o=u[i];if(!o.name||!(o.name in e)||o.type==="file"||o.type==="submit"||o.type==="button")continue;const f=e[o.name];if(o.type==="checkbox")o.checked=Array.isArray(f)?f.indexOf(o.value)!==-1:!!f,s.push(o);else if(o.type==="radio")o.checked=o.value===String(f),s.push(o);else if(o.type==="select-multiple"){if(Array.isArray(f))for(let l=0;l<o.options.length;l++)o.options[l].selected=f.indexOf(o.options[l].value)!==-1;s.push(o)}else o.value=f,s.push(o)}return s}function x(n,e,u,s,i={}){const o=i.extraAttributes||[],f=i.onAttributeChange||null,l=i.onInit||null;function g(y){const t=y||document.body;C(t,n,e,u),l&&l(t)}return L(function(){const y=new MutationObserver(function(a){for(let c=0;c<a.length;c++){const h=a[c];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(C(p,n,e,u),l&&l(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const d=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",r=Array.from(p.querySelectorAll(d));p.matches&&p.matches(d)&&r.push(p);for(let _=0;_<r.length;_++){const b=r[_];if(!document.contains(b)){const v=b[e];v&&typeof v.destroy=="function"&&v.destroy()}}}}}else h.type==="attributes"&&(f&&h.target[e]?f(h.target,h.attributeName):(C(h.target,n,e,u),l&&l(h.target)))}});let t=[];if(n.indexOf("[")!==-1){const a=/\[([\w-]+)/g;let c;for(;(c=a.exec(n))!==null;)t.push(c[1])}else t.push(n);y.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:t.concat(o)})},s),window[e]=g,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){g(document.body)}):g(document.body),g}const A={};function D(n,e){A[n]=e}function k(n){return A[n]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=D,window.lnCore.getDataMapper=k),(function(){const n="data-ln-autosave",e="lnAutosave",u="data-ln-autosave-clear",s="data-ln-autosave-debounce-input",i="ln-autosave:";if(window[e]!==void 0)return;function f(t){const a=l(t);if(!a){console.warn("ln-autosave: form needs an id or data-ln-autosave value",t);return}this.dom=t,this.key=a;let c=null;function h(){const d=S(t);try{localStorage.setItem(a,JSON.stringify(d))}catch{return}E(t,"ln-autosave:saved",{target:t,data:d})}function m(){let d;try{d=localStorage.getItem(a)}catch{return}if(!d)return;let r;try{r=JSON.parse(d)}catch{return}if(O(t,"ln-autosave:before-restore",{target:t,data:r}).defaultPrevented)return;const b=T(t,r);for(let v=0;v<b.length;v++)b[v].dispatchEvent(new Event("input",{bubbles:!0})),b[v].dispatchEvent(new Event("change",{bubbles:!0}));E(t,"ln-autosave:restored",{target:t,data:r})}function p(){try{localStorage.removeItem(a)}catch{return}E(t,"ln-autosave:cleared",{target:t})}this._onFocusout=function(d){const r=d.target;g(r)&&r.name&&h()},this._onChange=function(d){const r=d.target;g(r)&&r.name&&h()},this._onSubmit=function(){p()},this._onReset=function(){p()},this._onClearClick=function(d){d.target.closest("["+u+"]")&&p()},t.addEventListener("focusout",this._onFocusout),t.addEventListener("change",this._onChange),t.addEventListener("submit",this._onSubmit),t.addEventListener("reset",this._onReset),t.addEventListener("click",this._onClearClick);const w=y(t);return w>0&&(this._onInput=function(d){const r=d.target;!g(r)||!r.name||(c!==null&&clearTimeout(c),c=setTimeout(h,w))},t.addEventListener("input",this._onInput)),this._getInputTimer=function(){return c},m(),this}f.prototype.destroy=function(){if(this.dom[e]){if(this.dom.removeEventListener("focusout",this._onFocusout),this.dom.removeEventListener("change",this._onChange),this.dom.removeEventListener("submit",this._onSubmit),this.dom.removeEventListener("reset",this._onReset),this.dom.removeEventListener("click",this._onClearClick),this._onInput){this.dom.removeEventListener("input",this._onInput);const t=this._getInputTimer();t!==null&&clearTimeout(t)}E(this.dom,"ln-autosave:destroyed",{target:this.dom}),delete this.dom[e]}};function l(t){const c=t.getAttribute(n)||t.id;return c?i+window.location.pathname+":"+c:null}function g(t){const a=t.tagName;return a==="INPUT"||a==="TEXTAREA"||a==="SELECT"}function y(t){if(!t.hasAttribute(s))return 0;const a=t.getAttribute(s);if(a===""||a===null)return 1e3;const c=parseInt(a,10);return isNaN(c)||c<0?(console.warn("ln-autosave: invalid debounce value, using default",t),1e3):c}x(n,e,f,"ln-autosave")})()})();
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const e=console.warn;console.warn=function(...t){typeof t[0]=="string"&&(t[0].startsWith("[ln-")||t[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||e.apply(console,t)}}const E={};function F(e,t){E[e]||(E[e]=document.querySelector('[data-ln-template="'+e+'"]'));const s=E[e];return s?s.content.cloneNode(!0):(console.warn("["+(t||"ln-core")+'] Template "'+e+'" not found'),null)}function w(e,t,s){e.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:s||{}}))}function I(e,t,s){const u=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:s||{}});return e.dispatchEvent(u),u}function x(e,t){if(!e||!t)return e;const s=e.querySelectorAll("[data-ln-field]");for(let i=0;i<s.length;i++){const l=s[i],n=l.getAttribute("data-ln-field");t[n]!=null&&(l.textContent=t[n])}const u=e.querySelectorAll("[data-ln-attr]");for(let i=0;i<u.length;i++){const l=u[i],n=l.getAttribute("data-ln-attr").split(",");for(let r=0;r<n.length;r++){const o=n[r].trim().split(":");if(o.length!==2)continue;const d=o[0].trim(),f=o[1].trim();t[f]!=null&&l.setAttribute(d,t[f])}}const c=e.querySelectorAll("[data-ln-show]");for(let i=0;i<c.length;i++){const l=c[i],n=l.getAttribute("data-ln-show");n in t&&l.classList.toggle("hidden",!t[n])}const a=e.querySelectorAll("[data-ln-class]");for(let i=0;i<a.length;i++){const l=a[i],n=l.getAttribute("data-ln-class").split(",");for(let r=0;r<n.length;r++){const o=n[r].trim().split(":");if(o.length!==2)continue;const d=o[0].trim(),f=o[1].trim();f in t&&l.classList.toggle(d,!!t[f])}}return e}function M(e,t){e.matches&&e.matches("[data-ln-form], [data-ln-fillable]")&&e.dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));const s=e.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let u=0;u<s.length;u++)s[u].dispatchEvent(new CustomEvent("ln-fill",{detail:t??null,bubbles:!0}));return e}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore._fillBound||(window.lnCore._fillBound=!0,document.addEventListener("ln-fill",function(e){if(!(!e.target.matches||!e.target.matches("[data-ln-fillable]")))if(e.detail)x(e.target,e.detail);else{const t=e.target.querySelectorAll("[data-ln-field]");for(let s=0;s<t.length;s++)t[s].textContent=""}})));function k(e,t){if(!e||!t)return e;const s=document.createTreeWalker(e,NodeFilter.SHOW_TEXT);for(;s.nextNode();){const a=s.currentNode;a.textContent.indexOf("{{")!==-1&&(a.textContent=a.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(i,l){return t[l]!==void 0?t[l]:""}))}const u=function(a,i){return t[i]!==void 0?t[i]:""},c=Array.from(e.querySelectorAll("*"));e.nodeType===1&&c.push(e);for(let a=0;a<c.length;a++){const i=c[a],l=i.attributes;for(let n=0;n<l.length;n++){const r=l[n];r.value.indexOf("{{")!==-1&&i.setAttribute(r.name,r.value.replace(/\{\{\s*(\w+)\s*\}\}/g,u))}}return e}function R(e,t,s,u,c,a){const i={};for(let n=0;n<e.children.length;n++){const r=e.children[n],o=r.getAttribute("data-ln-key");o&&(i[o]=r)}const l=document.createDocumentFragment();for(let n=0;n<t.length;n++){const r=t[n],o=String(u(r));let d=i[o];if(d)c(d,r,n);else{const f=F(s,a);if(!f||(k(f,r),d=f.firstElementChild,!d))continue;d.setAttribute("data-ln-key",o),c(d,r,n)}l.appendChild(d)}e.textContent="",e.appendChild(l)}function S(e,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){S(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function _(e,t,s,u){if(e.nodeType!==1)return;const a=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",i=Array.from(e.querySelectorAll(a));e.matches&&e.matches(a)&&i.push(e);for(const l of i)l[s]||(l[s]=new u(l))}function q(e,t){const s=!!(t&&t.typed),u=t&&t.exclude,c={},a=e.elements,i={};if(s)for(let l=0;l<a.length;l++){const n=a[l];n.name&&n.type==="checkbox"&&!n.disabled&&(i[n.name]=(i[n.name]||0)+1)}for(let l=0;l<a.length;l++){const n=a[l];if(!(!n.name||n.disabled||n.type==="file"||n.type==="submit"||n.type==="button")&&!(u&&n.matches&&n.matches(u)))if(n.type==="checkbox")s&&i[n.name]===1?c[n.name]=n.checked:(c[n.name]||(c[n.name]=[]),n.checked&&c[n.name].push(n.value));else if(n.type==="radio")n.checked&&(c[n.name]=n.value);else if(n.type==="select-multiple"){c[n.name]=[];for(let r=0;r<n.options.length;r++)n.options[r].selected&&c[n.name].push(n.options[r].value)}else if(s&&n.type==="hidden")c[n.name]=n.value;else if(s&&(n.type==="number"||n.type==="range")){const r=Number(n.value);c[n.name]=n.value===""||isNaN(r)?null:r}else c[n.name]=n.value}return c}function U(e){if(typeof e!="string")return!!e;const t=e.trim().toLowerCase();return t!=="false"&&t!=="0"&&t!==""&&t!=="off"&&t!=="no"}function j(e,t){const s=e.elements,u=[],c={};for(let a=0;a<s.length;a++){const i=s[a];i.name&&i.type==="checkbox"&&(c[i.name]=(c[i.name]||0)+1)}for(let a=0;a<s.length;a++){const i=s[a];if(i.type==="file"||i.type==="submit"||i.type==="button")continue;const l=i.getAttribute("data-ln-fill-as")||i.name;if(!l||!(l in t))continue;const n=t[l];if(i.type==="checkbox"){if(Array.isArray(n))i.checked=n.indexOf(i.value)!==-1;else if(c[i.name]>1){const r=String(n).split(",").map(function(o){return o.trim()});i.checked=r.indexOf(i.value)!==-1}else i.checked=U(n);u.push(i)}else if(i.type==="radio")i.checked=i.value===String(n),u.push(i);else if(i.type==="select-multiple"){if(Array.isArray(n))for(let r=0;r<i.options.length;r++)i.options[r].selected=n.indexOf(i.options[r].value)!==-1;u.push(i)}else i.value=n,u.push(i)}return u}function B(e,t,s,u,c={}){const a=c.extraAttributes||[],i=c.onAttributeChange||null,l=c.onInit||null;function n(r){const o=r||document.body;_(o,e,t,s),l&&l(o)}return S(function(){const r=new MutationObserver(function(d){for(let f=0;f<d.length;f++){const m=d[f];if(m.type==="childList"){for(let y=0;y<m.addedNodes.length;y++){const g=m.addedNodes[y];g.nodeType===1&&(_(g,e,t,s),l&&l(g))}for(let y=0;y<m.removedNodes.length;y++){const g=m.removedNodes[y];if(g.nodeType===1){const p=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",h=Array.from(g.querySelectorAll(p));g.matches&&g.matches(p)&&h.push(g);for(let C=0;C<h.length;C++){const v=h[C];if(!document.contains(v)){const b=v[t];b&&typeof b.destroy=="function"&&b.destroy()}}}}}else m.type==="attributes"&&(i&&m.target[t]?i(m.target,m.attributeName):(_(m.target,e,t,s),l&&l(m.target)))}});let o=[];if(e.indexOf("[")!==-1){const d=/\[([\w-]+)/g;let f;for(;(f=d.exec(e))!==null;)o.push(f[1])}else o.push(e);r.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:o.concat(a)})},u),window[t]=n,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){n(document.body)}):n(document.body),n}const T={};function W(e,t){T[e]=t}function K(e){return T[e]||{ingress:t=>t,egress:t=>t}}const O={};function N(e,t){if(!e||typeof t!="object")return;const s=e.toLowerCase().split("-")[0];O[s]=t}function P(e){if(!e)return null;const t=e.toLowerCase().split("-")[0];return O[t]||null}N("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=W,window.lnCore.getDataMapper=K,window.lnCore.registerLocaleFallback=N,window.lnCore.getLocaleFallback=P,window.lnCore.fillTemplate=k,window.lnCore.fill=x,window.lnCore.lnFill=M,window.lnCore.renderList=R);function D(e){return(e||"").replace(/^#/,"")}function A(e){const t=e===void 0?location.hash:e,s={},u=D(t);if(!u)return s;const c=u.split("&");for(let a=0;a<c.length;a++){const i=c[a];if(!i)continue;const l=i.indexOf(":"),n=l>-1?i.slice(0,l):i,r=l>-1?i.slice(l+1):"";if(n)try{s[n]=decodeURIComponent(r)}catch{s[n]=r}}return s}function G(e){if(!e)return null;const t=A();return e in t?t[e]:null}function X(e,t){if(!e)return;const s=A();t==null?delete s[e]:s[e]=String(t);const c=Object.keys(s).map(function(a){const i=s[a];return i===""?a:a+":"+encodeURIComponent(i)}).join("&");D(location.hash)!==c&&(location.hash=c)}function J(e){return e.button===1||e.ctrlKey||e.metaKey||e.shiftKey?!1:(e.preventDefault(),!0)}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.hashParse=A,window.lnCore.hashGet=G,window.lnCore.hashSet=X,window.lnCore.hashLinkClick=J),(function(){const e="data-ln-autosave",t="lnAutosave",s="data-ln-autosave-clear",u="data-ln-autosave-debounce-input",c="ln-autosave:";if(window[t]!==void 0)return;function i(o){const d=l(o);if(!d){console.warn("ln-autosave: form needs an id or data-ln-autosave value",o);return}this.dom=o,this.key=d;let f=null;function m(){const p=q(o,{exclude:"[data-ln-autosave-exclude]"});try{localStorage.setItem(d,JSON.stringify(p))}catch{return}w(o,"ln-autosave:saved",{target:o,data:p})}function y(){let p;try{p=localStorage.getItem(d)}catch{return}if(!p)return;let h;try{h=JSON.parse(p)}catch{return}if(I(o,"ln-autosave:before-restore",{target:o,data:h}).defaultPrevented)return;const v=j(o,h);for(let b=0;b<v.length;b++)v[b].dispatchEvent(new Event("input",{bubbles:!0})),v[b].dispatchEvent(new Event("change",{bubbles:!0}));w(o,"ln-autosave:restored",{target:o,data:h})}function g(){try{localStorage.removeItem(d)}catch{return}w(o,"ln-autosave:cleared",{target:o})}this._onFocusout=function(p){const h=p.target;n(h)&&h.name&&!h.hasAttribute("data-ln-autosave-exclude")&&m()},this._onChange=function(p){const h=p.target;n(h)&&h.name&&!h.hasAttribute("data-ln-autosave-exclude")&&m()},this._onSubmit=function(){g()},this._onReset=function(){g()},this._onClearClick=function(p){p.target.closest("["+s+"]")&&g()},o.addEventListener("focusout",this._onFocusout),o.addEventListener("change",this._onChange),o.addEventListener("submit",this._onSubmit),o.addEventListener("reset",this._onReset),o.addEventListener("click",this._onClearClick);const L=r(o);return L>0&&(this._onInput=function(p){const h=p.target;!n(h)||!h.name||h.hasAttribute("data-ln-autosave-exclude")||(f!==null&&clearTimeout(f),f=setTimeout(m,L))},o.addEventListener("input",this._onInput)),this._getInputTimer=function(){return f},y(),this}i.prototype.destroy=function(){if(this.dom[t]){if(this.dom.removeEventListener("focusout",this._onFocusout),this.dom.removeEventListener("change",this._onChange),this.dom.removeEventListener("submit",this._onSubmit),this.dom.removeEventListener("reset",this._onReset),this.dom.removeEventListener("click",this._onClearClick),this._onInput){this.dom.removeEventListener("input",this._onInput);const o=this._getInputTimer();o!==null&&clearTimeout(o)}w(this.dom,"ln-autosave:destroyed",{target:this.dom}),delete this.dom[t]}};function l(o){const f=o.getAttribute(e)||o.id;return f?c+window.location.pathname+":"+f:null}function n(o){const d=o.tagName;return d==="INPUT"||d==="TEXTAREA"||d==="SELECT"}function r(o){if(!o.hasAttribute(u))return 0;const d=o.getAttribute(u);if(d===""||d===null)return 1e3;const f=parseInt(d,10);return isNaN(f)||f<0?(console.warn("ln-autosave: invalid debounce value, using default",o),1e3):f}B(e,t,i,"ln-autosave")})()})();
|
|
@@ -26,7 +26,7 @@ import { dispatch, dispatchCancelable, serializeForm, populateForm, registerComp
|
|
|
26
26
|
|
|
27
27
|
// Closure-scoped helpers (NOT on prototype — internal only)
|
|
28
28
|
function _save() {
|
|
29
|
-
const data = serializeForm(form);
|
|
29
|
+
const data = serializeForm(form, { exclude: '[data-ln-autosave-exclude]' });
|
|
30
30
|
try { localStorage.setItem(key, JSON.stringify(data)); }
|
|
31
31
|
catch (e) { return; }
|
|
32
32
|
dispatch(form, 'ln-autosave:saved', { target: form, data: data });
|
|
@@ -56,12 +56,12 @@ import { dispatch, dispatchCancelable, serializeForm, populateForm, registerComp
|
|
|
56
56
|
// Listener handlers (held on `this` for symmetric removal in destroy)
|
|
57
57
|
this._onFocusout = function (e) {
|
|
58
58
|
const el = e.target;
|
|
59
|
-
if (_isFormField(el) && el.name) _save();
|
|
59
|
+
if (_isFormField(el) && el.name && !el.hasAttribute('data-ln-autosave-exclude')) _save();
|
|
60
60
|
};
|
|
61
61
|
|
|
62
62
|
this._onChange = function (e) {
|
|
63
63
|
const el = e.target;
|
|
64
|
-
if (_isFormField(el) && el.name) _save();
|
|
64
|
+
if (_isFormField(el) && el.name && !el.hasAttribute('data-ln-autosave-exclude')) _save();
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
this._onSubmit = function () { _clear(); };
|
|
@@ -84,7 +84,7 @@ import { dispatch, dispatchCancelable, serializeForm, populateForm, registerComp
|
|
|
84
84
|
if (debounceMs > 0) {
|
|
85
85
|
this._onInput = function (e) {
|
|
86
86
|
const el = e.target;
|
|
87
|
-
if (!_isFormField(el) || !el.name) return;
|
|
87
|
+
if (!_isFormField(el) || !el.name || el.hasAttribute('data-ln-autosave-exclude')) return;
|
|
88
88
|
if (inputTimer !== null) clearTimeout(inputTimer);
|
|
89
89
|
inputTimer = setTimeout(_save, debounceMs);
|
|
90
90
|
};
|