@keenmate/pure-admin-core 2.9.0-rc01 → 2.9.0-rc04
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 +61 -14
- package/dist/css/main.css +748 -2
- package/package.json +3 -1
- package/snippets/buttons.html +51 -0
- package/snippets/cards.html +34 -4
- package/snippets/comparison.html +26 -22
- package/snippets/manifest.json +180 -115
- package/snippets/range-group.html +125 -0
- package/snippets/splitter.html +216 -0
- package/snippets/statistics.html +31 -0
- package/src/js/btn-split-auto-absorb.js +327 -0
- package/src/js/command-palette.js +472 -0
- package/src/js/file-selector.js +1275 -0
- package/src/js/internal/logging.js +121 -0
- package/src/js/logic-tree-renderer.js +303 -0
- package/src/js/modal-dialogs.js +460 -0
- package/src/js/overflow.js +371 -0
- package/src/js/pa-stat-fit.js +184 -0
- package/src/js/range-group.js +663 -0
- package/src/js/search-autocomplete-v2.js +907 -0
- package/src/js/search-autocomplete.js +434 -0
- package/src/js/settings-panel.js +245 -0
- package/src/js/split-button.js +141 -0
- package/src/js/splitter.js +1323 -0
- package/src/js/toast-service.js +302 -0
- package/src/js/tooltips-popovers.js +275 -0
- package/src/js/virtual-scroll.js +143 -0
- package/src/js/virtual-textbox.js +803 -0
- package/src/scss/_core.scss +10 -0
- package/src/scss/core-components/_buttons.scss +59 -0
- package/src/scss/core-components/_cards.scss +206 -0
- package/src/scss/core-components/_overflow.scss +50 -0
- package/src/scss/core-components/_range-group.scss +474 -0
- package/src/scss/core-components/_splitter.scss +206 -0
- package/src/scss/core-components/_statistics.scss +163 -0
- package/src/scss/variables/_components.scss +56 -2
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
<!-- ================================
|
|
2
|
+
SPLITTER
|
|
3
|
+
Pure Admin Visual Framework
|
|
4
|
+
|
|
5
|
+
Resizable container with two or more panes. Drag a gutter to resize,
|
|
6
|
+
double-click to collapse / restore, arrow keys nudge size when the
|
|
7
|
+
gutter is focused.
|
|
8
|
+
|
|
9
|
+
CSS + structure here; drag, keyboard, persistence behaviour is wired up
|
|
10
|
+
by packages/core/src/js/splitter.js. The script auto-initializes on
|
|
11
|
+
[data-pa-splitter] at DOMContentLoaded.
|
|
12
|
+
|
|
13
|
+
Markup model: panes and gutters alternate (pane, gutter, pane, gutter,
|
|
14
|
+
…, pane). Each pane carries its own sizing attributes — there's no
|
|
15
|
+
"start" / "end" shorthand, every pane is generic. Works for any
|
|
16
|
+
N ≥ 2.
|
|
17
|
+
|
|
18
|
+
Orientation modifiers (match flexbox direction of the panes):
|
|
19
|
+
--horizontal panes side-by-side, vertical gutter (default)
|
|
20
|
+
--vertical panes stacked, horizontal gutter
|
|
21
|
+
|
|
22
|
+
The splitter takes whatever width/height its parent gives it — make
|
|
23
|
+
sure the parent has a sized cross axis (height for --horizontal,
|
|
24
|
+
width for --vertical) or the panes will collapse.
|
|
25
|
+
================================ -->
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
<!-- ================================
|
|
29
|
+
HORIZONTAL SPLIT (2 panes)
|
|
30
|
+
Side-by-side panes; vertical gutter. The first pane has a 200px
|
|
31
|
+
floor and 60% ceiling, defaults to 280px; the second pane absorbs
|
|
32
|
+
the leftover. Persists under "demo-sidebar".
|
|
33
|
+
================================ -->
|
|
34
|
+
|
|
35
|
+
<div class="pa-splitter pa-splitter--horizontal"
|
|
36
|
+
data-pa-splitter
|
|
37
|
+
data-pa-splitter-id="demo-sidebar"
|
|
38
|
+
style="height: 400px;">
|
|
39
|
+
<div class="pa-splitter__pane"
|
|
40
|
+
data-pa-splitter-size="280px"
|
|
41
|
+
data-pa-splitter-min="200px"
|
|
42
|
+
data-pa-splitter-max="60%">
|
|
43
|
+
<!-- Your sidebar / list / nav content -->
|
|
44
|
+
</div>
|
|
45
|
+
<div class="pa-splitter__gutter"
|
|
46
|
+
role="separator"
|
|
47
|
+
aria-orientation="vertical"
|
|
48
|
+
tabindex="0"></div>
|
|
49
|
+
<div class="pa-splitter__pane">
|
|
50
|
+
<!-- Your main / detail content (absorbs the leftover) -->
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
<!-- ================================
|
|
56
|
+
VERTICAL SPLIT (2 panes)
|
|
57
|
+
Stacked panes; horizontal gutter. Useful for editor + console / log
|
|
58
|
+
pane patterns.
|
|
59
|
+
================================ -->
|
|
60
|
+
|
|
61
|
+
<div class="pa-splitter pa-splitter--vertical"
|
|
62
|
+
data-pa-splitter
|
|
63
|
+
data-pa-splitter-id="demo-console"
|
|
64
|
+
style="height: 400px;">
|
|
65
|
+
<div class="pa-splitter__pane"
|
|
66
|
+
data-pa-splitter-size="60%"
|
|
67
|
+
data-pa-splitter-min="80px"
|
|
68
|
+
data-pa-splitter-max="80%">
|
|
69
|
+
<!-- Editor / primary content -->
|
|
70
|
+
</div>
|
|
71
|
+
<div class="pa-splitter__gutter"
|
|
72
|
+
role="separator"
|
|
73
|
+
aria-orientation="horizontal"
|
|
74
|
+
tabindex="0"></div>
|
|
75
|
+
<div class="pa-splitter__pane">
|
|
76
|
+
<!-- Console / log / secondary -->
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
<!-- ================================
|
|
82
|
+
SPACED CARDS
|
|
83
|
+
Set `gap` on the splitter root to put breathing room between the
|
|
84
|
+
panes and the gutter — the JS subtracts it from the available
|
|
85
|
+
space so percent constraints still resolve correctly. Override
|
|
86
|
+
gutter thickness via the --pa-splitter-gutter-size custom property.
|
|
87
|
+
================================ -->
|
|
88
|
+
|
|
89
|
+
<div class="pa-splitter pa-splitter--horizontal"
|
|
90
|
+
data-pa-splitter
|
|
91
|
+
style="height: 280px; gap: 1.6rem; --pa-splitter-gutter-size: 1rem;">
|
|
92
|
+
<div class="pa-splitter__pane"
|
|
93
|
+
data-pa-splitter-size="40%"
|
|
94
|
+
data-pa-splitter-min="25%"
|
|
95
|
+
data-pa-splitter-max="75%"
|
|
96
|
+
style="padding: 0;">
|
|
97
|
+
<div class="pa-card" style="height: 100%; margin: 0;">…</div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="pa-splitter__gutter"
|
|
100
|
+
role="separator"
|
|
101
|
+
aria-orientation="vertical"
|
|
102
|
+
tabindex="0"></div>
|
|
103
|
+
<div class="pa-splitter__pane" style="padding: 0;">
|
|
104
|
+
<div class="pa-card" style="height: 100%; margin: 0;">…</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
<!-- ================================
|
|
110
|
+
N-PANE LAYOUT (3 panes — sidebar + content + inspector)
|
|
111
|
+
Per-pane attributes drive sizing. The first and last panes opt into
|
|
112
|
+
rail collapse via the minimize marker. Panes without a
|
|
113
|
+
data-pa-splitter-size share the leftover equally.
|
|
114
|
+
================================ -->
|
|
115
|
+
|
|
116
|
+
<div class="pa-splitter pa-splitter--horizontal"
|
|
117
|
+
data-pa-splitter
|
|
118
|
+
data-pa-splitter-id="demo-three-pane"
|
|
119
|
+
style="height: 400px;">
|
|
120
|
+
<div class="pa-splitter__pane"
|
|
121
|
+
data-pa-splitter-size="240px"
|
|
122
|
+
data-pa-splitter-min="180px"
|
|
123
|
+
data-pa-splitter-max="360px"
|
|
124
|
+
data-pa-splitter-minimize>
|
|
125
|
+
<!-- File tree / nav (minimizable to a left rail) -->
|
|
126
|
+
</div>
|
|
127
|
+
<div class="pa-splitter__gutter"
|
|
128
|
+
role="separator"
|
|
129
|
+
aria-orientation="vertical"
|
|
130
|
+
tabindex="0"></div>
|
|
131
|
+
<div class="pa-splitter__pane"
|
|
132
|
+
data-pa-splitter-min="240px">
|
|
133
|
+
<!-- Editor / main content (fills the rest) -->
|
|
134
|
+
</div>
|
|
135
|
+
<div class="pa-splitter__gutter"
|
|
136
|
+
role="separator"
|
|
137
|
+
aria-orientation="vertical"
|
|
138
|
+
tabindex="0"></div>
|
|
139
|
+
<div class="pa-splitter__pane"
|
|
140
|
+
data-pa-splitter-size="280px"
|
|
141
|
+
data-pa-splitter-min="220px"
|
|
142
|
+
data-pa-splitter-max="420px"
|
|
143
|
+
data-pa-splitter-minimize>
|
|
144
|
+
<!-- Inspector / detail (minimizable to a right rail) -->
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
<!-- ================================
|
|
150
|
+
DATA ATTRIBUTES — root
|
|
151
|
+
data-pa-splitter marker; required
|
|
152
|
+
data-pa-splitter-id enables localStorage persistence
|
|
153
|
+
under "pa-splitter:<id>"
|
|
154
|
+
data-pa-splitter-step keyboard step in px (default: 10)
|
|
155
|
+
data-pa-splitter-rail-size rail width in px (default: 40)
|
|
156
|
+
data-pa-splitter-minimize-threshold drag-to-rail snap ratio (default: 0.40)
|
|
157
|
+
|
|
158
|
+
DATA ATTRIBUTES — per pane
|
|
159
|
+
data-pa-splitter-size "240px" or "30%" (default: shared leftover)
|
|
160
|
+
data-pa-splitter-min "150px" or "10%" (default: 0)
|
|
161
|
+
data-pa-splitter-max "400px" or "50%" (default: available)
|
|
162
|
+
data-pa-splitter-minimize marker — pane can collapse to a rail.
|
|
163
|
+
First / last panes dock against the
|
|
164
|
+
container edge; middle panes collapse
|
|
165
|
+
in place and split released slack
|
|
166
|
+
between both neighbours.
|
|
167
|
+
|
|
168
|
+
CHILD ATTRIBUTES
|
|
169
|
+
data-pa-splitter-toggle put on any element inside the splitter
|
|
170
|
+
(typically a button in the card header)
|
|
171
|
+
to trigger collapse / restore on click.
|
|
172
|
+
Toggles the enclosing pane if it's
|
|
173
|
+
minimizable.
|
|
174
|
+
|
|
175
|
+
RAIL-TITLE HOOK (non-card content)
|
|
176
|
+
Any element marked with `[data-pa-splitter-rail-title]` inside a
|
|
177
|
+
minimized pane rotates to vertical writing (sideways-rl). Use this
|
|
178
|
+
when you put plain content (not a `.pa-card`) in a minimizable pane:
|
|
179
|
+
|
|
180
|
+
<div class="pa-splitter__pane" data-pa-splitter-minimize>
|
|
181
|
+
<div data-pa-splitter-rail-title>📁 Files</div>
|
|
182
|
+
<!-- The rest of your pane content here -->
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
Cards adapt automatically — `_cards.scss` rotates the card header
|
|
186
|
+
inside any `.pa-splitter__pane--minimized` without needing the
|
|
187
|
+
attribute. No markup change needed for the card snippets above.
|
|
188
|
+
|
|
189
|
+
CSS CUSTOMIZATION
|
|
190
|
+
--pa-splitter-gutter-size gutter thickness (default: 6px)
|
|
191
|
+
--pa-splitter-rail-size rail width when a pane is minimized
|
|
192
|
+
(default: 40px / $splitter-rail-size).
|
|
193
|
+
JS picks this up via getComputedStyle, so
|
|
194
|
+
setting it on `:root` or per-instance via
|
|
195
|
+
inline style overrides the rail width
|
|
196
|
+
without also setting the data-attr.
|
|
197
|
+
gap space between panes and gutter
|
|
198
|
+
(native flexbox property)
|
|
199
|
+
|
|
200
|
+
MODIFIER CLASSES
|
|
201
|
+
pa-splitter--minimize-mirror flip the minimized title 180°
|
|
202
|
+
(transform: scale(-1,-1) on the heading
|
|
203
|
+
inside any `[data-pa-splitter-rail-title]`
|
|
204
|
+
or `.pa-card__header`)
|
|
205
|
+
pa-splitter--accordion (auto, JS-managed) added when the
|
|
206
|
+
container is narrower than the sum of
|
|
207
|
+
all pane mins + gutters/gaps/padding
|
|
208
|
+
AND there are 2+ minimizable panes.
|
|
209
|
+
While active, restoring one pane
|
|
210
|
+
auto-rails the others.
|
|
211
|
+
|
|
212
|
+
JAVASCRIPT API (window.PaSplitter)
|
|
213
|
+
PaSplitter.init(el) initialize a single element (idempotent)
|
|
214
|
+
PaSplitter.initAll(root?) initialize all uninitialized splitters
|
|
215
|
+
under root (default: document)
|
|
216
|
+
================================ -->
|
package/snippets/statistics.html
CHANGED
|
@@ -140,6 +140,23 @@
|
|
|
140
140
|
</div>
|
|
141
141
|
</div>
|
|
142
142
|
|
|
143
|
+
<!-- Fit + progressive-disclosure mode (--square[data-pa-stat-fit])
|
|
144
|
+
Opt in with the data-pa-stat-fit attribute and load pa-stat-fit.js.
|
|
145
|
+
The number is sized to fill the tile (never overflows, any char count);
|
|
146
|
+
lower-priority rows reveal as the tile earns BOTH width and height:
|
|
147
|
+
number → symbol → label → change → context.
|
|
148
|
+
Authored markup stays flat — the JS wraps __number + __symbol into a
|
|
149
|
+
__slot > __group at runtime. REQUIRES a height source on the tile
|
|
150
|
+
(grid cell with a set height / explicit height / aspect-ratio), because
|
|
151
|
+
container-type: size makes the box size independently of its content. -->
|
|
152
|
+
<div class="pa-stat pa-stat--square pa-stat--info" data-pa-stat-fit style="height: 16rem;">
|
|
153
|
+
<span class="pa-stat__symbol">$</span>
|
|
154
|
+
<span class="pa-stat__number">847K</span>
|
|
155
|
+
<div class="pa-stat__label">Monthly Revenue</div>
|
|
156
|
+
<div class="pa-stat__change pa-stat__change--positive">▲ 12.5% vs last month</div>
|
|
157
|
+
<div class="pa-stat__context">Updated 2 min ago</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
143
160
|
<!-- All six square colour variants -->
|
|
144
161
|
<div class="pa-stat pa-stat--square pa-stat--primary"> <div class="pa-stat__number">…</div><div class="pa-stat__symbol">…</div><div class="pa-stat__label">Primary</div> </div>
|
|
145
162
|
<div class="pa-stat pa-stat--square pa-stat--success"> <div class="pa-stat__number">…</div><div class="pa-stat__symbol">…</div><div class="pa-stat__label">Success</div> </div>
|
|
@@ -169,6 +186,13 @@ SUB-ELEMENTS:
|
|
|
169
186
|
(red), --neutral (muted).
|
|
170
187
|
.pa-stat__symbol Square variant only — the decorative
|
|
171
188
|
watermark symbol on the inline-end side.
|
|
189
|
+
.pa-stat__context Square fit-mode only — lowest-priority
|
|
190
|
+
(P4) caption row (e.g. "Updated 2m ago").
|
|
191
|
+
.pa-stat__slot Square fit-mode only — created by
|
|
192
|
+
pa-stat-fit.js; wraps the fit group.
|
|
193
|
+
.pa-stat__group Square fit-mode only — created by
|
|
194
|
+
pa-stat-fit.js; holds __number + __symbol
|
|
195
|
+
and carries the JS-computed font-size.
|
|
172
196
|
|
|
173
197
|
VARIANTS (mutually exclusive):
|
|
174
198
|
(default) Icon + content row. Pair with
|
|
@@ -180,6 +204,13 @@ VARIANTS (mutually exclusive):
|
|
|
180
204
|
--square Coloured tile, big __number, decorative
|
|
181
205
|
__symbol, bottom-pinned __label. Apply
|
|
182
206
|
a colour modifier on the same element.
|
|
207
|
+
[data-pa-stat-fit] Attribute (not a class), combine WITH
|
|
208
|
+
--square. Fit-to-box number + priority
|
|
209
|
+
disclosure ladder (number → symbol →
|
|
210
|
+
label → change → context, revealed as the
|
|
211
|
+
tile gains width AND height). Needs
|
|
212
|
+
pa-stat-fit.js loaded and a height source
|
|
213
|
+
on the tile.
|
|
183
214
|
|
|
184
215
|
SQUARE COLOUR MODIFIERS (apply with --square):
|
|
185
216
|
--primary, --success, --info, --warning, --danger, --secondary
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Admin — Split Button Auto-Absorb
|
|
3
|
+
*
|
|
4
|
+
* Extends the split button (.pa-btn-split) to automatically absorb adjacent
|
|
5
|
+
* sibling buttons sitting next to it inside a `.pa-btn-toolbar` row, when
|
|
6
|
+
* the row can't fit them all on one line. Absorbed items drop into the
|
|
7
|
+
* split button's own dropdown menu and return to the row as space comes
|
|
8
|
+
* back. Reuses the existing split-button toggle / menu / Floating UI
|
|
9
|
+
* positioning — no parallel chrome.
|
|
10
|
+
*
|
|
11
|
+
* Opt-in markup:
|
|
12
|
+
*
|
|
13
|
+
* <div class="pa-btn-toolbar">
|
|
14
|
+
* <button class="pa-btn pa-btn--primary">Save</button>
|
|
15
|
+
* <button class="pa-btn pa-btn--primary">Format</button>
|
|
16
|
+
* <button class="pa-btn pa-btn--primary"
|
|
17
|
+
* data-pa-absorb-priority="10">Refresh</button>
|
|
18
|
+
* <div class="pa-btn-split pa-btn-split--auto-absorb">
|
|
19
|
+
* <button class="pa-btn pa-btn--primary">Run</button>
|
|
20
|
+
* <button class="pa-btn pa-btn--primary pa-btn-split__toggle"
|
|
21
|
+
* onclick="toggleSplitMenu(event)">
|
|
22
|
+
* <i class="fas fa-chevron-down pa-btn-split__chevron"></i>
|
|
23
|
+
* </button>
|
|
24
|
+
* <div class="pa-btn-split__menu">
|
|
25
|
+
* <div class="pa-btn-split__menu-inner">
|
|
26
|
+
* <button class="pa-btn-split__item">Run with options…</button>
|
|
27
|
+
* </div>
|
|
28
|
+
* </div>
|
|
29
|
+
* </div>
|
|
30
|
+
* </div>
|
|
31
|
+
*
|
|
32
|
+
* Drop order: lowest `data-pa-absorb-priority` (default 0) drops first;
|
|
33
|
+
* ties broken by DOM order with the sibling closest to the split button
|
|
34
|
+
* dropping first (`data-pa-absorb-from="end"`, default). Flip with
|
|
35
|
+
* `="start"` to drop the leftmost sibling first instead.
|
|
36
|
+
*
|
|
37
|
+
* Absorbed items keep their original click handlers (the DOM node is
|
|
38
|
+
* preserved). On absorb, their classList is replaced with
|
|
39
|
+
* `pa-btn-split__item` so they pick up the standard menu-row styling; the
|
|
40
|
+
* original classList is stashed on the element and restored when the item
|
|
41
|
+
* returns to the bar.
|
|
42
|
+
*
|
|
43
|
+
* Existing static menu items (rendered into `__menu-inner` server-side)
|
|
44
|
+
* stay BELOW absorbed items: the first static child is captured at init
|
|
45
|
+
* and used as the insertBefore anchor.
|
|
46
|
+
*
|
|
47
|
+
* Layout contract: the bar needs `min-width: 0; overflow: hidden;
|
|
48
|
+
* flex-shrink: 1` so it actually shrinks below its content (otherwise
|
|
49
|
+
* `scrollWidth > clientWidth` never goes truthful). `_buttons.scss`
|
|
50
|
+
* supplies that for `.pa-btn-toolbar`; any flex row with equivalent
|
|
51
|
+
* styling works.
|
|
52
|
+
*
|
|
53
|
+
* Opt in to console diagnostics: `window.PA_BTN_SPLIT_AUTO_ABSORB_DEBUG = true`
|
|
54
|
+
* before page load.
|
|
55
|
+
*
|
|
56
|
+
* Public API (window.PaBtnSplitAutoAbsorb):
|
|
57
|
+
* init(splitContainer) — idempotent single-container init
|
|
58
|
+
* initAll(scope) — initialize all uninitialized auto-absorb split
|
|
59
|
+
* buttons under scope
|
|
60
|
+
*/
|
|
61
|
+
(function () {
|
|
62
|
+
'use strict';
|
|
63
|
+
|
|
64
|
+
var INIT_FLAG = '__paBtnSplitAutoAbsorbInit';
|
|
65
|
+
var SELECTOR = '.pa-btn-split.pa-btn-split--auto-absorb';
|
|
66
|
+
var STASH_KEY = '__paAbsorbOrigClass';
|
|
67
|
+
|
|
68
|
+
function init(splitContainer) {
|
|
69
|
+
if (!splitContainer || splitContainer[INIT_FLAG]) return;
|
|
70
|
+
if (!splitContainer.classList.contains('pa-btn-split')) return;
|
|
71
|
+
if (!splitContainer.classList.contains('pa-btn-split--auto-absorb')) return;
|
|
72
|
+
|
|
73
|
+
var bar = splitContainer.parentNode;
|
|
74
|
+
if (!bar || bar.nodeType !== 1) return;
|
|
75
|
+
|
|
76
|
+
var menu = splitContainer.querySelector('.pa-btn-split__menu');
|
|
77
|
+
var menuInner = menu && menu.querySelector('.pa-btn-split__menu-inner');
|
|
78
|
+
if (!menuInner) return;
|
|
79
|
+
|
|
80
|
+
splitContainer[INIT_FLAG] = true;
|
|
81
|
+
|
|
82
|
+
// First static menu child captured at init — absorbed items are
|
|
83
|
+
// inserted BEFORE it so static items stay at the bottom of the menu.
|
|
84
|
+
// If there are no static items this stays null and insertBefore
|
|
85
|
+
// degrades to appendChild, which is exactly what we want.
|
|
86
|
+
var firstStaticItem = menuInner.firstElementChild || null;
|
|
87
|
+
|
|
88
|
+
// The split button's OWN primary action (the non-toggle `.pa-btn`) and
|
|
89
|
+
// its toggle. When even an empty bar can't fit the whole split button,
|
|
90
|
+
// the primary folds into the menu as a last resort and only the toggle
|
|
91
|
+
// stays visible (see absorbPrimary / the final relayout step).
|
|
92
|
+
var toggleBtn = splitContainer.querySelector('.pa-btn-split__toggle');
|
|
93
|
+
var primaryBtn = null;
|
|
94
|
+
for (var pc = splitContainer.firstElementChild; pc; pc = pc.nextElementSibling) {
|
|
95
|
+
if (pc !== toggleBtn && pc.classList &&
|
|
96
|
+
pc.classList.contains('pa-btn') &&
|
|
97
|
+
!pc.classList.contains('pa-btn-split__toggle')) {
|
|
98
|
+
primaryBtn = pc;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
var canCollapsePrimary = !!(primaryBtn && toggleBtn);
|
|
103
|
+
|
|
104
|
+
// Collect siblings that come BEFORE the split container in the bar.
|
|
105
|
+
// We snapshot at init time — siblings added after init won't be
|
|
106
|
+
// tracked (call init again, or refactor to MutationObserver later).
|
|
107
|
+
var candidates = [];
|
|
108
|
+
var idx = 0;
|
|
109
|
+
for (var node = bar.firstElementChild; node && node !== splitContainer; node = node.nextElementSibling) {
|
|
110
|
+
var p = parseInt(node.getAttribute('data-pa-absorb-priority'), 10);
|
|
111
|
+
candidates.push({
|
|
112
|
+
el: node,
|
|
113
|
+
priority: isNaN(p) ? 0 : p,
|
|
114
|
+
domIndex: idx
|
|
115
|
+
});
|
|
116
|
+
idx++;
|
|
117
|
+
}
|
|
118
|
+
if (candidates.length === 0 && !canCollapsePrimary) return;
|
|
119
|
+
|
|
120
|
+
var dropOrder = [];
|
|
121
|
+
function buildDropOrder() {
|
|
122
|
+
var fromAttr = splitContainer.getAttribute('data-pa-absorb-from');
|
|
123
|
+
var dropFromStart = fromAttr === 'start';
|
|
124
|
+
dropOrder = candidates.slice().sort(function (a, b) {
|
|
125
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
126
|
+
// Default ("end") drops the sibling nearest the split
|
|
127
|
+
// button first — feels right because that's the one
|
|
128
|
+
// visually adjacent to the menu it falls into.
|
|
129
|
+
return dropFromStart ? (a.domIndex - b.domIndex) : (b.domIndex - a.domIndex);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
buildDropOrder();
|
|
133
|
+
|
|
134
|
+
var DEBUG = window.PA_BTN_SPLIT_AUTO_ABSORB_DEBUG === true;
|
|
135
|
+
var label = '[pa-btn-split-auto-absorb#' + (splitContainer.id || candidates.map(function (c) { return c.el.tagName; }).join('-')).slice(0, 40) + ']';
|
|
136
|
+
function log() {
|
|
137
|
+
if (!DEBUG) return;
|
|
138
|
+
var args = Array.prototype.slice.call(arguments);
|
|
139
|
+
console.log.apply(console, [label].concat(args));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Rewrite any `<span class="pa-btn__icon">` inside an absorbed item
|
|
143
|
+
// to the canonical `pa-btn-split__item-icon`. The pa-btn icon column
|
|
144
|
+
// width is scoped to `.pa-btn:has(.pa-btn__icon) .pa-btn__icon` —
|
|
145
|
+
// once the outer button stops being a `.pa-btn`, that rule no
|
|
146
|
+
// longer matches and the icon span goes auto-width, throwing
|
|
147
|
+
// absorbed labels out of column with static items.
|
|
148
|
+
function rewriteIcons(el) {
|
|
149
|
+
var icons = el.querySelectorAll('.pa-btn__icon');
|
|
150
|
+
for (var i = 0; i < icons.length; i++) {
|
|
151
|
+
if (icons[i][STASH_KEY] == null) {
|
|
152
|
+
icons[i][STASH_KEY] = icons[i].className;
|
|
153
|
+
}
|
|
154
|
+
icons[i].className = 'pa-btn-split__item-icon';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function restoreIcons(el) {
|
|
159
|
+
// After a previous absorb cycle the icon spans now carry our
|
|
160
|
+
// rewritten class. Find them and put their original class back.
|
|
161
|
+
var icons = el.querySelectorAll('.pa-btn-split__item-icon');
|
|
162
|
+
for (var i = 0; i < icons.length; i++) {
|
|
163
|
+
if (icons[i][STASH_KEY] != null) {
|
|
164
|
+
icons[i].className = icons[i][STASH_KEY];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function moveToMenu(el) {
|
|
170
|
+
if (el[STASH_KEY] == null) {
|
|
171
|
+
el[STASH_KEY] = el.className;
|
|
172
|
+
}
|
|
173
|
+
el.className = 'pa-btn-split__item';
|
|
174
|
+
rewriteIcons(el);
|
|
175
|
+
menuInner.insertBefore(el, firstStaticItem);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function moveToBar(el) {
|
|
179
|
+
if (el[STASH_KEY] != null) {
|
|
180
|
+
el.className = el[STASH_KEY];
|
|
181
|
+
restoreIcons(el);
|
|
182
|
+
}
|
|
183
|
+
// insertBefore moves the node if already attached, so calling
|
|
184
|
+
// it unconditionally keeps siblings in their original DOM
|
|
185
|
+
// order even after a drop-from-start cycle reshuffled them.
|
|
186
|
+
bar.insertBefore(el, splitContainer);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Last-resort collapse: fold the split button's OWN primary action
|
|
190
|
+
// into the menu (as the top item) so only the toggle chevron remains
|
|
191
|
+
// when even an otherwise-empty bar can't fit the full split button.
|
|
192
|
+
var primaryAbsorbed = false;
|
|
193
|
+
function absorbPrimary() {
|
|
194
|
+
if (primaryAbsorbed || !canCollapsePrimary) return;
|
|
195
|
+
if (primaryBtn[STASH_KEY] == null) primaryBtn[STASH_KEY] = primaryBtn.className;
|
|
196
|
+
primaryBtn.className = 'pa-btn-split__item';
|
|
197
|
+
rewriteIcons(primaryBtn);
|
|
198
|
+
// Above absorbed siblings + static items — the primary action is
|
|
199
|
+
// the most important row, so it sits at the top of the menu.
|
|
200
|
+
menuInner.insertBefore(primaryBtn, menuInner.firstChild);
|
|
201
|
+
splitContainer.classList.add('pa-btn-split--collapsed');
|
|
202
|
+
primaryAbsorbed = true;
|
|
203
|
+
}
|
|
204
|
+
function restorePrimary() {
|
|
205
|
+
if (!primaryAbsorbed || !canCollapsePrimary) return;
|
|
206
|
+
if (primaryBtn[STASH_KEY] != null) {
|
|
207
|
+
primaryBtn.className = primaryBtn[STASH_KEY];
|
|
208
|
+
restoreIcons(primaryBtn);
|
|
209
|
+
}
|
|
210
|
+
// Back to its slot: immediately before the toggle.
|
|
211
|
+
splitContainer.insertBefore(primaryBtn, toggleBtn);
|
|
212
|
+
splitContainer.classList.remove('pa-btn-split--collapsed');
|
|
213
|
+
primaryAbsorbed = false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function relayout() {
|
|
217
|
+
log('relayout entered');
|
|
218
|
+
|
|
219
|
+
// Step 1 — fully expand before measuring: primary action back in
|
|
220
|
+
// the split button, all candidates back in the bar in original
|
|
221
|
+
// DOM order (immediately before the split container).
|
|
222
|
+
restorePrimary();
|
|
223
|
+
candidates.slice().sort(function (a, b) {
|
|
224
|
+
return a.domIndex - b.domIndex;
|
|
225
|
+
}).forEach(function (item) {
|
|
226
|
+
moveToBar(item.el);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
var clientW = bar.clientWidth;
|
|
230
|
+
var scrollW = bar.scrollWidth;
|
|
231
|
+
log('step 2', {
|
|
232
|
+
clientW: clientW,
|
|
233
|
+
scrollW: scrollW,
|
|
234
|
+
fitsAll: scrollW <= clientW
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (scrollW <= clientW) {
|
|
238
|
+
log('all fits');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Step 3 — absorb siblings in drop order until it fits.
|
|
243
|
+
for (var i = 0; i < dropOrder.length; i++) {
|
|
244
|
+
if (bar.scrollWidth <= bar.clientWidth) break;
|
|
245
|
+
moveToMenu(dropOrder[i].el);
|
|
246
|
+
log('absorbed', dropOrder[i].el);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Step 4 — last resort: every sibling is absorbed and it STILL
|
|
250
|
+
// overflows (a very narrow card header). Fold the split button's
|
|
251
|
+
// own primary action into the menu so only the toggle remains;
|
|
252
|
+
// otherwise the primary + toggle spill past the bar's
|
|
253
|
+
// overflow:hidden edge and the toggle becomes unclickable.
|
|
254
|
+
if (bar.scrollWidth > bar.clientWidth) {
|
|
255
|
+
absorbPrimary();
|
|
256
|
+
log('absorbed primary — collapsed to toggle');
|
|
257
|
+
}
|
|
258
|
+
log('done');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// A stable ancestor whose width tracks the card / window, NOT our button
|
|
262
|
+
// shuffling: the nearest `.pa-card__header` (its width is the card width
|
|
263
|
+
// no matter how the shrinkable `.pa-card__actions` collapses around the
|
|
264
|
+
// toolbar), else the toolbar's own parent for standalone use (a sized /
|
|
265
|
+
// resizable wrapper). It must NOT be the shrinkable actions wrapper —
|
|
266
|
+
// that resizes in response to OUR own absorb/restore, which fed a
|
|
267
|
+
// nondeterministic feedback storm (the reload-vs-resize hysteresis).
|
|
268
|
+
var widthHost = (bar.closest && bar.closest('.pa-card__header')) || bar.parentNode;
|
|
269
|
+
|
|
270
|
+
// Coalesce a burst of size changes (ours + real layout) into ONE
|
|
271
|
+
// relayout on the next frame. relayout is deterministic — it always
|
|
272
|
+
// measures from a full restore — so one pass per frame converges without
|
|
273
|
+
// the ResizeObserver loop-limit suppression that can freeze a transient
|
|
274
|
+
// state.
|
|
275
|
+
var relayoutScheduled = false;
|
|
276
|
+
function scheduleRelayout() {
|
|
277
|
+
if (relayoutScheduled) return;
|
|
278
|
+
relayoutScheduled = true;
|
|
279
|
+
requestAnimationFrame(function () {
|
|
280
|
+
relayoutScheduled = false;
|
|
281
|
+
relayout();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// First pass after layout settles.
|
|
286
|
+
requestAnimationFrame(relayout);
|
|
287
|
+
|
|
288
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
289
|
+
var ro = new ResizeObserver(scheduleRelayout);
|
|
290
|
+
// The toolbar itself — catches ANY change in the space available to
|
|
291
|
+
// it, including the header title being given a larger `min-width`
|
|
292
|
+
// floor (which widens the title and narrows the toolbar WITHOUT
|
|
293
|
+
// changing the header's outer width). A bar-only observer isn't
|
|
294
|
+
// enough on its own: once the toolbar collapses to a lone toggle its
|
|
295
|
+
// box stops shrinking, so…
|
|
296
|
+
ro.observe(bar);
|
|
297
|
+
// …also watch the stable width host, so widening the card re-fires
|
|
298
|
+
// and lets relayout re-expand from the collapsed state.
|
|
299
|
+
if (widthHost && widthHost.nodeType === 1 && widthHost !== bar) {
|
|
300
|
+
ro.observe(widthHost);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// React to live `data-pa-absorb-from` flips so consumers can change
|
|
305
|
+
// drop direction at runtime without re-initializing.
|
|
306
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
307
|
+
var mo = new MutationObserver(function () {
|
|
308
|
+
buildDropOrder();
|
|
309
|
+
relayout();
|
|
310
|
+
});
|
|
311
|
+
mo.observe(splitContainer, { attributes: true, attributeFilter: ['data-pa-absorb-from'] });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function initAll(scope) {
|
|
316
|
+
var nodes = (scope || document).querySelectorAll(SELECTOR);
|
|
317
|
+
for (var i = 0; i < nodes.length; i++) init(nodes[i]);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
window.PaBtnSplitAutoAbsorb = { init: init, initAll: initAll };
|
|
321
|
+
|
|
322
|
+
if (document.readyState === 'loading') {
|
|
323
|
+
document.addEventListener('DOMContentLoaded', function () { initAll(); });
|
|
324
|
+
} else {
|
|
325
|
+
initAll();
|
|
326
|
+
}
|
|
327
|
+
})();
|