@livenetworks/ashlar 1.5.1 → 1.6.1

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 (217) 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 +165 -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 +82 -42
  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 +92 -2
  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 -56
  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 -25
  173. package/scss/components/_form.scss +7 -0
  174. package/scss/components/_ln-table.scss +13 -3
  175. package/scss/components/_sort.scss +10 -0
  176. package/scss/components/_table.scss +36 -23
  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 +11 -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 +3 -5
  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 +2 -0
  198. package/scss/config/mixins/_ln-table.scss +41 -2
  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 +85 -60
  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 +27 -9
  209. package/scss/ln-ashlar.scss +7 -1
  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 -1087
  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 -156
@@ -1,199 +1,99 @@
1
- import { dispatch, serializeForm, populateForm, registerComponent } from '../../ln-core';
2
-
3
- (function () {
4
- const DOM_SELECTOR = 'data-ln-form';
5
- const DOM_ATTRIBUTE = 'lnForm';
6
- const AUTO_ATTR = 'data-ln-form-auto';
7
- const DEBOUNCE_ATTR = 'data-ln-form-debounce';
8
- const VALIDATE_SELECTOR = 'data-ln-validate';
9
- const VALIDATE_ATTRIBUTE = 'lnValidate';
10
-
11
- if (window[DOM_ATTRIBUTE] !== undefined) return;
12
-
13
- // ─── Component ─────────────────────────────────────────────
14
-
15
- function _component(form) {
16
- this.dom = form;
17
- this._debounceTimer = null;
18
-
19
- const self = this;
20
-
21
- this._onValid = function () {
22
- self._updateSubmitButton();
23
- };
24
-
25
- this._onInvalid = function () {
26
- self._updateSubmitButton();
27
- };
28
-
29
- this._onSubmit = function (e) {
30
- e.preventDefault();
31
- self.submit();
32
- };
33
-
34
- this._onFill = function (e) {
35
- if (e.detail) self.fill(e.detail);
36
- };
37
-
38
- this._onFormReset = function () {
39
- self.reset();
40
- };
41
-
42
- this._onNativeReset = function () {
43
- setTimeout(function () { self._resetValidation(); }, 0);
44
- };
45
-
46
- form.addEventListener('ln-validate:valid', this._onValid);
47
- form.addEventListener('ln-validate:invalid', this._onInvalid);
48
- form.addEventListener('submit', this._onSubmit);
49
- form.addEventListener('ln-form:fill', this._onFill);
50
- form.addEventListener('ln-form:reset', this._onFormReset);
51
- form.addEventListener('reset', this._onNativeReset);
52
-
53
- // Auto-submit
54
- this._onAutoInput = null;
55
- if (form.hasAttribute(AUTO_ATTR)) {
56
- const debounceMs = parseInt(form.getAttribute(DEBOUNCE_ATTR)) || 0;
57
- this._onAutoInput = function () {
58
- if (debounceMs > 0) {
59
- clearTimeout(self._debounceTimer);
60
- self._debounceTimer = setTimeout(function () { self.submit(); }, debounceMs);
61
- } else {
62
- self.submit();
63
- }
64
- };
65
- form.addEventListener('input', this._onAutoInput);
66
- form.addEventListener('change', this._onAutoInput);
67
- }
68
-
69
- // Initial submit button state
70
- this._updateSubmitButton();
71
-
72
- return this;
73
- }
74
-
75
- _component.prototype._updateSubmitButton = function () {
76
- const buttons = this.dom.querySelectorAll('button[type="submit"], input[type="submit"]');
77
- if (!buttons.length) return;
78
-
79
- const fields = this.dom.querySelectorAll('[' + VALIDATE_SELECTOR + ']');
80
- let shouldDisable = false;
81
-
82
- if (fields.length > 0) {
83
- // Disable if any field is invalid OR if no fields have been touched yet
84
- let anyTouched = false;
85
- let anyInvalid = false;
86
- for (let i = 0; i < fields.length; i++) {
87
- const instance = fields[i][VALIDATE_ATTRIBUTE];
88
- if (instance && instance._touched) anyTouched = true;
89
- if (!fields[i].checkValidity()) anyInvalid = true;
90
- }
91
- shouldDisable = anyInvalid || !anyTouched;
92
- }
93
-
94
- for (let j = 0; j < buttons.length; j++) {
95
- buttons[j].disabled = shouldDisable;
96
- }
97
- };
98
-
99
- _component.prototype.fill = function (data) {
100
- const filled = populateForm(this.dom, data);
101
-
102
- // Trigger events so ln-validate picks up the changes.
103
- // Mirror the same isChangeBased logic as ln-validate:
104
- // SELECT/checkbox/radio -> 'change', everything else -> 'input'
105
- for (let k = 0; k < filled.length; k++) {
106
- const el = filled[k];
107
- const isChangeBased = el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio';
108
- el.dispatchEvent(new Event(isChangeBased ? 'change' : 'input', { bubbles: true }));
109
- }
110
- };
111
-
112
- _component.prototype.submit = function () {
113
- // Force-validate all fields
114
- const fields = this.dom.querySelectorAll('[' + VALIDATE_SELECTOR + ']');
115
- let allValid = true;
116
-
117
- for (let i = 0; i < fields.length; i++) {
118
- const instance = fields[i][VALIDATE_ATTRIBUTE];
119
- if (instance) {
120
- if (!instance.validate()) allValid = false;
121
- }
122
- }
123
-
124
- if (!allValid) return;
125
-
126
- const data = serializeForm(this.dom);
127
- dispatch(this.dom, 'ln-form:submit', { data: data });
128
- };
129
-
130
- _component.prototype.reset = function () {
131
- this.dom.reset();
132
-
133
- // Mirror fill() — dispatch input/change so reactive consumers
134
- // (ln-autoresize, ln-validate, custom listeners) re-react to the
135
- // cleared values. dom.reset() clears .value but does NOT fire
136
- // input/change events; without these dispatches, ln-autoresize
137
- // keeps its previous height, etc.
138
- //
139
- // Order matters: this loop MUST run BEFORE _resetValidation().
140
- // ln-validate's input handler will mark default-empty required
141
- // fields as invalid (touched + validate); _resetValidation()
142
- // below clears that transient state. Moving _resetValidation
143
- // above the dispatch loop would leave fields visibly invalid.
144
- const fields = this.dom.querySelectorAll('input, textarea, select');
145
- for (let k = 0; k < fields.length; k++) {
146
- const el = fields[k];
147
- const isChangeBased = el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio';
148
- el.dispatchEvent(new Event(isChangeBased ? 'change' : 'input', { bubbles: true }));
149
- }
150
-
151
- this._resetValidation();
152
-
153
- // Notify high-level subscribers (custom controls that hold their
154
- // own value and cannot be reset via input/change). Distinct from
155
- // the incoming 'ln-form:reset' request event to avoid a loop.
156
- dispatch(this.dom, 'ln-form:reset-complete', { target: this.dom });
157
- };
158
-
159
- _component.prototype._resetValidation = function () {
160
- const fields = this.dom.querySelectorAll('[' + VALIDATE_SELECTOR + ']');
161
- for (let i = 0; i < fields.length; i++) {
162
- const instance = fields[i][VALIDATE_ATTRIBUTE];
163
- if (instance) instance.reset();
164
- }
165
-
166
- this._updateSubmitButton();
167
- };
168
-
169
- Object.defineProperty(_component.prototype, 'isValid', {
170
- get: function () {
171
- const fields = this.dom.querySelectorAll('[' + VALIDATE_SELECTOR + ']');
172
- for (let i = 0; i < fields.length; i++) {
173
- if (!fields[i].checkValidity()) return false;
174
- }
175
- return true;
176
- }
177
- });
178
-
179
- _component.prototype.destroy = function () {
180
- if (!this.dom[DOM_ATTRIBUTE]) return;
181
- this.dom.removeEventListener('ln-validate:valid', this._onValid);
182
- this.dom.removeEventListener('ln-validate:invalid', this._onInvalid);
183
- this.dom.removeEventListener('submit', this._onSubmit);
184
- this.dom.removeEventListener('ln-form:fill', this._onFill);
185
- this.dom.removeEventListener('ln-form:reset', this._onFormReset);
186
- this.dom.removeEventListener('reset', this._onNativeReset);
187
- if (this._onAutoInput) {
188
- this.dom.removeEventListener('input', this._onAutoInput);
189
- this.dom.removeEventListener('change', this._onAutoInput);
190
- }
191
- clearTimeout(this._debounceTimer);
192
- dispatch(this.dom, 'ln-form:destroyed', { target: this.dom });
193
- delete this.dom[DOM_ATTRIBUTE];
194
- };
195
-
196
- // ─── Init ──────────────────────────────────────────────────
197
-
198
- registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-form');
199
- })();
1
+ import { populateForm, dispatch, registerComponent } from '../../ln-core';
2
+
3
+ (function () {
4
+ const DOM_SELECTOR = 'data-ln-form';
5
+ const DOM_ATTRIBUTE = 'lnForm';
6
+ const ACTION_EDIT_ATTR = 'data-ln-form-action-edit';
7
+ const ACTION_METHOD_ATTR = 'data-ln-form-action-method';
8
+
9
+ if (window[DOM_ATTRIBUTE] !== undefined) return;
10
+
11
+ // ─── Component ─────────────────────────────────────────────
12
+
13
+ function _component(form) {
14
+ this.dom = form;
15
+ this._baseAction = form.getAttribute('action') || '';
16
+
17
+ const self = this;
18
+
19
+ this._onLnFill = function (e) {
20
+ // Guard: only handle direct dispatches at this form, not bubbled
21
+ // events from [data-ln-fillable] children inside the form.
22
+ if (e.target !== self.dom) return;
23
+ if (e.detail) {
24
+ self.fill(e.detail);
25
+ self._applyActionMode(e.detail);
26
+ } else {
27
+ // Null record = "new" mode. Native reset fires the 'reset'
28
+ // event, which restores the base action below.
29
+ self.dom.reset();
30
+ }
31
+ };
32
+
33
+ this._onReset = function () {
34
+ self._applyActionMode(null);
35
+ };
36
+
37
+ form.addEventListener('ln-fill', this._onLnFill);
38
+ form.addEventListener('reset', this._onReset);
39
+
40
+ return this;
41
+ }
42
+
43
+ _component.prototype.fill = function (data) {
44
+ const filled = populateForm(this.dom, data);
45
+
46
+ // Trigger events so reactive consumers (ln-validate, ln-autoresize)
47
+ // pick up the changes. SELECT/checkbox/radio -> 'change',
48
+ // everything else -> 'input'.
49
+ for (let k = 0; k < filled.length; k++) {
50
+ const el = filled[k];
51
+ const isChangeBased = el.tagName === 'SELECT' || el.type === 'checkbox' || el.type === 'radio';
52
+ el.dispatchEvent(new Event(isChangeBased ? 'change' : 'input', { bubbles: true }));
53
+ }
54
+ };
55
+
56
+ _component.prototype._ensureMethodInput = function () {
57
+ let input = this.dom.querySelector('input[name="_method"]');
58
+ if (!input) {
59
+ input = document.createElement('input');
60
+ input.type = 'hidden';
61
+ input.name = '_method';
62
+ input.value = '';
63
+ this.dom.appendChild(input);
64
+ }
65
+ return input;
66
+ };
67
+
68
+ _component.prototype._applyActionMode = function (record) {
69
+ if (!this.dom.hasAttribute(ACTION_EDIT_ATTR)) return;
70
+
71
+ const id = record && record.id != null && record.id !== '' ? record.id : null;
72
+ const methodInput = this._ensureMethodInput();
73
+
74
+ if (id !== null) {
75
+ const template = this.dom.getAttribute(ACTION_EDIT_ATTR);
76
+ if (template) {
77
+ this.dom.setAttribute('action', template.replace(':id', encodeURIComponent(id)));
78
+ } else {
79
+ this.dom.setAttribute('action', this._baseAction.replace(/\/$/, '') + '/' + encodeURIComponent(id));
80
+ }
81
+ methodInput.value = this.dom.getAttribute(ACTION_METHOD_ATTR) || 'PUT';
82
+ } else {
83
+ this.dom.setAttribute('action', this._baseAction);
84
+ methodInput.value = '';
85
+ }
86
+ };
87
+
88
+ _component.prototype.destroy = function () {
89
+ if (!this.dom[DOM_ATTRIBUTE]) return;
90
+ this.dom.removeEventListener('ln-fill', this._onLnFill);
91
+ this.dom.removeEventListener('reset', this._onReset);
92
+ dispatch(this.dom, 'ln-form:destroyed', { target: this.dom });
93
+ delete this.dom[DOM_ATTRIBUTE];
94
+ };
95
+
96
+ // ─── Init ──────────────────────────────────────────────────
97
+
98
+ registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-form');
99
+ })();
@@ -1,89 +1,125 @@
1
- # ln-http
2
-
3
- A zero-dependency, global **HTTP Concurrency Coordinator** that intercepts browser network operations to prevent race conditions, out-of-order responses, and duplicate submission side-effects.
4
-
5
- It manages requests on two distinct pipelines: **Path A** (transparent GET/HEAD URL-deduplication wrapping `window.fetch`) and **Path B** (explicit key-based event-driven cancellations for POST/PUT/DELETE).
6
-
7
- ---
8
-
9
- ## 🧭 Philosophy & Architecture
10
-
11
- 1. **Path A (Transparent GET/HEAD Concurrency):** Automatically intercepts global `fetch()` calls. If a GET/HEAD request to the exact same URL is already in-flight (e.g., search-as-you-type), the predecessor is instantly aborted. POST and unsafe methods are bypassed to preserve intent.
12
- 2. **Path B (Event-Driven Keyed Concurrency):** Listens globally for `ln-http:request` events containing a distinct `key` (e.g. `reorder-list-1`). A new dispatch instantly aborts any existing in-flight request bearing the same key (any method), preventing double-submit or drag-and-drop overlaps.
13
- 3. **Composition, Not Modification:** `ln-http` is a transport supervisor. It does not inject headers, manipulate bodies, or parse responses. It focuses entirely on socket cancellation via standard browser `AbortController` signals.
14
-
15
- ---
16
-
17
- ## 📦 Minimal Blueprint
18
-
19
- ### Path A (Transparent URL-Deduplication)
20
- Just use the standard native `fetch` API. Older identical GET requests are aborted automatically.
21
- ```js
22
- // Rapid keystrokes abort previous search GETs transparently
23
- try {
24
- const res = await fetch('/api/search?q=query');
25
- const data = await res.json();
26
- } catch (err) {
27
- if (err.name === 'AbortError') return; // Swallowed abort
28
- }
29
- ```
30
-
31
- ### Path B (Event-Driven Concurrency)
32
- Dispatch an `ln-http:request` event with a unique `key` from any element.
33
- ```js
34
- element.dispatchEvent(new CustomEvent('ln-http:request', {
35
- bubbles: true, // Must bubble to document!
36
- detail: {
37
- url: '/api/items/reorder',
38
- method: 'POST',
39
- body: JSON.stringify({ ids }),
40
- key: 'items-reorder'
41
- }
42
- }));
43
- ```
44
-
45
- ---
46
-
47
- ## 🛠️ Declarative API Contract
48
-
49
- ### Path B Request Object (`detail`)
50
-
51
- | Parameter | Type | Default | Description |
52
- | :--- | :--- | :--- | :--- |
53
- | `url` | `string` | *Required* | Target endpoint URL. |
54
- | `method` | `string` | `'GET'` | HTTP verb. Automatically capitalized. |
55
- | `body` | `any` | `null` | Request payload (JSON string, FormData, Blob, etc.). |
56
- | `key` | `string` | `null` | Unique identifier to cancel previous in-flight requests under this key. |
57
- | `signal` | `AbortSignal` | `null` | Optional external signal to compose with the internal abort controller. |
58
-
59
- ### Global JavaScript API (`window.lnHttp`)
60
-
61
- | Member | Type | Description |
62
- | :--- | :--- | :--- |
63
- | `cancel(url)` | `(url: string) => boolean` | Aborts all Path A in-flight requests matching `url`. |
64
- | `cancelByKey(key)` | `(key: string) => boolean` | Aborts the Path B in-flight request matching `key`. |
65
- | `cancelAll()` | `() => void` | Aborts all active in-flight requests (both paths). |
66
- | `inflight` | `getter` | Returns snapshot of active requests: `{ url, method }` or `{ key }`. |
67
- | `destroy()` | `() => void` | Clears all pending requests, removes event listeners, restores native `fetch`. |
68
-
69
- ---
70
-
71
- ## ⚡ DOM Events (Path B response lifecycle)
72
-
73
- Both events bubble from the element that dispatched the original `'ln-http:request'`.
74
-
75
- ### `ln-http:response`
76
- Fired when `fetch` resolves. The consumer must branch on `ok`/`status` and parse the raw body.
77
- - `detail`: `{ ok: boolean, status: number, response: Response }`
78
-
79
- ### `ln-http:error`
80
- Fired when network-level failures reject the fetch promise (excluding aborts).
81
- - `detail`: `{ ok: false, status: 0, error: Error }`
82
-
83
- ---
84
-
85
- ## ⚠️ Common Pitfalls
86
-
87
- - **Forgetting `bubbles: true`:** Path B listens on the `document` level. Events dispatched without `bubbles: true` will never reach the service and fail silently.
88
- - **Ignoring `AbortError`:** Canceled Path A GET promises reject with an `AbortError`. Presenters must explicitly check and catch this error to avoid logging false failures.
89
- - **Accessing response body twice:** `response` in the `ln-http:response` detail is a native `Response` stream. It can only be parsed (e.g., `.json()`, `.text()`) once.
1
+ # ln-http
2
+
3
+ A zero-dependency, global **HTTP Concurrency Coordinator** that intercepts browser network operations to prevent race conditions, out-of-order responses, and duplicate submission side-effects.
4
+
5
+ It manages requests on two distinct pipelines: **Path A** (transparent GET/HEAD URL-deduplication wrapping `window.fetch`) and **Path B** (explicit key-based event-driven cancellations for POST/PUT/DELETE).
6
+
7
+ ---
8
+
9
+ ## 🧭 Philosophy & Architecture
10
+
11
+ 1. **Path A (Transparent GET/HEAD Concurrency):** Automatically intercepts global `fetch()` calls. If a GET/HEAD request to the exact same URL is already in-flight (e.g., search-as-you-type), the predecessor is instantly aborted. POST and unsafe methods are bypassed to preserve intent.
12
+ 2. **Path B (Event-Driven Keyed Concurrency):** Listens globally for `ln-http:request` events containing a distinct `key` (e.g. `reorder-list-1`). A new dispatch instantly aborts any existing in-flight request bearing the same key (any method), preventing double-submit or drag-and-drop overlaps.
13
+ 3. **Composition, Not Modification:** `ln-http` is a transport supervisor. It does not inject headers, manipulate bodies, or parse responses. It focuses entirely on socket cancellation via standard browser `AbortController` signals.
14
+
15
+ ---
16
+
17
+ ## 📦 Minimal Blueprint
18
+
19
+ ### Path A (Transparent URL-Deduplication)
20
+ Just use the standard native `fetch` API. Older identical GET requests are aborted automatically.
21
+ ```js
22
+ // Rapid keystrokes abort previous search GETs transparently
23
+ try {
24
+ const res = await fetch('/api/search?q=query');
25
+ const data = await res.json();
26
+ } catch (err) {
27
+ if (err.name === 'AbortError') return; // Swallowed abort
28
+ }
29
+ ```
30
+
31
+ ### Path B (Event-Driven Concurrency)
32
+ Dispatch an `ln-http:request` event with a unique `key` from any element.
33
+ ```js
34
+ element.dispatchEvent(new CustomEvent('ln-http:request', {
35
+ bubbles: true, // Must bubble to document!
36
+ detail: {
37
+ url: '/api/items/reorder',
38
+ method: 'POST',
39
+ body: JSON.stringify({ ids }),
40
+ key: 'items-reorder'
41
+ }
42
+ }));
43
+ ```
44
+
45
+ ---
46
+
47
+ ## 🛠️ Declarative API Contract
48
+
49
+ ### Path B Request Object (`detail`)
50
+
51
+ | Parameter | Type | Default | Description |
52
+ | :--- | :--- | :--- | :--- |
53
+ | `url` | `string` | *Required* | Target endpoint URL. |
54
+ | `method` | `string` | `'GET'` | HTTP verb. Automatically capitalized. |
55
+ | `body` | `any` | `null` | Request payload (JSON string, FormData, Blob, etc.). |
56
+ | `key` | `string` | `null` | Unique identifier to cancel previous in-flight requests under this key. |
57
+ | `signal` | `AbortSignal` | `null` | Optional external signal to compose with the internal abort controller. |
58
+
59
+ ### Global JavaScript API (`window.lnHttp`)
60
+
61
+ | Member | Type | Description |
62
+ | :--- | :--- | :--- |
63
+ | `cancel(url)` | `(url: string) => boolean` | Aborts all Path A in-flight requests matching `url`. |
64
+ | `cancelByKey(key)` | `(key: string) => boolean` | Aborts the Path B in-flight request matching `key`. |
65
+ | `cancelAll()` | `() => void` | Aborts all active in-flight requests (both paths). |
66
+ | `inflight` | `getter` | Returns snapshot of active requests: `{ url, method }` or `{ key }`. |
67
+ | `destroy()` | `() => void` | Clears all pending requests, removes event listeners, restores native `fetch`. |
68
+
69
+ ---
70
+
71
+ ## ⚡ DOM Events (Path B response lifecycle)
72
+
73
+ Both events bubble from the element that dispatched the original `'ln-http:request'`.
74
+
75
+ ### `ln-http:response`
76
+ Fired when `fetch` resolves. The consumer must branch on `ok`/`status` and parse the raw body.
77
+ - `detail`: `{ ok: boolean, status: number, response: Response }`
78
+
79
+ ### `ln-http:error`
80
+ Fired when network-level failures reject the fetch promise (excluding aborts).
81
+ - `detail`: `{ ok: false, status: 0, error: Error }`
82
+
83
+ ---
84
+
85
+ ## ⚠️ Common Pitfalls
86
+
87
+ - **Forgetting `bubbles: true`:** Path B listens on the `document` level. Events dispatched without `bubbles: true` will never reach the service and fail silently.
88
+ - **Ignoring `AbortError`:** Canceled Path A GET promises reject with an `AbortError`. Presenters must explicitly check and catch this error to avoid logging false failures.
89
+ - **Accessing response body twice:** `response` in the `ln-http:response` detail is a native `Response` stream. It can only be parsed (e.g., `.json()`, `.text()`) once.
90
+
91
+ ---
92
+
93
+ ## 🔧 Internals
94
+
95
+ Source: `js/ln-http/ln-http.js`. A **service-style global**: wraps `window.fetch`, listens on `document`, no DOM instance, no `data-ln-*`, no MutationObserver. Two closure-private `Map`s hold all state — `_inflight` (`"METHOD URL" → AbortController`, Path A) and `_keyed` (`consumer key → AbortController`, Path B) — plus `_origFetch`, the unwrapped `fetch` captured at load, used to restore on `destroy()`.
96
+
97
+ ### Parasitic design
98
+
99
+ Other library components (`ln-form`, `ln-ajax`, `ln-store`, `ln-table`, `ln-icon`) call `fetch()` directly with no reference to ln-http. If ln-http loads, those calls are silently routed through the wrapper and inherit Path A dedup; if it doesn't load, they still work, just without dedup. No component has a hard dependency on ln-http.
100
+
101
+ ### Path A — wrapped `fetch`
102
+
103
+ Every `fetch()` call in the page flows through `_wrappedFetch`: extract URL (`string`/`URL`/`Request`, in that precedence) and method (`options.method` > `Request.method` > `'GET'`), build key `method + ' ' + url`. If idempotent (**GET/HEAD only** — DELETE/PUT are RFC-idempotent but dropping an in-flight one because a newer one landed silently discards user intent, so auto-dedup is deliberately conservative) and a same-key entry exists, abort + delete it. A fresh `AbortController` is always created and composed with any consumer-passed `options.signal` (a `{ once: true }` listener re-aborts the wrapper's controller when the consumer's signal fires). The merged options substitute the composed signal; `_origFetch` is called with them. On settle, `.finally` deletes the `_inflight` entry **only if it still holds this call's controller** — an identity check that prevents a newer same-key call's fresher entry from being wiped by an older call's cleanup.
104
+
105
+ The wrapper never touches the response — no `.json()`, no header injection, no body mutation. The returned Promise is `_origFetch`'s own, with only the signal substituted.
106
+
107
+ ### Path B — `ln-http:request` event
108
+
109
+ A single `document` listener (`_onRequest`) handles `ln-http:request`. It reads `e.detail` (bails silently if `detail.url` is missing), captures `e.target` for the eventual response dispatch, resolves method (`opts.method || (opts.body ? 'POST' : 'GET')`), and — if `opts.key` is truthy — aborts+deletes any existing `_keyed` entry under that key (method-agnostic; no method check). A fresh controller is composed with `opts.signal` the same way as Path A, and stored in `_keyed` **only if a key was given** (keyless dispatches are one-shots; `cancelByKey` can't reach them). `opts.body` is forwarded with a strict `!== undefined` check, so intentional falsy bodies (`0`, `''`, `false`) still get sent.
110
+
111
+ The actual network call **re-enters the wrapped `fetch`** (not `_origFetch`) — so a Path B GET with a key is tracked in both maps simultaneously via two separate, signal-linked `AbortController`s; aborting the Path B entry cascades into aborting the Path A one via the composed signal, but not vice-versa.
112
+
113
+ On resolve (any HTTP status): identity-checked `_keyed` cleanup, then dispatch `ln-http:response` `{ ok, status, response }` — the raw, unparsed `Response`. On reject: identity-checked cleanup runs **before** the `AbortError` check (correct because the identity check itself distinguishes "superseded" from "actually failed" — if superseded, the map already holds the newer controller and the check no-ops); non-abort errors dispatch `ln-http:error` `{ ok: false, status: 0, error }`.
114
+
115
+ ### Init / destroy
116
+
117
+ Script-load IIFE bails early if `window.lnHttp` already exists (double-load guard — prevents double-wrapping `fetch`, which would double every dedup update per call). Otherwise: capture `_origFetch`, init both maps, install `_wrappedFetch` on `window.fetch`, register the `ln-http:request` listener, publish `window.lnHttp`. `destroy()` reverses all of it (`cancelAll()`, remove listener, restore `window.fetch = _origFetch`, `delete window.lnHttp`) — meant for hot-reload/test teardown, not production use.
118
+
119
+ ### Why two maps, why raw Response
120
+
121
+ `_inflight` is unconditional per-request bookkeeping the wrapper does for every idempotent call; `_keyed` is opt-in, consumer-chosen supersede semantics via Path B. Merging them would force synthetic keys onto one or the other. The `Response` is forwarded unparsed because its body can only be read once (`.json()`/`.text()`/etc. are mutually exclusive) and ln-http doesn't know — or want to presume — which the consumer needs; two listeners on the same target must `.clone()` if both need the body, same as any other `fetch` consumer.
122
+
123
+ ### Risk surface
124
+
125
+ Wrapping `window.fetch` is a global mutation — third-party scripts (analytics, A/B SDKs) get wrapped too, but since Path A only touches GET/HEAD, the practical effect is limited to their own racing GETs cancelling each other. A `fetch` polyfill installed *after* ln-http loads will overwrite the wrapper and bypass it; one installed before is captured as `_origFetch` and composes fine.
@@ -1 +1 @@
1
- (function(){"use strict";function d(s,e,r){s.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:r||{}}))}const h={};function g(s,e){h[s]=e}function b(s){return h[s]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=g,window.lnCore.getDataMapper=b),(function(){if(window.lnHttp)return;const s=window.fetch.bind(window),e=new Map,r=new Map;function m(t){return typeof t=="string"?t:t instanceof URL?t.href:t instanceof Request?t.url:String(t)}function E(t,n){return n&&n.method?String(n.method).toUpperCase():t instanceof Request?t.method.toUpperCase():"GET"}function y(t,n){return n+" "+t}function _(t){return t==="GET"||t==="HEAD"}function p(t,n){n=n||{};const i=m(t),f=E(t,n),o=y(i,f);_(f)&&e.has(o)&&(e.get(o).abort(),e.delete(o));const a=new AbortController,c=n.signal;c&&(c.aborted?a.abort(c.reason):c.addEventListener("abort",function(){a.abort(c.reason)},{once:!0}));const u=Object.assign({},n,{signal:a.signal});return e.set(o,a),s(t,u).finally(function(){e.get(o)===a&&e.delete(o)})}p.toString=function(){return"function fetch() { [ln-http wrapped] }"},window.fetch=p;function w(t){const n=t.detail||{};if(!n.url)return;const i=t.target,f=(n.method||(n.body?"POST":"GET")).toUpperCase(),o=n.key;o&&r.has(o)&&(r.get(o).abort(),r.delete(o));const a=new AbortController,c=n.signal;c&&(c.aborted?a.abort(c.reason):c.addEventListener("abort",function(){a.abort(c.reason)},{once:!0})),o&&r.set(o,a);const u={method:f,signal:a.signal};n.body!==void 0&&(u.body=n.body),window.fetch(n.url,u).then(function(l){o&&r.get(o)===a&&r.delete(o),d(i,"ln-http:response",{ok:l.ok,status:l.status,response:l})}).catch(function(l){o&&r.get(o)===a&&r.delete(o),!(l&&l.name==="AbortError")&&d(i,"ln-http:error",{ok:!1,status:0,error:l})})}document.addEventListener("ln-http:request",w),window.lnHttp={cancel:function(t){let n=!1;return e.forEach(function(i,f){f.endsWith(" "+t)&&(i.abort(),e.delete(f),n=!0)}),n},cancelByKey:function(t){return r.has(t)?(r.get(t).abort(),r.delete(t),!0):!1},cancelAll:function(){e.forEach(function(t){t.abort()}),e.clear(),r.forEach(function(t){t.abort()}),r.clear()},get inflight(){const t=[];return e.forEach(function(n,i){const f=i.indexOf(" ");t.push({method:i.slice(0,f),url:i.slice(f+1)})}),r.forEach(function(n,i){t.push({key:i})}),t},destroy:function(){window.lnHttp.cancelAll(),document.removeEventListener("ln-http:request",w),window.fetch=s,delete window.lnHttp}}})()})();
1
+ (function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...n){typeof n[0]=="string"&&(n[0].startsWith("[ln-")||n[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,n)}}const m={};function _(t,n){m[t]||(m[t]=document.querySelector('[data-ln-template="'+t+'"]'));const o=m[t];return o?o.content.cloneNode(!0):(console.warn("["+(n||"ln-core")+'] Template "'+t+'" not found'),null)}function y(t,n,o){t.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:o||{}}))}function E(t,n){if(!t||!n)return t;const o=t.querySelectorAll("[data-ln-field]");for(let l=0;l<o.length;l++){const c=o[l],r=c.getAttribute("data-ln-field");n[r]!=null&&(c.textContent=n[r])}const u=t.querySelectorAll("[data-ln-attr]");for(let l=0;l<u.length;l++){const c=u[l],r=c.getAttribute("data-ln-attr").split(",");for(let e=0;e<r.length;e++){const i=r[e].trim().split(":");if(i.length!==2)continue;const f=i[0].trim(),s=i[1].trim();n[s]!=null&&c.setAttribute(f,n[s])}}const d=t.querySelectorAll("[data-ln-show]");for(let l=0;l<d.length;l++){const c=d[l],r=c.getAttribute("data-ln-show");r in n&&c.classList.toggle("hidden",!n[r])}const a=t.querySelectorAll("[data-ln-class]");for(let l=0;l<a.length;l++){const c=a[l],r=c.getAttribute("data-ln-class").split(",");for(let e=0;e<r.length;e++){const i=r[e].trim().split(":");if(i.length!==2)continue;const f=i[0].trim(),s=i[1].trim();s in n&&c.classList.toggle(f,!!n[s])}}return t}function x(t,n){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:n??null,bubbles:!0}));const o=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let u=0;u<o.length;u++)o[u].dispatchEvent(new CustomEvent("ln-fill",{detail:n??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)E(t.target,t.detail);else{const n=t.target.querySelectorAll("[data-ln-field]");for(let o=0;o<n.length;o++)n[o].textContent=""}})));function L(t,n){if(!t||!n)return t;const o=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;o.nextNode();){const a=o.currentNode;a.textContent.indexOf("{{")!==-1&&(a.textContent=a.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(l,c){return n[c]!==void 0?n[c]:""}))}const u=function(a,l){return n[l]!==void 0?n[l]:""},d=Array.from(t.querySelectorAll("*"));t.nodeType===1&&d.push(t);for(let a=0;a<d.length;a++){const l=d[a],c=l.attributes;for(let r=0;r<c.length;r++){const e=c[r];e.value.indexOf("{{")!==-1&&l.setAttribute(e.name,e.value.replace(/\{\{\s*(\w+)\s*\}\}/g,u))}}return t}function q(t,n,o,u,d,a){const l={};for(let r=0;r<t.children.length;r++){const e=t.children[r],i=e.getAttribute("data-ln-key");i&&(l[i]=e)}const c=document.createDocumentFragment();for(let r=0;r<n.length;r++){const e=n[r],i=String(u(e));let f=l[i];if(f)d(f,e,r);else{const s=_(o,a);if(!s||(L(s,e),f=s.firstElementChild,!f))continue;f.setAttribute("data-ln-key",i),d(f,e,r)}c.appendChild(f)}t.textContent="",t.appendChild(c)}const A={};function T(t,n){A[t]=n}function F(t){return A[t]||{ingress:n=>n,egress:n=>n}}const k={};function v(t,n){if(!t||typeof n!="object")return;const o=t.toLowerCase().split("-")[0];k[o]=n}function D(t){if(!t)return null;const n=t.toLowerCase().split("-")[0];return k[n]||null}v("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=T,window.lnCore.getDataMapper=F,window.lnCore.registerLocaleFallback=v,window.lnCore.getLocaleFallback=D,window.lnCore.fillTemplate=L,window.lnCore.fill=E,window.lnCore.lnFill=x,window.lnCore.renderList=q);function S(t){return(t||"").replace(/^#/,"")}function C(t){const n=t===void 0?location.hash:t,o={},u=S(n);if(!u)return o;const d=u.split("&");for(let a=0;a<d.length;a++){const l=d[a];if(!l)continue;const c=l.indexOf(":"),r=c>-1?l.slice(0,c):l,e=c>-1?l.slice(c+1):"";if(r)try{o[r]=decodeURIComponent(e)}catch{o[r]=e}}return o}function O(t){if(!t)return null;const n=C();return t in n?n[t]:null}function H(t,n){if(!t)return;const o=C();n==null?delete o[t]:o[t]=String(n);const d=Object.keys(o).map(function(a){const l=o[a];return l===""?a:a+":"+encodeURIComponent(l)}).join("&");S(location.hash)!==d&&(location.hash=d)}function M(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=C,window.lnCore.hashGet=O,window.lnCore.hashSet=H,window.lnCore.hashLinkClick=M),(function(){if(window.lnHttp)return;const t=window.fetch.bind(window),n=new Map,o=new Map;function u(e){return typeof e=="string"?e:e instanceof URL?e.href:e instanceof Request?e.url:String(e)}function d(e,i){return i&&i.method?String(i.method).toUpperCase():e instanceof Request?e.method.toUpperCase():"GET"}function a(e,i){return i+" "+e}function l(e){return e==="GET"||e==="HEAD"}function c(e,i){i=i||{};const f=u(e),s=d(e,i),w=a(f,s);l(s)&&n.has(w)&&(n.get(w).abort(),n.delete(w));const h=new AbortController,p=i.signal;let b=null;p&&(p.aborted?h.abort(p.reason):(b=function(){h.abort(p.reason)},p.addEventListener("abort",b,{once:!0})));const g=Object.assign({},i,{signal:h.signal});return n.set(w,h),t(e,g).finally(function(){p&&b&&p.removeEventListener("abort",b),n.get(w)===h&&n.delete(w)})}c.toString=function(){return"function fetch() { [ln-http wrapped] }"},window.fetch=c;function r(e){if(!e.detail||!e.detail.url)return;const i=e.target,f=(e.detail.method||(e.detail.body?"POST":"GET")).toUpperCase(),s=e.detail.key;s&&o.has(s)&&(o.get(s).abort(),o.delete(s));const w=new AbortController,h=e.detail.signal;let p=null;h&&(h.aborted?w.abort(h.reason):(p=function(){w.abort(h.reason)},h.addEventListener("abort",p,{once:!0}))),s&&o.set(s,w);const b={method:f,signal:w.signal};e.detail.body!==void 0&&(b.body=e.detail.body),window.fetch(e.detail.url,b).then(function(g){h&&p&&h.removeEventListener("abort",p),s&&o.get(s)===w&&o.delete(s),y(i,"ln-http:response",{ok:g.ok,status:g.status,response:g})}).catch(function(g){h&&p&&h.removeEventListener("abort",p),s&&o.get(s)===w&&o.delete(s),!(g&&g.name==="AbortError")&&y(i,"ln-http:error",{ok:!1,status:0,error:g})})}document.addEventListener("ln-http:request",r),window.lnHttp={cancel:function(e){let i=!1;return n.forEach(function(f,s){s.endsWith(" "+e)&&(f.abort(),n.delete(s),i=!0)}),i},cancelByKey:function(e){return o.has(e)?(o.get(e).abort(),o.delete(e),!0):!1},cancelAll:function(){n.forEach(function(e){e.abort()}),n.clear(),o.forEach(function(e){e.abort()}),o.clear()},get inflight(){const e=[];return n.forEach(function(i,f){const s=f.indexOf(" ");e.push({method:f.slice(0,s),url:f.slice(s+1)})}),o.forEach(function(i,f){e.push({key:f})}),e},destroy:function(){window.lnHttp.cancelAll(),document.removeEventListener("ln-http:request",r),window.fetch=t,delete window.lnHttp}}})()})();