@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,88 @@
|
|
|
1
|
+
# ln-icons
|
|
2
|
+
|
|
3
|
+
A zero-dependency, local-first **On-Demand SVG Sprite Generator** that dynamically monitors, fetches, and compiles SVG icons at runtime.
|
|
4
|
+
|
|
5
|
+
Instead of bundling thousands of heavy vector paths or requiring complex manual build steps, it intercepts standard DOM `<use>` tags, fetches vector definitions from a remote CDN, caches them in `localStorage`, and injects them into a single unified hidden SVG sprite sheet.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 🧭 Philosophy & Architecture
|
|
10
|
+
|
|
11
|
+
1. **Declarative On-Demand Rendering:** Icons are declared directly in HTML. The component monitors the DOM via `MutationObserver` for `<use>` references with `#ln-` and `#lnc-` prefixes. It only fetches and compiles icons that are actively present on the page.
|
|
12
|
+
2. **Dual-Prefix Routing:**
|
|
13
|
+
- **`#ln-{name}`**: Automatically routes to the [Tabler Icons](https://tabler.io/icons) library fetched from a public CDN. No configuration required.
|
|
14
|
+
- **`#lnc-{name}`**: Routes to a custom corporate CDN defined via global window settings.
|
|
15
|
+
3. **Local Caching Layer:** Fetched SVG path structures are instantly cached in `localStorage` under `lni:{id}`. Subsequent visits render icons instantly with zero network roundtrips.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 📦 Minimal Blueprint
|
|
20
|
+
|
|
21
|
+
### Native Tabler Icon
|
|
22
|
+
```html
|
|
23
|
+
<svg class="ln-icon" aria-hidden="true">
|
|
24
|
+
<use href="#ln-home"></use>
|
|
25
|
+
</svg>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Custom Asset Icon
|
|
29
|
+
Define your custom CDN endpoint before importing the library:
|
|
30
|
+
```html
|
|
31
|
+
<script>
|
|
32
|
+
window.LN_ICONS_CUSTOM_CDN = "https://cdn.mycompany.com/assets/icons";
|
|
33
|
+
</script>
|
|
34
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
35
|
+
|
|
36
|
+
<!-- Renders icon from your custom CDN -->
|
|
37
|
+
<svg class="ln-icon" aria-hidden="true">
|
|
38
|
+
<use href="#lnc-corporate-logo"></use>
|
|
39
|
+
</svg>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🛠️ Declarative API Contract
|
|
45
|
+
|
|
46
|
+
### CSS Utility Classes
|
|
47
|
+
|
|
48
|
+
Configure icon sizes and alignments using standard CSS classes:
|
|
49
|
+
|
|
50
|
+
| Class | Size | Description |
|
|
51
|
+
| :--- | :--- | :--- |
|
|
52
|
+
| `ln-icon` | `1.25rem` | Base styles, sets `fill: none`, `stroke: currentColor`, inherits color. |
|
|
53
|
+
| `ln-icon--sm` | `1rem` | Small icon, designed for inline text badges or buttons. |
|
|
54
|
+
| `ln-icon--lg` | `1.5rem` | Large icon, designed for toolbar buttons. |
|
|
55
|
+
| `ln-icon--xl` | `4rem` | Extra-large icon, designed for empty state illustrations. |
|
|
56
|
+
| `ln-chevron` | — | Automatically rotates `90deg` when an ancestor `.is-active` class is toggled. |
|
|
57
|
+
|
|
58
|
+
### Global Configuration (`window`)
|
|
59
|
+
|
|
60
|
+
Configure these properties before script initialization:
|
|
61
|
+
|
|
62
|
+
| Variable | Default | Description |
|
|
63
|
+
| :--- | :--- | :--- |
|
|
64
|
+
| `LN_ICONS_CDN` | `https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline` | Base CDN URL for Tabler Icons. |
|
|
65
|
+
| `LN_ICONS_CUSTOM_CDN` | `null` | Base CDN URL for custom `#lnc-` prefixed SVG resources. |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ⚡ Dynamic Interaction Flow
|
|
70
|
+
|
|
71
|
+
### Automated Mutation Observability
|
|
72
|
+
The loader observes the DOM continuously. When new content is injected (e.g. by `ln-ajax` or `ln-store`), any new icon `<use>` tag is intercepted, resolved, and rendered.
|
|
73
|
+
|
|
74
|
+
### Dynamic Attribute Swaps
|
|
75
|
+
Modifying the `href` attribute of a `<use>` element dynamically via JavaScript triggers automatic resolution of the new target icon:
|
|
76
|
+
```javascript
|
|
77
|
+
const useElement = document.querySelector('use');
|
|
78
|
+
// Dynamically fetches and switches the icon to a checkmark
|
|
79
|
+
useElement.setAttribute('href', '#ln-check');
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## ⚠️ Common Pitfalls
|
|
85
|
+
|
|
86
|
+
- **Forgetting `ln-icon` Class:** Standard SVGs default to `100%` width/height. Failing to include the `ln-icon` class will cause the icon to blow up to full viewport size.
|
|
87
|
+
- **Incorrect Prefix Configuration:** Forgetting to define `window.LN_ICONS_CUSTOM_CDN` when using `#lnc-` will cause the loader to fail silently with undefined endpoint errors.
|
|
88
|
+
- **Omitting `aria-hidden="true"`:** Screen readers attempt to read SVG nodes. Always decorate decorative icons with `aria-hidden="true"`, or include an `aria-label` on their parent button.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";(function(){const h="ln-icons-sprite",a="#ln-",o="#lnc-",d=new Set,l=new Set;let c=null;const C=(window.LN_ICONS_CDN||"https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline").replace(/\/$/,""),g=(window.LN_ICONS_CUSTOM_CDN||"").replace(/\/$/,""),u="lni:",m="lni:v",S="1";function v(){try{if(localStorage.getItem(m)!==S){for(let t=localStorage.length-1;t>=0;t--){const n=localStorage.key(t);n&&n.indexOf(u)===0&&localStorage.removeItem(n)}localStorage.setItem(m,S)}}catch{}}v();function N(){return c||(c=document.getElementById(h),c||(c=document.createElementNS("http://www.w3.org/2000/svg","svg"),c.id=h,c.setAttribute("hidden",""),c.setAttribute("aria-hidden","true"),c.appendChild(document.createElementNS("http://www.w3.org/2000/svg","defs")),document.body.insertBefore(c,document.body.firstChild))),c}function A(t){return t.indexOf(o)===0?g+"/"+t.slice(o.length)+".svg":C+"/"+t.slice(a.length)+".svg"}function w(t,n){const e=n.match(/viewBox="([^"]+)"/),i=e?e[1]:"0 0 24 24",s=n.match(/<svg[^>]*>([\s\S]*?)<\/svg>/i),I=s?s[1].trim():"",y=n.match(/<svg([^>]*)>/i),O=y?y[1]:"",r=document.createElementNS("http://www.w3.org/2000/svg","symbol");r.id=t,r.setAttribute("viewBox",i),["fill","stroke","stroke-width","stroke-linecap","stroke-linejoin"].forEach(function(_){const b=O.match(new RegExp(_+'="([^"]*)"'));b&&r.setAttribute(_,b[1])}),r.innerHTML=I,N().querySelector("defs").appendChild(r)}function f(t){if(d.has(t)||l.has(t)||t.indexOf(o)===0&&!g)return;const n=t.slice(1);try{const e=localStorage.getItem(u+n);if(e){w(n,e),d.add(t);return}}catch{}l.add(t),fetch(A(t)).then(function(e){if(!e.ok)throw new Error(e.status);return e.text()}).then(function(e){w(n,e),d.add(t),l.delete(t);try{localStorage.setItem(u+n,e)}catch{}}).catch(function(){l.delete(t)})}function E(t){const n='use[href^="'+a+'"], use[href^="'+o+'"]',e=t.querySelectorAll?t.querySelectorAll(n):[];if(t.matches&&t.matches(n)){const i=t.getAttribute("href");i&&f(i)}Array.prototype.forEach.call(e,function(i){const s=i.getAttribute("href");s&&f(s)})}function p(){E(document),new MutationObserver(function(t){t.forEach(function(n){if(n.type==="childList")n.addedNodes.forEach(function(e){e.nodeType===1&&E(e)});else if(n.type==="attributes"&&n.attributeName==="href"){const e=n.target.getAttribute("href");e&&(e.indexOf(a)===0||e.indexOf(o)===0)&&f(e)}})}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["href"]})}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",p):p()})()})();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// SVG icon loader — fetches icons on demand, builds a hidden sprite.
|
|
2
|
+
// Fetched SVGs are cached in localStorage (prefix "lni:") so subsequent
|
|
3
|
+
// page loads resolve instantly without network requests.
|
|
4
|
+
//
|
|
5
|
+
// Two prefixes:
|
|
6
|
+
// #ln-{name} → Tabler CDN (https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline)
|
|
7
|
+
// #lnc-{name} → Custom CDN (window.LN_ICONS_CUSTOM_CDN)
|
|
8
|
+
//
|
|
9
|
+
// Config — set on window BEFORE this script loads:
|
|
10
|
+
// window.LN_ICONS_CDN = 'https://...' (override Tabler CDN base)
|
|
11
|
+
// window.LN_ICONS_CUSTOM_CDN = 'https://...' (required for lnc- icons)
|
|
12
|
+
//
|
|
13
|
+
// Cache: bump CACHE_VERSION to invalidate all cached icons.
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// <svg class="ln-icon" aria-hidden="true"><use href="#ln-home"></use></svg>
|
|
17
|
+
// <svg class="ln-icon" aria-hidden="true"><use href="#lnc-file-pdf"></use></svg>
|
|
18
|
+
|
|
19
|
+
(function () {
|
|
20
|
+
const SPRITE_ID = 'ln-icons-sprite';
|
|
21
|
+
const PREFIX_LN = '#ln-';
|
|
22
|
+
const PREFIX_LNC = '#lnc-';
|
|
23
|
+
|
|
24
|
+
const loaded = new Set();
|
|
25
|
+
const pending = new Set();
|
|
26
|
+
let spriteEl = null;
|
|
27
|
+
|
|
28
|
+
// Pinned version for cache stability — bump explicitly when upgrading. Override: window.LN_ICONS_CDN
|
|
29
|
+
const BASE_CDN = (window.LN_ICONS_CDN || 'https://cdn.jsdelivr.net/npm/@tabler/icons@3.31.0/icons/outline').replace(/\/$/, '');
|
|
30
|
+
const CUSTOM_CDN = (window.LN_ICONS_CUSTOM_CDN || '').replace(/\/$/, '');
|
|
31
|
+
|
|
32
|
+
const CACHE_PREFIX = 'lni:';
|
|
33
|
+
const CACHE_VER_KEY = 'lni:v';
|
|
34
|
+
const CACHE_VERSION = '1';
|
|
35
|
+
|
|
36
|
+
function _checkCacheVersion() {
|
|
37
|
+
try {
|
|
38
|
+
if (localStorage.getItem(CACHE_VER_KEY) !== CACHE_VERSION) {
|
|
39
|
+
for (let i = localStorage.length - 1; i >= 0; i--) {
|
|
40
|
+
const key = localStorage.key(i);
|
|
41
|
+
if (key && key.indexOf(CACHE_PREFIX) === 0) localStorage.removeItem(key);
|
|
42
|
+
}
|
|
43
|
+
localStorage.setItem(CACHE_VER_KEY, CACHE_VERSION);
|
|
44
|
+
}
|
|
45
|
+
} catch (e) { /* localStorage unavailable or full — proceed without cache */ }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_checkCacheVersion();
|
|
49
|
+
|
|
50
|
+
function _getSprite() {
|
|
51
|
+
if (!spriteEl) {
|
|
52
|
+
spriteEl = document.getElementById(SPRITE_ID);
|
|
53
|
+
if (!spriteEl) {
|
|
54
|
+
spriteEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
55
|
+
spriteEl.id = SPRITE_ID;
|
|
56
|
+
spriteEl.setAttribute('hidden', '');
|
|
57
|
+
spriteEl.setAttribute('aria-hidden', 'true');
|
|
58
|
+
spriteEl.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'defs'));
|
|
59
|
+
document.body.insertBefore(spriteEl, document.body.firstChild);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return spriteEl;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _url(href) {
|
|
66
|
+
if (href.indexOf(PREFIX_LNC) === 0) return CUSTOM_CDN + '/' + href.slice(PREFIX_LNC.length) + '.svg';
|
|
67
|
+
return BASE_CDN + '/' + href.slice(PREFIX_LN.length) + '.svg';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _addSymbol(id, raw) {
|
|
71
|
+
const viewBoxMatch = raw.match(/viewBox="([^"]+)"/);
|
|
72
|
+
const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
|
|
73
|
+
const innerMatch = raw.match(/<svg[^>]*>([\s\S]*?)<\/svg>/i);
|
|
74
|
+
const inner = innerMatch ? innerMatch[1].trim() : '';
|
|
75
|
+
const attrsMatch = raw.match(/<svg([^>]*)>/i);
|
|
76
|
+
const rawAttrs = attrsMatch ? attrsMatch[1] : '';
|
|
77
|
+
|
|
78
|
+
const sym = document.createElementNS('http://www.w3.org/2000/svg', 'symbol');
|
|
79
|
+
sym.id = id;
|
|
80
|
+
sym.setAttribute('viewBox', viewBox);
|
|
81
|
+
|
|
82
|
+
['fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin'].forEach(function (attr) {
|
|
83
|
+
const m = rawAttrs.match(new RegExp(attr + '="([^"]*)"'));
|
|
84
|
+
if (m) sym.setAttribute(attr, m[1]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Trust boundary: SVG content fetched from pinned CDN version (see BASE_CDN) or localStorage cache
|
|
88
|
+
sym.innerHTML = inner;
|
|
89
|
+
_getSprite().querySelector('defs').appendChild(sym);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _load(href) {
|
|
93
|
+
if (loaded.has(href) || pending.has(href)) return;
|
|
94
|
+
if (href.indexOf(PREFIX_LNC) === 0 && !CUSTOM_CDN) return;
|
|
95
|
+
|
|
96
|
+
const id = href.slice(1);
|
|
97
|
+
|
|
98
|
+
// Check localStorage first
|
|
99
|
+
try {
|
|
100
|
+
const cached = localStorage.getItem(CACHE_PREFIX + id);
|
|
101
|
+
if (cached) {
|
|
102
|
+
_addSymbol(id, cached);
|
|
103
|
+
loaded.add(href);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
} catch (e) { /* proceed to fetch */ }
|
|
107
|
+
|
|
108
|
+
pending.add(href);
|
|
109
|
+
|
|
110
|
+
fetch(_url(href))
|
|
111
|
+
.then(function (r) {
|
|
112
|
+
if (!r.ok) throw new Error(r.status);
|
|
113
|
+
return r.text();
|
|
114
|
+
})
|
|
115
|
+
.then(function (raw) {
|
|
116
|
+
_addSymbol(id, raw);
|
|
117
|
+
loaded.add(href);
|
|
118
|
+
pending.delete(href);
|
|
119
|
+
try { localStorage.setItem(CACHE_PREFIX + id, raw); } catch (e) { /* storage full */ }
|
|
120
|
+
})
|
|
121
|
+
.catch(function () {
|
|
122
|
+
pending.delete(href);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function _scan(root) {
|
|
127
|
+
const sel = 'use[href^="' + PREFIX_LN + '"], use[href^="' + PREFIX_LNC + '"]';
|
|
128
|
+
const uses = root.querySelectorAll ? root.querySelectorAll(sel) : [];
|
|
129
|
+
if (root.matches && root.matches(sel)) {
|
|
130
|
+
const h = root.getAttribute('href');
|
|
131
|
+
if (h) _load(h);
|
|
132
|
+
}
|
|
133
|
+
Array.prototype.forEach.call(uses, function (use) {
|
|
134
|
+
const h = use.getAttribute('href');
|
|
135
|
+
if (h) _load(h);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _init() {
|
|
140
|
+
_scan(document);
|
|
141
|
+
new MutationObserver(function (mutations) {
|
|
142
|
+
mutations.forEach(function (m) {
|
|
143
|
+
if (m.type === 'childList') {
|
|
144
|
+
m.addedNodes.forEach(function (node) {
|
|
145
|
+
if (node.nodeType === 1) _scan(node);
|
|
146
|
+
});
|
|
147
|
+
} else if (m.type === 'attributes' && m.attributeName === 'href') {
|
|
148
|
+
// A <use href="..."> was swapped at runtime (e.g. ln-confirm
|
|
149
|
+
// replacing an icon). Trigger a load for the new target.
|
|
150
|
+
const h = m.target.getAttribute('href');
|
|
151
|
+
if (h && (h.indexOf(PREFIX_LN) === 0 || h.indexOf(PREFIX_LNC) === 0)) {
|
|
152
|
+
_load(h);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}).observe(document.body, {
|
|
157
|
+
childList: true,
|
|
158
|
+
subtree: true,
|
|
159
|
+
attributes: true,
|
|
160
|
+
attributeFilter: ['href']
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (document.readyState === 'loading') {
|
|
165
|
+
document.addEventListener('DOMContentLoaded', _init);
|
|
166
|
+
} else {
|
|
167
|
+
_init();
|
|
168
|
+
}
|
|
169
|
+
})();
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# ln-link
|
|
2
|
+
|
|
3
|
+
> Turns a container into a click-navigable surface by triggering
|
|
4
|
+
> `.click()` on the first inner `<a>`. Hover surfaces the URL in a
|
|
5
|
+
> bottom-left status bar that mirrors the browser's native preview.
|
|
6
|
+
|
|
7
|
+
## Philosophy
|
|
8
|
+
|
|
9
|
+
### The first `<a>` rule
|
|
10
|
+
|
|
11
|
+
The contract is `row.querySelector('a')` — the first `<a>` in document
|
|
12
|
+
order. A table row whose primary action is "View" puts the View link
|
|
13
|
+
in cell 1. The component does not consult a `data-primary` attribute
|
|
14
|
+
or any other hint; position IS the contract.
|
|
15
|
+
|
|
16
|
+
### Augmentation, not replacement
|
|
17
|
+
|
|
18
|
+
The component does not intercept the platform's link click. When the
|
|
19
|
+
row chrome is clicked, the component resolves the first `<a>` and
|
|
20
|
+
calls `link.click()` — the platform handles everything from there:
|
|
21
|
+
`target="_blank"`, `download`, `[data-ln-ajax]` body delegation,
|
|
22
|
+
browser extensions. Composing with other components costs nothing.
|
|
23
|
+
|
|
24
|
+
### Interactive children stay interactive
|
|
25
|
+
|
|
26
|
+
Buttons, inputs, selects, textareas, and nested anchors inside a
|
|
27
|
+
row are skipped — clicks originating from those elements never
|
|
28
|
+
trigger row navigation. A row can carry a Delete button, a
|
|
29
|
+
dropdown, and a checkbox alongside the record link without
|
|
30
|
+
collision.
|
|
31
|
+
|
|
32
|
+
### Three modes, one component
|
|
33
|
+
|
|
34
|
+
The container's `tagName` decides how rows are found:
|
|
35
|
+
|
|
36
|
+
- **`<table>`** — the component finds the `<tbody>` (or falls back
|
|
37
|
+
to the table itself if no `<tbody>` exists), iterates each `<tr>`,
|
|
38
|
+
wires each row.
|
|
39
|
+
- **`<tbody>`** — same row iteration; you placed the attribute one
|
|
40
|
+
level lower.
|
|
41
|
+
- **Anything else** (`<li>`, `<article>`, `<tr>`, `<div>`) — the
|
|
42
|
+
element itself is the single clickable row.
|
|
43
|
+
|
|
44
|
+
## HTML contract
|
|
45
|
+
|
|
46
|
+
### Table — all tbody rows become clickable
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<table data-ln-link>
|
|
50
|
+
<thead>
|
|
51
|
+
<tr>
|
|
52
|
+
<th>Name</th>
|
|
53
|
+
<th>Email</th>
|
|
54
|
+
<th>Status</th>
|
|
55
|
+
<th>Actions</th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
<tr>
|
|
60
|
+
<td><a href="/users/1">Marko Petrovski</a></td>
|
|
61
|
+
<td>marko@example.com</td>
|
|
62
|
+
<td><span class="badge success">Active</span></td>
|
|
63
|
+
<td>
|
|
64
|
+
<!-- This button is skipped — clicking it does NOT navigate -->
|
|
65
|
+
<button>Delete</button>
|
|
66
|
+
</td>
|
|
67
|
+
</tr>
|
|
68
|
+
<tr>
|
|
69
|
+
<td><a href="/users/2">Ana Stojanova</a></td>
|
|
70
|
+
<td>ana@example.com</td>
|
|
71
|
+
<td><span class="badge error">Inactive</span></td>
|
|
72
|
+
<td>
|
|
73
|
+
<button>Delete</button>
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Apply `link-row` mixin to the rows for `cursor: pointer` and
|
|
81
|
+
`user-select: none`:
|
|
82
|
+
|
|
83
|
+
```scss
|
|
84
|
+
// project SCSS
|
|
85
|
+
#users-table tr { @include link-row; }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### List of cards
|
|
89
|
+
|
|
90
|
+
```html
|
|
91
|
+
<ul data-ln-link>
|
|
92
|
+
<li>
|
|
93
|
+
<a href="/projects/1"><h3>Project Alpha</h3></a>
|
|
94
|
+
<p>Launched 2025. Click anywhere on this card to open.</p>
|
|
95
|
+
</li>
|
|
96
|
+
<li>
|
|
97
|
+
<a href="/projects/2"><h3>Project Beta</h3></a>
|
|
98
|
+
<p>In progress. Buttons inside cards are still independently clickable.</p>
|
|
99
|
+
<button>Archive</button>
|
|
100
|
+
</li>
|
|
101
|
+
</ul>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
In `<ul>` mode, each `<li>` is a row. The component iterates them
|
|
105
|
+
exactly as it iterates `<tr>` elements in table mode.
|
|
106
|
+
|
|
107
|
+
### Generic article
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<article data-ln-link>
|
|
111
|
+
<h4><a href="/posts/42">Dashboard overview</a></h4>
|
|
112
|
+
<p>The entire article is clickable — not just the link text.</p>
|
|
113
|
+
</article>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
In generic mode the `<article>` itself is the row. No children are
|
|
117
|
+
iterated; a single set of listeners attaches to the element.
|
|
118
|
+
|
|
119
|
+
### Direct `<tr>` — one row only
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<table>
|
|
123
|
+
<tbody>
|
|
124
|
+
<tr data-ln-link>
|
|
125
|
+
<!-- Only this row is navigable; the others are plain rows -->
|
|
126
|
+
<td><a href="/users/99">Special row</a></td>
|
|
127
|
+
</tr>
|
|
128
|
+
<tr>
|
|
129
|
+
<td>Plain row — not navigable</td>
|
|
130
|
+
</tr>
|
|
131
|
+
</tbody>
|
|
132
|
+
</table>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Placing `data-ln-link` on a `<tr>` directly puts it in generic mode
|
|
136
|
+
(the `TR` branch in `_initContainer`) so the row itself becomes the
|
|
137
|
+
single clickable element. Useful when only a subset of rows should be
|
|
138
|
+
navigable.
|
|
139
|
+
|
|
140
|
+
## Attributes
|
|
141
|
+
|
|
142
|
+
| Attribute | On | Description |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `data-ln-link` | `<table>`, `<tbody>`, `<tr>`, or any element | Activates clickable-row behavior. On `<table>` or `<tbody>`, every `<tr>` in the `<tbody>` becomes individually navigable. On any other element, the element itself is the row. |
|
|
145
|
+
|
|
146
|
+
## Events
|
|
147
|
+
|
|
148
|
+
| Event | Bubbles | Cancelable | `detail` | Dispatched on |
|
|
149
|
+
|---|---|---|---|---|
|
|
150
|
+
| `ln-link:navigate` | yes | yes | `{ target, href, link }` | The row element |
|
|
151
|
+
|
|
152
|
+
The event is dispatched BEFORE `link.click()` is called.
|
|
153
|
+
`detail.target` is the row element, `detail.href` is the resolved
|
|
154
|
+
href string, `detail.link` is the `<a>` element. Calling
|
|
155
|
+
`e.preventDefault()` stops navigation entirely.
|
|
156
|
+
|
|
157
|
+
The event does NOT fire on the modifier-key path (Ctrl+click,
|
|
158
|
+
Cmd+click, middle-click). That path calls `window.open(href, '_blank')`
|
|
159
|
+
directly and bypasses the event dispatch.
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
// Log navigation
|
|
163
|
+
document.querySelector('table[data-ln-link]').addEventListener('ln-link:navigate', function (e) {
|
|
164
|
+
analytics.track('row_click', { href: e.detail.href });
|
|
165
|
+
// Don't call e.preventDefault() — navigation continues normally.
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Conditional cancel — e.g. prevent navigation while a form is dirty
|
|
169
|
+
document.addEventListener('ln-link:navigate', function (e) {
|
|
170
|
+
if (formIsDirty) {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
showUnsavedWarning(e.detail.href);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## JS API
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
// Re-scan a root for [data-ln-link] containers and initialize them.
|
|
181
|
+
// Called automatically on DOMContentLoaded and by the MutationObserver.
|
|
182
|
+
// Call manually only for Shadow DOM roots the observer cannot see.
|
|
183
|
+
window.lnLink.init(root);
|
|
184
|
+
|
|
185
|
+
// Clean up listeners on a single container.
|
|
186
|
+
// Operates on the PASSED container only — does NOT traverse descendants.
|
|
187
|
+
// To destroy multiple containers, call destroy on each separately.
|
|
188
|
+
window.lnLink.destroy(container);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`init` traverses descendants; `destroy` does not. See
|
|
192
|
+
[`docs/js/link.md`](../../docs/js/link.md) for the full mechanics
|
|
193
|
+
behind this asymmetry.
|
|
194
|
+
|
|
195
|
+
## What it does NOT do
|
|
196
|
+
|
|
197
|
+
- **Does not intercept clicks on the `<a>` itself.** Direct anchor
|
|
198
|
+
clicks follow the platform default. The row click is an additional
|
|
199
|
+
surface, not a replacement.
|
|
200
|
+
- **Does not fire `ln-link:navigate` on the modifier-key path.**
|
|
201
|
+
Ctrl/Cmd+click and middle-click call `window.open` directly; the
|
|
202
|
+
event is not dispatched and cannot be cancelled.
|
|
203
|
+
- **Does not re-check anchor-less rows on subsequent `init()`
|
|
204
|
+
calls.** A row without an `<a>` at init time still receives the
|
|
205
|
+
per-row guard flag, so re-init is a no-op. To wire a row after
|
|
206
|
+
adding its anchor dynamically, call `destroy(container)` then
|
|
207
|
+
`init(container)`.
|
|
208
|
+
- **Does not wire keyboard navigation.** The row element is not
|
|
209
|
+
made focusable. Keyboard users tab to the inner `<a>` and
|
|
210
|
+
activate it normally.
|
|
211
|
+
- **Does not own appearance.** `cursor: pointer` and
|
|
212
|
+
`user-select: none` come from `@mixin link-row`
|
|
213
|
+
(`scss/config/mixins/_link.scss`); the status bar styling comes
|
|
214
|
+
from `@mixin link-status`. JS owns behavior; SCSS owns
|
|
215
|
+
appearance.
|
|
216
|
+
|
|
217
|
+
## Cross-component composition
|
|
218
|
+
|
|
219
|
+
### With ln-ajax
|
|
220
|
+
|
|
221
|
+
`ln-link` and `ln-ajax` compose without any wiring. `ln-link`
|
|
222
|
+
triggers `link.click()` on the inner `<a>`; `ln-ajax` listens for
|
|
223
|
+
`click` events at the body via event delegation and intercepts
|
|
224
|
+
qualifying anchor clicks. The `link.click()` call produces a real
|
|
225
|
+
`click` event on a real `<a>`, so `ln-ajax` sees it exactly as if the
|
|
226
|
+
user had clicked the link directly.
|
|
227
|
+
|
|
228
|
+
```html
|
|
229
|
+
<!-- ln-ajax wraps the table; ln-link is on the table itself -->
|
|
230
|
+
<section data-ln-ajax>
|
|
231
|
+
<table data-ln-link>
|
|
232
|
+
<thead>
|
|
233
|
+
<tr><th>Name</th><th>Email</th></tr>
|
|
234
|
+
</thead>
|
|
235
|
+
<tbody>
|
|
236
|
+
<tr>
|
|
237
|
+
<td><a href="/users/1" data-ln-ajax-target="main">Marko</a></td>
|
|
238
|
+
<td>marko@example.com</td>
|
|
239
|
+
</tr>
|
|
240
|
+
</tbody>
|
|
241
|
+
</table>
|
|
242
|
+
</section>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
No special configuration. The `data-ln-ajax-target` attribute on the
|
|
246
|
+
`<a>` is respected because `ln-ajax` is processing the click on the
|
|
247
|
+
real anchor, not on the row.
|
|
248
|
+
|
|
249
|
+
### With ln-confirm
|
|
250
|
+
|
|
251
|
+
`ln-confirm` attaches to a `<button>` and intercepts that button's
|
|
252
|
+
click; `ln-link` triggers `.click()` on an `<a>`. There is no shared
|
|
253
|
+
click event for `ln-confirm` to intercept.
|
|
254
|
+
|
|
255
|
+
For confirmation flows on row navigation, use `ln-link:navigate`
|
|
256
|
+
with `e.preventDefault()` instead — see §Events.
|
|
257
|
+
|
|
258
|
+
### With ln-data-table
|
|
259
|
+
|
|
260
|
+
`ln-data-table` inserts `<tr>` elements into a `<tbody>` from a
|
|
261
|
+
template. `ln-link`'s MutationObserver wires each new row as it
|
|
262
|
+
arrives — no manual re-init is needed after `ln-data-table`
|
|
263
|
+
populates the body.
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<table data-ln-link data-ln-table>
|
|
267
|
+
<thead>...</thead>
|
|
268
|
+
<tbody>
|
|
269
|
+
<!-- ln-data-table inserts <tr> elements here dynamically;
|
|
270
|
+
ln-link wires them automatically. -->
|
|
271
|
+
</tbody>
|
|
272
|
+
</table>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Integration & Development
|
|
276
|
+
|
|
277
|
+
### Integration
|
|
278
|
+
|
|
279
|
+
To integrate `ln-link` into your application, you can choose between loading the unified bundle or importing the component standalone.
|
|
280
|
+
|
|
281
|
+
#### In-Bundle (Standard Integration)
|
|
282
|
+
This is the recommended approach for standard integration, loading the component as part of the main `ln-ashlar` bundle:
|
|
283
|
+
```html
|
|
284
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### Standalone (Zero-Dependency IIFE)
|
|
288
|
+
If you only need this component and want to avoid loading the full bundle, you can load it standalone using its zero-dependency compiled IIFE:
|
|
289
|
+
```html
|
|
290
|
+
<script src="js/ln-link/ln-link.js" defer></script>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Source Files
|
|
294
|
+
|
|
295
|
+
- **Active Development (Source of Truth)**: [js/ln-link/src/ln-link.js](file:///c:/laragon/www/ln-ashlar/js/ln-link/src/ln-link.js)
|
|
296
|
+
- **Compiled Standalone Distribution**: [js/ln-link/ln-link.js](file:///c:/laragon/www/ln-ashlar/js/ln-link/ln-link.js)
|
|
297
|
+
|
|
298
|
+
## See also
|
|
299
|
+
|
|
300
|
+
- [`../../docs/js/link.md`](../../docs/js/link.md) — architecture mirror (internal state, observer topology, click-flow, registration pattern)
|
|
301
|
+
- [`../../docs/js/ajax.md`](../../docs/js/ajax.md) — ln-ajax interop details
|
|
302
|
+
- [`../../scss/config/mixins/_link.scss`](../../scss/config/mixins/_link.scss) — `link-row` and `link-status` mixins
|
|
303
|
+
- [`../../demo/admin/src/pages/link.html`](../../demo/admin/src/pages/link.html) — interactive demo
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";function v(r,t,l){const u=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:l||{}});return r.dispatchEvent(u),u}function d(r,t){if(!document.body){document.addEventListener("DOMContentLoaded",function(){d(r,t)}),console.warn("["+t+'] Script loaded before <body> — add "defer" to your <script> tag');return}r()}const a={};function _(r,t){a[r]=t}function E(r){return a[r]||{ingress:t=>t,egress:t=>t}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=_,window.lnCore.getDataMapper=E),(function(){const r="data-ln-link",t="lnLink";if(window[t]!==void 0)return;let l=null;function u(){l=document.createElement("div"),l.className="ln-link-status",document.body.appendChild(l)}function m(e){l&&(l.textContent=e,l.classList.add("ln-link-status--visible"))}function p(){l&&l.classList.remove("ln-link-status--visible")}function g(e,n){if(n.target.closest("a, button, input, select, textarea"))return;const i=e.querySelector("a");if(!i)return;const o=i.getAttribute("href");if(!o)return;if(n.ctrlKey||n.metaKey||n.button===1){window.open(o,"_blank");return}v(e,"ln-link:navigate",{target:e,href:o,link:i}).defaultPrevented||i.click()}function C(e){const n=e.querySelector("a");if(!n)return;const i=n.getAttribute("href");i&&m(i)}function f(){p()}function s(e){e[t+"Row"]||(e[t+"Row"]=!0,e.querySelector("a")&&(e._lnLinkClick=function(n){g(e,n)},e._lnLinkEnter=function(){C(e)},e.addEventListener("click",e._lnLinkClick),e.addEventListener("mouseenter",e._lnLinkEnter),e.addEventListener("mouseleave",f)))}function b(e){e[t+"Row"]&&(e._lnLinkClick&&e.removeEventListener("click",e._lnLinkClick),e._lnLinkEnter&&e.removeEventListener("mouseenter",e._lnLinkEnter),e.removeEventListener("mouseleave",f),delete e._lnLinkClick,delete e._lnLinkEnter,delete e[t+"Row"])}function h(e){if(!e[t+"Init"])return;const n=e.tagName;if(n==="TABLE"||n==="TBODY"){const i=n==="TABLE"&&e.querySelector("tbody")||e;for(const o of i.querySelectorAll("tr"))b(o)}else b(e);delete e[t+"Init"]}function L(e){if(e[t+"Init"])return;e[t+"Init"]=!0;const n=e.tagName;if(n==="TABLE"||n==="TBODY"){const i=n==="TABLE"&&e.querySelector("tbody")||e;for(const o of i.querySelectorAll("tr"))s(o)}else s(e)}function c(e){e.hasAttribute&&e.hasAttribute(r)&&L(e);const n=e.querySelectorAll?e.querySelectorAll("["+r+"]"):[];for(const i of n)L(i)}function S(){d(function(){new MutationObserver(function(n){for(const i of n)if(i.type==="childList")for(const o of i.addedNodes)o.nodeType===1&&(c(o),o.tagName==="TR"&&o.closest("["+r+"]")&&s(o));else i.type==="attributes"&&c(i.target)}).observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:[r]})},"ln-link")}function k(e){c(e)}window[t]={init:k,destroy:h};function y(){u(),S(),k(document.body)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",y):y()})()})();
|