@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.
- package/README.md +189 -177
- package/js/COMPONENTS.md +55 -49
- package/js/index.js +22 -5
- package/js/ln-accordion/README.md +186 -137
- package/js/ln-accordion/ln-accordion.js +1 -1
- package/js/ln-ajax/README.md +73 -4
- package/js/ln-ajax/ln-ajax.js +1 -1
- package/js/ln-ajax/src/ln-ajax.js +13 -13
- package/js/ln-api-connector/README.md +261 -150
- package/js/ln-api-connector/ln-api-connector.js +1 -1
- package/js/ln-api-connector/src/ln-api-connector.js +368 -265
- package/js/ln-api-queue/README.md +219 -0
- package/js/ln-api-queue/ln-api-queue.js +1 -0
- package/js/ln-api-queue/src/ln-api-queue.js +320 -0
- package/js/ln-api-queue/src/queue-storage.js +455 -0
- package/js/ln-autoresize/README.md +47 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -1
- package/js/ln-autosave/README.md +33 -1
- package/js/ln-autosave/ln-autosave.js +1 -1
- package/js/ln-autosave/src/ln-autosave.js +4 -4
- package/js/ln-circular-progress/README.md +27 -13
- package/js/ln-circular-progress/ln-circular-progress.js +1 -1
- package/js/ln-circular-progress/src/ln-circular-progress.js +18 -5
- package/js/ln-confirm/README.md +162 -86
- package/js/ln-confirm/_ln-confirm.scss +11 -0
- package/js/ln-confirm/ln-confirm.js +1 -1
- package/js/ln-confirm/src/ln-confirm.js +77 -41
- package/js/ln-core/README.md +896 -0
- package/js/ln-core/hash.js +92 -0
- package/js/ln-core/helpers.js +337 -8
- package/js/ln-core/index.js +4 -2
- package/js/ln-core/persist.js +29 -3
- package/js/ln-core/positioning.js +1 -35
- package/js/ln-core/window-cache.js +213 -0
- package/js/ln-couchdb-connector/README.md +177 -156
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -1
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +398 -348
- package/js/ln-data-coordinator/README.md +536 -165
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -1
- package/js/ln-data-coordinator/src/data-read-policy.js +18 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +1042 -249
- package/js/ln-data-coordinator/src/mutation-receipts.js +46 -0
- package/js/ln-data-store/README.md +241 -94
- package/js/ln-data-store/ln-data-store.js +1 -1
- package/js/ln-data-store/src/ln-data-store.js +694 -699
- package/js/ln-date/README.md +39 -1
- package/js/ln-date/ln-date-dev.scss +14 -0
- package/js/ln-date/ln-date.js +1 -1
- package/js/ln-date/src/ln-date.js +236 -178
- package/js/ln-debug/README.md +58 -0
- package/js/ln-debug/ln-debug.js +1 -0
- package/js/ln-debug/src/ln-debug.js +19 -0
- package/js/ln-dropdown/README.md +31 -8
- package/js/ln-dropdown/ln-dropdown.js +1 -1
- package/js/ln-dropdown/src/ln-dropdown.js +30 -13
- package/js/ln-editor/README.md +274 -0
- package/js/ln-editor/ln-editor.js +1 -0
- package/js/ln-editor/ln-editor.scss +43 -0
- package/js/ln-editor/src/ln-editor.js +619 -0
- package/js/ln-external-links/README.md +44 -5
- package/js/ln-external-links/ln-external-links.js +1 -1
- package/js/ln-fill/README.md +199 -0
- package/js/ln-fill/ln-fill.js +1 -0
- package/js/ln-fill/src/ln-fill.js +87 -0
- package/js/ln-filter/README.md +189 -99
- package/js/ln-filter/ln-filter-dev.scss +8 -0
- package/js/ln-filter/ln-filter.js +1 -1
- package/js/ln-filter/src/ln-filter.js +22 -7
- package/js/ln-form/README.md +212 -49
- package/js/ln-form/ln-form.js +1 -1
- package/js/ln-form/src/ln-form.js +99 -199
- package/js/ln-http/README.md +125 -89
- package/js/ln-http/ln-http.js +1 -1
- package/js/ln-http/src/ln-http.js +32 -13
- package/js/ln-icon/README.md +130 -0
- package/js/ln-icon/ln-icon.js +1 -0
- package/js/{ln-icons/src/ln-icons.js → ln-icon/src/ln-icon.js} +20 -15
- package/js/ln-link/README.md +47 -9
- package/js/ln-link/ln-link.js +1 -1
- package/js/ln-link/src/ln-link.js +22 -6
- package/js/ln-list/README.md +129 -0
- package/js/ln-list/ln-list.js +1 -0
- package/js/ln-list/ln-list.scss +28 -0
- package/js/ln-list/src/ln-list.js +1216 -0
- package/js/ln-modal/README.md +185 -154
- package/js/ln-modal/ln-modal-dev.scss +13 -0
- package/js/ln-modal/ln-modal.js +1 -1
- package/js/ln-modal/ln-modal.scss +28 -4
- package/js/ln-modal/src/ln-modal.js +137 -201
- package/js/ln-modal-coordinator/README.md +99 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator-dev.scss +8 -0
- package/js/ln-modal-coordinator/ln-modal-coordinator.js +1 -0
- package/js/ln-modal-coordinator/src/ln-modal-coordinator.js +345 -0
- package/js/ln-nav/README.md +46 -3
- package/js/ln-nav/ln-nav.js +1 -1
- package/js/ln-nav/src/ln-nav.js +92 -127
- package/js/ln-number/README.md +28 -1
- package/js/ln-number/ln-number-dev.scss +13 -0
- package/js/ln-number/ln-number.js +1 -1
- package/js/ln-number/src/ln-number.js +443 -350
- package/js/ln-options/README.md +66 -0
- package/js/ln-options/ln-options.js +1 -0
- package/js/ln-options/src/ln-options.js +74 -0
- package/js/ln-popover/README.md +40 -5
- package/js/ln-popover/ln-popover.js +1 -1
- package/js/ln-popover/src/ln-popover.js +38 -11
- package/js/ln-progress/README.md +413 -442
- package/js/ln-progress/ln-progress.js +1 -1
- package/js/ln-progress/src/ln-progress.js +98 -150
- package/js/ln-router/README.md +432 -0
- package/js/ln-router/ln-router.js +1 -0
- package/js/ln-router/src/ln-router.js +539 -0
- package/js/ln-search/README.md +158 -83
- package/js/ln-search/ln-search-dev.scss +8 -0
- package/js/ln-search/ln-search.js +1 -1
- package/js/ln-search/src/ln-search.js +12 -9
- package/js/ln-slug/README.md +110 -0
- package/js/ln-slug/ln-slug.js +1 -0
- package/js/ln-slug/src/ln-slug.js +74 -0
- package/js/ln-sort/README.md +170 -0
- package/js/ln-sort/ln-sort-dev.scss +8 -0
- package/js/ln-sort/ln-sort.js +1 -0
- package/js/ln-sort/src/ln-sort.js +178 -0
- package/js/ln-sortable/README.md +133 -95
- package/js/ln-sortable/ln-sortable.js +1 -1
- package/js/ln-stat/README.md +55 -0
- package/js/ln-stat/ln-stat.js +1 -0
- package/js/ln-stat/src/ln-stat.js +55 -0
- package/js/ln-table/README.md +285 -101
- package/js/ln-table/ln-table-dev.scss +17 -0
- package/js/ln-table/ln-table.js +1 -1
- package/js/ln-table/ln-table.scss +1 -1
- package/js/ln-table/src/ln-table.js +1493 -473
- package/js/ln-table-coordinator/README.md +73 -0
- package/js/ln-table-coordinator/ln-table-coordinator.js +1 -0
- package/js/ln-table-coordinator/src/ln-table-coordinator.js +160 -0
- package/js/ln-tabs/README.md +221 -137
- package/js/ln-tabs/ln-tabs-dev.scss +18 -0
- package/js/ln-tabs/ln-tabs.js +1 -1
- package/js/ln-tabs/src/ln-tabs.js +41 -24
- package/js/ln-time/README.md +14 -0
- package/js/ln-time/ln-time.js +1 -1
- package/js/ln-time/src/ln-time.js +223 -192
- package/js/ln-toast/README.md +178 -122
- package/js/ln-toast/ln-toast.js +1 -15
- package/js/ln-toast/src/ln-toast.js +232 -210
- package/js/ln-toast/template.html +16 -12
- package/js/ln-toggle/README.md +61 -2
- package/js/ln-toggle/ln-toggle-dev.scss +13 -0
- package/js/ln-toggle/ln-toggle.js +1 -1
- package/js/ln-toggle/src/ln-toggle.js +56 -43
- package/js/ln-tooltip/README.md +95 -58
- package/js/ln-tooltip/ln-tooltip-dev.scss +9 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -1
- package/js/ln-tooltip/src/ln-tooltip.js +45 -6
- package/js/ln-translations/README.md +118 -96
- package/js/ln-translations/ln-translations.js +1 -1
- package/js/ln-translations/src/ln-translations.js +10 -5
- package/js/ln-upload/README.md +85 -10
- package/js/ln-upload/ln-upload.js +1 -1
- package/js/ln-upload/src/ln-upload.js +26 -10
- package/js/ln-validate/README.md +72 -0
- package/js/ln-validate/ln-validate-dev.scss +15 -0
- package/js/ln-validate/ln-validate.js +1 -1
- package/js/ln-validate/src/ln-validate.js +64 -0
- package/package.json +61 -55
- package/scss/base/_global.scss +2 -2
- package/scss/base/_typography.scss +1 -2
- package/scss/components/_date.scss +1 -0
- package/scss/components/_dropdown.scss +0 -5
- package/scss/components/_editor.scss +5 -0
- package/scss/components/_filter.scss +26 -0
- package/scss/components/_form.scss +7 -0
- package/scss/components/_ln-table.scss +14 -5
- package/scss/components/_sort.scss +10 -0
- package/scss/components/_table.scss +68 -1
- package/scss/components/_toast.scss +68 -41
- package/scss/components/_toggle.scss +16 -0
- package/scss/config/_density.scss +205 -117
- package/scss/config/_icons.scss +10 -9
- package/scss/config/_tokens.scss +69 -47
- package/scss/config/mixins/_accordion.scss +2 -7
- package/scss/config/mixins/_alert.scss +4 -4
- package/scss/config/mixins/_breadcrumbs.scss +2 -2
- package/scss/config/mixins/_btn.scss +1 -1
- package/scss/config/mixins/_card.scss +89 -14
- package/scss/config/mixins/_collapsible.scss +3 -0
- package/scss/config/mixins/_colors.scss +1 -1
- package/scss/config/mixins/_diagnostics.scss +48 -0
- package/scss/config/mixins/_display.scss +1 -0
- package/scss/config/mixins/_dropdown.scss +3 -5
- package/scss/config/mixins/_editor.scss +124 -0
- package/scss/config/mixins/_empty-state.scss +7 -0
- package/scss/config/mixins/_filter.scss +47 -0
- package/scss/config/mixins/_footer.scss +1 -2
- package/scss/config/mixins/_form.scss +207 -21
- package/scss/config/mixins/_index.scss +4 -1
- package/scss/config/mixins/_ln-table.scss +51 -93
- package/scss/config/mixins/_modal.scss +149 -66
- package/scss/config/mixins/_nav.scss +3 -4
- package/scss/config/mixins/_popover.scss +39 -0
- package/scss/config/mixins/_sidebar.scss +1 -1
- package/scss/config/mixins/_sort.scss +42 -0
- package/scss/config/mixins/_table.scss +232 -38
- package/scss/config/mixins/_toast.scss +179 -152
- package/scss/config/mixins/_tooltip.scss +17 -7
- package/scss/config/mixins/_typography.scss +4 -2
- package/scss/ln-ashlar-dev.scss +39 -0
- package/scss/ln-ashlar.scss +8 -2
- package/js/ln-data-table/README.md +0 -110
- package/js/ln-data-table/ln-data-table.js +0 -1
- package/js/ln-data-table/ln-data-table.scss +0 -10
- package/js/ln-data-table/src/ln-data-table.js +0 -1103
- package/js/ln-icons/README.md +0 -88
- package/js/ln-icons/ln-icons.js +0 -1
- package/js/ln-table/ln-table-sort.js +0 -1
- package/js/ln-table/src/ln-table-sort.js +0 -168
- package/scss/components/_data-table.scss +0 -83
- package/scss/config/mixins/_data-table.scss +0 -346
package/js/ln-search/README.md
CHANGED
|
@@ -1,83 +1,158 @@
|
|
|
1
|
-
# ln-search
|
|
2
|
-
|
|
3
|
-
A zero-dependency, event-driven **Debounced Search Primitive** that intercepts text input, debounces keystrokes, and coordinates list-filtering states.
|
|
4
|
-
|
|
5
|
-
It acts as an intent announcer. It does not own data filtering directly; instead, it announces a normalized search query on the target element via a cancelable event, allowing tables, lists, or custom server integrations to intercept and handle the query or fall back to a built-in DOM-walking filter.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1
|
+
# ln-search
|
|
2
|
+
|
|
3
|
+
A zero-dependency, event-driven **Debounced Search Primitive** that intercepts text input, debounces keystrokes, and coordinates list-filtering states.
|
|
4
|
+
|
|
5
|
+
It acts as an intent announcer. It does not own data filtering directly; instead, it announces a normalized search query on the target element via a cancelable event, allowing tables, lists, or custom server integrations to intercept and handle the query or fall back to a built-in DOM-walking filter.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ✅ Canonical Markup — Copy This (REQUIRED)
|
|
10
|
+
|
|
11
|
+
**Hard rule — non-negotiable.** Every search or filter text input MUST use the
|
|
12
|
+
full `.search` chrome below: the leading magnifier icon **and** the clear
|
|
13
|
+
("x") button. This applies everywhere — toolbars, table headers, **and
|
|
14
|
+
popovers/dropdowns**. The previously-accepted "lightweight" bare
|
|
15
|
+
`<input data-ln-search>` (no icon, no clear button) is **no longer acceptable
|
|
16
|
+
for authoring**.
|
|
17
|
+
|
|
18
|
+
Copy this block. Change only the `placeholder`, the
|
|
19
|
+
`data-ln-search="<targetId>"`, and the `aria-label`. If a field needs a
|
|
20
|
+
page-specific tweak, add one extra class beside `search`
|
|
21
|
+
(`class="search my-extra"`) — never alter the structure, the icons, or the
|
|
22
|
+
clear button.
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<label class="search">
|
|
26
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-search"></use></svg>
|
|
27
|
+
<input type="search" placeholder="Search …" data-ln-search="<targetId>">
|
|
28
|
+
<button type="button" data-ln-search-clear aria-label="Clear search">
|
|
29
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
|
|
30
|
+
</button>
|
|
31
|
+
</label>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For deep targeting (e.g. checkbox lists inside a popover) add
|
|
35
|
+
`data-ln-search-items="<selector>"` to the **same `<input>`** — the chrome is
|
|
36
|
+
identical:
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<input type="search" placeholder="Search …" data-ln-search="<targetId>" data-ln-search-items="label">
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🧭 Philosophy & Architecture
|
|
45
|
+
|
|
46
|
+
1. **Decoupled Search Intent:** The component does not manipulate collections directly. It debounces keystrokes (150ms trailing-edge) and dispatches a cancelable `ln-search:change` custom event on the **target** element (not the input).
|
|
47
|
+
2. **The Default DOM-Walk Fallback:** If no listener calls `e.preventDefault()`, the component automatically runs a fast DOM-walk on the target's children, applying `data-ln-search-hide="true"` to non-matching elements.
|
|
48
|
+
3. **Reactive Form-Restore Hook:** During page reloads or back-button navigation, browsers auto-fill input fields before scripts initialize. `ln-search` detects this pre-filled state at construction and schedules an initial search dispatch via `queueMicrotask` to ensure all listener components have fully initialized first.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 📦 Minimal Blueprint
|
|
53
|
+
|
|
54
|
+
### Standard List Filtering (Zero-JS Fallback)
|
|
55
|
+
Bind the search input to a list `id` via `data-ln-search`.
|
|
56
|
+
|
|
57
|
+
> ⚠️ **Not an authoring pattern.** The component still *accepts* a bare input
|
|
58
|
+
> (backward-compat / JS-internal fallback), but you must always author the full
|
|
59
|
+
> `.search` chrome shown in [Canonical Markup](#-canonical-markup--copy-this-required).
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<input type="search" placeholder="Search..." data-ln-search="countries-list">
|
|
63
|
+
|
|
64
|
+
<ul id="countries-list">
|
|
65
|
+
<li>Argentina</li>
|
|
66
|
+
<li>Brazil</li>
|
|
67
|
+
<li>Canada</li>
|
|
68
|
+
</ul>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Premium Icon Wrapper with Clear Button
|
|
72
|
+
The canonical input-host form places `data-ln-search` directly on the `<input>`
|
|
73
|
+
and uses `class="search"` for the compact icon-group chrome (leading magnifier,
|
|
74
|
+
recessed fill, clear button):
|
|
75
|
+
```html
|
|
76
|
+
<label class="search">
|
|
77
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-search"></use></svg>
|
|
78
|
+
<input type="search" placeholder="Search countries..." data-ln-search="countries-list">
|
|
79
|
+
<button type="button" data-ln-search-clear aria-label="Clear search">
|
|
80
|
+
<svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
|
|
81
|
+
</button>
|
|
82
|
+
</label>
|
|
83
|
+
```
|
|
84
|
+
`class="search"` binds `@include search` (library `scss/components/_form.scss`).
|
|
85
|
+
Wrapper-host (`data-ln-search` on the `<label>`) is retained only so existing
|
|
86
|
+
markup keeps working — input-host is **required** for all new code.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🛠️ Declarative API Contract
|
|
91
|
+
|
|
92
|
+
### HTML Attributes
|
|
93
|
+
|
|
94
|
+
| Attribute | Elements | Description |
|
|
95
|
+
| :--- | :--- | :--- |
|
|
96
|
+
| `data-ln-search` | `<input>` (canonical); `<label>`/wrapper (backward-compat) | Component root and namespace. Value is the `id` of the target to filter. |
|
|
97
|
+
| `data-ln-search-items` | Same as root | Opt-in. Deep CSS selector (e.g. `tbody tr`) to query deep matching elements instead of direct children. |
|
|
98
|
+
| `data-ln-search-clear` | `<button>` | Identifies the clear button. Click clears input and triggers synchronous search reset. |
|
|
99
|
+
| `data-ln-search-hide` | Children of target | *State*. Automatically toggled on non-matching elements (`display: none !important`). |
|
|
100
|
+
|
|
101
|
+
### JavaScript API (`el.lnSearch`)
|
|
102
|
+
|
|
103
|
+
| Member | Type | Description |
|
|
104
|
+
| :--- | :--- | :--- |
|
|
105
|
+
| `targetId` | `string` | The ID of the target element. |
|
|
106
|
+
| `input` | `HTMLInputElement` | The resolved input element driving the search. |
|
|
107
|
+
| `destroy()` | `() => void` | Removes listeners, cancels debounces, and tears down the instance. |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## ⚡ DOM Events
|
|
112
|
+
|
|
113
|
+
### `ln-search:change`
|
|
114
|
+
Dispatched on the **target** element whenever the search term changes.
|
|
115
|
+
- **Cancelable**: Yes. Calling `e.preventDefault()` disables the default DOM-walk.
|
|
116
|
+
- **Payload (`detail`)**: `{ term: string, targetId: string }` (where `term` is lowercased and trimmed).
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## ⚠️ Common Pitfalls
|
|
121
|
+
|
|
122
|
+
- **Driving an `ln-table` search box:** placing `data-ln-search="<tableId>"` on the search input (or its label wrapper) is the sole supported pattern, but the consumer differs by mode. **SSR mode** consumes `ln-search:change` directly on the table. **Data-driven mode** requires the table to be wrapped in `[data-ln-table-coordinator]`, which receives `ln-search:change` and translates it into `ln-table:set-search`, joined with sort and column filters into the `ln-table:request-data` payload. Note: `data-ln-table-search` has been removed — use `data-ln-search="<tableId>"` instead.
|
|
123
|
+
- **Bypassing Debounce via Native Input Listeners:** Listening to native `input` events directly will bypass the 150ms debounce and execute expensive logic on every single keystroke. Always listen to `ln-search:change`.
|
|
124
|
+
- **Programmatic Value Mutations:** Assigning `input.value = "text"` programmatically does not trigger search. You must manually dispatch an `input` event:
|
|
125
|
+
```javascript
|
|
126
|
+
input.value = 'Argentina';
|
|
127
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🔧 Internals
|
|
133
|
+
|
|
134
|
+
Source: `js/ln-search/ln-search.js` (114 lines). One instance per `[data-ln-search]`, stored at `element.lnSearch`. `targetId` is captured once at construction; `document.getElementById(targetId)` is re-resolved on every search — cheaper than caching a node across AJAX re-renders.
|
|
135
|
+
|
|
136
|
+
### Debounce shape
|
|
137
|
+
|
|
138
|
+
Trailing-edge, 150ms, cancel-and-reschedule per keystroke — no leading-edge fire (would flicker partial filtering mid-type). The clear button bypasses the debounce entirely, calling `_search('')` synchronously.
|
|
139
|
+
|
|
140
|
+
### Dispatch flow
|
|
141
|
+
|
|
142
|
+
`_search` resolves the target, dispatches cancelable `ln-search:change` on it (not on the input — so consumers listen on the thing they care about), and only runs the default DOM-walk if `!evt.defaultPrevented`. There is no separate `:before-change` event — the cancelable shape of `:change` IS the before-hook.
|
|
143
|
+
|
|
144
|
+
### Form-restore detection
|
|
145
|
+
|
|
146
|
+
If the input already has a value at construction (browser autofill / back-button restore), the initial search is scheduled via `queueMicrotask` rather than fired synchronously — other components (`ln-table`) may not have attached their `ln-search:change` listener yet during the same `DOMContentLoaded` burst; the microtask waits for the current task to drain first.
|
|
147
|
+
|
|
148
|
+
### Clear-button resolution
|
|
149
|
+
|
|
150
|
+
Scope for locating `[data-ln-search-clear]` is `input.parentElement` when `data-ln-search` sits on the input (canonical), or `dom` itself in the legacy wrapper-host form. Null-guarded — a missing button just means no clear wiring.
|
|
151
|
+
|
|
152
|
+
### Cross-component coordination
|
|
153
|
+
|
|
154
|
+
No imports of other components. In SSR mode, `ln-table` consumes `ln-search:change` via `preventDefault()` and runs its own in-memory filter; in data-driven mode, `ln-table-coordinator` consumes it instead and translates it into `ln-table:set-search`. `ln-filter` runs independently on the same target through a parallel `data-ln-search-hide` / `data-ln-filter-hide` pair — CSS unions both, neither component is aware of the other.
|
|
155
|
+
|
|
156
|
+
### Destroy
|
|
157
|
+
|
|
158
|
+
Cancels the pending debounce timer, removes both listeners, removes the init marker. Does NOT walk the target to clear `data-ln-search-hide` — that is a rendering side effect, not the search component's own state.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(){"use strict";function
|
|
1
|
+
(function(){"use strict";if(typeof window<"u"){const t=console.warn;console.warn=function(...e){typeof e[0]=="string"&&(e[0].startsWith("[ln-")||e[0].startsWith("[lnCore"))&&!(document.documentElement.hasAttribute("data-ln-debug")||document.body&&document.body.hasAttribute("data-ln-debug"))||t.apply(console,e)}}const g={};function q(t,e){g[t]||(g[t]=document.querySelector('[data-ln-template="'+t+'"]'));const o=g[t];return o?o.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function I(t,e,o){const a=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:o||{}});return t.dispatchEvent(a),a}function A(t,e){if(!t||!e)return t;const o=t.querySelectorAll("[data-ln-field]");for(let n=0;n<o.length;n++){const r=o[n],l=r.getAttribute("data-ln-field");e[l]!=null&&(r.textContent=e[l])}const a=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<a.length;n++){const r=a[n],l=r.getAttribute("data-ln-attr").split(",");for(let s=0;s<l.length;s++){const u=l[s].trim().split(":");if(u.length!==2)continue;const d=u[0].trim(),f=u[1].trim();e[f]!=null&&r.setAttribute(d,e[f])}}const c=t.querySelectorAll("[data-ln-show]");for(let n=0;n<c.length;n++){const r=c[n],l=r.getAttribute("data-ln-show");l in e&&r.classList.toggle("hidden",!e[l])}const i=t.querySelectorAll("[data-ln-class]");for(let n=0;n<i.length;n++){const r=i[n],l=r.getAttribute("data-ln-class").split(",");for(let s=0;s<l.length;s++){const u=l[s].trim().split(":");if(u.length!==2)continue;const d=u[0].trim(),f=u[1].trim();f in e&&r.classList.toggle(d,!!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 o=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let a=0;a<o.length;a++)o[a].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)A(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let o=0;o<e.length;o++)e[o].textContent=""}})));function v(t,e){if(!t||!e)return t;const o=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;o.nextNode();){const i=o.currentNode;i.textContent.indexOf("{{")!==-1&&(i.textContent=i.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,r){return e[r]!==void 0?e[r]:""}))}const a=function(i,n){return e[n]!==void 0?e[n]:""},c=Array.from(t.querySelectorAll("*"));t.nodeType===1&&c.push(t);for(let i=0;i<c.length;i++){const n=c[i],r=n.attributes;for(let l=0;l<r.length;l++){const s=r[l];s.value.indexOf("{{")!==-1&&n.setAttribute(s.name,s.value.replace(/\{\{\s*(\w+)\s*\}\}/g,a))}}return t}function N(t,e,o,a,c,i){const n={};for(let l=0;l<t.children.length;l++){const s=t.children[l],u=s.getAttribute("data-ln-key");u&&(n[u]=s)}const r=document.createDocumentFragment();for(let l=0;l<e.length;l++){const s=e[l],u=String(a(s));let d=n[u];if(d)c(d,s,l);else{const f=q(o,i);if(!f||(v(f,s),d=f.firstElementChild,!d))continue;d.setAttribute("data-ln-key",u),c(d,s,l)}r.appendChild(d)}t.textContent="",t.appendChild(r)}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 w(t,e,o,a){if(t.nodeType!==1)return;const i=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(i));t.matches&&t.matches(i)&&n.push(t);for(const r of n)r[o]||(r[o]=new a(r))}function B(t,e,o,a,c={}){const i=c.extraAttributes||[],n=c.onAttributeChange||null,r=c.onInit||null;function l(s){const u=s||document.body;w(u,t,e,o),r&&r(u)}return S(function(){const s=new MutationObserver(function(d){for(let f=0;f<d.length;f++){const h=d[f];if(h.type==="childList"){for(let m=0;m<h.addedNodes.length;m++){const p=h.addedNodes[m];p.nodeType===1&&(w(p,t,e,o),r&&r(p))}for(let m=0;m<h.removedNodes.length;m++){const p=h.removedNodes[m];if(p.nodeType===1){const O=t.indexOf("[")!==-1||t.indexOf(".")!==-1||t.indexOf("#")!==-1?t:"["+t+"]",y=Array.from(p.querySelectorAll(O));p.matches&&p.matches(O)&&y.push(p);for(let C=0;C<y.length;C++){const k=y[C];if(!document.contains(k)){const _=k[e];_&&typeof _.destroy=="function"&&_.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(w(h.target,t,e,o),r&&r(h.target)))}});let u=[];if(t.indexOf("[")!==-1){const d=/\[([\w-]+)/g;let f;for(;(f=d.exec(t))!==null;)u.push(f[1])}else u.push(t);s.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:u.concat(i)})},a),window[e]=l,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){l(document.body)}):l(document.body),l}const E={};function M(t,e){E[t]=e}function j(t){return E[t]||{ingress:e=>e,egress:e=>e}}const L={};function T(t,e){if(!t||typeof e!="object")return;const o=t.toLowerCase().split("-")[0];L[o]=e}function U(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return L[e]||null}T("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=M,window.lnCore.getDataMapper=j,window.lnCore.registerLocaleFallback=T,window.lnCore.getLocaleFallback=U,window.lnCore.fillTemplate=v,window.lnCore.fill=A,window.lnCore.lnFill=D,window.lnCore.renderList=N);function x(t){return(t||"").replace(/^#/,"")}function b(t){const e=t===void 0?location.hash:t,o={},a=x(e);if(!a)return o;const c=a.split("&");for(let i=0;i<c.length;i++){const n=c[i];if(!n)continue;const r=n.indexOf(":"),l=r>-1?n.slice(0,r):n,s=r>-1?n.slice(r+1):"";if(l)try{o[l]=decodeURIComponent(s)}catch{o[l]=s}}return o}function W(t){if(!t)return null;const e=b();return t in e?e[t]:null}function F(t,e){if(!t)return;const o=b();e==null?delete o[t]:o[t]=String(e);const c=Object.keys(o).map(function(i){const n=o[i];return n===""?i:i+":"+encodeURIComponent(n)}).join("&");x(location.hash)!==c&&(location.hash=c)}function R(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=b,window.lnCore.hashGet=W,window.lnCore.hashSet=F,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-search",e="lnSearch",o="data-ln-search-hide";if(window[e]!==void 0)return;function c(i){this.dom=i,this.targetId=i.getAttribute(t);const n=i.tagName;this.input=n==="INPUT"||n==="TEXTAREA"?i:i.querySelector('[name="search"]')||i.querySelector('input[type="search"]')||i.querySelector('input[type="text"]'),this.itemsSelector=i.getAttribute("data-ln-search-items")||null;const r=i.getAttribute("data-ln-search-debounce");if(this.debounceTime=r!==null?parseInt(r,10):500,isNaN(this.debounceTime)&&(this.debounceTime=500),this._debounceTimer=null,this._attachHandler(),this.input&&this.input.value.trim()){const l=this;queueMicrotask(function(){l._search(l.input.value.trim().toLowerCase())})}return this}c.prototype._attachHandler=function(){if(!this.input)return;const i=this,n=this.dom===this.input?this.input.parentElement:this.dom;this._clearBtn=n?n.querySelector("[data-ln-search-clear]"):null,this._clearBtn&&(this._onClear=function(){i.input.value="",i._search(""),i.input.focus()},this._clearBtn.addEventListener("click",this._onClear)),this._onInput=function(){clearTimeout(i._debounceTimer),i._debounceTimer=setTimeout(function(){i._search(i.input.value.trim().toLowerCase())},i.debounceTime)},this.input.addEventListener("input",this._onInput)},c.prototype._search=function(i){const n=document.getElementById(this.targetId);if(!n||I(n,"ln-search:change",{term:i,targetId:this.targetId}).defaultPrevented)return;const l=this.itemsSelector?n.querySelectorAll(this.itemsSelector):n.children;for(let s=0;s<l.length;s++){const u=l[s];u.removeAttribute(o),i&&!u.textContent.replace(/\s+/g," ").toLowerCase().includes(i)&&u.setAttribute(o,"true")}},c.prototype.destroy=function(){this.dom[e]&&(clearTimeout(this._debounceTimer),this.input&&this._onInput&&this.input.removeEventListener("input",this._onInput),this._clearBtn&&this._onClear&&this._clearBtn.removeEventListener("click",this._onClear),delete this.dom[e])},B(t,e,c,"ln-search")})()})();
|
|
@@ -3,17 +3,14 @@ import { dispatchCancelable, registerComponent } from '../../ln-core';
|
|
|
3
3
|
(function () {
|
|
4
4
|
const DOM_SELECTOR = 'data-ln-search';
|
|
5
5
|
const DOM_ATTRIBUTE = 'lnSearch';
|
|
6
|
-
const INIT_ATTR = 'data-ln-search-initialized';
|
|
7
6
|
const HIDE_ATTR = 'data-ln-search-hide';
|
|
8
|
-
const DEBOUNCE_MS =
|
|
7
|
+
const DEBOUNCE_MS = 500;
|
|
9
8
|
|
|
10
9
|
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
11
10
|
|
|
12
11
|
// ─── Component ─────────────────────────────────────────────
|
|
13
12
|
|
|
14
13
|
function _component(dom) {
|
|
15
|
-
if (dom.hasAttribute(INIT_ATTR)) return this;
|
|
16
|
-
|
|
17
14
|
this.dom = dom;
|
|
18
15
|
this.targetId = dom.getAttribute(DOM_SELECTOR);
|
|
19
16
|
|
|
@@ -25,6 +22,11 @@ import { dispatchCancelable, registerComponent } from '../../ln-core';
|
|
|
25
22
|
|| dom.querySelector('input[type="text"]');
|
|
26
23
|
|
|
27
24
|
this.itemsSelector = dom.getAttribute('data-ln-search-items') || null;
|
|
25
|
+
|
|
26
|
+
const debounceVal = dom.getAttribute('data-ln-search-debounce');
|
|
27
|
+
this.debounceTime = debounceVal !== null ? parseInt(debounceVal, 10) : DEBOUNCE_MS;
|
|
28
|
+
if (isNaN(this.debounceTime)) this.debounceTime = DEBOUNCE_MS;
|
|
29
|
+
|
|
28
30
|
this._debounceTimer = null;
|
|
29
31
|
|
|
30
32
|
this._attachHandler();
|
|
@@ -38,7 +40,6 @@ import { dispatchCancelable, registerComponent } from '../../ln-core';
|
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
dom.setAttribute(INIT_ATTR, '');
|
|
42
43
|
return this;
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -48,8 +49,11 @@ import { dispatchCancelable, registerComponent } from '../../ln-core';
|
|
|
48
49
|
if (!this.input) return;
|
|
49
50
|
const self = this;
|
|
50
51
|
|
|
51
|
-
// Clear button
|
|
52
|
-
|
|
52
|
+
// Clear button: when data-ln-search is on the input itself, the clear button
|
|
53
|
+
// lives in the input's immediate wrapper (inputs have no element children).
|
|
54
|
+
// Otherwise (wrapper-host) scope is dom — unchanged behavior.
|
|
55
|
+
const scope = (this.dom === this.input) ? this.input.parentElement : this.dom;
|
|
56
|
+
this._clearBtn = scope ? scope.querySelector('[data-ln-search-clear]') : null;
|
|
53
57
|
if (this._clearBtn) {
|
|
54
58
|
this._onClear = function () {
|
|
55
59
|
self.input.value = '';
|
|
@@ -63,7 +67,7 @@ import { dispatchCancelable, registerComponent } from '../../ln-core';
|
|
|
63
67
|
clearTimeout(self._debounceTimer);
|
|
64
68
|
self._debounceTimer = setTimeout(function () {
|
|
65
69
|
self._search(self.input.value.trim().toLowerCase());
|
|
66
|
-
},
|
|
70
|
+
}, self.debounceTime);
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
this.input.addEventListener('input', this._onInput);
|
|
@@ -104,7 +108,6 @@ import { dispatchCancelable, registerComponent } from '../../ln-core';
|
|
|
104
108
|
if (this._clearBtn && this._onClear) {
|
|
105
109
|
this._clearBtn.removeEventListener('click', this._onClear);
|
|
106
110
|
}
|
|
107
|
-
this.dom.removeAttribute(INIT_ATTR);
|
|
108
111
|
delete this.dom[DOM_ATTRIBUTE];
|
|
109
112
|
};
|
|
110
113
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ln-slug
|
|
2
|
+
|
|
3
|
+
Auto-slug micro-component — mirrors a slugified version of a source field into a slug field while the slug field is pristine. Stops mirroring once the user types directly into the slug field; resumes if the slug field is cleared.
|
|
4
|
+
|
|
5
|
+
## Attributes
|
|
6
|
+
|
|
7
|
+
| Attribute | On | Description |
|
|
8
|
+
|-----------|-----|-------------|
|
|
9
|
+
| `data-ln-slug-from="fieldName"` | `<input>` | Activates slug behavior. Value is the `name` of the source field in the same `<form>`. |
|
|
10
|
+
|
|
11
|
+
## HTML Example
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<form>
|
|
15
|
+
<label>
|
|
16
|
+
Name
|
|
17
|
+
<input type="text" name="name">
|
|
18
|
+
</label>
|
|
19
|
+
<label>
|
|
20
|
+
Slug
|
|
21
|
+
<input type="text" name="slug" data-ln-slug-from="name">
|
|
22
|
+
</label>
|
|
23
|
+
</form>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Pristine Rules
|
|
27
|
+
|
|
28
|
+
The component tracks a `_pristine` flag per instance. Mirroring only runs while pristine.
|
|
29
|
+
|
|
30
|
+
| Situation | Result |
|
|
31
|
+
|-----------|--------|
|
|
32
|
+
| Slug field is empty at init | pristine = `true` (mirroring active) |
|
|
33
|
+
| Slug field is non-empty at init (server-rendered) | pristine = `false` (never overwrite) |
|
|
34
|
+
| User types non-empty value into slug field | pristine = `false` (mirroring stops) |
|
|
35
|
+
| User clears slug field | pristine = `true` (mirroring resumes) |
|
|
36
|
+
| `lnForm.fill` sets a non-empty slug (synthetic input) | pristine = `false` (loaded slug preserved) |
|
|
37
|
+
| Form reset clears slug to empty (synthetic input) | pristine = `true` (mirroring resumes) |
|
|
38
|
+
|
|
39
|
+
**Unified rule:** in the slug's own input handler — `if (_mirroring) return; _pristine = (value === '')`. Both trusted and untrusted inputs follow the same rule once the component's own mirror echo is guarded by the `_mirroring` flag.
|
|
40
|
+
|
|
41
|
+
## Loop Guard
|
|
42
|
+
|
|
43
|
+
When the source field triggers a mirror, the component:
|
|
44
|
+
1. Sets `_mirroring = true`
|
|
45
|
+
2. Sets `slug.value = slugify(source.value)`
|
|
46
|
+
3. Dispatches a synthetic `input` event (bubbles, so `ln-validate`/`ln-form` auto-submit react)
|
|
47
|
+
4. Sets `_mirroring = false`
|
|
48
|
+
|
|
49
|
+
The slug's own input handler early-returns while `_mirroring` is true, so the synthetic echo does not flip `_pristine` off.
|
|
50
|
+
|
|
51
|
+
## Slugify Rules
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
lowercase → replace non-alphanumeric runs with `-` → collapse multiple `-` → strip leading/trailing `-`
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example: `"Hello World!"` → `"hello-world"`, `" test--value "` → `"test-value"`.
|
|
58
|
+
|
|
59
|
+
## Limitations
|
|
60
|
+
|
|
61
|
+
- **ASCII-only (v1):** non-ASCII characters (Cyrillic, Macedonian, accented Latin, CJK) are stripped entirely — they fall into the `[^a-z0-9]+` replacement and become `-`. No transliteration in v1.
|
|
62
|
+
- **Source read at init:** `data-ln-slug-from` is read once at construction via `form.elements[name]`. If the source field is added dynamically after init, the component will not find it (warn + bail). Re-initialize the slug input after DOM changes.
|
|
63
|
+
- **Single source field:** if multiple fields share the same `name` (RadioNodeList), the component warns and bails. One text field → one slug.
|
|
64
|
+
|
|
65
|
+
## Events
|
|
66
|
+
|
|
67
|
+
The component dispatches a synthetic `new Event('input', { bubbles: true })` on the slug field after each mirror. This causes `ln-validate` to re-validate and `ln-form`'s auto-submit debounce to fire. No custom `ln-slug:*` events are emitted.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 🔧 Internals
|
|
72
|
+
|
|
73
|
+
Source: `js/ln-slug/ln-slug.js`. Single IIFE, `registerComponent`, per-element instance — same micro-component pattern as `ln-autoresize` (no Proxy/state machine, no templates).
|
|
74
|
+
|
|
75
|
+
### Instance state
|
|
76
|
+
|
|
77
|
+
| Property | Type | Description |
|
|
78
|
+
|----------|------|-------------|
|
|
79
|
+
| `dom` | `HTMLInputElement` | The slug input element |
|
|
80
|
+
| `source` | `HTMLInputElement` | The resolved source field (`form.elements[name]`) |
|
|
81
|
+
| `_pristine` | `boolean` | Whether the slug field is pristine (mirroring active) |
|
|
82
|
+
| `_mirroring` | `boolean` | Guard flag — true only while the component is dispatching its own synthetic echo |
|
|
83
|
+
|
|
84
|
+
### Mirror flow
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
source field 'input' event (isTrusted)
|
|
88
|
+
→ _onSource: if (!_pristine) return
|
|
89
|
+
→ _mirror():
|
|
90
|
+
_mirroring = true
|
|
91
|
+
slug.value = slugify(source.value)
|
|
92
|
+
slug.dispatchEvent(new Event('input', { bubbles: true }))
|
|
93
|
+
→ _onSlug fires → _mirroring true → early return (echo ignored)
|
|
94
|
+
→ ln-validate / ln-form auto-submit react (bubbled event)
|
|
95
|
+
_mirroring = false
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The `_mirroring` guard is what disambiguates the component's own echo from external programmatic fills — both are `isTrusted === false`, separated only by the flag.
|
|
99
|
+
|
|
100
|
+
### Initialization guards
|
|
101
|
+
|
|
102
|
+
The constructor warns and bails (returns `this` without attaching listeners) when:
|
|
103
|
+
- `dom.tagName !== 'INPUT'` — component applied to non-input
|
|
104
|
+
- `dom.form` is null — slug input not inside a `<form>`
|
|
105
|
+
- `form.elements[name]` is falsy — source field not found
|
|
106
|
+
- `typeof source.addEventListener !== 'function'` — source is a RadioNodeList (same-name group)
|
|
107
|
+
|
|
108
|
+
### Destroy
|
|
109
|
+
|
|
110
|
+
`destroy()` removes `_onSource` from the source field and `_onSlug` from the slug field, then deletes `dom[DOM_ATTRIBUTE]`. Guarded by `if (!this.dom[DOM_ATTRIBUTE]) return`, making it idempotent.
|
|
@@ -0,0 +1 @@
|
|
|
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 m={};function N(t,e){m[t]||(m[t]=document.querySelector('[data-ln-template="'+t+'"]'));const r=m[t];return r?r.content.cloneNode(!0):(console.warn("["+(e||"ln-core")+'] Template "'+t+'" not found'),null)}function S(t,e){if(!t||!e)return t;const r=t.querySelectorAll("[data-ln-field]");for(let n=0;n<r.length;n++){const i=r[n],o=i.getAttribute("data-ln-field");e[o]!=null&&(i.textContent=e[o])}const c=t.querySelectorAll("[data-ln-attr]");for(let n=0;n<c.length;n++){const i=c[n],o=i.getAttribute("data-ln-attr").split(",");for(let u=0;u<o.length;u++){const a=o[u].trim().split(":");if(a.length!==2)continue;const f=a[0].trim(),d=a[1].trim();e[d]!=null&&i.setAttribute(f,e[d])}}const l=t.querySelectorAll("[data-ln-show]");for(let n=0;n<l.length;n++){const i=l[n],o=i.getAttribute("data-ln-show");o in e&&i.classList.toggle("hidden",!e[o])}const s=t.querySelectorAll("[data-ln-class]");for(let n=0;n<s.length;n++){const i=s[n],o=i.getAttribute("data-ln-class").split(",");for(let u=0;u<o.length;u++){const a=o[u].trim().split(":");if(a.length!==2)continue;const f=a[0].trim(),d=a[1].trim();d in e&&i.classList.toggle(f,!!e[d])}}return t}function q(t,e){t.matches&&t.matches("[data-ln-form], [data-ln-fillable]")&&t.dispatchEvent(new CustomEvent("ln-fill",{detail:e??null,bubbles:!0}));const r=t.querySelectorAll("[data-ln-form], [data-ln-fillable]");for(let c=0;c<r.length;c++)r[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)S(t.target,t.detail);else{const e=t.target.querySelectorAll("[data-ln-field]");for(let r=0;r<e.length;r++)e[r].textContent=""}})));function v(t,e){if(!t||!e)return t;const r=document.createTreeWalker(t,NodeFilter.SHOW_TEXT);for(;r.nextNode();){const s=r.currentNode;s.textContent.indexOf("{{")!==-1&&(s.textContent=s.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(n,i){return e[i]!==void 0?e[i]:""}))}const c=function(s,n){return e[n]!==void 0?e[n]:""},l=Array.from(t.querySelectorAll("*"));t.nodeType===1&&l.push(t);for(let s=0;s<l.length;s++){const n=l[s],i=n.attributes;for(let o=0;o<i.length;o++){const u=i[o];u.value.indexOf("{{")!==-1&&n.setAttribute(u.name,u.value.replace(/\{\{\s*(\w+)\s*\}\}/g,c))}}return t}function D(t,e,r,c,l,s){const n={};for(let o=0;o<t.children.length;o++){const u=t.children[o],a=u.getAttribute("data-ln-key");a&&(n[a]=u)}const i=document.createDocumentFragment();for(let o=0;o<e.length;o++){const u=e[o],a=String(c(u));let f=n[a];if(f)l(f,u,o);else{const d=N(r,s);if(!d||(v(d,u),f=d.firstElementChild,!f))continue;f.setAttribute("data-ln-key",a),l(f,u,o)}i.appendChild(f)}t.textContent="",t.appendChild(i)}function A(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){A(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function w(t,e,r,c){if(t.nodeType!==1)return;const s=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",n=Array.from(t.querySelectorAll(s));t.matches&&t.matches(s)&&n.push(t);for(const i of n)i[r]||(i[r]=new c(i))}function M(t,e,r,c,l={}){const s=l.extraAttributes||[],n=l.onAttributeChange||null,i=l.onInit||null;function o(u){const a=u||document.body;w(a,t,e,r),i&&i(a)}return A(function(){const u=new MutationObserver(function(f){for(let d=0;d<f.length;d++){const h=f[d];if(h.type==="childList"){for(let g=0;g<h.addedNodes.length;g++){const p=h.addedNodes[g];p.nodeType===1&&(w(p,t,e,r),i&&i(p))}for(let g=0;g<h.removedNodes.length;g++){const p=h.removedNodes[g];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 L=T[e];L&&typeof L.destroy=="function"&&L.destroy()}}}}}else h.type==="attributes"&&(n&&h.target[e]?n(h.target,h.attributeName):(w(h.target,t,e,r),i&&i(h.target)))}});let a=[];if(t.indexOf("[")!==-1){const f=/\[([\w-]+)/g;let d;for(;(d=f.exec(t))!==null;)a.push(d[1])}else a.push(t);u.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:a.concat(s)})},c),window[e]=o,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){o(document.body)}):o(document.body),o}const _={};function j(t,e){_[t]=e}function W(t){return _[t]||{ingress:e=>e,egress:e=>e}}const x={};function E(t,e){if(!t||typeof e!="object")return;const r=t.toLowerCase().split("-")[0];x[r]=e}function F(t){if(!t)return null;const e=t.toLowerCase().split("-")[0];return x[e]||null}E("mk",{monthsLong:["јануари","февруари","март","април","мај","јуни","јули","август","септември","октомври","ноември","декември"],monthsShort:["јан","фев","мар","апр","мај","јун","јул","авг","септ","окт","ноем","дек"],daysLong:["недела","понеделник","вторник","среда","четврток","петок","сабота"],daysShort:["нед","пон","вт","ср","чет","пет","саб"]}),typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=j,window.lnCore.getDataMapper=W,window.lnCore.registerLocaleFallback=E,window.lnCore.getLocaleFallback=F,window.lnCore.fillTemplate=v,window.lnCore.fill=S,window.lnCore.lnFill=q,window.lnCore.renderList=D);function O(t){return(t||"").replace(/^#/,"")}function y(t){const e=t===void 0?location.hash:t,r={},c=O(e);if(!c)return r;const l=c.split("&");for(let s=0;s<l.length;s++){const n=l[s];if(!n)continue;const i=n.indexOf(":"),o=i>-1?n.slice(0,i):n,u=i>-1?n.slice(i+1):"";if(o)try{r[o]=decodeURIComponent(u)}catch{r[o]=u}}return r}function I(t){if(!t)return null;const e=y();return t in e?e[t]:null}function B(t,e){if(!t)return;const r=y();e==null?delete r[t]:r[t]=String(e);const l=Object.keys(r).map(function(s){const n=r[s];return n===""?s:s+":"+encodeURIComponent(n)}).join("&");O(location.hash)!==l&&(location.hash=l)}function R(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=I,window.lnCore.hashSet=B,window.lnCore.hashLinkClick=R),(function(){const t="data-ln-slug-from",e="lnSlug";if(window[e]!==void 0)return;function r(l){return String(l).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,"")}function c(l){if(l.tagName!=="INPUT")return console.warn("[ln-slug] Can only be applied to <input>, got:",l.tagName),this;const s=l.form;if(!s)return console.warn("[ln-slug] Slug input is not inside a <form>:",l),this;const n=l.getAttribute(t),i=s.elements[n];if(!i)return console.warn('[ln-slug] Source field "'+n+'" not found in form:',l),this;if(typeof i.addEventListener!="function")return console.warn('[ln-slug] Source field "'+n+'" is a RadioNodeList (same-name group) — single source field required:',l),this;this.dom=l,this.source=i,this._pristine=l.value==="",this._mirroring=!1;const o=this;return this._onSource=function(){o._pristine&&o._mirror()},this._onSlug=function(){o._mirroring||(o._pristine=o.dom.value==="")},i.addEventListener("input",this._onSource),l.addEventListener("input",this._onSlug),this}c.prototype._mirror=function(){this._mirroring=!0,this.dom.value=r(this.source.value),this.dom.dispatchEvent(new Event("input",{bubbles:!0})),this._mirroring=!1},c.prototype.destroy=function(){this.dom[e]&&(this.source.removeEventListener("input",this._onSource),this.dom.removeEventListener("input",this._onSlug),delete this.dom[e])},M(t,e,c,"ln-slug")})()})();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { registerComponent } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_SELECTOR = 'data-ln-slug-from';
|
|
5
|
+
const DOM_ATTRIBUTE = 'lnSlug';
|
|
6
|
+
|
|
7
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
8
|
+
|
|
9
|
+
function slugify(s) {
|
|
10
|
+
return String(s).toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/-+/g, '-')
|
|
13
|
+
.replace(/^-|-$/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function _component(dom) {
|
|
17
|
+
if (dom.tagName !== 'INPUT') {
|
|
18
|
+
console.warn('[ln-slug] Can only be applied to <input>, got:', dom.tagName);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
const form = dom.form;
|
|
22
|
+
if (!form) {
|
|
23
|
+
console.warn('[ln-slug] Slug input is not inside a <form>:', dom);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
const sourceName = dom.getAttribute(DOM_SELECTOR);
|
|
27
|
+
const source = form.elements[sourceName];
|
|
28
|
+
if (!source) {
|
|
29
|
+
console.warn('[ln-slug] Source field "' + sourceName + '" not found in form:', dom);
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
if (typeof source.addEventListener !== 'function') {
|
|
33
|
+
console.warn('[ln-slug] Source field "' + sourceName + '" is a RadioNodeList (same-name group) — single source field required:', dom);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.dom = dom;
|
|
38
|
+
this.source = source;
|
|
39
|
+
this._pristine = dom.value === '';
|
|
40
|
+
this._mirroring = false;
|
|
41
|
+
|
|
42
|
+
const self = this;
|
|
43
|
+
|
|
44
|
+
this._onSource = function () {
|
|
45
|
+
if (!self._pristine) return;
|
|
46
|
+
self._mirror();
|
|
47
|
+
};
|
|
48
|
+
this._onSlug = function () {
|
|
49
|
+
if (self._mirroring) return;
|
|
50
|
+
self._pristine = (self.dom.value === '');
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
source.addEventListener('input', this._onSource);
|
|
54
|
+
dom.addEventListener('input', this._onSlug);
|
|
55
|
+
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_component.prototype._mirror = function () {
|
|
60
|
+
this._mirroring = true;
|
|
61
|
+
this.dom.value = slugify(this.source.value);
|
|
62
|
+
this.dom.dispatchEvent(new Event('input', { bubbles: true }));
|
|
63
|
+
this._mirroring = false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
_component.prototype.destroy = function () {
|
|
67
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
68
|
+
this.source.removeEventListener('input', this._onSource);
|
|
69
|
+
this.dom.removeEventListener('input', this._onSlug);
|
|
70
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-slug');
|
|
74
|
+
})();
|