@livenetworks/ashlar 1.4.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.
Files changed (219) hide show
  1. package/README.md +189 -177
  2. package/js/COMPONENTS.md +55 -49
  3. package/js/index.js +22 -5
  4. package/js/ln-accordion/README.md +186 -137
  5. package/js/ln-accordion/ln-accordion.js +1 -1
  6. package/js/ln-ajax/README.md +73 -4
  7. package/js/ln-ajax/ln-ajax.js +1 -1
  8. package/js/ln-ajax/src/ln-ajax.js +13 -13
  9. package/js/ln-api-connector/README.md +261 -150
  10. package/js/ln-api-connector/ln-api-connector.js +1 -1
  11. package/js/ln-api-connector/src/ln-api-connector.js +368 -265
  12. package/js/ln-api-queue/README.md +219 -0
  13. package/js/ln-api-queue/ln-api-queue.js +1 -0
  14. package/js/ln-api-queue/src/ln-api-queue.js +320 -0
  15. package/js/ln-api-queue/src/queue-storage.js +455 -0
  16. package/js/ln-autoresize/README.md +47 -0
  17. package/js/ln-autoresize/ln-autoresize.js +1 -1
  18. package/js/ln-autosave/README.md +33 -1
  19. package/js/ln-autosave/ln-autosave.js +1 -1
  20. package/js/ln-autosave/src/ln-autosave.js +4 -4
  21. package/js/ln-circular-progress/README.md +27 -13
  22. package/js/ln-circular-progress/ln-circular-progress.js +1 -1
  23. package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
  24. package/js/ln-confirm/README.md +162 -86
  25. package/js/ln-confirm/_ln-confirm.scss +11 -0
  26. package/js/ln-confirm/ln-confirm.js +1 -1
  27. package/js/ln-confirm/src/ln-confirm.js +77 -41
  28. package/js/ln-core/README.md +896 -0
  29. package/js/ln-core/hash.js +92 -0
  30. package/js/ln-core/helpers.js +337 -8
  31. package/js/ln-core/index.js +4 -2
  32. package/js/ln-core/persist.js +29 -3
  33. package/js/ln-core/positioning.js +1 -35
  34. package/js/ln-core/window-cache.js +213 -0
  35. package/js/ln-couchdb-connector/README.md +177 -156
  36. package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
  37. package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
  38. package/js/ln-data-coordinator/README.md +536 -165
  39. package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
  40. package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
  41. package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
  42. package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
  43. package/js/ln-data-store/README.md +241 -94
  44. package/js/ln-data-store/ln-data-store.js +1 -1
  45. package/js/ln-data-store/src/ln-data-store.js +694 -699
  46. package/js/ln-date/README.md +39 -1
  47. package/js/ln-date/ln-date-dev.scss +14 -0
  48. package/js/ln-date/ln-date.js +1 -1
  49. package/js/ln-date/src/ln-date.js +236 -178
  50. package/js/ln-debug/README.md +58 -0
  51. package/js/ln-debug/ln-debug.js +1 -0
  52. package/js/ln-debug/src/ln-debug.js +19 -0
  53. package/js/ln-dropdown/README.md +31 -8
  54. package/js/ln-dropdown/ln-dropdown.js +1 -1
  55. package/js/ln-dropdown/src/ln-dropdown.js +30 -13
  56. package/js/ln-editor/README.md +274 -0
  57. package/js/ln-editor/ln-editor.js +1 -0
  58. package/js/ln-editor/ln-editor.scss +43 -0
  59. package/js/ln-editor/src/ln-editor.js +619 -0
  60. package/js/ln-external-links/README.md +44 -5
  61. package/js/ln-external-links/ln-external-links.js +1 -1
  62. package/js/ln-fill/README.md +199 -0
  63. package/js/ln-fill/ln-fill.js +1 -0
  64. package/js/ln-fill/src/ln-fill.js +87 -0
  65. package/js/ln-filter/README.md +189 -99
  66. package/js/ln-filter/ln-filter-dev.scss +8 -0
  67. package/js/ln-filter/ln-filter.js +1 -1
  68. package/js/ln-filter/src/ln-filter.js +22 -7
  69. package/js/ln-form/README.md +212 -49
  70. package/js/ln-form/ln-form.js +1 -1
  71. package/js/ln-form/src/ln-form.js +99 -199
  72. package/js/ln-http/README.md +125 -89
  73. package/js/ln-http/ln-http.js +1 -1
  74. package/js/ln-http/src/ln-http.js +32 -13
  75. package/js/ln-icon/README.md +130 -0
  76. package/js/ln-icon/ln-icon.js +1 -0
  77. package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
  78. package/js/ln-link/README.md +47 -9
  79. package/js/ln-link/ln-link.js +1 -1
  80. package/js/ln-link/src/ln-link.js +22 -6
  81. package/js/ln-list/README.md +129 -0
  82. package/js/ln-list/ln-list.js +1 -0
  83. package/js/ln-list/ln-list.scss +28 -0
  84. package/js/ln-list/src/ln-list.js +1216 -0
  85. package/js/ln-modal/README.md +185 -154
  86. package/js/ln-modal/ln-modal-dev.scss +13 -0
  87. package/js/ln-modal/ln-modal.js +1 -1
  88. package/js/ln-modal/ln-modal.scss +28 -4
  89. package/js/ln-modal/src/ln-modal.js +137 -201
  90. package/js/ln-modal-coordinator/README.md +99 -0
  91. package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
  92. package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
  93. package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
  94. package/js/ln-nav/README.md +46 -3
  95. package/js/ln-nav/ln-nav.js +1 -1
  96. package/js/ln-nav/src/ln-nav.js +92 -127
  97. package/js/ln-number/README.md +28 -1
  98. package/js/ln-number/ln-number-dev.scss +13 -0
  99. package/js/ln-number/ln-number.js +1 -1
  100. package/js/ln-number/src/ln-number.js +443 -350
  101. package/js/ln-options/README.md +66 -0
  102. package/js/ln-options/ln-options.js +1 -0
  103. package/js/ln-options/src/ln-options.js +74 -0
  104. package/js/ln-popover/README.md +40 -5
  105. package/js/ln-popover/ln-popover.js +1 -1
  106. package/js/ln-popover/src/ln-popover.js +38 -11
  107. package/js/ln-progress/README.md +413 -442
  108. package/js/ln-progress/ln-progress.js +1 -1
  109. package/js/ln-progress/src/ln-progress.js +98 -150
  110. package/js/ln-router/README.md +432 -0
  111. package/js/ln-router/ln-router.js +1 -0
  112. package/js/ln-router/src/ln-router.js +539 -0
  113. package/js/ln-search/README.md +158 -83
  114. package/js/ln-search/ln-search-dev.scss +8 -0
  115. package/js/ln-search/ln-search.js +1 -1
  116. package/js/ln-search/src/ln-search.js +12 -9
  117. package/js/ln-slug/README.md +110 -0
  118. package/js/ln-slug/ln-slug.js +1 -0
  119. package/js/ln-slug/src/ln-slug.js +74 -0
  120. package/js/ln-sort/README.md +170 -0
  121. package/js/ln-sort/ln-sort-dev.scss +8 -0
  122. package/js/ln-sort/ln-sort.js +1 -0
  123. package/js/ln-sort/src/ln-sort.js +178 -0
  124. package/js/ln-sortable/README.md +133 -95
  125. package/js/ln-sortable/ln-sortable.js +1 -1
  126. package/js/ln-stat/README.md +55 -0
  127. package/js/ln-stat/ln-stat.js +1 -0
  128. package/js/ln-stat/src/ln-stat.js +55 -0
  129. package/js/ln-table/README.md +285 -101
  130. package/js/ln-table/ln-table-dev.scss +17 -0
  131. package/js/ln-table/ln-table.js +1 -1
  132. package/js/ln-table/ln-table.scss +1 -1
  133. package/js/ln-table/src/ln-table.js +1493 -473
  134. package/js/ln-table-coordinator/README.md +73 -0
  135. package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
  136. package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
  137. package/js/ln-tabs/README.md +221 -137
  138. package/js/ln-tabs/ln-tabs-dev.scss +18 -0
  139. package/js/ln-tabs/ln-tabs.js +1 -1
  140. package/js/ln-tabs/src/ln-tabs.js +41 -24
  141. package/js/ln-time/README.md +14 -0
  142. package/js/ln-time/ln-time.js +1 -1
  143. package/js/ln-time/src/ln-time.js +223 -192
  144. package/js/ln-toast/README.md +178 -122
  145. package/js/ln-toast/ln-toast.js +1 -15
  146. package/js/ln-toast/src/ln-toast.js +232 -210
  147. package/js/ln-toast/template.html +16 -12
  148. package/js/ln-toggle/README.md +61 -2
  149. package/js/ln-toggle/ln-toggle-dev.scss +13 -0
  150. package/js/ln-toggle/ln-toggle.js +1 -1
  151. package/js/ln-toggle/src/ln-toggle.js +56 -43
  152. package/js/ln-tooltip/README.md +95 -58
  153. package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
  154. package/js/ln-tooltip/ln-tooltip.js +1 -1
  155. package/js/ln-tooltip/src/ln-tooltip.js +45 -6
  156. package/js/ln-translations/README.md +118 -96
  157. package/js/ln-translations/ln-translations.js +1 -1
  158. package/js/ln-translations/src/ln-translations.js +10 -5
  159. package/js/ln-upload/README.md +85 -10
  160. package/js/ln-upload/ln-upload.js +1 -1
  161. package/js/ln-upload/src/ln-upload.js +26 -10
  162. package/js/ln-validate/README.md +72 -0
  163. package/js/ln-validate/ln-validate-dev.scss +15 -0
  164. package/js/ln-validate/ln-validate.js +1 -1
  165. package/js/ln-validate/src/ln-validate.js +64 -0
  166. package/package.json +61 -55
  167. package/scss/base/_global.scss +2 -2
  168. package/scss/base/_typography.scss +1 -2
  169. package/scss/components/_date.scss +1 -0
  170. package/scss/components/_dropdown.scss +0 -5
  171. package/scss/components/_editor.scss +5 -0
  172. package/scss/components/_filter.scss +26 -0
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +14 -5
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +68 -1
  177. package/scss/components/_toast.scss +68 -41
  178. package/scss/components/_toggle.scss +16 -0
  179. package/scss/config/_density.scss +205 -117
  180. package/scss/config/_icons.scss +10 -9
  181. package/scss/config/_tokens.scss +69 -47
  182. package/scss/config/mixins/_accordion.scss +2 -7
  183. package/scss/config/mixins/_alert.scss +4 -4
  184. package/scss/config/mixins/_breadcrumbs.scss +2 -2
  185. package/scss/config/mixins/_btn.scss +1 -1
  186. package/scss/config/mixins/_card.scss +89 -14
  187. package/scss/config/mixins/_collapsible.scss +3 -0
  188. package/scss/config/mixins/_colors.scss +1 -1
  189. package/scss/config/mixins/_diagnostics.scss +48 -0
  190. package/scss/config/mixins/_display.scss +1 -0
  191. package/scss/config/mixins/_dropdown.scss +3 -5
  192. package/scss/config/mixins/_editor.scss +124 -0
  193. package/scss/config/mixins/_empty-state.scss +7 -0
  194. package/scss/config/mixins/_filter.scss +47 -0
  195. package/scss/config/mixins/_footer.scss +1 -2
  196. package/scss/config/mixins/_form.scss +207 -21
  197. package/scss/config/mixins/_index.scss +4 -1
  198. package/scss/config/mixins/_ln-table.scss +51 -93
  199. package/scss/config/mixins/_modal.scss +149 -66
  200. package/scss/config/mixins/_nav.scss +3 -4
  201. package/scss/config/mixins/_popover.scss +39 -0
  202. package/scss/config/mixins/_sidebar.scss +1 -1
  203. package/scss/config/mixins/_sort.scss +42 -0
  204. package/scss/config/mixins/_table.scss +232 -38
  205. package/scss/config/mixins/_toast.scss +179 -152
  206. package/scss/config/mixins/_tooltip.scss +17 -7
  207. package/scss/config/mixins/_typography.scss +4 -2
  208. package/scss/ln-ashlar-dev.scss +39 -0
  209. package/scss/ln-ashlar.scss +8 -2
  210. package/js/ln-data-table/README.md +0 -110
  211. package/js/ln-data-table/ln-data-table.js +0 -1
  212. package/js/ln-data-table/ln-data-table.scss +0 -10
  213. package/js/ln-data-table/src/ln-data-table.js +0 -1103
  214. package/js/ln-icons/README.md +0 -88
  215. package/js/ln-icons/ln-icons.js +0 -1
  216. package/js/ln-table/ln-table-sort.js +0 -1
  217. package/js/ln-table/src/ln-table-sort.js +0 -168
  218. package/scss/components/_data-table.scss +0 -83
  219. package/scss/config/mixins/_data-table.scss +0 -346
@@ -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(){x(e,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}e()}function m(e,t,o,r){if(e.nodeType!==1)return;const l=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",c=Array.from(e.querySelectorAll(l));e.matches&&e.matches(l)&&c.push(e);for(const i of c)i[o]||(i[o]=new r(i))}function v(e,t,o,r,u={}){const l=u.extraAttributes||[],c=u.onAttributeChange||null,i=u.onInit||null;function h(y){const a=y||document.body;m(a,e,t,o),i&&i(a)}return x(function(){const y=new MutationObserver(function(p){for(let f=0;f<p.length;f++){const n=p[f];if(n.type==="childList"){for(let d=0;d<n.addedNodes.length;d++){const s=n.addedNodes[d];s.nodeType===1&&(m(s,e,t,o),i&&i(s))}for(let d=0;d<n.removedNodes.length;d++){const s=n.removedNodes[d];if(s.nodeType===1){const C=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",g=Array.from(s.querySelectorAll(C));s.matches&&s.matches(C)&&g.push(s);for(let w=0;w<g.length;w++){const O=g[w];if(!document.contains(O)){const b=O[t];b&&typeof b.destroy=="function"&&b.destroy()}}}}}else n.type==="attributes"&&(c&&n.target[t]?c(n.target,n.attributeName):(m(n.target,e,t,o),i&&i(n.target)))}});let a=[];if(e.indexOf("[")!==-1){const p=/\[([\w-]+)/g;let f;for(;(f=p.exec(e))!==null;)a.push(f[1])}else a.push(e);y.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(l)})},r),window[t]=h,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){h(document.body)}):h(document.body),h}const A={};function E(e,t){A[e]=t}function M(e){return A[e]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=E,window.lnCore.getDataMapper=M),(function(){const e="data-ln-autoresize",t="lnAutoresize";if(window[t]!==void 0)return;function o(r){if(r.tagName!=="TEXTAREA")return console.warn("[ln-autoresize] Can only be applied to <textarea>, got:",r.tagName),this;this.dom=r;const u=this;return this._onInput=function(){u._resize()},r.addEventListener("input",this._onInput),this._resize(),this}o.prototype._resize=function(){this.dom.style.height="auto",this.dom.style.height=this.dom.scrollHeight+"px"},o.prototype.destroy=function(){this.dom[t]&&(this.dom.removeEventListener("input",this._onInput),this.dom.style.height="",delete this.dom[t])},v(e,t,o,"ln-autoresize")})()})();
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")})()})();
@@ -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 `lnAutoresize` property on the form element:
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
  };