@livenetworks/ashlar 1.3.2
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 +177 -0
- package/js/COMPONENTS.md +1102 -0
- package/js/index.js +41 -0
- package/js/ln-accordion/README.md +137 -0
- package/js/ln-accordion/ln-accordion.js +1 -0
- package/js/ln-accordion/src/ln-accordion.js +41 -0
- package/js/ln-ajax/README.md +91 -0
- package/js/ln-ajax/ln-ajax.js +1 -0
- package/js/ln-ajax/src/ln-ajax.js +277 -0
- package/js/ln-api-connector/README.md +150 -0
- package/js/ln-api-connector/ln-api-connector.js +1 -0
- package/js/ln-api-connector/src/ln-api-connector.js +265 -0
- package/js/ln-autoresize/README.md +80 -0
- package/js/ln-autoresize/ln-autoresize.js +1 -0
- package/js/ln-autoresize/src/ln-autoresize.js +47 -0
- package/js/ln-autosave/README.md +92 -0
- package/js/ln-autosave/ln-autosave.js +1 -0
- package/js/ln-autosave/src/ln-autosave.js +147 -0
- package/js/ln-circular-progress/README.md +161 -0
- package/js/ln-circular-progress/ln-circular-progress.js +1 -0
- package/js/ln-circular-progress/src/ln-circular-progress.js +133 -0
- package/js/ln-confirm/README.md +86 -0
- package/js/ln-confirm/_ln-confirm.scss +13 -0
- package/js/ln-confirm/ln-confirm.js +1 -0
- package/js/ln-confirm/src/ln-confirm.js +131 -0
- package/js/ln-core/crypto.js +83 -0
- package/js/ln-core/helpers.js +411 -0
- package/js/ln-core/index.js +5 -0
- package/js/ln-core/persist.js +71 -0
- package/js/ln-core/positioning.js +207 -0
- package/js/ln-core/reactive.js +74 -0
- package/js/ln-couchdb-connector/README.md +156 -0
- package/js/ln-couchdb-connector/ln-couchdb-connector.js +1 -0
- package/js/ln-couchdb-connector/src/ln-couchdb-connector.js +348 -0
- package/js/ln-data-coordinator/README.md +165 -0
- package/js/ln-data-coordinator/ln-data-coordinator.js +1 -0
- package/js/ln-data-coordinator/src/ln-data-coordinator.js +249 -0
- package/js/ln-data-store/README.md +94 -0
- package/js/ln-data-store/ln-data-store.js +1 -0
- package/js/ln-data-store/src/ln-data-store.js +699 -0
- package/js/ln-data-table/README.md +110 -0
- package/js/ln-data-table/ln-data-table.js +1 -0
- package/js/ln-data-table/ln-data-table.scss +10 -0
- package/js/ln-data-table/src/ln-data-table.js +1103 -0
- package/js/ln-date/README.md +151 -0
- package/js/ln-date/ln-date.js +1 -0
- package/js/ln-date/src/ln-date.js +442 -0
- package/js/ln-dropdown/README.md +117 -0
- package/js/ln-dropdown/ln-dropdown.js +1 -0
- package/js/ln-dropdown/ln-dropdown.scss +15 -0
- package/js/ln-dropdown/src/ln-dropdown.js +174 -0
- package/js/ln-external-links/README.md +341 -0
- package/js/ln-external-links/ln-external-links.js +1 -0
- package/js/ln-external-links/src/ln-external-links.js +116 -0
- package/js/ln-filter/README.md +99 -0
- package/js/ln-filter/ln-filter.js +1 -0
- package/js/ln-filter/ln-filter.scss +7 -0
- package/js/ln-filter/src/ln-filter.js +404 -0
- package/js/ln-form/README.md +101 -0
- package/js/ln-form/ln-form.js +1 -0
- package/js/ln-form/src/ln-form.js +199 -0
- package/js/ln-http/README.md +89 -0
- package/js/ln-http/ln-http.js +1 -0
- package/js/ln-http/src/ln-http.js +219 -0
- package/js/ln-icons/README.md +88 -0
- package/js/ln-icons/ln-icons.js +1 -0
- package/js/ln-icons/src/ln-icons.js +169 -0
- package/js/ln-link/README.md +303 -0
- package/js/ln-link/ln-link.js +1 -0
- package/js/ln-link/src/ln-link.js +196 -0
- package/js/ln-modal/README.md +154 -0
- package/js/ln-modal/ln-modal.js +1 -0
- package/js/ln-modal/ln-modal.scss +11 -0
- package/js/ln-modal/src/ln-modal.js +201 -0
- package/js/ln-nav/README.md +70 -0
- package/js/ln-nav/ln-nav.js +1 -0
- package/js/ln-nav/src/ln-nav.js +177 -0
- package/js/ln-number/README.md +122 -0
- package/js/ln-number/ln-number.js +1 -0
- package/js/ln-number/src/ln-number.js +302 -0
- package/js/ln-popover/README.md +127 -0
- package/js/ln-popover/ln-popover.js +1 -0
- package/js/ln-popover/src/ln-popover.js +288 -0
- package/js/ln-progress/README.md +442 -0
- package/js/ln-progress/ln-progress.js +1 -0
- package/js/ln-progress/src/ln-progress.js +150 -0
- package/js/ln-search/README.md +83 -0
- package/js/ln-search/ln-search.js +1 -0
- package/js/ln-search/ln-search.scss +7 -0
- package/js/ln-search/src/ln-search.js +114 -0
- package/js/ln-sortable/README.md +95 -0
- package/js/ln-sortable/ln-sortable.js +1 -0
- package/js/ln-sortable/src/ln-sortable.js +203 -0
- package/js/ln-table/README.md +101 -0
- package/js/ln-table/ln-table-sort.js +1 -0
- package/js/ln-table/ln-table.js +1 -0
- package/js/ln-table/ln-table.scss +11 -0
- package/js/ln-table/src/ln-table-sort.js +168 -0
- package/js/ln-table/src/ln-table.js +473 -0
- package/js/ln-tabs/README.md +137 -0
- package/js/ln-tabs/ln-tabs.js +1 -0
- package/js/ln-tabs/src/ln-tabs.js +171 -0
- package/js/ln-time/README.md +81 -0
- package/js/ln-time/ln-time.js +1 -0
- package/js/ln-time/src/ln-time.js +192 -0
- package/js/ln-toast/README.md +122 -0
- package/js/ln-toast/ln-toast.js +15 -0
- package/js/ln-toast/src/ln-toast.js +210 -0
- package/js/ln-toast/template.html +14 -0
- package/js/ln-toggle/README.md +137 -0
- package/js/ln-toggle/ln-toggle.js +1 -0
- package/js/ln-toggle/src/ln-toggle.js +139 -0
- package/js/ln-tooltip/README.md +58 -0
- package/js/ln-tooltip/ln-tooltip.js +1 -0
- package/js/ln-tooltip/ln-tooltip.scss +9 -0
- package/js/ln-tooltip/src/ln-tooltip.js +169 -0
- package/js/ln-translations/README.md +96 -0
- package/js/ln-translations/ln-translations.js +1 -0
- package/js/ln-translations/src/ln-translations.js +275 -0
- package/js/ln-upload/README.md +180 -0
- package/js/ln-upload/ln-upload.js +1 -0
- package/js/ln-upload/ln-upload.scss +20 -0
- package/js/ln-upload/src/ln-upload.js +407 -0
- package/js/ln-validate/README.md +108 -0
- package/js/ln-validate/ln-validate.js +1 -0
- package/js/ln-validate/src/ln-validate.js +160 -0
- package/package.json +55 -0
- package/scss/base/_global.scss +83 -0
- package/scss/base/_reset.scss +17 -0
- package/scss/base/_typography.scss +125 -0
- package/scss/components/_accordion.scss +34 -0
- package/scss/components/_ajax.scss +15 -0
- package/scss/components/_alert.scss +5 -0
- package/scss/components/_app-shell.scss +15 -0
- package/scss/components/_avatar.scss +6 -0
- package/scss/components/_breadcrumbs.scss +33 -0
- package/scss/components/_button.scss +20 -0
- package/scss/components/_card.scss +10 -0
- package/scss/components/_chip.scss +5 -0
- package/scss/components/_circular-progress.scss +29 -0
- package/scss/components/_confirm.scss +5 -0
- package/scss/components/_data-table.scss +83 -0
- package/scss/components/_dropdown.scss +25 -0
- package/scss/components/_empty-state.scss +22 -0
- package/scss/components/_form.scss +100 -0
- package/scss/components/_layout.scss +8 -0
- package/scss/components/_link.scss +11 -0
- package/scss/components/_ln-table.scss +60 -0
- package/scss/components/_loader.scss +6 -0
- package/scss/components/_modal.scss +20 -0
- package/scss/components/_nav.scss +9 -0
- package/scss/components/_page-header.scss +10 -0
- package/scss/components/_popover.scss +10 -0
- package/scss/components/_progress.scss +17 -0
- package/scss/components/_prose.scss +5 -0
- package/scss/components/_scrollbar.scss +32 -0
- package/scss/components/_sections.scss +12 -0
- package/scss/components/_sidebar.scss +5 -0
- package/scss/components/_stat-card.scss +5 -0
- package/scss/components/_status-badge.scss +4 -0
- package/scss/components/_stepper.scss +5 -0
- package/scss/components/_table.scss +19 -0
- package/scss/components/_tabs.scss +21 -0
- package/scss/components/_timeline.scss +14 -0
- package/scss/components/_toast.scss +41 -0
- package/scss/components/_toggle.scss +81 -0
- package/scss/components/_tooltip.scss +18 -0
- package/scss/components/_translations.scss +111 -0
- package/scss/components/_upload.scss +51 -0
- package/scss/config/_breakpoints.scss +72 -0
- package/scss/config/_density.scss +117 -0
- package/scss/config/_icons.scss +37 -0
- package/scss/config/_mixins.scss +13 -0
- package/scss/config/_theme.scss +216 -0
- package/scss/config/_tokens.scss +419 -0
- package/scss/config/mixins/_accordion.scss +52 -0
- package/scss/config/mixins/_ajax.scss +39 -0
- package/scss/config/mixins/_alert.scss +82 -0
- package/scss/config/mixins/_app-shell.scss +312 -0
- package/scss/config/mixins/_avatar.scss +109 -0
- package/scss/config/mixins/_borders.scss +36 -0
- package/scss/config/mixins/_breadcrumbs.scss +72 -0
- package/scss/config/mixins/_breakpoints.scss +62 -0
- package/scss/config/mixins/_btn.scss +179 -0
- package/scss/config/mixins/_card.scss +338 -0
- package/scss/config/mixins/_chip.scss +66 -0
- package/scss/config/mixins/_circular-progress.scss +71 -0
- package/scss/config/mixins/_collapsible.scss +24 -0
- package/scss/config/mixins/_colors.scss +46 -0
- package/scss/config/mixins/_confirm.scss +31 -0
- package/scss/config/mixins/_data-table.scss +346 -0
- package/scss/config/mixins/_display.scss +32 -0
- package/scss/config/mixins/_dropdown.scss +143 -0
- package/scss/config/mixins/_empty-state.scss +30 -0
- package/scss/config/mixins/_focus.scss +55 -0
- package/scss/config/mixins/_footer.scss +42 -0
- package/scss/config/mixins/_form.scss +601 -0
- package/scss/config/mixins/_index.scss +58 -0
- package/scss/config/mixins/_interaction.scss +15 -0
- package/scss/config/mixins/_kbd.scss +22 -0
- package/scss/config/mixins/_layout.scss +117 -0
- package/scss/config/mixins/_link.scss +55 -0
- package/scss/config/mixins/_ln-table.scss +420 -0
- package/scss/config/mixins/_loader.scss +26 -0
- package/scss/config/mixins/_modal.scss +66 -0
- package/scss/config/mixins/_motion.scss +19 -0
- package/scss/config/mixins/_nav.scss +273 -0
- package/scss/config/mixins/_page-header.scss +69 -0
- package/scss/config/mixins/_popover.scss +25 -0
- package/scss/config/mixins/_position.scss +32 -0
- package/scss/config/mixins/_progress.scss +56 -0
- package/scss/config/mixins/_prose.scss +127 -0
- package/scss/config/mixins/_shadows.scss +8 -0
- package/scss/config/mixins/_sidebar.scss +95 -0
- package/scss/config/mixins/_sizing.scss +6 -0
- package/scss/config/mixins/_spacing.scss +19 -0
- package/scss/config/mixins/_stat-card.scss +68 -0
- package/scss/config/mixins/_status-badge.scss +83 -0
- package/scss/config/mixins/_stepper.scss +78 -0
- package/scss/config/mixins/_table.scss +215 -0
- package/scss/config/mixins/_tabs.scss +64 -0
- package/scss/config/mixins/_timeline.scss +69 -0
- package/scss/config/mixins/_toast.scss +148 -0
- package/scss/config/mixins/_tooltip.scss +111 -0
- package/scss/config/mixins/_transitions.scss +10 -0
- package/scss/config/mixins/_translations.scss +124 -0
- package/scss/config/mixins/_typography.scss +57 -0
- package/scss/config/mixins/_upload.scss +168 -0
- package/scss/ln-ashlar.scss +62 -0
- package/scss/tabler-icons.txt +5039 -0
- package/scss/utilities/_animations.scss +83 -0
- package/scss/utilities/_utilities.scss +49 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# ln-progress
|
|
2
|
+
|
|
3
|
+
> A passive linear-progress renderer. Author writes
|
|
4
|
+
> `data-ln-progress="42"` on a bar element; the component picks up the
|
|
5
|
+
> change via MutationObserver and writes the new `width` as a
|
|
6
|
+
> percentage. 145 lines of JS.
|
|
7
|
+
|
|
8
|
+
## Integration
|
|
9
|
+
|
|
10
|
+
### In-Bundle (Standard Integration)
|
|
11
|
+
To load `ln-progress` as part of the unified `ln-ashlar` bundle, include the main script:
|
|
12
|
+
```html
|
|
13
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Standalone (Zero-Dependency IIFE)
|
|
17
|
+
If you only need the progress bar component, load the compiled zero-dependency IIFE directly:
|
|
18
|
+
```html
|
|
19
|
+
<script src="js/ln-progress/ln-progress.js" defer></script>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Source Files & Development
|
|
23
|
+
- **Active Development Source**: [js/ln-progress/src/ln-progress.js](file:///c:/laragon/www/ln-ashlar/js/ln-progress/src/ln-progress.js) — The source of truth for component logic.
|
|
24
|
+
- **Compiled Standalone**: [js/ln-progress/ln-progress.js](file:///c:/laragon/www/ln-ashlar/js/ln-progress/ln-progress.js) — The compiled, ready-to-use standalone bundle.
|
|
25
|
+
|
|
26
|
+
## Philosophy
|
|
27
|
+
|
|
28
|
+
A linear progress indicator is mechanically a track `<div>` wrapping
|
|
29
|
+
one or more child bar `<div>`s whose `width` is a percentage of the
|
|
30
|
+
track. The maths is `width = (value / max) * 100%`. What `ln-progress`
|
|
31
|
+
adds is the reactive plumbing: when the width gets recomputed, what
|
|
32
|
+
triggers a re-render, where the maximum comes from when multiple bars
|
|
33
|
+
share a denominator, and what survives a `destroy()` call.
|
|
34
|
+
|
|
35
|
+
The contract is simple: **the attribute IS the state**. There is no
|
|
36
|
+
`element.setValue(42)` API and no imperative entry point. You set
|
|
37
|
+
`data-ln-progress="42"` on a bar; the MutationObserver sees the
|
|
38
|
+
change; `_render` runs; the bar redraws. State is visible in DOM
|
|
39
|
+
Inspector at all times, can be set declaratively from server templates,
|
|
40
|
+
and survives `outerHTML` round-trips.
|
|
41
|
+
|
|
42
|
+
The component supports stacked bars sharing one denominator: declare
|
|
43
|
+
`data-ln-progress-max` on the parent track and every child bar reads
|
|
44
|
+
that as its max. This is what makes "4 done + 2 in progress + 1
|
|
45
|
+
pending = 7 total" render as three bars filling exactly one track.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<div class="progress">
|
|
51
|
+
<div data-ln-progress="75" class="success"></div>
|
|
52
|
+
</div>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A green bar at 75% of the track, no JS on the consumer side. The
|
|
56
|
+
`class="progress"` on the wrapper marks it as a track (pure CSS hook,
|
|
57
|
+
no JS instance is created). The `data-ln-progress="75"` on the child
|
|
58
|
+
marks it as a bar (instance is created, `style.width = "75%"` is
|
|
59
|
+
written).
|
|
60
|
+
|
|
61
|
+
## Attributes
|
|
62
|
+
|
|
63
|
+
| Attribute | On | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `data-ln-progress="N"` | bar element | Current value of the bar. Number; can be negative or above max — the rendered percentage clamps to 0–100. Empty / non-numeric values render as 0%. |
|
|
66
|
+
| `data-ln-progress-max="N"` | bar element | Maximum value for this bar. Default `100`. Used as the denominator. Setting `max="0"` collapses to 0%. |
|
|
67
|
+
| `data-ln-progress-max="N"` | track element | Shared max for all bar children. Wins over each bar's own max. The track itself uses `class="progress"` for styling — no JS instance is created on it. |
|
|
68
|
+
| `class="success"` / `.warning` / `.error` | bar element | Colour variant. Plain CSS, no JS involvement. |
|
|
69
|
+
|
|
70
|
+
The track is marked with `class="progress"` (pure CSS hook). The bar
|
|
71
|
+
is marked with `data-ln-progress="N"` (JS instance).
|
|
72
|
+
|
|
73
|
+
The component writes `role="progressbar"`, `aria-valuemin="0"`,
|
|
74
|
+
`aria-valuemax`, and `aria-valuenow` on **each bar element** on every
|
|
75
|
+
render. `aria-valuenow` is clamped to `[0, max]` to match the rendered
|
|
76
|
+
bar (so an overshoot value of `150` against `max=100` reports as
|
|
77
|
+
`100`). Add `aria-label` on the bar yourself for screen-reader context
|
|
78
|
+
— the component does not invent a label.
|
|
79
|
+
|
|
80
|
+
## Events
|
|
81
|
+
|
|
82
|
+
One event, bubbles, not cancelable.
|
|
83
|
+
|
|
84
|
+
| Event | `detail` | Dispatched on | Dispatched when |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| `ln-progress:change` | `{ target: HTMLElement, value: number, max: number, percentage: number }` | the bar element | every `_render` call: at construction, on every value or max attribute change (bar's own value, bar's own max, or parent track's max if observed) |
|
|
87
|
+
|
|
88
|
+
`detail.value` is the raw `parseFloat` of the value attribute —
|
|
89
|
+
unclamped. `detail.max` is the resolved max (parent's first, then
|
|
90
|
+
bar's own, then 100). `detail.percentage` is the clamped 0–100
|
|
91
|
+
percentage written to `style.width`.
|
|
92
|
+
|
|
93
|
+
The event fires once per attribute write. Writing
|
|
94
|
+
`data-ln-progress="50"` then `"75"` synchronously fires the event
|
|
95
|
+
twice. The CSS transition smooths the visual to a single sweep, but
|
|
96
|
+
the event count is per-write.
|
|
97
|
+
|
|
98
|
+
There is no `:initialized` and no `:destroyed` event. The first
|
|
99
|
+
`:change` fires inside `_render` during construction and serves as
|
|
100
|
+
the init signal for any consumer listening at the document level.
|
|
101
|
+
|
|
102
|
+
## API
|
|
103
|
+
|
|
104
|
+
`window.lnProgress(root)` re-runs the init scan over `root`. The
|
|
105
|
+
document-level observer already covers AJAX inserts and
|
|
106
|
+
`data-ln-progress` attribute additions; call this manually only when
|
|
107
|
+
you inject markup into a Shadow DOM root or another document context
|
|
108
|
+
the observer cannot see.
|
|
109
|
+
|
|
110
|
+
`el.lnProgress` on a bar exposes:
|
|
111
|
+
|
|
112
|
+
| Property | Type | Description |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `dom` | `HTMLElement` | back-reference to the bar element |
|
|
115
|
+
| `_attrObserver` | `MutationObserver` | watches the bar's own `data-ln-progress` and `data-ln-progress-max` |
|
|
116
|
+
| `_parentObserver` | `MutationObserver \| null` | watches the parent track's `data-ln-progress-max` if the parent had that attribute at construction; `null` otherwise |
|
|
117
|
+
| `destroy()` | method | disconnects both observers, deletes `el.lnProgress`. Does NOT remove `data-ln-progress` itself, the colour class, or the inline `style.width`. |
|
|
118
|
+
|
|
119
|
+
There is no `setValue(n)`, `setMax(n)`, `update()`, or `redraw()`
|
|
120
|
+
method. The attribute is the API. A programmatic update is
|
|
121
|
+
`el.setAttribute('data-ln-progress', n)`, exactly the same as a
|
|
122
|
+
server-side render that writes the attribute into markup. To force a
|
|
123
|
+
re-render without changing the value, write the same value back —
|
|
124
|
+
the MutationObserver fires on any write, including identical ones.
|
|
125
|
+
|
|
126
|
+
## Max priority resolution
|
|
127
|
+
|
|
128
|
+
`_render` resolves the maximum in this order:
|
|
129
|
+
|
|
130
|
+
| Priority | Source | Truthy check |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| 1 | parent track's `data-ln-progress-max` parsed as float | parses to a finite number > 0 |
|
|
133
|
+
| 2 | bar's own `data-ln-progress-max` parsed as float | parses to a finite number > 0 |
|
|
134
|
+
| 3 | `100` (default) | always |
|
|
135
|
+
|
|
136
|
+
The chain is `parentMax || ownMax || 100`. Two consequences:
|
|
137
|
+
|
|
138
|
+
1. A parent declaring `data-ln-progress-max="0"` falls through to the
|
|
139
|
+
bar's own max, because `0` is falsy in JS. Same for non-numeric
|
|
140
|
+
values like `data-ln-progress-max="abc"` — `NaN` is falsy.
|
|
141
|
+
2. An own-max declaration is silently ignored when the parent declares
|
|
142
|
+
one. If a bar carries `data-ln-progress-max="50"` and the parent
|
|
143
|
+
carries `data-ln-progress-max="100"`, the parent wins — the bar
|
|
144
|
+
measures against 100. If you genuinely need per-bar maxes inside a
|
|
145
|
+
shared-max parent, do not declare the parent max; set each bar's
|
|
146
|
+
own max individually and accept that the bars no longer fill the
|
|
147
|
+
track in proportional fashion.
|
|
148
|
+
|
|
149
|
+
## Examples
|
|
150
|
+
|
|
151
|
+
### Minimal — fixed value
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<div class="progress">
|
|
155
|
+
<div data-ln-progress="75" class="success"></div>
|
|
156
|
+
</div>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The simplest case. Green bar at 75%, no JS on the consumer side.
|
|
160
|
+
|
|
161
|
+
### Reactive — change value at runtime
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<div id="upload-track" class="progress">
|
|
165
|
+
<div id="upload-bar" data-ln-progress="0" class="success"
|
|
166
|
+
aria-label="Upload progress"></div>
|
|
167
|
+
</div>
|
|
168
|
+
<button id="advance">Advance</button>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
let pct = 0;
|
|
173
|
+
document.getElementById('advance').addEventListener('click', function () {
|
|
174
|
+
pct = Math.min(100, pct + 10);
|
|
175
|
+
document.getElementById('upload-bar').setAttribute('data-ln-progress', pct);
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Each attribute write triggers a re-render and a `:change` event. The
|
|
180
|
+
CSS transition smoothly animates between widths. The component writes
|
|
181
|
+
`aria-valuenow` on the bar automatically — no manual sync needed.
|
|
182
|
+
|
|
183
|
+
### Custom max — non-percentage scale (per-bar)
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<!-- 7.5 GB of 10 GB — bar fills 75% of the track -->
|
|
187
|
+
<div class="progress">
|
|
188
|
+
<div data-ln-progress="7.5" data-ln-progress-max="10" class="warning"></div>
|
|
189
|
+
</div>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`max="10"` shifts the denominator so 7.5 maps to 75%. There is no
|
|
193
|
+
text label slot inside the bar; place descriptive text as a sibling
|
|
194
|
+
element if you need it.
|
|
195
|
+
|
|
196
|
+
### Stacked — multiple bars, one track, default max=100
|
|
197
|
+
|
|
198
|
+
```html
|
|
199
|
+
<!-- CPU 40% / RAM 30% / Disk 30% — sums to 100% of the track -->
|
|
200
|
+
<div class="progress">
|
|
201
|
+
<div data-ln-progress="40" class="success" aria-label="CPU usage"></div>
|
|
202
|
+
<div data-ln-progress="30" class="warning" aria-label="RAM usage"></div>
|
|
203
|
+
<div data-ln-progress="30" class="error" aria-label="Disk usage"></div>
|
|
204
|
+
</div>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Each bar uses the default max of 100. Three bars at 40 + 30 + 30 fill
|
|
208
|
+
the track exactly. If the values sum to less than 100, the remainder
|
|
209
|
+
of the track stays empty (the track's recessed background shows
|
|
210
|
+
through). If more than 100, each bar still renders at its clamped
|
|
211
|
+
0–100 width and the visuals overflow the track horizontally — the
|
|
212
|
+
SCSS `overflow: hidden` clips the overflow so the visual stays clean,
|
|
213
|
+
but the maths is wrong.
|
|
214
|
+
|
|
215
|
+
### Stacked — shared max on the parent track
|
|
216
|
+
|
|
217
|
+
```html
|
|
218
|
+
<!-- 4 done + 2 in progress + 1 pending = 7 total — fills the track exactly -->
|
|
219
|
+
<div class="progress" data-ln-progress-max="7">
|
|
220
|
+
<div data-ln-progress="4" class="success" aria-label="Done"></div>
|
|
221
|
+
<div data-ln-progress="2" class="warning" aria-label="In progress"></div>
|
|
222
|
+
<div data-ln-progress="1" class="error" aria-label="Pending"></div>
|
|
223
|
+
</div>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The track carries `data-ln-progress-max="7"`. Each child bar's
|
|
227
|
+
`_render` reads the parent's max first and uses `7` as the
|
|
228
|
+
denominator. Bar widths are 4/7 ≈ 57.1%, 2/7 ≈ 28.6%, 1/7 ≈ 14.3%,
|
|
229
|
+
summing to 100%.
|
|
230
|
+
|
|
231
|
+
Changing the parent's max at runtime
|
|
232
|
+
(`track.setAttribute('data-ln-progress-max', '10')`) re-renders each
|
|
233
|
+
child via the per-bar `_parentObserver` — but only for bars that
|
|
234
|
+
were constructed when the parent already had the attribute. See
|
|
235
|
+
"Common mistakes" item 4.
|
|
236
|
+
|
|
237
|
+
### Upload progress — driving from XHR
|
|
238
|
+
|
|
239
|
+
```html
|
|
240
|
+
<form id="upload-form">
|
|
241
|
+
<input type="file" name="file">
|
|
242
|
+
<div id="upload-track" class="progress">
|
|
243
|
+
<div id="upload-bar" data-ln-progress="0" class="success"
|
|
244
|
+
aria-label="Upload progress"></div>
|
|
245
|
+
</div>
|
|
246
|
+
<button type="submit">Upload</button>
|
|
247
|
+
</form>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
document.getElementById('upload-form').addEventListener('submit', function (e) {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
const bar = document.getElementById('upload-bar');
|
|
254
|
+
const file = e.target.querySelector('input[type=file]').files[0];
|
|
255
|
+
if (!file) return;
|
|
256
|
+
|
|
257
|
+
const xhr = new XMLHttpRequest();
|
|
258
|
+
const fd = new FormData(e.target);
|
|
259
|
+
|
|
260
|
+
xhr.upload.addEventListener('progress', function (ev) {
|
|
261
|
+
if (!ev.lengthComputable) return;
|
|
262
|
+
const pct = Math.round((ev.loaded / ev.total) * 100);
|
|
263
|
+
bar.setAttribute('data-ln-progress', pct);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
xhr.upload.addEventListener('load', function () {
|
|
267
|
+
bar.setAttribute('data-ln-progress', 100);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
xhr.open('POST', '/upload');
|
|
271
|
+
xhr.send(fd);
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
ARIA on the bar is managed automatically by `ln-progress` —
|
|
276
|
+
`aria-valuenow` updates on every attribute write. The `progress`
|
|
277
|
+
event fires often during a large upload — every 16ms or so on a fast
|
|
278
|
+
connection. Each call rewrites the attribute, the MutationObserver
|
|
279
|
+
fires, `_render` runs, and the `:change` event dispatches. If
|
|
280
|
+
profiling shows it as a hot spot, throttle the JS-side write to
|
|
281
|
+
every 100ms before calling `setAttribute`.
|
|
282
|
+
|
|
283
|
+
### Threshold-based colour — consumer code
|
|
284
|
+
|
|
285
|
+
The component has no automatic threshold colours. `class="success"`
|
|
286
|
+
/ `.warning` / `.error` are static — JS does not flip them based on
|
|
287
|
+
value. If you want red below 30% and green above 70%, that is
|
|
288
|
+
consumer code:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
function setProgressWithThreshold(bar, value) {
|
|
292
|
+
bar.setAttribute('data-ln-progress', value);
|
|
293
|
+
bar.classList.remove('success', 'warning', 'error');
|
|
294
|
+
if (value < 30) bar.classList.add('error');
|
|
295
|
+
else if (value < 70) bar.classList.add('warning');
|
|
296
|
+
else bar.classList.add('success');
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The colour-class swap is a `classList` write — no MutationObserver
|
|
301
|
+
fires for it (the component watches `data-ln-progress` and
|
|
302
|
+
`data-ln-progress-max`, not `class`). The new colour applies via plain
|
|
303
|
+
CSS rules in `scss/components/_progress.scss`.
|
|
304
|
+
|
|
305
|
+
## What `ln-progress` does NOT do
|
|
306
|
+
|
|
307
|
+
- **Does NOT watch the parent for late-added max.** `_listenParent`
|
|
308
|
+
runs once at construction and bails immediately if the parent does
|
|
309
|
+
not currently have `data-ln-progress-max`. If the parent gains the
|
|
310
|
+
max later, the bar will not re-render in response. See "Common
|
|
311
|
+
mistakes" item 4.
|
|
312
|
+
- **Does NOT clamp the input attribute.** It clamps the *computed
|
|
313
|
+
percentage* between 0 and 100 before writing the width. The
|
|
314
|
+
attribute can hold any number; reading
|
|
315
|
+
`el.getAttribute('data-ln-progress')` after writing `'150'` returns
|
|
316
|
+
`'150'`, not `'100'`.
|
|
317
|
+
- **Does NOT have an indeterminate / striped / pulsing mode.**
|
|
318
|
+
No animated-stripes overlay, no shimmer, no pulse. For an
|
|
319
|
+
indeterminate indicator on an unbounded operation, use
|
|
320
|
+
`@mixin loader` (`scss/config/mixins/_loader.scss`) — different
|
|
321
|
+
visual idiom, separate primitive.
|
|
322
|
+
- **Does NOT debounce or throttle rapid-fire value changes.** Every
|
|
323
|
+
attribute write triggers a synchronous `_render` call. For
|
|
324
|
+
high-frequency flows (60+ writes/sec), the consumer is responsible
|
|
325
|
+
for throttling.
|
|
326
|
+
|
|
327
|
+
## Common mistakes
|
|
328
|
+
|
|
329
|
+
### 1. Setting `style.width` manually inside the markup
|
|
330
|
+
|
|
331
|
+
```html
|
|
332
|
+
<!-- WRONG — JS overwrites style.width on construction -->
|
|
333
|
+
<div class="progress">
|
|
334
|
+
<div data-ln-progress="75" class="success" style="width: 50%"></div>
|
|
335
|
+
</div>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
The constructor's first action is `_render`, which writes
|
|
339
|
+
`this.dom.style.width = percentage + '%'`. Whatever inline width you
|
|
340
|
+
set in markup is overwritten on the first paint. If you want the bar
|
|
341
|
+
to render at a specific width, set the value via `data-ln-progress`,
|
|
342
|
+
not `style.width`.
|
|
343
|
+
|
|
344
|
+
### 2. Mixing percentage values with absolute max
|
|
345
|
+
|
|
346
|
+
```html
|
|
347
|
+
<!-- WRONG — value is "50" interpreted as a count, not 50% -->
|
|
348
|
+
<div class="progress">
|
|
349
|
+
<div data-ln-progress="50" data-ln-progress-max="500" class="success"></div>
|
|
350
|
+
</div>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
This renders 50/500 = 10% of the track, probably not what the author
|
|
354
|
+
meant. The component does not interpret the value as a percentage; it
|
|
355
|
+
treats it as a count against the max. Either write the absolute count
|
|
356
|
+
(`250` for "50% of 500") or drop the max and use the default of 100
|
|
357
|
+
with the percentage as the value.
|
|
358
|
+
|
|
359
|
+
### 3. Multiple bars sharing a parent without declaring parent-max
|
|
360
|
+
|
|
361
|
+
```html
|
|
362
|
+
<!-- WRONG — three bars at 4 / 2 / 1, default max=100 each → 7% of track filled -->
|
|
363
|
+
<div class="progress">
|
|
364
|
+
<div data-ln-progress="4" class="success"></div>
|
|
365
|
+
<div data-ln-progress="2" class="warning"></div>
|
|
366
|
+
<div data-ln-progress="1" class="error"></div>
|
|
367
|
+
</div>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
If the intent was "4 + 2 + 1 = 7 items, all visible proportionally,"
|
|
371
|
+
declare the parent's max:
|
|
372
|
+
|
|
373
|
+
```html
|
|
374
|
+
<!-- RIGHT -->
|
|
375
|
+
<div class="progress" data-ln-progress-max="7">
|
|
376
|
+
<div data-ln-progress="4" class="success"></div>
|
|
377
|
+
<div data-ln-progress="2" class="warning"></div>
|
|
378
|
+
<div data-ln-progress="1" class="error"></div>
|
|
379
|
+
</div>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Each bar reads parent max (`7`) as the denominator, and 4/7 + 2/7 +
|
|
383
|
+
1/7 fills the track exactly.
|
|
384
|
+
|
|
385
|
+
### 4. Setting `data-ln-progress-max` on the parent AFTER construction
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
// WRONG — bars constructed without parent max, _listenParent never attached
|
|
389
|
+
const track = document.getElementById('my-track');
|
|
390
|
+
track.setAttribute('data-ln-progress-max', '7');
|
|
391
|
+
// → bars do not re-render in response to this write
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
`_listenParent` runs once at construction and bails immediately if
|
|
395
|
+
the parent did not declare `data-ln-progress-max` at that moment.
|
|
396
|
+
The parent observer is conditional, not lazy. If the parent gains
|
|
397
|
+
the attribute later, no per-bar observer exists to catch it.
|
|
398
|
+
|
|
399
|
+
Three resolutions:
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
// Option A — declare parent max in markup so it exists at construction
|
|
403
|
+
<div class="progress" data-ln-progress-max="7"> ... </div>
|
|
404
|
+
|
|
405
|
+
// Option B — after setting the parent max, force each bar to re-render by
|
|
406
|
+
// rewriting one of its own observed attributes
|
|
407
|
+
track.setAttribute('data-ln-progress-max', '7');
|
|
408
|
+
track.querySelectorAll('[data-ln-progress]').forEach(function (bar) {
|
|
409
|
+
bar.setAttribute('data-ln-progress', bar.getAttribute('data-ln-progress'));
|
|
410
|
+
// → bar's own _attrObserver fires → _render re-resolves max from scratch
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Option C — destroy and re-init
|
|
414
|
+
track.querySelectorAll('[data-ln-progress]').forEach(function (bar) {
|
|
415
|
+
if (bar.lnProgress) bar.lnProgress.destroy();
|
|
416
|
+
});
|
|
417
|
+
window.lnProgress(track);
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
Option A is canonical. Option B is the right runtime workaround when
|
|
421
|
+
the markup was static-with-default-max and you need to switch to
|
|
422
|
+
shared-max dynamically.
|
|
423
|
+
|
|
424
|
+
### 5. Forgetting `class="progress"` on the track wrapper
|
|
425
|
+
|
|
426
|
+
Without the class, the wrapper has no styling — no recessed
|
|
427
|
+
background, no rounded edges, no overflow clip. The bar still
|
|
428
|
+
renders correctly but visually escapes its track.
|
|
429
|
+
|
|
430
|
+
## Related
|
|
431
|
+
|
|
432
|
+
- **`@mixin progress`** (`scss/config/mixins/_progress.scss`) — the
|
|
433
|
+
recipe. Defines the track height, the recessed background, the
|
|
434
|
+
rounded full radius, the overflow clip, and the inner-bar rules
|
|
435
|
+
(initial `width: 0`, `transition: width var(--transition-base)`
|
|
436
|
+
gated through `motion-safe`, rounded right edge on the last child).
|
|
437
|
+
- **`scss/components/_progress.scss`** — applies the base mixin to
|
|
438
|
+
`.progress` and applies the `.success` / `.warning` /
|
|
439
|
+
`.error` colour variants.
|
|
440
|
+
- **Architecture deep-dive:** [`docs/js/progress.md`](../../docs/js/progress.md)
|
|
441
|
+
for the construction flow, the three observers, the max-priority
|
|
442
|
+
resolution, and the late-parent-max edge case.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";function b(a,t,o){a.dispatchEvent(new CustomEvent(t,{bubbles:!0,detail:o||{}}))}function c(a,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){c(a,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}a()}const f={};function p(a,t){f[a]=t}function m(a){return f[a]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=p,window.lnCore.getDataMapper=m),(function(){const a="[data-ln-progress]",t="lnProgress";if(window[t]!==void 0)return;function o(e){u(e)}function u(e){const r=Array.from(e.querySelectorAll(a));for(const n of r)n[t]||(n[t]=new d(n));e.hasAttribute&&e.hasAttribute("data-ln-progress")&&!e[t]&&(e[t]=new d(e))}function d(e){return this.dom=e,this._attrObserver=null,this._parentObserver=null,l.call(this),g.call(this),v.call(this),this}d.prototype.destroy=function(){this.dom[t]&&(this._attrObserver&&this._attrObserver.disconnect(),this._parentObserver&&this._parentObserver.disconnect(),delete this.dom[t])};function h(){c(function(){new MutationObserver(function(r){for(const n of r)if(n.type==="childList")for(const s of n.addedNodes)s.nodeType===1&&u(s);else n.type==="attributes"&&u(n.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["data-ln-progress"]})},"ln-progress")}h();function g(){const e=this,r=new MutationObserver(function(n){for(const s of n)(s.attributeName==="data-ln-progress"||s.attributeName==="data-ln-progress-max")&&l.call(e)});r.observe(this.dom,{attributes:!0,attributeFilter:["data-ln-progress","data-ln-progress-max"]}),this._attrObserver=r}function v(){const e=this,r=this.dom.parentElement;if(!r||!r.hasAttribute("data-ln-progress-max"))return;const n=new MutationObserver(function(s){for(const i of s)i.attributeName==="data-ln-progress-max"&&l.call(e)});n.observe(r,{attributes:!0,attributeFilter:["data-ln-progress-max"]}),this._parentObserver=n}function l(){const e=parseFloat(this.dom.getAttribute("data-ln-progress"))||0,r=this.dom.parentElement,s=(r&&r.hasAttribute("data-ln-progress-max")?parseFloat(r.getAttribute("data-ln-progress-max")):null)||parseFloat(this.dom.getAttribute("data-ln-progress-max"))||100;let i=s>0?e/s*100:0;i<0&&(i=0),i>100&&(i=100),this.dom.style.width=i+"%";const w=Math.max(0,Math.min(e,s));this.dom.setAttribute("role","progressbar"),this.dom.setAttribute("aria-valuemin","0"),this.dom.setAttribute("aria-valuemax",String(s)),this.dom.setAttribute("aria-valuenow",String(w)),b(this.dom,"ln-progress:change",{target:this.dom,value:e,max:s,percentage:i})}window[t]=o,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){o(document.body)}):o(document.body)})()})();
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { dispatch, guardBody } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_SELECTOR = '[data-ln-progress]';
|
|
5
|
+
const DOM_ATTRIBUTE = 'lnProgress';
|
|
6
|
+
|
|
7
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
8
|
+
|
|
9
|
+
function constructor(domRoot) {
|
|
10
|
+
findElements(domRoot);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Local findElements — intentional divergence from ln-core helper:
|
|
14
|
+
// inlines selector + constructor (no ComponentClass parameter) and
|
|
15
|
+
// includes a domRoot self-match branch so the childList re-init
|
|
16
|
+
// path picks up the inserted root itself.
|
|
17
|
+
function findElements(domRoot) {
|
|
18
|
+
const items = Array.from(domRoot.querySelectorAll(DOM_SELECTOR));
|
|
19
|
+
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
if (!item[DOM_ATTRIBUTE]) {
|
|
22
|
+
item[DOM_ATTRIBUTE] = new _constructor(item);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (domRoot.hasAttribute && domRoot.hasAttribute('data-ln-progress') && !domRoot[DOM_ATTRIBUTE]) {
|
|
27
|
+
domRoot[DOM_ATTRIBUTE] = new _constructor(domRoot);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function _constructor(dom) {
|
|
32
|
+
this.dom = dom;
|
|
33
|
+
this._attrObserver = null;
|
|
34
|
+
this._parentObserver = null;
|
|
35
|
+
_render.call(this);
|
|
36
|
+
_listenValues.call(this);
|
|
37
|
+
_listenParent.call(this);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_constructor.prototype.destroy = function () {
|
|
42
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
43
|
+
if (this._attrObserver) {
|
|
44
|
+
this._attrObserver.disconnect();
|
|
45
|
+
}
|
|
46
|
+
if (this._parentObserver) {
|
|
47
|
+
this._parentObserver.disconnect();
|
|
48
|
+
}
|
|
49
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function _domObserver() {
|
|
53
|
+
guardBody(function () {
|
|
54
|
+
const observer = new MutationObserver(function (mutations) {
|
|
55
|
+
for (const mutation of mutations) {
|
|
56
|
+
if (mutation.type === "childList") {
|
|
57
|
+
for (const item of mutation.addedNodes) {
|
|
58
|
+
if (item.nodeType === 1) {
|
|
59
|
+
findElements(item);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} else if (mutation.type === 'attributes') {
|
|
63
|
+
findElements(mutation.target);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
observer.observe(document.body, {
|
|
69
|
+
childList: true,
|
|
70
|
+
subtree: true,
|
|
71
|
+
attributes: true,
|
|
72
|
+
attributeFilter: ['data-ln-progress']
|
|
73
|
+
});
|
|
74
|
+
}, 'ln-progress');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_domObserver();
|
|
78
|
+
|
|
79
|
+
function _listenValues() {
|
|
80
|
+
const self = this;
|
|
81
|
+
const observer = new MutationObserver(function (mutations) {
|
|
82
|
+
for (const mutation of mutations) {
|
|
83
|
+
if (mutation.attributeName === 'data-ln-progress' || mutation.attributeName === 'data-ln-progress-max') {
|
|
84
|
+
_render.call(self);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
observer.observe(this.dom, {
|
|
90
|
+
attributes: true,
|
|
91
|
+
attributeFilter: ['data-ln-progress', 'data-ln-progress-max']
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this._attrObserver = observer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function _listenParent() {
|
|
98
|
+
const self = this;
|
|
99
|
+
const parent = this.dom.parentElement;
|
|
100
|
+
if (!parent || !parent.hasAttribute('data-ln-progress-max')) return;
|
|
101
|
+
|
|
102
|
+
const observer = new MutationObserver(function (mutations) {
|
|
103
|
+
for (const mutation of mutations) {
|
|
104
|
+
if (mutation.attributeName === 'data-ln-progress-max') {
|
|
105
|
+
_render.call(self);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
observer.observe(parent, {
|
|
111
|
+
attributes: true,
|
|
112
|
+
attributeFilter: ['data-ln-progress-max']
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
this._parentObserver = observer;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function _render() {
|
|
119
|
+
const value = parseFloat(this.dom.getAttribute('data-ln-progress')) || 0;
|
|
120
|
+
const parent = this.dom.parentElement;
|
|
121
|
+
const parentMax = parent && parent.hasAttribute('data-ln-progress-max')
|
|
122
|
+
? parseFloat(parent.getAttribute('data-ln-progress-max'))
|
|
123
|
+
: null;
|
|
124
|
+
const max = parentMax || parseFloat(this.dom.getAttribute('data-ln-progress-max')) || 100;
|
|
125
|
+
let percentage = (max > 0) ? (value / max) * 100 : 0;
|
|
126
|
+
|
|
127
|
+
if (percentage < 0) percentage = 0;
|
|
128
|
+
if (percentage > 100) percentage = 100;
|
|
129
|
+
|
|
130
|
+
this.dom.style.width = percentage + '%';
|
|
131
|
+
|
|
132
|
+
const clampedValue = Math.max(0, Math.min(value, max));
|
|
133
|
+
this.dom.setAttribute('role', 'progressbar');
|
|
134
|
+
this.dom.setAttribute('aria-valuemin', '0');
|
|
135
|
+
this.dom.setAttribute('aria-valuemax', String(max));
|
|
136
|
+
this.dom.setAttribute('aria-valuenow', String(clampedValue));
|
|
137
|
+
|
|
138
|
+
dispatch(this.dom, 'ln-progress:change', { target: this.dom, value: value, max: max, percentage: percentage });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
window[DOM_ATTRIBUTE] = constructor;
|
|
142
|
+
|
|
143
|
+
if (document.readyState === 'loading') {
|
|
144
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
145
|
+
constructor(document.body);
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
constructor(document.body);
|
|
149
|
+
}
|
|
150
|
+
})();
|
|
@@ -0,0 +1,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
|
+
## 🧭 Philosophy & Architecture
|
|
10
|
+
|
|
11
|
+
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).
|
|
12
|
+
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.
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📦 Minimal Blueprint
|
|
18
|
+
|
|
19
|
+
### Standard List Filtering (Zero-JS Fallback)
|
|
20
|
+
Bind the search input to a list `id` via `data-ln-search`.
|
|
21
|
+
```html
|
|
22
|
+
<input type="search" placeholder="Search..." data-ln-search="countries-list">
|
|
23
|
+
|
|
24
|
+
<ul id="countries-list">
|
|
25
|
+
<li>Argentina</li>
|
|
26
|
+
<li>Brazil</li>
|
|
27
|
+
<li>Canada</li>
|
|
28
|
+
</ul>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Premium Icon Wrapper with Clear Button
|
|
32
|
+
When placed on a wrapper container, the component automatically resolves the nested input.
|
|
33
|
+
```html
|
|
34
|
+
<label data-ln-search="countries-list">
|
|
35
|
+
<svg class="ln-icon ln-icon--sm" aria-hidden="true"><use href="#ln-search"></use></svg>
|
|
36
|
+
<input type="search" placeholder="Search countries...">
|
|
37
|
+
<button type="button" data-ln-search-clear aria-label="Clear search">
|
|
38
|
+
<svg class="ln-icon ln-icon--sm" aria-hidden="true"><use href="#ln-x"></use></svg>
|
|
39
|
+
</button>
|
|
40
|
+
</label>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 🛠️ Declarative API Contract
|
|
46
|
+
|
|
47
|
+
### HTML Attributes
|
|
48
|
+
|
|
49
|
+
| Attribute | Elements | Description |
|
|
50
|
+
| :--- | :--- | :--- |
|
|
51
|
+
| `data-ln-search` | `<input>`, `<label>` | Component root and namespace. Value is the `id` of the target to filter. |
|
|
52
|
+
| `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. |
|
|
53
|
+
| `data-ln-search-clear` | `<button>` | Identifies the clear button. Click clears input and triggers synchronous search reset. |
|
|
54
|
+
| `data-ln-search-hide` | Children of target | *State*. Automatically toggled on non-matching elements (`display: none !important`). |
|
|
55
|
+
|
|
56
|
+
### JavaScript API (`el.lnSearch`)
|
|
57
|
+
|
|
58
|
+
| Member | Type | Description |
|
|
59
|
+
| :--- | :--- | :--- |
|
|
60
|
+
| `targetId` | `string` | The ID of the target element. |
|
|
61
|
+
| `input` | `HTMLInputElement` | The resolved input element driving the search. |
|
|
62
|
+
| `destroy()` | `() => void` | Removes listeners, cancels debounces, and tears down the instance. |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## ⚡ DOM Events
|
|
67
|
+
|
|
68
|
+
### `ln-search:change`
|
|
69
|
+
Dispatched on the **target** element whenever the search term changes.
|
|
70
|
+
- **Cancelable**: Yes. Calling `e.preventDefault()` disables the default DOM-walk.
|
|
71
|
+
- **Payload (`detail`)**: `{ term: string, targetId: string }` (where `term` is lowercased and trimmed).
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## ⚠️ Common Pitfalls
|
|
76
|
+
|
|
77
|
+
- **Confusing with `ln-data-table` Search:** `ln-data-table` manages its own internal search input via `data-ln-data-table-search` to join sorting and pagination into unified API request arrays. Do not place `ln-search` in front of `ln-data-table`.
|
|
78
|
+
- **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`.
|
|
79
|
+
- **Programmatic Value Mutations:** Assigning `input.value = "text"` programmatically does not trigger search. You must manually dispatch an `input` event:
|
|
80
|
+
```javascript
|
|
81
|
+
input.value = 'Argentina';
|
|
82
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
83
|
+
```
|