@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,127 @@
|
|
|
1
|
+
# ln-popover
|
|
2
|
+
|
|
3
|
+
> Viewport-aware click-triggered rich-content overlays, managed reactively via the DOM.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Philosophy & Mindset
|
|
8
|
+
|
|
9
|
+
In `ln-ashlar`, the core design principle is **orthogonality**. Rather than creating a heavy component that bundles state, positioning algorithms, LIFO click stacks, and styles, `ln-popover` splits them cleanly:
|
|
10
|
+
|
|
11
|
+
1. **State & Positioning (JavaScript)**: The `ln-popover` script handles binary `open` / `closed` states in the DOM, manages view-port boundaries to flip placement as needed, restores keyboard focus gracefully, and coordinates LIFO (Last-In, First-Out) keyboard ESC dismissal stacks.
|
|
12
|
+
2. **Visual Presentation (CSS)**: Visual layouts, background shadows, and borders are handled in Vanilla CSS. The library ships a premium `@mixin popover` to style popover cards.
|
|
13
|
+
3. **Trigger decoupling (HTML)**: Triggers (`data-ln-popover-for="popoverId"`) and popover containers (`id="popoverId"`) are bound purely by ID, allowing the trigger button and the popup card to live anywhere in the document.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. Minimal Blueprint
|
|
18
|
+
|
|
19
|
+
Triggers and popovers are paired by ID. Wrap content in a `div` carrying the `data-ln-popover` attribute.
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<!-- Trigger anywhere -->
|
|
23
|
+
<button data-ln-popover-for="account-menu">Account</button>
|
|
24
|
+
|
|
25
|
+
<!-- Popover anywhere — role="dialog" and tabindex="-1" are injected automatically -->
|
|
26
|
+
<div data-ln-popover id="account-menu">
|
|
27
|
+
<p><strong>user@example.com</strong></p>
|
|
28
|
+
<nav>
|
|
29
|
+
<a href="/settings">Settings</a>
|
|
30
|
+
<a href="/logout">Sign out</a>
|
|
31
|
+
</nav>
|
|
32
|
+
</div>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Key Anatomy Rules
|
|
36
|
+
- **The Popover wrapper (`data-ln-popover`)**: Driven by the value `"open"` (open) and `"closed"` or empty (closed).
|
|
37
|
+
- **The Trigger (`data-ln-popover-for="id"`)**: Sets `aria-expanded` and manages focus automatically.
|
|
38
|
+
- **Visual styling**: Re-apply `@mixin popover` in your SCSS on the popover element.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 3. Declarative API & State Contract
|
|
43
|
+
|
|
44
|
+
There are no imperative JavaScript methods (like `open()` or `close()`) on the component instance. **The HTML attribute is the sole contract.**
|
|
45
|
+
|
|
46
|
+
Triggers, LIFO click outside handlers, viewport edge flips, and custom scripts all change state by writing the active attribute on the popover element:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const popover = document.getElementById('account-menu');
|
|
50
|
+
|
|
51
|
+
// Open the popover
|
|
52
|
+
popover.setAttribute('data-ln-popover', 'open');
|
|
53
|
+
|
|
54
|
+
// Close the popover
|
|
55
|
+
popover.setAttribute('data-ln-popover', 'closed');
|
|
56
|
+
|
|
57
|
+
// Read-only state query
|
|
58
|
+
popover.lnPopover.isOpen; // Returns true/false
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Attributes
|
|
62
|
+
- `data-ln-popover`: Placed on the popover card. Value `"open"` = open; `"closed"` = closed.
|
|
63
|
+
- `data-ln-popover-for="id"`: Placed on triggers referencing the popover ID.
|
|
64
|
+
- `data-ln-popover-position="top|bottom|left|right"`: Preferred placement side. Default: `bottom`.
|
|
65
|
+
- `data-ln-popover-placement`: Set automatically by JS to indicate the *actual* active side after auto-flip is resolved.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 4. Transition Events
|
|
70
|
+
|
|
71
|
+
All events bubble. The dispatch target is the popover element itself.
|
|
72
|
+
|
|
73
|
+
| Event | Cancelable | `detail` | Dispatched When |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| **`ln-popover:before-open`** | **Yes** | `{ popoverId, target, trigger }` | Before opening. `event.preventDefault()` cancels the transition and reverts the attribute. |
|
|
76
|
+
| **`ln-popover:open`** | No | `{ popoverId, target, trigger }` | After popover is positioned, classes added, and focus moved. |
|
|
77
|
+
| **`ln-popover:before-close`** | **Yes** | `{ popoverId, target, trigger }` | Before closing. `event.preventDefault()` cancels the close and reverts the attribute. |
|
|
78
|
+
| **`ln-popover:close`** | No | `{ popoverId, target, trigger }` | After popover is closed and focus restored back to the trigger. |
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
// Example: Cancel open transition for unauthorized zones
|
|
82
|
+
document.addEventListener('ln-popover:before-open', (e) => {
|
|
83
|
+
if (e.detail.popoverId === 'admin-zone' && !currentUser.isAdmin) {
|
|
84
|
+
e.preventDefault(); // Reverts attribute back to "closed"
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 5. Integration Patterns
|
|
92
|
+
|
|
93
|
+
### A. Position Preferences & Auto-Flip
|
|
94
|
+
Preferred placement is easily configured. If there isn't enough room in the viewport, the positioning engine flips the popover to the opposite side:
|
|
95
|
+
```html
|
|
96
|
+
<div data-ln-popover data-ln-popover-position="right" id="help-menu">...</div>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### B. Nested Popovers (A stays open when B opens)
|
|
100
|
+
Opening B from inside A leaves A open. Pressing `ESC` once closes B first, and then A:
|
|
101
|
+
```html
|
|
102
|
+
<div data-ln-popover id="popover-a">
|
|
103
|
+
<button data-ln-popover-for="popover-b">Open B</button>
|
|
104
|
+
</div>
|
|
105
|
+
<div data-ln-popover id="popover-b">...</div>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 6. Integration & Source Files
|
|
111
|
+
|
|
112
|
+
- **Unified Bundle**: Loaded automatically with the main bundle:
|
|
113
|
+
```html
|
|
114
|
+
<script src="dist/ln-ashlar.iife.js" defer></script>
|
|
115
|
+
```
|
|
116
|
+
- **Standalone IIFE**: For lightweight pages, load the standalone, self-registering IIFE version:
|
|
117
|
+
```html
|
|
118
|
+
<script src="js/ln-popover/ln-popover.js" defer></script>
|
|
119
|
+
```
|
|
120
|
+
- **Active Source (ESM)**: Development source is located at [js/ln-popover/src/ln-popover.js](file:///c:/laragon/www/ln-ashlar/js/ln-popover/src/ln-popover.js).
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Related
|
|
125
|
+
- **[`ln-dropdown`](../ln-dropdown/README.md)** — Menu wrapper adding click-outside/teleportation.
|
|
126
|
+
- **[`ln-toggle`](../ln-toggle/README.md)** — Binary disclosure state primitive.
|
|
127
|
+
- **Architecture deep-dive** — [`docs/js/popover.md`](../../docs/js/popover.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";function A(t,e,n){t.dispatchEvent(new CustomEvent(e,{bubbles:!0,detail:n||{}}))}function O(t,e,n){const a=new CustomEvent(e,{bubbles:!0,cancelable:!0,detail:n||{}});return t.dispatchEvent(a),a}function k(t,e){if(!document.body){document.addEventListener("DOMContentLoaded",function(){k(t,e)}),console.warn("["+e+'] Script loaded before <body> — add "defer" to your <script> tag');return}t()}function E(t,e,n,a){if(t.nodeType!==1)return;const p=e.indexOf("[")!==-1||e.indexOf(".")!==-1||e.indexOf("#")!==-1?e:"["+e+"]",h=Array.from(t.querySelectorAll(p));t.matches&&t.matches(p)&&h.push(t);for(const l of h)l[n]||(l[n]=new a(l))}function I(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)}function x(t,e,n,a,u={}){const p=u.extraAttributes||[],h=u.onAttributeChange||null,l=u.onInit||null;function d(m){const i=m||document.body;E(i,t,e,n),l&&l(i)}return k(function(){const m=new MutationObserver(function(r){for(let c=0;c<r.length;c++){const s=r[c];if(s.type==="childList")for(let o=0;o<s.addedNodes.length;o++){const g=s.addedNodes[o];g.nodeType===1&&(E(g,t,e,n),l&&l(g))}else s.type==="attributes"&&(h&&s.target[e]?h(s.target,s.attributeName):(E(s.target,t,e,n),l&&l(s.target)))}});let i=[];if(t.indexOf("[")!==-1){const r=/\[([\w-]+)/g;let c;for(;(c=r.exec(t))!==null;)i.push(c[1])}else i.push(t);m.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:i.concat(p)})},a||(t.indexOf("[")===-1?t.replace("data-",""):"component")),window[e]=d,document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){d(document.body)}):d(document.body),d}const L={};function R(t,e){L[t]=e}function M(t){return L[t]||{ingress:e=>e,egress:e=>e}}typeof window<"u"&&(window.lnCore=window.lnCore||{},window.lnCore.registerDataMapper=R,window.lnCore.getDataMapper=M);function T(t,e,n,a){const u=a,p=window.innerWidth,h=window.innerHeight,l=e.width,d=e.height,m=(n||"bottom").split("-"),i=m[0],r=m[1]==="start"||m[1]==="end"?m[1]:"center",c={top:["top","bottom","right","left"],bottom:["bottom","top","right","left"],left:["left","right","top","bottom"],right:["right","left","top","bottom"]},s=c[i]||c.bottom;function o(f){return f==="top"||f==="bottom"?r==="start"?t.left:r==="end"?t.right-l:t.left+(t.width-l)/2:r==="start"?t.top:r==="end"?t.bottom-d:t.top+(t.height-d)/2}function g(f){let _,w,C=!0;return f==="top"?(_=t.top-u-d,w=o(f),_<0&&(C=!1)):f==="bottom"?(_=t.bottom+u,w=o(f),_+d>h&&(C=!1)):f==="left"?(_=o(f),w=t.left-u-l,w<0&&(C=!1)):(_=o(f),w=t.right+u,w+l>p&&(C=!1)),{top:_,left:w,side:f,fits:C}}let v=null;for(let f=0;f<s.length;f++){const _=g(s[f]);if(_.fits){v=_;break}}v||(v=g(s[0]));let b=v.top,y=v.left;return l>=p?y=0:(y<0&&(y=0),y+l>p&&(y=p-l)),d>=h?b=0:(b<0&&(b=0),b+d>h&&(b=h-d)),{top:b,left:y,placement:v.side}}function B(t){if(!t||t.parentNode===document.body)return function(){};const e=t.parentNode,n=document.createComment("ln-teleport");return e.insertBefore(n,t),document.body.appendChild(t),function(){n.parentNode&&(n.parentNode.insertBefore(t,n),n.parentNode.removeChild(n))}}function D(t){if(!t)return{width:0,height:0};const e=t.style,n=e.visibility,a=e.display,u=e.position;e.visibility="hidden",e.display="block",e.position="fixed";const p=t.offsetWidth,h=t.offsetHeight;return e.visibility=n,e.display=a,e.position=u,{width:p,height:h}}(function(){const t="data-ln-popover",e="lnPopover",n="data-ln-popover-for",a="data-ln-popover-position";if(window[e]!==void 0)return;const u=[];let p=null;function h(){p||(p=function(i){if(i.key!=="Escape"||u.length===0)return;u[u.length-1].close()},document.addEventListener("keydown",p))}function l(){u.length>0||p&&(document.removeEventListener("keydown",p),p=null)}function d(i){return this.dom=i,this.isOpen=i.getAttribute(t)==="open",this.trigger=null,this._teleportRestore=null,this._previousFocus=null,this._boundDocClick=null,this._docClickTimeout=null,this._boundReposition=null,i.hasAttribute("tabindex")||i.setAttribute("tabindex","-1"),i.hasAttribute("role")||i.setAttribute("role","dialog"),this.isOpen&&this._applyOpen(null),this}d.prototype.open=function(i){this.isOpen||(this.trigger=i||null,this.dom.setAttribute(t,"open"))},d.prototype.close=function(){this.isOpen&&this.dom.setAttribute(t,"closed")},d.prototype.toggle=function(i){this.isOpen?this.close():this.open(i)},d.prototype._applyOpen=function(i){this.isOpen=!0,i&&(this.trigger=i),this._previousFocus=document.activeElement,this._teleportRestore=B(this.dom);const r=D(this.dom);if(this.trigger){const g=this.trigger.getBoundingClientRect(),v=this.dom.getAttribute(a)||"bottom",b=T(g,r,v,8);this.dom.style.top=b.top+"px",this.dom.style.left=b.left+"px",this.dom.setAttribute("data-ln-popover-placement",b.placement),this.trigger.setAttribute("aria-expanded","true")}const c=this.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),s=Array.prototype.find.call(c,I);s?s.focus():this.dom.focus();const o=this;this._boundDocClick=function(g){o.dom.contains(g.target)||o.trigger&&o.trigger.contains(g.target)||o.close()},o._docClickTimeout=setTimeout(function(){o._docClickTimeout=null,document.addEventListener("click",o._boundDocClick)},0),this._boundReposition=function(){if(!o.trigger)return;const g=o.trigger.getBoundingClientRect(),v=D(o.dom),b=o.dom.getAttribute(a)||"bottom",y=T(g,v,b,8);o.dom.style.top=y.top+"px",o.dom.style.left=y.left+"px",o.dom.setAttribute("data-ln-popover-placement",y.placement)},window.addEventListener("scroll",this._boundReposition,{passive:!0,capture:!0}),window.addEventListener("resize",this._boundReposition),u.push(this),h(),A(this.dom,"ln-popover:open",{popoverId:this.dom.id,target:this.dom,trigger:this.trigger})},d.prototype._applyClose=function(){this.isOpen=!1,this._docClickTimeout&&(clearTimeout(this._docClickTimeout),this._docClickTimeout=null),this._boundDocClick&&(document.removeEventListener("click",this._boundDocClick),this._boundDocClick=null),this._boundReposition&&(window.removeEventListener("scroll",this._boundReposition,{capture:!0}),window.removeEventListener("resize",this._boundReposition),this._boundReposition=null),this.dom.style.top="",this.dom.style.left="",this.dom.removeAttribute("data-ln-popover-placement"),this.trigger&&this.trigger.setAttribute("aria-expanded","false"),this._teleportRestore&&(this._teleportRestore(),this._teleportRestore=null);const i=u.indexOf(this);i!==-1&&u.splice(i,1),l(),this._previousFocus&&this.trigger&&this._previousFocus===this.trigger?this.trigger.focus():this.trigger&&document.activeElement===document.body&&this.trigger.focus(),this._previousFocus=null,A(this.dom,"ln-popover:close",{popoverId:this.dom.id,target:this.dom,trigger:this.trigger}),this.trigger=null},d.prototype.destroy=function(){this.dom[e]&&(this.isOpen&&this._applyClose(),delete this.dom[e],A(this.dom,"ln-popover:destroyed",{popoverId:this.dom.id,target:this.dom}))};function m(i){this.dom=i;const r=i.getAttribute(n);return i.setAttribute("aria-haspopup","dialog"),i.setAttribute("aria-expanded","false"),i.setAttribute("aria-controls",r),this._onClick=function(c){if(c.ctrlKey||c.metaKey||c.button===1)return;c.preventDefault();const s=document.getElementById(r);!s||!s[e]||s[e].toggle(i)},i.addEventListener("click",this._onClick),this}m.prototype.destroy=function(){this.dom.removeEventListener("click",this._onClick),delete this.dom[e+"Trigger"]},x(t,e,d,"ln-popover",{onAttributeChange:function(i){const r=i[e];if(!r)return;const s=i.getAttribute(t)==="open";if(s!==r.isOpen)if(s){if(O(i,"ln-popover:before-open",{popoverId:i.id,target:i,trigger:r.trigger}).defaultPrevented){i.setAttribute(t,"closed");return}r._applyOpen(r.trigger)}else{if(O(i,"ln-popover:before-close",{popoverId:i.id,target:i,trigger:r.trigger}).defaultPrevented){i.setAttribute(t,"open");return}r._applyClose()}}}),x(n,e+"Trigger",m,"ln-popover-trigger")})()})();
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { dispatch, dispatchCancelable, computePlacement, teleportToBody, measureHidden, isVisible, registerComponent } from '../../ln-core';
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
const DOM_SELECTOR = 'data-ln-popover';
|
|
5
|
+
const DOM_ATTRIBUTE = 'lnPopover';
|
|
6
|
+
const TRIGGER_SELECTOR = 'data-ln-popover-for';
|
|
7
|
+
const POSITION_SELECTOR = 'data-ln-popover-position';
|
|
8
|
+
|
|
9
|
+
if (window[DOM_ATTRIBUTE] !== undefined) return;
|
|
10
|
+
|
|
11
|
+
// ─── Open-stack (Escape closes top of stack) ───────────────
|
|
12
|
+
|
|
13
|
+
const openStack = [];
|
|
14
|
+
let escListener = null;
|
|
15
|
+
|
|
16
|
+
function _ensureEscListener() {
|
|
17
|
+
if (escListener) return;
|
|
18
|
+
escListener = function (e) {
|
|
19
|
+
if (e.key !== 'Escape') return;
|
|
20
|
+
if (openStack.length === 0) return;
|
|
21
|
+
const top = openStack[openStack.length - 1];
|
|
22
|
+
top.close();
|
|
23
|
+
};
|
|
24
|
+
document.addEventListener('keydown', escListener);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function _maybeRemoveEscListener() {
|
|
28
|
+
if (openStack.length > 0) return;
|
|
29
|
+
if (!escListener) return;
|
|
30
|
+
document.removeEventListener('keydown', escListener);
|
|
31
|
+
escListener = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Component ─────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function _component(dom) {
|
|
37
|
+
this.dom = dom;
|
|
38
|
+
this.isOpen = dom.getAttribute(DOM_SELECTOR) === 'open';
|
|
39
|
+
this.trigger = null;
|
|
40
|
+
this._teleportRestore = null;
|
|
41
|
+
this._previousFocus = null;
|
|
42
|
+
this._boundDocClick = null;
|
|
43
|
+
this._docClickTimeout = null;
|
|
44
|
+
this._boundReposition = null;
|
|
45
|
+
|
|
46
|
+
// Make the popover container itself programmatically focusable
|
|
47
|
+
// as a fallback when it has no focusable children.
|
|
48
|
+
if (!dom.hasAttribute('tabindex')) {
|
|
49
|
+
dom.setAttribute('tabindex', '-1');
|
|
50
|
+
}
|
|
51
|
+
if (!dom.hasAttribute('role')) {
|
|
52
|
+
dom.setAttribute('role', 'dialog');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If the markup says open at boot, sync immediately.
|
|
56
|
+
if (this.isOpen) {
|
|
57
|
+
this._applyOpen(null);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_component.prototype.open = function (trigger) {
|
|
64
|
+
if (this.isOpen) return;
|
|
65
|
+
this.trigger = trigger || null;
|
|
66
|
+
this.dom.setAttribute(DOM_SELECTOR, 'open');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
_component.prototype.close = function () {
|
|
70
|
+
if (!this.isOpen) return;
|
|
71
|
+
this.dom.setAttribute(DOM_SELECTOR, 'closed');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
_component.prototype.toggle = function (trigger) {
|
|
75
|
+
if (this.isOpen) {
|
|
76
|
+
this.close();
|
|
77
|
+
} else {
|
|
78
|
+
this.open(trigger);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ─── Apply open/close (called from _syncAttribute) ─────────
|
|
83
|
+
|
|
84
|
+
_component.prototype._applyOpen = function (trigger) {
|
|
85
|
+
this.isOpen = true;
|
|
86
|
+
if (trigger) this.trigger = trigger;
|
|
87
|
+
this._previousFocus = document.activeElement;
|
|
88
|
+
|
|
89
|
+
// Teleport into <body> so position:fixed coordinates are reliable
|
|
90
|
+
// regardless of any ancestor with `transform` or `contain`.
|
|
91
|
+
this._teleportRestore = teleportToBody(this.dom);
|
|
92
|
+
|
|
93
|
+
// Measure (works even though it just got `display: block` by the
|
|
94
|
+
// attribute switch — measureHidden is safe either way).
|
|
95
|
+
const size = measureHidden(this.dom);
|
|
96
|
+
|
|
97
|
+
// Position relative to the trigger (if any).
|
|
98
|
+
if (this.trigger) {
|
|
99
|
+
const rect = this.trigger.getBoundingClientRect();
|
|
100
|
+
const preferred = this.dom.getAttribute(POSITION_SELECTOR) || 'bottom';
|
|
101
|
+
const placement = computePlacement(rect, size, preferred, 8);
|
|
102
|
+
// Inline coordinates are unavoidable for floating UI; CSS
|
|
103
|
+
// supplies position:fixed via the co-located scss, JS only
|
|
104
|
+
// writes `top`/`left`. This is consistent with ln-dropdown.
|
|
105
|
+
this.dom.style.top = placement.top + 'px';
|
|
106
|
+
this.dom.style.left = placement.left + 'px';
|
|
107
|
+
this.dom.setAttribute('data-ln-popover-placement', placement.placement);
|
|
108
|
+
this.trigger.setAttribute('aria-expanded', 'true');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Focus management — first visible focusable, or popover itself.
|
|
112
|
+
const allFocusable = this.dom.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])');
|
|
113
|
+
const focusable = Array.prototype.find.call(allFocusable, isVisible);
|
|
114
|
+
if (focusable) {
|
|
115
|
+
focusable.focus();
|
|
116
|
+
} else {
|
|
117
|
+
this.dom.focus();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Outside-click listener (delayed by one tick so the opening
|
|
121
|
+
// click doesn't immediately close).
|
|
122
|
+
const self = this;
|
|
123
|
+
this._boundDocClick = function (e) {
|
|
124
|
+
if (self.dom.contains(e.target)) return;
|
|
125
|
+
if (self.trigger && self.trigger.contains(e.target)) return;
|
|
126
|
+
self.close();
|
|
127
|
+
};
|
|
128
|
+
self._docClickTimeout = setTimeout(function () {
|
|
129
|
+
self._docClickTimeout = null;
|
|
130
|
+
document.addEventListener('click', self._boundDocClick);
|
|
131
|
+
}, 0);
|
|
132
|
+
|
|
133
|
+
// Reposition on scroll/resize while open.
|
|
134
|
+
this._boundReposition = function () {
|
|
135
|
+
if (!self.trigger) return;
|
|
136
|
+
const r = self.trigger.getBoundingClientRect();
|
|
137
|
+
const sz = measureHidden(self.dom);
|
|
138
|
+
const preferred = self.dom.getAttribute(POSITION_SELECTOR) || 'bottom';
|
|
139
|
+
const p = computePlacement(r, sz, preferred, 8);
|
|
140
|
+
self.dom.style.top = p.top + 'px';
|
|
141
|
+
self.dom.style.left = p.left + 'px';
|
|
142
|
+
self.dom.setAttribute('data-ln-popover-placement', p.placement);
|
|
143
|
+
};
|
|
144
|
+
window.addEventListener('scroll', this._boundReposition, { passive: true, capture: true });
|
|
145
|
+
window.addEventListener('resize', this._boundReposition);
|
|
146
|
+
|
|
147
|
+
openStack.push(this);
|
|
148
|
+
_ensureEscListener();
|
|
149
|
+
|
|
150
|
+
dispatch(this.dom, 'ln-popover:open', {
|
|
151
|
+
popoverId: this.dom.id,
|
|
152
|
+
target: this.dom,
|
|
153
|
+
trigger: this.trigger
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
_component.prototype._applyClose = function () {
|
|
158
|
+
this.isOpen = false;
|
|
159
|
+
|
|
160
|
+
if (this._docClickTimeout) {
|
|
161
|
+
clearTimeout(this._docClickTimeout);
|
|
162
|
+
this._docClickTimeout = null;
|
|
163
|
+
}
|
|
164
|
+
if (this._boundDocClick) {
|
|
165
|
+
document.removeEventListener('click', this._boundDocClick);
|
|
166
|
+
this._boundDocClick = null;
|
|
167
|
+
}
|
|
168
|
+
if (this._boundReposition) {
|
|
169
|
+
window.removeEventListener('scroll', this._boundReposition, { capture: true });
|
|
170
|
+
window.removeEventListener('resize', this._boundReposition);
|
|
171
|
+
this._boundReposition = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Clear positioning inline styles so re-open re-measures cleanly.
|
|
175
|
+
this.dom.style.top = '';
|
|
176
|
+
this.dom.style.left = '';
|
|
177
|
+
this.dom.removeAttribute('data-ln-popover-placement');
|
|
178
|
+
|
|
179
|
+
if (this.trigger) {
|
|
180
|
+
this.trigger.setAttribute('aria-expanded', 'false');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Restore teleport.
|
|
184
|
+
if (this._teleportRestore) {
|
|
185
|
+
this._teleportRestore();
|
|
186
|
+
this._teleportRestore = null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Remove from open stack.
|
|
190
|
+
const idx = openStack.indexOf(this);
|
|
191
|
+
if (idx !== -1) openStack.splice(idx, 1);
|
|
192
|
+
_maybeRemoveEscListener();
|
|
193
|
+
|
|
194
|
+
// Restore focus to trigger if Escape closed it; on outside-click
|
|
195
|
+
// we deliberately don't yank focus.
|
|
196
|
+
if (this._previousFocus && this.trigger && this._previousFocus === this.trigger) {
|
|
197
|
+
this.trigger.focus();
|
|
198
|
+
} else if (this.trigger && document.activeElement === document.body) {
|
|
199
|
+
// User dismissed via Escape — focus was inside popover; return it.
|
|
200
|
+
this.trigger.focus();
|
|
201
|
+
}
|
|
202
|
+
this._previousFocus = null;
|
|
203
|
+
|
|
204
|
+
dispatch(this.dom, 'ln-popover:close', {
|
|
205
|
+
popoverId: this.dom.id,
|
|
206
|
+
target: this.dom,
|
|
207
|
+
trigger: this.trigger
|
|
208
|
+
});
|
|
209
|
+
this.trigger = null;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
_component.prototype.destroy = function () {
|
|
213
|
+
if (!this.dom[DOM_ATTRIBUTE]) return;
|
|
214
|
+
if (this.isOpen) this._applyClose();
|
|
215
|
+
delete this.dom[DOM_ATTRIBUTE];
|
|
216
|
+
dispatch(this.dom, 'ln-popover:destroyed', {
|
|
217
|
+
popoverId: this.dom.id,
|
|
218
|
+
target: this.dom
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// ─── Trigger Component ─────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
function _triggerComponent(dom) {
|
|
225
|
+
this.dom = dom;
|
|
226
|
+
const popoverId = dom.getAttribute(TRIGGER_SELECTOR);
|
|
227
|
+
dom.setAttribute('aria-haspopup', 'dialog');
|
|
228
|
+
dom.setAttribute('aria-expanded', 'false');
|
|
229
|
+
dom.setAttribute('aria-controls', popoverId);
|
|
230
|
+
|
|
231
|
+
const self = this;
|
|
232
|
+
this._onClick = function (e) {
|
|
233
|
+
if (e.ctrlKey || e.metaKey || e.button === 1) return;
|
|
234
|
+
e.preventDefault();
|
|
235
|
+
const target = document.getElementById(popoverId);
|
|
236
|
+
if (!target || !target[DOM_ATTRIBUTE]) return;
|
|
237
|
+
target[DOM_ATTRIBUTE].toggle(dom);
|
|
238
|
+
};
|
|
239
|
+
dom.addEventListener('click', this._onClick);
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_triggerComponent.prototype.destroy = function () {
|
|
244
|
+
this.dom.removeEventListener('click', this._onClick);
|
|
245
|
+
delete this.dom[DOM_ATTRIBUTE + 'Trigger'];
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// ─── Registration ──────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
registerComponent(DOM_SELECTOR, DOM_ATTRIBUTE, _component, 'ln-popover', {
|
|
251
|
+
onAttributeChange: function (el) {
|
|
252
|
+
const instance = el[DOM_ATTRIBUTE];
|
|
253
|
+
if (!instance) return;
|
|
254
|
+
|
|
255
|
+
const value = el.getAttribute(DOM_SELECTOR);
|
|
256
|
+
const shouldBeOpen = value === 'open';
|
|
257
|
+
|
|
258
|
+
if (shouldBeOpen === instance.isOpen) return;
|
|
259
|
+
|
|
260
|
+
if (shouldBeOpen) {
|
|
261
|
+
const before = dispatchCancelable(el, 'ln-popover:before-open', {
|
|
262
|
+
popoverId: el.id,
|
|
263
|
+
target: el,
|
|
264
|
+
trigger: instance.trigger
|
|
265
|
+
});
|
|
266
|
+
if (before.defaultPrevented) {
|
|
267
|
+
el.setAttribute(DOM_SELECTOR, 'closed');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
instance._applyOpen(instance.trigger);
|
|
271
|
+
} else {
|
|
272
|
+
const before = dispatchCancelable(el, 'ln-popover:before-close', {
|
|
273
|
+
popoverId: el.id,
|
|
274
|
+
target: el,
|
|
275
|
+
trigger: instance.trigger
|
|
276
|
+
});
|
|
277
|
+
if (before.defaultPrevented) {
|
|
278
|
+
el.setAttribute(DOM_SELECTOR, 'open');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
instance._applyClose();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
registerComponent(TRIGGER_SELECTOR, DOM_ATTRIBUTE + 'Trigger', _triggerComponent, 'ln-popover-trigger');
|
|
287
|
+
})();
|
|
288
|
+
|