@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 @@
|
|
|
1
|
+
(function(){"use strict";function d(o,n,a){o.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:a||{}}))}function s(o,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){s(o,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}o()}const u={};function l(o,n){u[o]=n}function f(o){return u[o]||{ingress:n=>n,egress:n=>n}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=l,window.lnCore.getDataMapper=f),(function(){const o="lnExternalLinks";if(window[o]!==void 0)return;function n(e){return e.hostname&&e.hostname!==window.location.hostname}function a(e){if(e.getAttribute("data-ln-external-link")==="processed"||!n(e))return;e.target="_blank";const t=(e.rel||"").split(/\s+/).filter(Boolean);t.includes("noopener")||t.push("noopener"),t.includes("noreferrer")||t.push("noreferrer"),e.rel=t.join(" ");const i=document.createElement("span");i.className="sr-only",i.textContent="(opens in new tab)",e.appendChild(i),e.setAttribute("data-ln-external-link","processed"),d(e,"ln-external-links:processed",{link:e,href:e.href})}function c(e){e=e||document.body;for(const t of e.querySelectorAll("a, area"))a(t)}function p(){s(function(){document.body.addEventListener("click",function(e){const t=e.target.closest("a, area");t&&t.getAttribute("data-ln-external-link")==="processed"&&d(t,"ln-external-links:clicked",{link:t,href:t.href,text:t.textContent||t.title||""})})},"ln-external-links")}function b(){s(function(){new MutationObserver(function(t){for(const i of t){if(i.type==="childList"){for(const r of i.addedNodes)if(r.nodeType===1&&(r.matches&&(r.matches("a")||r.matches("area"))&&a(r),r.querySelectorAll))for(const m of r.querySelectorAll("a, area"))a(m)}if(i.type==="attributes"&&i.attributeName==="href"){const r=i.target;r.matches&&(r.matches("a")||r.matches("area"))&&a(r)}}}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})},"ln-external-links")}function h(){p(),b(),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){c()}):c()}window[o]={process:c},h()})()})();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { dispatch, guardBody } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function() {
|
|
4
|
+
const DOM_ATTRIBUTE = 'lnExternalLinks';
|
|
5
|
+
|
|
6
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
7
|
+
|
|
8
|
+
function _isExternalLink(link) {
|
|
9
|
+
return link.hostname && link.hostname !== window.location.hostname;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function _processLink(link) {
|
|
13
|
+
if (link.getAttribute('data-ln-external-link') === 'processed') return;
|
|
14
|
+
if (!_isExternalLink(link)) return;
|
|
15
|
+
|
|
16
|
+
link.target = '_blank';
|
|
17
|
+
const existing = (link.rel || '').split(/\s+/).filter(Boolean);
|
|
18
|
+
if (!existing.includes('noopener')) existing.push('noopener');
|
|
19
|
+
if (!existing.includes('noreferrer')) existing.push('noreferrer');
|
|
20
|
+
link.rel = existing.join(' ');
|
|
21
|
+
|
|
22
|
+
const hint = document.createElement('span');
|
|
23
|
+
hint.className = 'sr-only';
|
|
24
|
+
hint.textContent = '(opens in new tab)';
|
|
25
|
+
link.appendChild(hint);
|
|
26
|
+
|
|
27
|
+
link.setAttribute('data-ln-external-link', 'processed');
|
|
28
|
+
|
|
29
|
+
dispatch(link, 'ln-external-links:processed', {
|
|
30
|
+
link: link,
|
|
31
|
+
href: link.href
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _processLinks(container) {
|
|
36
|
+
container = container || document.body;
|
|
37
|
+
|
|
38
|
+
for (const link of container.querySelectorAll('a, area')) {
|
|
39
|
+
_processLink(link);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _setupClickTracking() {
|
|
44
|
+
guardBody(function() {
|
|
45
|
+
document.body.addEventListener('click', function(e) {
|
|
46
|
+
const link = e.target.closest('a, area');
|
|
47
|
+
if (!link) return;
|
|
48
|
+
|
|
49
|
+
if (link.getAttribute('data-ln-external-link') === 'processed') {
|
|
50
|
+
dispatch(link, 'ln-external-links:clicked', {
|
|
51
|
+
link: link,
|
|
52
|
+
href: link.href,
|
|
53
|
+
text: link.textContent || link.title || ''
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}, 'ln-external-links');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _domObserver() {
|
|
61
|
+
guardBody(function() {
|
|
62
|
+
const observer = new MutationObserver(function(mutations) {
|
|
63
|
+
for (const mutation of mutations) {
|
|
64
|
+
if (mutation.type === 'childList') {
|
|
65
|
+
for (const node of mutation.addedNodes) {
|
|
66
|
+
if (node.nodeType === 1) {
|
|
67
|
+
if (node.matches && (node.matches('a') || node.matches('area'))) {
|
|
68
|
+
_processLink(node);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (node.querySelectorAll) {
|
|
72
|
+
for (const link of node.querySelectorAll('a, area')) {
|
|
73
|
+
_processLink(link);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'href') {
|
|
81
|
+
const target = mutation.target;
|
|
82
|
+
if (target.matches && (target.matches('a') || target.matches('area'))) {
|
|
83
|
+
_processLink(target);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
observer.observe(document.body, {
|
|
90
|
+
childList: true,
|
|
91
|
+
subtree: true,
|
|
92
|
+
attributes: true,
|
|
93
|
+
attributeFilter: ['href']
|
|
94
|
+
});
|
|
95
|
+
}, 'ln-external-links');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _initialize() {
|
|
99
|
+
_setupClickTracking();
|
|
100
|
+
_domObserver();
|
|
101
|
+
|
|
102
|
+
if (document.readyState === 'loading') {
|
|
103
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
104
|
+
_processLinks();
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
_processLinks();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
window[DOM_ATTRIBUTE] = {
|
|
112
|
+
process: _processLinks
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
_initialize();
|
|
116
|
+
})();
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# ln-filter
|
|
2
|
+
|
|
3
|
+
A zero-dependency, event-driven **Generic List & Table Filter Primitive** that manages item visibility states through declarative checkbox controls.
|
|
4
|
+
|
|
5
|
+
It filters target elements either by comparing child dataset attributes (for custom cards/lists) or scanning table column cell contents (for plain HTML tables). It operates independently of and in harmony with `ln-search`, combining multiple filter criteria seamlessly.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🧭 Philosophy & Architecture
|
|
10
|
+
|
|
11
|
+
1. **Declarative State Model:** The component has no custom imperative state-change methods. Filter state is driven entirely by native checkboxes. External scripts update selections by writing `input.checked = true` and dispatching a standard bubbled `change` event.
|
|
12
|
+
2. **Sentinel Mutual Exclusion:** The `data-ln-filter-reset` ("All") checkbox is kept in sync automatically: checking any value checkbox unchecks the reset sentinel; checking the sentinel unchecks all value inputs; unchecking all value inputs re-checks the sentinel.
|
|
13
|
+
3. **Table Column & Auto-Population Mode:** By defining `data-ln-filter-col="N"`, the component filters plain HTML `<table>` rows by column cell text. When a `<template>` tag is nested inside, the component automatically populates value checkboxes from the column's unique values on page load.
|
|
14
|
+
4. **Local State Persistence:** Adding the `data-ln-persist` attribute saves active filter selections to `localStorage` under `lnf:{id}`, ensuring filter states survive page reloads and browser transitions.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 📦 Minimal Blueprint
|
|
19
|
+
|
|
20
|
+
### Generic List Attribute Filter (Zero-JS)
|
|
21
|
+
Bind the filter to a container `id`. Target items declare attributes matching the filter key.
|
|
22
|
+
```html
|
|
23
|
+
<nav data-ln-filter="employees-list">
|
|
24
|
+
<!-- Reset Sentinel -->
|
|
25
|
+
<label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-reset checked> All</label>
|
|
26
|
+
<!-- Values -->
|
|
27
|
+
<label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="design"> Design</label>
|
|
28
|
+
<label><input type="checkbox" data-ln-filter-key="category" data-ln-filter-value="dev"> Development</label>
|
|
29
|
+
</nav>
|
|
30
|
+
|
|
31
|
+
<ul id="employees-list">
|
|
32
|
+
<li data-category="design">Ana Petrova — UI Designer</li>
|
|
33
|
+
<li data-category="dev">Marko Nikolov — Developer</li>
|
|
34
|
+
</ul>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Table Column Filter with Auto-Population & Persistence
|
|
38
|
+
Auto-populates filter checkboxes from Column Index `2` (Department) and saves state to storage.
|
|
39
|
+
```html
|
|
40
|
+
<nav id="dept-filter" data-ln-filter="users-table" data-ln-filter-col="2" data-ln-persist>
|
|
41
|
+
<label><input type="checkbox" data-ln-filter-key="dept" data-ln-filter-reset checked> All Departments</label>
|
|
42
|
+
<!-- Checklist generated dynamically here -->
|
|
43
|
+
<template>
|
|
44
|
+
<label><input type="checkbox"> {{ text }}</label>
|
|
45
|
+
</template>
|
|
46
|
+
</nav>
|
|
47
|
+
|
|
48
|
+
<table id="users-table">
|
|
49
|
+
<thead>
|
|
50
|
+
<tr><th>ID</th><th>Name</th><th>Department</th></tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody>
|
|
53
|
+
<tr><td>1</td><td>Ana Petrova</td><td>Engineering</td></tr>
|
|
54
|
+
<tr><td>2</td><td>Marko Nikolov</td><td>Design</td></tr>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 🛠️ Declarative API Contract
|
|
62
|
+
|
|
63
|
+
### HTML Attributes
|
|
64
|
+
|
|
65
|
+
| Attribute | Elements | Description |
|
|
66
|
+
| :--- | :--- | :--- |
|
|
67
|
+
| `data-ln-filter` | Container root | Component root. Value is the `id` of the target container whose items are filtered. |
|
|
68
|
+
| `data-ln-filter-key` | `<input type="checkbox">` | The field name representing the target dataset attribute (e.g. `category` matches `data-category`). |
|
|
69
|
+
| `data-ln-filter-value` | `<input type="checkbox">` | The value to match. Active checkboxes show matching items; others are hidden. |
|
|
70
|
+
| `data-ln-filter-reset` | `<input type="checkbox">` | Marks the reset ("All") sentinel. |
|
|
71
|
+
| `data-ln-filter-col` | Container root | Opt-in. 0-based column index to filter plain `<table>` rows by column cell text. |
|
|
72
|
+
| `data-ln-persist` | Container root | Opt-in. Persists active checkbox selections in `localStorage` under `lnf:{id}`. |
|
|
73
|
+
| `data-ln-filter-hide` | Children of target | *State*. Automatically toggled on non-matching elements (`display: none !important`). |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ⚡ DOM Events
|
|
78
|
+
|
|
79
|
+
Events are dispatched on **both** the filter navigation container and the target element (dual dispatch).
|
|
80
|
+
|
|
81
|
+
### `ln-filter:changed`
|
|
82
|
+
Fired when any filter selection is modified.
|
|
83
|
+
- **Payload (`detail`)**: `{ key: string, values: string[] }` (where `values` lists active filter options).
|
|
84
|
+
|
|
85
|
+
### `ln-filter:reset`
|
|
86
|
+
Fired when the reset sentinel is activated.
|
|
87
|
+
- **Payload (`detail`)**: `{}`
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## ⚠️ Common Pitfalls
|
|
92
|
+
|
|
93
|
+
- **Driving State Programmatically Without Events:** Changing `input.checked = true` using JavaScript does not trigger browser `change` listeners. You must explicitly dispatch the event:
|
|
94
|
+
```javascript
|
|
95
|
+
input.checked = true;
|
|
96
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
97
|
+
```
|
|
98
|
+
- **Missing `id` on Persisted Filters:** The `data-ln-persist` storage key relies on the filter element's ID (e.g. `<nav id="my-filter" data-ln-persist>`). If the ID is missing, the component will fail to initialize persistence.
|
|
99
|
+
- **Filtering Coordinated Tables:** `ln-filter` is designed for static lists or plain native tables. Do not target virtualised components like `ln-table` or `ln-data-table`, which manage their own column filters internally.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";function S(r,n,l){r.dispatchEvent(new CustomEvent(n,{bubbles:!0,detail:l||{}}))}function D(r,n){if(!r||!n)return r;const l=document.createTreeWalker(r,NodeFilter.SHOW_TEXT);for(;l.nextNode();){const o=l.currentNode;o.textContent.indexOf("{{")!==-1&&(o.textContent=o.textContent.replace(/\{\{\s*(\w+)\s*\}\}/g,function(m,d){return n[d]!==void 0?n[d]:""}))}return r}function B(r,n){if(!document.body){document.addEventListener("DOMContentLoaded",function(){B(r,n)}),console.warn("["+n+'] Script loaded before <body> — add "defer" to your <script> tag');return}r()}function R(r,n,l,o){if(r.nodeType!==1)return;const d=n.indexOf("[")!==-1||n.indexOf(".")!==-1||n.indexOf("#")!==-1?n:"["+n+"]",T=Array.from(r.querySelectorAll(d));r.matches&&r.matches(d)&&T.push(r);for(const y of T)y[l]||(y[l]=new o(y))}function F(r,n,l,o,m={}){const d=m.extraAttributes||[],T=m.onAttributeChange||null,y=m.onInit||null;function A(b){const _=b||document.body;R(_,r,n,l),y&&y(_)}return B(function(){const b=new MutationObserver(function(I){for(let w=0;w<I.length;w++){const f=I[w];if(f.type==="childList")for(let t=0;t<f.addedNodes.length;t++){const e=f.addedNodes[t];e.nodeType===1&&(R(e,r,n,l),y&&y(e))}else f.type==="attributes"&&(T&&f.target[n]?T(f.target,f.attributeName):(R(f.target,r,n,l),y&&y(f.target)))}});let _=[];if(r.indexOf("[")!==-1){const I=/\[([\w-]+)/g;let w;for(;(w=I.exec(r))!==null;)_.push(w[1])}else _.push(r);b.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:_.concat(d)})},o),window[n]=A,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){A(document.body)}):A(document.body),A}const L={};function K(r,n){L[r]=n}function H(r){return L[r]||{ingress:n=>n,egress:n=>n}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=K,window.lnCore.getDataMapper=H);function j(r,n){let l=!1;return function(){l||(l=!0,queueMicrotask(function(){l=!1,r(),n&&n()}))}}const W="ln:";function J(){return location.pathname.replace(/\/+$/,"").toLowerCase()||"/"}function q(r,n){const l=n.getAttribute("data-ln-persist"),o=l!==null&&l!==""?l:n.id;return o?W+r+":"+J()+":"+o:(console.warn('[ln-persist] Element requires id or data-ln-persist="key"',n),null)}function U(r,n){const l=q(r,n);if(!l)return null;try{const o=localStorage.getItem(l);return o!==null?JSON.parse(o):null}catch{return null}}function M(r,n,l){const o=q(r,n);if(o)try{localStorage.setItem(o,JSON.stringify(l))}catch{}}(function(){const r="data-ln-filter",n="lnFilter",l="data-ln-filter-initialized",o="data-ln-filter-key",m="data-ln-filter-value",d="data-ln-filter-hide",T="data-ln-filter-reset",y="data-ln-filter-col",A=new WeakMap;if(window[n]!==void 0)return;function b(t){return t.hasAttribute(T)||t.getAttribute(m)===""}function _(t){let e=null;const i=[];for(let s=0;s<t.inputs.length;s++){const u=t.inputs[s];if(u.checked&&!b(u)){e===null&&(e=u.getAttribute(o));const c=u.getAttribute(m);c&&i.push(c)}}return{key:e,values:i}}function I(t,e){if(t.length!==e.length)return!0;for(let i=0;i<t.length;i++)if(t[i]!==e[i])return!0;return!1}function w(t){const e=t.dom,i=t.colIndex,s=e.querySelector("template");if(!s||i===null)return;const u=document.getElementById(t.targetId);if(!u)return;const c=u.tagName==="TABLE"?u:u.querySelector("table");if(!c||u.hasAttribute("data-ln-table"))return;const h={},a=[],C=c.tBodies;for(let g=0;g<C.length;g++){const p=C[g].rows;for(let v=0;v<p.length;v++){const O=p[v].cells[i],E=O?O.textContent.trim():"";E&&!h[E]&&(h[E]=!0,a.push(E))}}a.sort(function(g,p){return g.localeCompare(p)});const k=e.querySelector("["+o+"]"),x=k?k.getAttribute(o):e.getAttribute("data-ln-filter-key")||"col"+i;for(let g=0;g<a.length;g++){const p=s.content.cloneNode(!0),v=p.querySelector("input");v&&(v.setAttribute(o,x),v.setAttribute(m,a[g]),D(p,{text:a[g]}),e.appendChild(p))}}function f(t){if(t.hasAttribute(l))return this;this.dom=t,this.targetId=t.getAttribute(r);const e=t.getAttribute(y);this.colIndex=e!==null?parseInt(e,10):null,w(this),this.inputs=Array.from(t.querySelectorAll("["+o+"]")),this._filterKey=this.inputs.length>0?this.inputs[0].getAttribute(o):null,this._lastSnapshot=null;const i=this,s=j(function(){i._render()},function(){i._afterRender()});this._queueRender=s,this._attachHandlers();let u=!1;if(t.hasAttribute("data-ln-persist")){const c=U("filter",t);if(c&&c.key&&Array.isArray(c.values)&&c.values.length>0){for(let h=0;h<this.inputs.length;h++){const a=this.inputs[h];b(a)?a.checked=!1:a.getAttribute(o)===c.key&&c.values.indexOf(a.getAttribute(m))!==-1?a.checked=!0:a.checked=!1}s(),u=!0}}if(!u){for(let c=0;c<this.inputs.length;c++)if(this.inputs[c].checked&&!b(this.inputs[c])){s();break}}return t.setAttribute(l,""),this}f.prototype._attachHandlers=function(){const t=this;this.inputs.forEach(function(e){e[n+"Bound"]||(e[n+"Bound"]=!0,e._lnFilterChange=function(){if(b(e)){for(let i=0;i<t.inputs.length;i++)b(t.inputs[i])||(t.inputs[i].checked=!1);e.checked=!0,t._queueRender();return}if(e.checked)for(let i=0;i<t.inputs.length;i++)b(t.inputs[i])&&(t.inputs[i].checked=!1);else{let i=!1;for(let s=0;s<t.inputs.length;s++)if(!b(t.inputs[s])&&t.inputs[s].checked){i=!0;break}if(!i)for(let s=0;s<t.inputs.length;s++)b(t.inputs[s])&&(t.inputs[s].checked=!0)}t._queueRender()},e.addEventListener("change",e._lnFilterChange))})},f.prototype._render=function(){const t=this,e=_(this),i=e.key===null||e.values.length===0,s=[];for(let u=0;u<e.values.length;u++)s.push(e.values[u].toLowerCase());if(t.colIndex!==null)t._filterTableRows(e);else{const u=document.getElementById(t.targetId);if(!u)return;const c=u.children;for(let h=0;h<c.length;h++){const a=c[h];if(i){a.removeAttribute(d);continue}const C=a.getAttribute("data-"+e.key);a.removeAttribute(d),C!==null&&s.indexOf(C.toLowerCase())===-1&&a.setAttribute(d,"true")}}},f.prototype._afterRender=function(){const t=_(this),e=this._lastSnapshot;if(!e||e.key!==t.key||I(e.values,t.values)){this._dispatchOnBoth("ln-filter:changed",{key:t.key,values:t.values.slice()});const s=e&&e.values.length>0,u=t.values.length===0;s&&u&&this._dispatchOnBoth("ln-filter:reset",{}),this._lastSnapshot={key:t.key,values:t.values.slice()}}this.dom.hasAttribute("data-ln-persist")&&(t.key&&t.values.length>0?M("filter",this.dom,{key:t.key,values:t.values.slice()}):M("filter",this.dom,null))},f.prototype._dispatchOnBoth=function(t,e){S(this.dom,t,e);const i=document.getElementById(this.targetId);i&&i!==this.dom&&S(i,t,e)},f.prototype._filterTableRows=function(t){const e=document.getElementById(this.targetId);if(!e)return;const i=e.tagName==="TABLE"?e:e.querySelector("table");if(!i||e.hasAttribute("data-ln-table"))return;const s=t.key||this._filterKey,u=t.values;A.has(i)||A.set(i,{});const c=A.get(i);if(s&&u.length>0){const k=[];for(let x=0;x<u.length;x++)k.push(u[x].toLowerCase());c[s]={col:this.colIndex,values:k}}else s&&delete c[s];const h=Object.keys(c),a=h.length>0,C=i.tBodies;for(let k=0;k<C.length;k++){const x=C[k].rows;for(let g=0;g<x.length;g++){const p=x[g];if(!a){p.removeAttribute(d);continue}let v=!0;for(let O=0;O<h.length;O++){const E=c[h[O]],N=p.cells[E.col],V=N?N.textContent.trim().toLowerCase():"";if(E.values.indexOf(V)===-1){v=!1;break}}v?p.removeAttribute(d):p.setAttribute(d,"true")}}},f.prototype.destroy=function(){if(this.dom[n]){if(this.colIndex!==null){const t=document.getElementById(this.targetId);if(t){const e=t.tagName==="TABLE"?t:t.querySelector("table");if(e&&A.has(e)){const i=A.get(e),s=this._filterKey;s&&i[s]&&delete i[s],Object.keys(i).length===0&&A.delete(e)}}}this.inputs.forEach(function(t){t._lnFilterChange&&(t.removeEventListener("change",t._lnFilterChange),delete t._lnFilterChange),delete t[n+"Bound"]}),this.dom.removeAttribute(l),delete this.dom[n]}},F(r,n,f,"ln-filter")})()})();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// ==========================================================================
|
|
2
|
+
// ln-filter — Attribute-based filter component
|
|
3
|
+
// ==========================================================================
|
|
4
|
+
|
|
5
|
+
[data-ln-filter-hide="true"] {
|
|
6
|
+
display: none !important;
|
|
7
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { dispatch, fillTemplate, registerComponent } from '../../ln-core';
|
|
2
|
+
import { createBatcher } from '../../ln-core';
|
|
3
|
+
import { persistGet, persistSet } from '../../ln-core';
|
|
4
|
+
|
|
5
|
+
(function () {
|
|
6
|
+
const DOM_SELECTOR = 'data-ln-filter';
|
|
7
|
+
const DOM_ATTRIBUTE = 'lnFilter';
|
|
8
|
+
const INIT_ATTR = 'data-ln-filter-initialized';
|
|
9
|
+
const KEY_ATTR = 'data-ln-filter-key';
|
|
10
|
+
const VALUE_ATTR = 'data-ln-filter-value';
|
|
11
|
+
const HIDE_ATTR = 'data-ln-filter-hide';
|
|
12
|
+
const RESET_ATTR = 'data-ln-filter-reset';
|
|
13
|
+
const COL_ATTR = 'data-ln-filter-col';
|
|
14
|
+
|
|
15
|
+
// Shared column filter state per table (AND across columns, OR within column)
|
|
16
|
+
const _tableFilters = new WeakMap();
|
|
17
|
+
|
|
18
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
19
|
+
|
|
20
|
+
function _isReset(input) {
|
|
21
|
+
return input.hasAttribute(RESET_ATTR) || input.getAttribute(VALUE_ATTR) === '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _deriveActive(self) {
|
|
25
|
+
let key = null;
|
|
26
|
+
const values = [];
|
|
27
|
+
for (let i = 0; i < self.inputs.length; i++) {
|
|
28
|
+
const input = self.inputs[i];
|
|
29
|
+
if (input.checked && !_isReset(input)) {
|
|
30
|
+
if (key === null) key = input.getAttribute(KEY_ATTR);
|
|
31
|
+
const v = input.getAttribute(VALUE_ATTR);
|
|
32
|
+
if (v) values.push(v);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { key: key, values: values };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function _arraysDiffer(a, b) {
|
|
39
|
+
if (a.length !== b.length) return true;
|
|
40
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Auto-populate from table column ───────────────────────
|
|
45
|
+
|
|
46
|
+
function _populateFromColumn(instance) {
|
|
47
|
+
const dom = instance.dom;
|
|
48
|
+
const colIndex = instance.colIndex;
|
|
49
|
+
const tmpl = dom.querySelector('template');
|
|
50
|
+
if (!tmpl || colIndex === null) return;
|
|
51
|
+
|
|
52
|
+
const target = document.getElementById(instance.targetId);
|
|
53
|
+
if (!target) return;
|
|
54
|
+
|
|
55
|
+
const table = target.tagName === 'TABLE' ? target : target.querySelector('table');
|
|
56
|
+
if (!table || target.hasAttribute('data-ln-table')) return;
|
|
57
|
+
|
|
58
|
+
// Collect unique values from column
|
|
59
|
+
const seen = {};
|
|
60
|
+
const values = [];
|
|
61
|
+
const bodies = table.tBodies;
|
|
62
|
+
for (let b = 0; b < bodies.length; b++) {
|
|
63
|
+
const rows = bodies[b].rows;
|
|
64
|
+
for (let r = 0; r < rows.length; r++) {
|
|
65
|
+
const cell = rows[r].cells[colIndex];
|
|
66
|
+
const text = cell ? cell.textContent.trim() : '';
|
|
67
|
+
if (text && !seen[text]) {
|
|
68
|
+
seen[text] = true;
|
|
69
|
+
values.push(text);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
values.sort(function (a, b) {
|
|
74
|
+
return a.localeCompare(b);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Determine the filter key from existing inputs or dom attribute
|
|
78
|
+
const existingInput = dom.querySelector('[' + KEY_ATTR + ']');
|
|
79
|
+
const filterKey = existingInput
|
|
80
|
+
? existingInput.getAttribute(KEY_ATTR)
|
|
81
|
+
: (dom.getAttribute('data-ln-filter-key') || 'col' + colIndex);
|
|
82
|
+
|
|
83
|
+
// Clone template for each value
|
|
84
|
+
for (let i = 0; i < values.length; i++) {
|
|
85
|
+
const clone = tmpl.content.cloneNode(true);
|
|
86
|
+
const input = clone.querySelector('input');
|
|
87
|
+
if (!input) continue;
|
|
88
|
+
input.setAttribute(KEY_ATTR, filterKey);
|
|
89
|
+
input.setAttribute(VALUE_ATTR, values[i]);
|
|
90
|
+
fillTemplate(clone, { text: values[i] });
|
|
91
|
+
dom.appendChild(clone);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Component ─────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function _component(dom) {
|
|
98
|
+
if (dom.hasAttribute(INIT_ATTR)) return this;
|
|
99
|
+
|
|
100
|
+
this.dom = dom;
|
|
101
|
+
this.targetId = dom.getAttribute(DOM_SELECTOR);
|
|
102
|
+
|
|
103
|
+
// Column index for table filtering (null = not a table column filter)
|
|
104
|
+
const colAttr = dom.getAttribute(COL_ATTR);
|
|
105
|
+
this.colIndex = colAttr !== null ? parseInt(colAttr, 10) : null;
|
|
106
|
+
|
|
107
|
+
// Auto-populate from table column if template present
|
|
108
|
+
_populateFromColumn(this);
|
|
109
|
+
|
|
110
|
+
// Collect inputs AFTER auto-populate (new inputs may have been added)
|
|
111
|
+
this.inputs = Array.from(dom.querySelectorAll('[' + KEY_ATTR + ']'));
|
|
112
|
+
this._filterKey = this.inputs.length > 0 ? this.inputs[0].getAttribute(KEY_ATTR) : null;
|
|
113
|
+
|
|
114
|
+
// Event-diff cache — null means never dispatched yet
|
|
115
|
+
this._lastSnapshot = null;
|
|
116
|
+
|
|
117
|
+
const self = this;
|
|
118
|
+
|
|
119
|
+
const queueRender = createBatcher(
|
|
120
|
+
function () { self._render(); },
|
|
121
|
+
function () { self._afterRender(); }
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Stash for use in change handler
|
|
125
|
+
this._queueRender = queueRender;
|
|
126
|
+
|
|
127
|
+
this._attachHandlers();
|
|
128
|
+
|
|
129
|
+
// ─── Restore persisted filter ─────────────────────────────
|
|
130
|
+
let _persistRestored = false;
|
|
131
|
+
if (dom.hasAttribute('data-ln-persist')) {
|
|
132
|
+
const saved = persistGet('filter', dom);
|
|
133
|
+
if (saved && saved.key && Array.isArray(saved.values) && saved.values.length > 0) {
|
|
134
|
+
// Write input.checked on matching inputs
|
|
135
|
+
for (let i = 0; i < this.inputs.length; i++) {
|
|
136
|
+
const input = this.inputs[i];
|
|
137
|
+
if (_isReset(input)) {
|
|
138
|
+
input.checked = false;
|
|
139
|
+
} else if (input.getAttribute(KEY_ATTR) === saved.key &&
|
|
140
|
+
saved.values.indexOf(input.getAttribute(VALUE_ATTR)) !== -1) {
|
|
141
|
+
input.checked = true;
|
|
142
|
+
} else {
|
|
143
|
+
input.checked = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
queueRender();
|
|
147
|
+
_persistRestored = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!_persistRestored) {
|
|
152
|
+
// DOM is already canonical — only schedule render if anything is pre-checked
|
|
153
|
+
// so visibility is applied and the initial ln-filter:changed fires (via
|
|
154
|
+
// null-snapshot diff in _afterRender).
|
|
155
|
+
for (let i = 0; i < this.inputs.length; i++) {
|
|
156
|
+
if (this.inputs[i].checked && !_isReset(this.inputs[i])) {
|
|
157
|
+
queueRender();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
dom.setAttribute(INIT_ATTR, '');
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Handlers ──────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
_component.prototype._attachHandlers = function () {
|
|
170
|
+
const self = this;
|
|
171
|
+
|
|
172
|
+
this.inputs.forEach(function (input) {
|
|
173
|
+
if (input[DOM_ATTRIBUTE + 'Bound']) return;
|
|
174
|
+
input[DOM_ATTRIBUTE + 'Bound'] = true;
|
|
175
|
+
|
|
176
|
+
input._lnFilterChange = function () {
|
|
177
|
+
if (_isReset(input)) {
|
|
178
|
+
// Reset sentinel — regardless of direction, enforce checked + clear values
|
|
179
|
+
for (let i = 0; i < self.inputs.length; i++) {
|
|
180
|
+
if (!_isReset(self.inputs[i])) self.inputs[i].checked = false;
|
|
181
|
+
}
|
|
182
|
+
// Force sentinel back to checked (clicking an already-checked sentinel
|
|
183
|
+
// would natively uncheck it; force back to checked)
|
|
184
|
+
input.checked = true;
|
|
185
|
+
self._queueRender();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (input.checked) {
|
|
190
|
+
// Mutual exclusion: uncheck all reset sentinels
|
|
191
|
+
for (let i = 0; i < self.inputs.length; i++) {
|
|
192
|
+
if (_isReset(self.inputs[i])) self.inputs[i].checked = false;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
// If no non-reset values remain checked, fall back to reset
|
|
196
|
+
let anyChecked = false;
|
|
197
|
+
for (let i = 0; i < self.inputs.length; i++) {
|
|
198
|
+
if (!_isReset(self.inputs[i]) && self.inputs[i].checked) {
|
|
199
|
+
anyChecked = true;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (!anyChecked) {
|
|
204
|
+
for (let i = 0; i < self.inputs.length; i++) {
|
|
205
|
+
if (_isReset(self.inputs[i])) self.inputs[i].checked = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
self._queueRender();
|
|
211
|
+
};
|
|
212
|
+
input.addEventListener('change', input._lnFilterChange);
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// ─── Render ────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
_component.prototype._render = function () {
|
|
219
|
+
const self = this;
|
|
220
|
+
const active = _deriveActive(this);
|
|
221
|
+
const isReset = active.key === null || active.values.length === 0;
|
|
222
|
+
|
|
223
|
+
// Build lowercase lookup for target filtering
|
|
224
|
+
const lowerValues = [];
|
|
225
|
+
for (let i = 0; i < active.values.length; i++) {
|
|
226
|
+
lowerValues.push(active.values[i].toLowerCase());
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Apply filter
|
|
230
|
+
if (self.colIndex !== null) {
|
|
231
|
+
// Table column filtering — shared multi-column logic
|
|
232
|
+
self._filterTableRows(active);
|
|
233
|
+
} else {
|
|
234
|
+
// Standard target-children filtering by data attribute
|
|
235
|
+
const target = document.getElementById(self.targetId);
|
|
236
|
+
if (!target) return;
|
|
237
|
+
|
|
238
|
+
const children = target.children;
|
|
239
|
+
for (let i = 0; i < children.length; i++) {
|
|
240
|
+
const el = children[i];
|
|
241
|
+
|
|
242
|
+
if (isReset) {
|
|
243
|
+
el.removeAttribute(HIDE_ATTR);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const attr = el.getAttribute('data-' + active.key);
|
|
248
|
+
el.removeAttribute(HIDE_ATTR);
|
|
249
|
+
|
|
250
|
+
if (attr === null) continue;
|
|
251
|
+
|
|
252
|
+
// OR logic: visible if attr matches ANY active value
|
|
253
|
+
if (lowerValues.indexOf(attr.toLowerCase()) === -1) {
|
|
254
|
+
el.setAttribute(HIDE_ATTR, 'true');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
_component.prototype._afterRender = function () {
|
|
261
|
+
const active = _deriveActive(this);
|
|
262
|
+
const prev = this._lastSnapshot;
|
|
263
|
+
const changed = !prev
|
|
264
|
+
|| prev.key !== active.key
|
|
265
|
+
|| _arraysDiffer(prev.values, active.values);
|
|
266
|
+
|
|
267
|
+
if (changed) {
|
|
268
|
+
// ln-filter:changed always fires when state moves
|
|
269
|
+
this._dispatchOnBoth('ln-filter:changed', {
|
|
270
|
+
key: active.key,
|
|
271
|
+
values: active.values.slice()
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ln-filter:reset fires only on transition into reset state
|
|
275
|
+
const wasActive = prev && prev.values.length > 0;
|
|
276
|
+
const nowReset = active.values.length === 0;
|
|
277
|
+
if (wasActive && nowReset) {
|
|
278
|
+
this._dispatchOnBoth('ln-filter:reset', {});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this._lastSnapshot = { key: active.key, values: active.values.slice() };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Persist current filter state
|
|
285
|
+
if (this.dom.hasAttribute('data-ln-persist')) {
|
|
286
|
+
if (active.key && active.values.length > 0) {
|
|
287
|
+
persistSet('filter', this.dom, { key: active.key, values: active.values.slice() });
|
|
288
|
+
} else {
|
|
289
|
+
persistSet('filter', this.dom, null);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
_component.prototype._dispatchOnBoth = function (eventName, detail) {
|
|
295
|
+
dispatch(this.dom, eventName, detail);
|
|
296
|
+
const target = document.getElementById(this.targetId);
|
|
297
|
+
if (target && target !== this.dom) {
|
|
298
|
+
dispatch(target, eventName, detail);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// ─── Table Row Filtering ───────────────────────────────────
|
|
303
|
+
|
|
304
|
+
_component.prototype._filterTableRows = function (active) {
|
|
305
|
+
const target = document.getElementById(this.targetId);
|
|
306
|
+
if (!target) return;
|
|
307
|
+
|
|
308
|
+
const table = target.tagName === 'TABLE' ? target : target.querySelector('table');
|
|
309
|
+
if (!table) return;
|
|
310
|
+
|
|
311
|
+
// Guard: don't filter if this is an ln-table (it handles its own filtering)
|
|
312
|
+
if (target.hasAttribute('data-ln-table')) return;
|
|
313
|
+
|
|
314
|
+
const key = active.key || this._filterKey;
|
|
315
|
+
const values = active.values;
|
|
316
|
+
|
|
317
|
+
// Get or create shared filter map for this table
|
|
318
|
+
if (!_tableFilters.has(table)) {
|
|
319
|
+
_tableFilters.set(table, {});
|
|
320
|
+
}
|
|
321
|
+
const filters = _tableFilters.get(table);
|
|
322
|
+
|
|
323
|
+
// Update this filter's entry
|
|
324
|
+
if (key && values.length > 0) {
|
|
325
|
+
const lower = [];
|
|
326
|
+
for (let i = 0; i < values.length; i++) {
|
|
327
|
+
lower.push(values[i].toLowerCase());
|
|
328
|
+
}
|
|
329
|
+
filters[key] = { col: this.colIndex, values: lower };
|
|
330
|
+
} else if (key) {
|
|
331
|
+
delete filters[key];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Check if any column filters are active
|
|
335
|
+
const filterKeys = Object.keys(filters);
|
|
336
|
+
const hasFilters = filterKeys.length > 0;
|
|
337
|
+
|
|
338
|
+
// Apply all active filters to all rows (AND across columns, OR within column)
|
|
339
|
+
const bodies = table.tBodies;
|
|
340
|
+
for (let b = 0; b < bodies.length; b++) {
|
|
341
|
+
const rows = bodies[b].rows;
|
|
342
|
+
for (let r = 0; r < rows.length; r++) {
|
|
343
|
+
const row = rows[r];
|
|
344
|
+
|
|
345
|
+
if (!hasFilters) {
|
|
346
|
+
row.removeAttribute(HIDE_ATTR);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let visible = true;
|
|
351
|
+
for (let f = 0; f < filterKeys.length; f++) {
|
|
352
|
+
const filter = filters[filterKeys[f]];
|
|
353
|
+
const cell = row.cells[filter.col];
|
|
354
|
+
const cellText = cell ? cell.textContent.trim().toLowerCase() : '';
|
|
355
|
+
// OR within column: visible if cell text matches ANY filter value
|
|
356
|
+
if (filter.values.indexOf(cellText) === -1) {
|
|
357
|
+
visible = false;
|
|
358
|
+
break; // AND across columns: fail fast
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (visible) {
|
|
363
|
+
row.removeAttribute(HIDE_ATTR);
|
|
364
|
+
} else {
|
|
365
|
+
row.setAttribute(HIDE_ATTR, 'true');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// ─── Destroy ───────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
_component.prototype.destroy = function () {
|
|
374
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
375
|
+
|
|
376
|
+
// Clean up table filter registry
|
|
377
|
+
if (this.colIndex !== null) {
|
|
378
|
+
const target = document.getElementById(this.targetId);
|
|
379
|
+
if (target) {
|
|
380
|
+
const table = target.tagName === 'TABLE' ? target : target.querySelector('table');
|
|
381
|
+
if (table && _tableFilters.has(table)) {
|
|
382
|
+
const filters = _tableFilters.get(table);
|
|
383
|
+
const key = this._filterKey;
|
|
384
|
+
if (key && filters[key]) delete filters[key];
|
|
385
|
+
if (Object.keys(filters).length === 0) _tableFilters.delete(table);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.inputs.forEach(function (input) {
|
|
391
|
+
if (input._lnFilterChange) {
|
|
392
|
+
input.removeEventListener('change', input._lnFilterChange);
|
|
393
|
+
delete input._lnFilterChange;
|
|
394
|
+
}
|
|
395
|
+
delete input[DOM_ATTRIBUTE + 'Bound'];
|
|
396
|
+
});
|
|
397
|
+
this.dom.removeAttribute(INIT_ATTR);
|
|
398
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// ─── Init ──────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-filter');
|
|
404
|
+
})();
|