@aquera/nile-elements 2.0.17 → 2.0.19
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 +8 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +629 -440
- package/dist/nile-breadcrumb/nile-breadcrumb.cjs.js +1 -1
- package/dist/nile-breadcrumb/nile-breadcrumb.cjs.js.map +1 -1
- package/dist/nile-breadcrumb/nile-breadcrumb.esm.js +2 -2
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js.map +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.cjs.js +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.cjs.js.map +1 -1
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.esm.js +14 -2
- package/dist/nile-breadcrumb-item/nile-breadcrumb-item.esm.js +4 -4
- package/dist/nile-minimap/index.cjs.js +2 -0
- package/dist/nile-minimap/index.cjs.js.map +1 -0
- package/dist/nile-minimap/index.esm.js +1 -0
- package/dist/nile-minimap/nile-minimap.cjs.js +2 -0
- package/dist/nile-minimap/nile-minimap.cjs.js.map +1 -0
- package/dist/nile-minimap/nile-minimap.css.cjs.js +2 -0
- package/dist/nile-minimap/nile-minimap.css.cjs.js.map +1 -0
- package/dist/nile-minimap/nile-minimap.css.esm.js +150 -0
- package/dist/nile-minimap/nile-minimap.esm.js +22 -0
- package/dist/nile-select/index.cjs.js +1 -1
- package/dist/nile-select/index.esm.js +1 -1
- package/dist/nile-select/nile-select.cjs.js +2 -2
- package/dist/nile-select/nile-select.cjs.js.map +1 -1
- package/dist/nile-select/nile-select.esm.js +1 -1
- package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
- package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
- package/dist/nile-select/virtual-scroll-helper.esm.js +48 -47
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -4
- package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
- package/dist/nile-virtual-select/nile-virtual-select.esm.js +2 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/nile-breadcrumb/nile-breadcrumb.d.ts +7 -0
- package/dist/src/nile-breadcrumb/nile-breadcrumb.js +24 -1
- package/dist/src/nile-breadcrumb/nile-breadcrumb.js.map +1 -1
- package/dist/src/nile-breadcrumb/nile-breadcrumb.test.js +51 -0
- package/dist/src/nile-breadcrumb/nile-breadcrumb.test.js.map +1 -1
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.css.js +12 -0
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.css.js.map +1 -1
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.d.ts +5 -0
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js +9 -0
- package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js.map +1 -1
- package/dist/src/nile-minimap/index.d.ts +1 -0
- package/dist/src/nile-minimap/index.js +2 -0
- package/dist/src/nile-minimap/index.js.map +1 -0
- package/dist/src/nile-minimap/nile-minimap.css.d.ts +12 -0
- package/dist/src/nile-minimap/nile-minimap.css.js +162 -0
- package/dist/src/nile-minimap/nile-minimap.css.js.map +1 -0
- package/dist/src/nile-minimap/nile-minimap.d.ts +131 -0
- package/dist/src/nile-minimap/nile-minimap.js +704 -0
- package/dist/src/nile-minimap/nile-minimap.js.map +1 -0
- package/dist/src/nile-minimap/nile-minimap.test.d.ts +1 -0
- package/dist/src/nile-minimap/nile-minimap.test.js +785 -0
- package/dist/src/nile-minimap/nile-minimap.test.js.map +1 -0
- package/dist/src/nile-select/nile-select.d.ts +1 -0
- package/dist/src/nile-select/nile-select.js +16 -1
- package/dist/src/nile-select/nile-select.js.map +1 -1
- package/dist/src/nile-select/nile-select.test.js +430 -5
- package/dist/src/nile-select/nile-select.test.js.map +1 -1
- package/dist/src/nile-select/virtual-scroll-helper.js +7 -4
- package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +3 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.js +14 -6
- package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.test.js +115 -1
- package/dist/src/nile-virtual-select/nile-virtual-select.test.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -0
- package/src/nile-breadcrumb/nile-breadcrumb.test.ts +57 -0
- package/src/nile-breadcrumb/nile-breadcrumb.ts +19 -0
- package/src/nile-breadcrumb-item/nile-breadcrumb-item.css.ts +12 -0
- package/src/nile-breadcrumb-item/nile-breadcrumb-item.ts +7 -0
- package/src/nile-minimap/index.ts +1 -0
- package/src/nile-minimap/nile-minimap.css.ts +164 -0
- package/src/nile-minimap/nile-minimap.test.ts +924 -0
- package/src/nile-minimap/nile-minimap.ts +796 -0
- package/src/nile-select/nile-select.test.ts +560 -5
- package/src/nile-select/nile-select.ts +21 -1
- package/src/nile-select/virtual-scroll-helper.ts +7 -4
- package/src/nile-virtual-select/nile-virtual-select.test.ts +139 -1
- package/src/nile-virtual-select/nile-virtual-select.ts +17 -6
- package/vscode-html-custom-data.json +138 -36
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Aquera Inc 2023
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the BSD-3-Clause license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html } from 'lit';
|
|
9
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
10
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
11
|
+
import type { CSSResultGroup, PropertyValues, TemplateResult } from 'lit';
|
|
12
|
+
import NileElement from '../internal/nile-element';
|
|
13
|
+
import { styles } from './nile-minimap.css';
|
|
14
|
+
|
|
15
|
+
export type NileMinimapOrientation = 'horizontal' | 'vertical' | 'auto';
|
|
16
|
+
export type NileMinimapMode = 'segments' | 'track';
|
|
17
|
+
export type NileMinimapAxis = 'horizontal' | 'vertical';
|
|
18
|
+
export type NileMinimapSource = 'drag' | 'click' | 'wheel' | 'keyboard';
|
|
19
|
+
|
|
20
|
+
/** Formats the `aria-valuetext` in segments mode. */
|
|
21
|
+
export type NileMinimapValueTextFormatter = (
|
|
22
|
+
first: number,
|
|
23
|
+
last: number,
|
|
24
|
+
total: number
|
|
25
|
+
) => string;
|
|
26
|
+
|
|
27
|
+
interface AxisMap {
|
|
28
|
+
len: 'clientWidth' | 'clientHeight';
|
|
29
|
+
size: 'scrollWidth' | 'scrollHeight';
|
|
30
|
+
pos: 'scrollLeft' | 'scrollTop';
|
|
31
|
+
border: 'clientLeft' | 'clientTop';
|
|
32
|
+
rectStart: 'left' | 'top';
|
|
33
|
+
rectExt: 'width' | 'height';
|
|
34
|
+
client: 'clientX' | 'clientY';
|
|
35
|
+
scrollKey: 'left' | 'top';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const AXES: Record<NileMinimapAxis, AxisMap> = {
|
|
39
|
+
horizontal: {
|
|
40
|
+
len: 'clientWidth',
|
|
41
|
+
size: 'scrollWidth',
|
|
42
|
+
pos: 'scrollLeft',
|
|
43
|
+
border: 'clientLeft',
|
|
44
|
+
rectStart: 'left',
|
|
45
|
+
rectExt: 'width',
|
|
46
|
+
client: 'clientX',
|
|
47
|
+
scrollKey: 'left',
|
|
48
|
+
},
|
|
49
|
+
vertical: {
|
|
50
|
+
len: 'clientHeight',
|
|
51
|
+
size: 'scrollHeight',
|
|
52
|
+
pos: 'scrollTop',
|
|
53
|
+
border: 'clientTop',
|
|
54
|
+
rectStart: 'top',
|
|
55
|
+
rectExt: 'height',
|
|
56
|
+
client: 'clientY',
|
|
57
|
+
scrollKey: 'top',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const MIN_THUMB = 14;
|
|
62
|
+
const MIN_SEG = 3;
|
|
63
|
+
const MAX_ITEMS = 48;
|
|
64
|
+
|
|
65
|
+
interface SegGeo {
|
|
66
|
+
start: number;
|
|
67
|
+
len: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const defaultValueText: NileMinimapValueTextFormatter = (
|
|
71
|
+
first,
|
|
72
|
+
last,
|
|
73
|
+
total
|
|
74
|
+
) => {
|
|
75
|
+
if (first === last) return `Item ${first} of ${total}`;
|
|
76
|
+
return `Items ${first}–${last} of ${total}`;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A small overview of any scrolling container. It mirrors the container's
|
|
81
|
+
* scroll position and lets people drag, click, wheel or key to another part of
|
|
82
|
+
* it. One axis at a time: horizontal for boards and wide tables, vertical for
|
|
83
|
+
* long documents and lists. It adds to the native scrollbar; it never replaces
|
|
84
|
+
* it.
|
|
85
|
+
*
|
|
86
|
+
* NxtGen theme only: styles use `--ng-*` tokens with no legacy `--nile-*`
|
|
87
|
+
* fallbacks, so the page must load the NxtGen theme (e.g. `nxtgen.css`).
|
|
88
|
+
*
|
|
89
|
+
* @tag nile-minimap
|
|
90
|
+
* @summary Scroll overview that attaches to a scroll container by id and lets people navigate it.
|
|
91
|
+
*
|
|
92
|
+
* @event nile-navigate - Fired before the minimap scrolls the container. Cancelable: `preventDefault()` skips the scroll. Detail: { position: number, ratio: number, source: 'drag' | 'click' | 'wheel' | 'keyboard', orientation: 'horizontal' | 'vertical' }
|
|
93
|
+
* @event nile-overflow-change - Fired once on first measure, then whenever overflow on the active axis starts or stops. Detail: { overflowing: boolean, orientation: 'horizontal' | 'vertical' }
|
|
94
|
+
*
|
|
95
|
+
* @csspart base - The container surface.
|
|
96
|
+
* @csspart track - The scaled area; drawn as a rail in track mode.
|
|
97
|
+
* @csspart segment - One block per item matched by `itemSelector`.
|
|
98
|
+
* @csspart viewport - The focusable, draggable thumb (`role="scrollbar"`).
|
|
99
|
+
*
|
|
100
|
+
* @cssproperty --nile-minimap-width - Width of the minimap (112px). Swaps with height in vertical mode.
|
|
101
|
+
* @cssproperty --nile-minimap-height - Height of the minimap (40px). Swaps with width in vertical mode.
|
|
102
|
+
*/
|
|
103
|
+
@customElement('nile-minimap')
|
|
104
|
+
export class NileMinimap extends NileElement {
|
|
105
|
+
static styles: CSSResultGroup = styles;
|
|
106
|
+
|
|
107
|
+
/** Id of the scroll container, resolved in the minimap's root node. The only way to attach. */
|
|
108
|
+
@property({ reflect: true }) for = '';
|
|
109
|
+
|
|
110
|
+
/** Which axis the minimap represents. `auto` picks the overflowing axis (horizontal first). */
|
|
111
|
+
@property({ reflect: true }) orientation: NileMinimapOrientation = 'auto';
|
|
112
|
+
|
|
113
|
+
/** `segments` draws one block per item; `track` draws a single rail. */
|
|
114
|
+
@property({ reflect: true }) mode: NileMinimapMode = 'segments';
|
|
115
|
+
|
|
116
|
+
/** Selector queried on the container to find the segment items. */
|
|
117
|
+
@property() itemSelector = ':scope > *';
|
|
118
|
+
|
|
119
|
+
/** Hides the host while nothing overflows. */
|
|
120
|
+
@property({ type: Boolean, reflect: true }) hideWhenFits = true;
|
|
121
|
+
|
|
122
|
+
/** Smooth scroll for click and keyboard. Always off under reduced motion. */
|
|
123
|
+
@property({ type: Boolean }) smooth = true;
|
|
124
|
+
|
|
125
|
+
/** Still mirrors scrolling, but can't be used. */
|
|
126
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
127
|
+
|
|
128
|
+
/** Accessible name on the viewport. */
|
|
129
|
+
@property() label = 'Scroll position';
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Keeps the minimap hidden until the container scrolls, then fades it out
|
|
133
|
+
* after `hideDelay` ms without scrolling. It stays visible while it is
|
|
134
|
+
* hovered, focused or being dragged. Hidden, it keeps its space and ignores
|
|
135
|
+
* the pointer.
|
|
136
|
+
*/
|
|
137
|
+
@property({ type: Boolean, reflect: true }) autoHide = false;
|
|
138
|
+
|
|
139
|
+
/** With `autoHide`, milliseconds without scrolling before the minimap fades out. */
|
|
140
|
+
@property({ type: Number }) hideDelay = 1200;
|
|
141
|
+
|
|
142
|
+
/** Builds the `aria-valuetext` in segments mode. */
|
|
143
|
+
@property({ attribute: false })
|
|
144
|
+
formatValueText: NileMinimapValueTextFormatter = defaultValueText;
|
|
145
|
+
|
|
146
|
+
@query('.base') private base!: HTMLDivElement;
|
|
147
|
+
|
|
148
|
+
@query('.track') private trackEl!: HTMLDivElement;
|
|
149
|
+
|
|
150
|
+
@query('.segments') private segsEl!: HTMLDivElement;
|
|
151
|
+
|
|
152
|
+
@query('.viewport') private viewport!: HTMLDivElement;
|
|
153
|
+
|
|
154
|
+
@state() private resolvedDir: NileMinimapAxis = 'horizontal';
|
|
155
|
+
|
|
156
|
+
private target: HTMLElement | null = null;
|
|
157
|
+
|
|
158
|
+
private axis: AxisMap = AXES.horizontal;
|
|
159
|
+
|
|
160
|
+
private geo: SegGeo[] = [];
|
|
161
|
+
|
|
162
|
+
private observed = new Set<Element>();
|
|
163
|
+
|
|
164
|
+
private overflowing: boolean | null = null;
|
|
165
|
+
|
|
166
|
+
private segOn = false;
|
|
167
|
+
|
|
168
|
+
private rtl = false;
|
|
169
|
+
|
|
170
|
+
private raf = 0;
|
|
171
|
+
|
|
172
|
+
private full = false;
|
|
173
|
+
|
|
174
|
+
private pending: number | null = null;
|
|
175
|
+
|
|
176
|
+
private pendingAt = 0;
|
|
177
|
+
|
|
178
|
+
private retried = false;
|
|
179
|
+
|
|
180
|
+
private retryRaf = 0;
|
|
181
|
+
|
|
182
|
+
// Live geometry cache used by the pointer/keyboard handlers.
|
|
183
|
+
private contentSize = 0;
|
|
184
|
+
|
|
185
|
+
private clientSize = 0;
|
|
186
|
+
|
|
187
|
+
private maxScroll = 0;
|
|
188
|
+
|
|
189
|
+
private trackLen = 0;
|
|
190
|
+
|
|
191
|
+
private vpLen = 0;
|
|
192
|
+
|
|
193
|
+
private ro: ResizeObserver | null = null;
|
|
194
|
+
|
|
195
|
+
private mo: MutationObserver | null = null;
|
|
196
|
+
|
|
197
|
+
private onScroll = () => {
|
|
198
|
+
this.schedule(false);
|
|
199
|
+
this.reveal();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// autoHide state: the minimap stays visible while any of these hold.
|
|
203
|
+
private hideTimer = 0;
|
|
204
|
+
|
|
205
|
+
private hovered = false;
|
|
206
|
+
|
|
207
|
+
private focused = false;
|
|
208
|
+
|
|
209
|
+
private onPointerEnter = () => {
|
|
210
|
+
this.hovered = true;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
private onPointerLeave = () => {
|
|
214
|
+
this.hovered = false;
|
|
215
|
+
this.queueHide();
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
private onFocusIn = () => {
|
|
219
|
+
this.focused = true;
|
|
220
|
+
this.reveal();
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
private onFocusOut = () => {
|
|
224
|
+
this.focused = false;
|
|
225
|
+
this.queueHide();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Drag state.
|
|
229
|
+
private drag: { pointer: number; start: number } | null = null;
|
|
230
|
+
|
|
231
|
+
private dragRaf = 0;
|
|
232
|
+
|
|
233
|
+
private lastMove: PointerEvent | null = null;
|
|
234
|
+
|
|
235
|
+
private reduceMotion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
236
|
+
|
|
237
|
+
connectedCallback(): void {
|
|
238
|
+
super.connectedCallback();
|
|
239
|
+
this.addEventListener('pointerenter', this.onPointerEnter);
|
|
240
|
+
this.addEventListener('pointerleave', this.onPointerLeave);
|
|
241
|
+
this.addEventListener('focusin', this.onFocusIn);
|
|
242
|
+
this.addEventListener('focusout', this.onFocusOut);
|
|
243
|
+
if (this.hasUpdated) {
|
|
244
|
+
this.attach();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
disconnectedCallback(): void {
|
|
249
|
+
this.detach();
|
|
250
|
+
this.removeEventListener('pointerenter', this.onPointerEnter);
|
|
251
|
+
this.removeEventListener('pointerleave', this.onPointerLeave);
|
|
252
|
+
this.removeEventListener('focusin', this.onFocusIn);
|
|
253
|
+
this.removeEventListener('focusout', this.onFocusOut);
|
|
254
|
+
clearTimeout(this.hideTimer);
|
|
255
|
+
this.hideTimer = 0;
|
|
256
|
+
super.disconnectedCallback();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
firstUpdated(): void {
|
|
260
|
+
this.bindInteractions();
|
|
261
|
+
this.attach();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
updated(changed: PropertyValues): void {
|
|
265
|
+
if (changed.has('autoHide')) {
|
|
266
|
+
clearTimeout(this.hideTimer);
|
|
267
|
+
this.hideTimer = 0;
|
|
268
|
+
this.toggleAttribute('data-idle', this.autoHide && !this.held);
|
|
269
|
+
}
|
|
270
|
+
if (changed.has('for')) {
|
|
271
|
+
this.attach();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
changed.has('orientation') ||
|
|
276
|
+
changed.has('mode') ||
|
|
277
|
+
changed.has('itemSelector') ||
|
|
278
|
+
changed.has('hideWhenFits') ||
|
|
279
|
+
changed.has('disabled') ||
|
|
280
|
+
changed.has('label')
|
|
281
|
+
) {
|
|
282
|
+
this.schedule(true);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
render(): TemplateResult {
|
|
287
|
+
return html`
|
|
288
|
+
<div
|
|
289
|
+
part="base"
|
|
290
|
+
class=${classMap({
|
|
291
|
+
base: true,
|
|
292
|
+
'base--track': !this.segOn,
|
|
293
|
+
'base--disabled': this.disabled,
|
|
294
|
+
'base--rtl': this.rtl && this.resolvedDir === 'horizontal',
|
|
295
|
+
})}
|
|
296
|
+
>
|
|
297
|
+
<div part="track" class="track">
|
|
298
|
+
<div class="segments"></div>
|
|
299
|
+
<div
|
|
300
|
+
part="viewport"
|
|
301
|
+
class="viewport"
|
|
302
|
+
role="scrollbar"
|
|
303
|
+
tabindex=${this.disabled ? -1 : 0}
|
|
304
|
+
aria-controls=${this.for}
|
|
305
|
+
aria-orientation=${this.resolvedDir}
|
|
306
|
+
aria-label=${this.label}
|
|
307
|
+
aria-valuemin="0"
|
|
308
|
+
aria-valuemax="100"
|
|
309
|
+
aria-valuenow="0"
|
|
310
|
+
></div>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- autoHide -------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
private get held(): boolean {
|
|
319
|
+
return this.hovered || this.focused || this.drag !== null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Shows an auto-hidden minimap and restarts the countdown to hiding it. */
|
|
323
|
+
private reveal(): void {
|
|
324
|
+
if (!this.autoHide) return;
|
|
325
|
+
this.removeAttribute('data-idle');
|
|
326
|
+
this.queueHide();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private queueHide(): void {
|
|
330
|
+
clearTimeout(this.hideTimer);
|
|
331
|
+
this.hideTimer = 0;
|
|
332
|
+
if (!this.autoHide) return;
|
|
333
|
+
this.hideTimer = window.setTimeout(() => {
|
|
334
|
+
this.hideTimer = 0;
|
|
335
|
+
if (this.autoHide && !this.held) this.setAttribute('data-idle', '');
|
|
336
|
+
}, Math.max(0, this.hideDelay));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// --- attach / detach ------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
private resolveTarget(): HTMLElement | null {
|
|
342
|
+
if (!this.for) return null;
|
|
343
|
+
const root = this.getRootNode() as Document | ShadowRoot;
|
|
344
|
+
if (root instanceof Document) {
|
|
345
|
+
return root.getElementById(this.for);
|
|
346
|
+
}
|
|
347
|
+
const shadow = root as ShadowRoot;
|
|
348
|
+
if (typeof shadow.getElementById === 'function') {
|
|
349
|
+
return shadow.getElementById(this.for) as HTMLElement | null;
|
|
350
|
+
}
|
|
351
|
+
return shadow.querySelector<HTMLElement>(`#${CSS.escape(this.for)}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private attach(): void {
|
|
355
|
+
this.detach();
|
|
356
|
+
const el = this.resolveTarget();
|
|
357
|
+
this.toggleAttribute('data-unresolved', !el);
|
|
358
|
+
if (!el) {
|
|
359
|
+
if (!this.retried && this.for) {
|
|
360
|
+
this.retried = true;
|
|
361
|
+
this.retryRaf = requestAnimationFrame(() => {
|
|
362
|
+
this.retryRaf = 0;
|
|
363
|
+
this.attach();
|
|
364
|
+
});
|
|
365
|
+
} else if (this.for) {
|
|
366
|
+
this.retried = false;
|
|
367
|
+
// eslint-disable-next-line no-console
|
|
368
|
+
console.warn(
|
|
369
|
+
`nile-minimap: no element with id "${this.for}" found in the minimap's root node.`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this.retried = false;
|
|
375
|
+
this.target = el;
|
|
376
|
+
|
|
377
|
+
this.ro = new ResizeObserver(() => this.schedule(true));
|
|
378
|
+
this.mo = new MutationObserver(() => this.schedule(true));
|
|
379
|
+
this.ro.observe(el);
|
|
380
|
+
this.ro.observe(this);
|
|
381
|
+
this.mo.observe(el, { childList: true, subtree: true });
|
|
382
|
+
el.addEventListener('scroll', this.onScroll, { passive: true });
|
|
383
|
+
|
|
384
|
+
// Defer the first measure out of the Lit update lifecycle: attach() runs
|
|
385
|
+
// synchronously from firstUpdated()/updated(), and layout() sets @state
|
|
386
|
+
// (resolvedDir) and calls requestUpdate(), which otherwise triggers the
|
|
387
|
+
// dev-mode "scheduled an update after an update completed" warning.
|
|
388
|
+
this.schedule(true);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private detach(): void {
|
|
392
|
+
if (this.raf) {
|
|
393
|
+
cancelAnimationFrame(this.raf);
|
|
394
|
+
this.raf = 0;
|
|
395
|
+
}
|
|
396
|
+
if (this.retryRaf) {
|
|
397
|
+
cancelAnimationFrame(this.retryRaf);
|
|
398
|
+
this.retryRaf = 0;
|
|
399
|
+
}
|
|
400
|
+
if (this.dragRaf) {
|
|
401
|
+
cancelAnimationFrame(this.dragRaf);
|
|
402
|
+
this.dragRaf = 0;
|
|
403
|
+
}
|
|
404
|
+
this.ro?.disconnect();
|
|
405
|
+
this.mo?.disconnect();
|
|
406
|
+
this.ro = null;
|
|
407
|
+
this.mo = null;
|
|
408
|
+
this.observed.clear();
|
|
409
|
+
if (this.target) {
|
|
410
|
+
this.target.removeEventListener('scroll', this.onScroll);
|
|
411
|
+
}
|
|
412
|
+
this.target = null;
|
|
413
|
+
this.drag = null;
|
|
414
|
+
this.full = false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// --- scheduling -----------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
private schedule(full: boolean): void {
|
|
420
|
+
if (full) this.full = true;
|
|
421
|
+
if (this.raf) return;
|
|
422
|
+
this.raf = requestAnimationFrame(() => {
|
|
423
|
+
this.raf = 0;
|
|
424
|
+
// The held element may have been re-rendered (e.g. *ngIf): re-resolve.
|
|
425
|
+
if (this.target && !this.target.isConnected) {
|
|
426
|
+
this.attach();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const needsLayout = this.full;
|
|
430
|
+
this.full = false;
|
|
431
|
+
if (needsLayout) this.layout();
|
|
432
|
+
else this.refresh();
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// --- measurement ----------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
private layout(): void {
|
|
439
|
+
const t = this.target;
|
|
440
|
+
if (!t) return;
|
|
441
|
+
|
|
442
|
+
let dir: NileMinimapAxis;
|
|
443
|
+
if (this.orientation === 'auto') {
|
|
444
|
+
const vOver = t.scrollHeight - t.clientHeight > 1;
|
|
445
|
+
const hOver = t.scrollWidth - t.clientWidth > 1;
|
|
446
|
+
dir = !hOver && vOver ? 'vertical' : 'horizontal';
|
|
447
|
+
} else {
|
|
448
|
+
dir = this.orientation;
|
|
449
|
+
}
|
|
450
|
+
this.resolvedDir = dir;
|
|
451
|
+
this.axis = AXES[dir];
|
|
452
|
+
this.rtl = dir === 'horizontal' && getComputedStyle(t).direction === 'rtl';
|
|
453
|
+
|
|
454
|
+
const a = this.axis;
|
|
455
|
+
const el = t as unknown as Record<string, number>;
|
|
456
|
+
const size = el[a.size];
|
|
457
|
+
const client = el[a.len];
|
|
458
|
+
const over = size - client > 1;
|
|
459
|
+
|
|
460
|
+
if (over !== this.overflowing) {
|
|
461
|
+
this.overflowing = over;
|
|
462
|
+
this.emit(
|
|
463
|
+
'nile-overflow-change',
|
|
464
|
+
{ overflowing: over, orientation: dir },
|
|
465
|
+
true,
|
|
466
|
+
true,
|
|
467
|
+
false
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// hideWhenFits toggles an internal attribute; never the consumer's [hidden].
|
|
472
|
+
if (this.hideWhenFits && !over) {
|
|
473
|
+
this.setAttribute('data-fit', '');
|
|
474
|
+
} else {
|
|
475
|
+
this.removeAttribute('data-fit');
|
|
476
|
+
}
|
|
477
|
+
if (dir === 'vertical') this.setAttribute('data-vertical', '');
|
|
478
|
+
else this.removeAttribute('data-vertical');
|
|
479
|
+
|
|
480
|
+
const items =
|
|
481
|
+
this.mode === 'segments'
|
|
482
|
+
? Array.from(t.querySelectorAll<HTMLElement>(this.itemSelector))
|
|
483
|
+
: [];
|
|
484
|
+
|
|
485
|
+
// Keep the per-item ResizeObserver set in sync without triggering loops.
|
|
486
|
+
for (const old of Array.from(this.observed)) {
|
|
487
|
+
if (!items.includes(old as HTMLElement)) {
|
|
488
|
+
this.ro?.unobserve(old);
|
|
489
|
+
this.observed.delete(old);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const it of items) {
|
|
493
|
+
if (!this.observed.has(it)) {
|
|
494
|
+
this.ro?.observe(it);
|
|
495
|
+
this.observed.add(it);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const tr = t.getBoundingClientRect() as unknown as Record<string, number>;
|
|
500
|
+
const scrollPos = el[a.pos];
|
|
501
|
+
this.geo = items.map(it => {
|
|
502
|
+
const r = it.getBoundingClientRect() as unknown as Record<string, number>;
|
|
503
|
+
return {
|
|
504
|
+
start: r[a.rectStart] - tr[a.rectStart] - el[a.border] + scrollPos,
|
|
505
|
+
len: r[a.rectExt],
|
|
506
|
+
};
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const trackLen = (this.trackEl as unknown as Record<string, number>)[a.len];
|
|
510
|
+
const scale = size ? trackLen / size : 0;
|
|
511
|
+
const dense =
|
|
512
|
+
this.geo.length > MAX_ITEMS ||
|
|
513
|
+
this.geo.some(g => g.len * scale < MIN_SEG);
|
|
514
|
+
this.segOn = this.mode === 'segments' && this.geo.length > 0 && !dense;
|
|
515
|
+
|
|
516
|
+
this.renderSegments(scale);
|
|
517
|
+
this.refresh();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private renderSegments(scale: number): void {
|
|
521
|
+
const segs = this.segsEl;
|
|
522
|
+
if (!segs) return;
|
|
523
|
+
if (!this.segOn) {
|
|
524
|
+
segs.textContent = '';
|
|
525
|
+
this.requestUpdate();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
while (segs.children.length < this.geo.length) {
|
|
529
|
+
const d = document.createElement('div');
|
|
530
|
+
d.className = 'segment';
|
|
531
|
+
d.setAttribute('part', 'segment');
|
|
532
|
+
segs.appendChild(d);
|
|
533
|
+
}
|
|
534
|
+
while (segs.children.length > this.geo.length) {
|
|
535
|
+
segs.lastChild?.remove();
|
|
536
|
+
}
|
|
537
|
+
this.geo.forEach((g, i) => {
|
|
538
|
+
const s = (segs.children[i] as HTMLElement).style;
|
|
539
|
+
s.setProperty('--s', `${(g.start * scale).toFixed(2)}px`);
|
|
540
|
+
s.setProperty('--l', `${(g.len * scale).toFixed(2)}px`);
|
|
541
|
+
});
|
|
542
|
+
this.requestUpdate();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private refresh(): void {
|
|
546
|
+
const t = this.target;
|
|
547
|
+
if (!t) return;
|
|
548
|
+
const a = this.axis;
|
|
549
|
+
const el = t as unknown as Record<string, number>;
|
|
550
|
+
const size = el[a.size];
|
|
551
|
+
const client = el[a.len];
|
|
552
|
+
const maxScroll = Math.max(0, size - client);
|
|
553
|
+
const trackLen = (this.trackEl as unknown as Record<string, number>)[a.len];
|
|
554
|
+
|
|
555
|
+
let vpLen = size
|
|
556
|
+
? Math.max(MIN_THUMB, (trackLen * client) / size)
|
|
557
|
+
: trackLen;
|
|
558
|
+
vpLen = Math.min(vpLen, trackLen);
|
|
559
|
+
const pos = Math.abs(el[a.pos]);
|
|
560
|
+
const ratio = maxScroll ? Math.min(1, Math.max(0, pos / maxScroll)) : 0;
|
|
561
|
+
const vpStart = ratio * (trackLen - vpLen);
|
|
562
|
+
|
|
563
|
+
this.contentSize = size;
|
|
564
|
+
this.clientSize = client;
|
|
565
|
+
this.maxScroll = maxScroll;
|
|
566
|
+
this.trackLen = trackLen;
|
|
567
|
+
this.vpLen = vpLen;
|
|
568
|
+
|
|
569
|
+
const vp = this.viewport;
|
|
570
|
+
if (vp) {
|
|
571
|
+
vp.style.setProperty('--vs', `${vpStart.toFixed(2)}px`);
|
|
572
|
+
vp.style.setProperty('--vl', `${vpLen.toFixed(2)}px`);
|
|
573
|
+
vp.setAttribute('aria-orientation', this.resolvedDir);
|
|
574
|
+
vp.setAttribute('aria-label', this.label);
|
|
575
|
+
vp.setAttribute('aria-controls', this.for);
|
|
576
|
+
vp.setAttribute('aria-valuenow', String(Math.round(ratio * 100)));
|
|
577
|
+
if (this.disabled) vp.setAttribute('aria-disabled', 'true');
|
|
578
|
+
else vp.removeAttribute('aria-disabled');
|
|
579
|
+
|
|
580
|
+
let valueText = '';
|
|
581
|
+
if (this.segOn) {
|
|
582
|
+
const visible: number[] = [];
|
|
583
|
+
this.geo.forEach((g, i) => {
|
|
584
|
+
if (g.start + g.len > pos + 1 && g.start < pos + client - 1) {
|
|
585
|
+
visible.push(i + 1);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
if (visible.length) {
|
|
589
|
+
valueText = this.formatValueText(
|
|
590
|
+
visible[0],
|
|
591
|
+
visible[visible.length - 1],
|
|
592
|
+
this.geo.length
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (valueText) vp.setAttribute('aria-valuetext', valueText);
|
|
597
|
+
else vp.removeAttribute('aria-valuetext');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (this.pending !== null && Math.abs(pos - this.pending) < 2) {
|
|
601
|
+
this.pending = null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// --- navigation -----------------------------------------------------------
|
|
606
|
+
|
|
607
|
+
private go(rawPos: number, source: NileMinimapSource, smooth: boolean): void {
|
|
608
|
+
const t = this.target;
|
|
609
|
+
if (!t) return;
|
|
610
|
+
const a = this.axis;
|
|
611
|
+
const el = t as unknown as Record<string, number>;
|
|
612
|
+
const max = el[a.size] - el[a.len];
|
|
613
|
+
const pos = Math.max(0, Math.min(max, rawPos));
|
|
614
|
+
|
|
615
|
+
const ev = this.emit(
|
|
616
|
+
'nile-navigate',
|
|
617
|
+
{
|
|
618
|
+
position: Math.round(pos),
|
|
619
|
+
ratio: max ? Number((pos / max).toFixed(3)) : 0,
|
|
620
|
+
source,
|
|
621
|
+
orientation: this.resolvedDir,
|
|
622
|
+
},
|
|
623
|
+
true,
|
|
624
|
+
true,
|
|
625
|
+
true
|
|
626
|
+
);
|
|
627
|
+
if (ev.defaultPrevented) return;
|
|
628
|
+
|
|
629
|
+
const sm = smooth && this.smooth && !this.reduceMotion.matches;
|
|
630
|
+
this.pending = sm ? pos : null;
|
|
631
|
+
this.pendingAt = performance.now();
|
|
632
|
+
const signed = this.rtl ? -pos : pos;
|
|
633
|
+
t.scrollTo({ [a.scrollKey]: signed, behavior: sm ? 'smooth' : 'auto' });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private cur(): number {
|
|
637
|
+
if (!this.target) return 0;
|
|
638
|
+
if (this.pending !== null && performance.now() - this.pendingAt < 600) {
|
|
639
|
+
return this.pending;
|
|
640
|
+
}
|
|
641
|
+
const el = this.target as unknown as Record<string, number>;
|
|
642
|
+
return Math.abs(el[this.axis.pos]);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
private step(dir: number): number {
|
|
646
|
+
const cur = this.cur();
|
|
647
|
+
if (this.segOn && this.geo.length) {
|
|
648
|
+
const inset = this.geo[0].start;
|
|
649
|
+
const stops = this.geo.map(g => Math.max(0, g.start - inset));
|
|
650
|
+
if (dir > 0) {
|
|
651
|
+
const next = stops.find(s => s > cur + 2);
|
|
652
|
+
return next === undefined ? Infinity : next;
|
|
653
|
+
}
|
|
654
|
+
const prev = [...stops].reverse().find(s => s < cur - 2);
|
|
655
|
+
return prev === undefined ? 0 : prev;
|
|
656
|
+
}
|
|
657
|
+
return cur + dir * Math.max(40, this.clientSize * 0.1);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// --- interactions ---------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
private bindInteractions(): void {
|
|
663
|
+
const vp = this.viewport;
|
|
664
|
+
const track = this.trackEl;
|
|
665
|
+
const { base } = this;
|
|
666
|
+
|
|
667
|
+
vp.addEventListener('pointerdown', e => {
|
|
668
|
+
if (this.disabled) return;
|
|
669
|
+
e.preventDefault();
|
|
670
|
+
e.stopPropagation();
|
|
671
|
+
try {
|
|
672
|
+
vp.setPointerCapture(e.pointerId);
|
|
673
|
+
} catch {
|
|
674
|
+
// Synthetic or already-released pointers can't be captured; drag still works.
|
|
675
|
+
}
|
|
676
|
+
const el = this.target as unknown as Record<string, number>;
|
|
677
|
+
this.drag = {
|
|
678
|
+
pointer: e[this.axis.client],
|
|
679
|
+
start: this.target ? Math.abs(el[this.axis.pos]) : 0,
|
|
680
|
+
};
|
|
681
|
+
this.base.classList.add('base--drag');
|
|
682
|
+
vp.focus({ preventScroll: true });
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
vp.addEventListener('pointermove', e => {
|
|
686
|
+
if (!this.drag) return;
|
|
687
|
+
this.lastMove = e;
|
|
688
|
+
if (this.dragRaf) return;
|
|
689
|
+
this.dragRaf = requestAnimationFrame(() => {
|
|
690
|
+
this.dragRaf = 0;
|
|
691
|
+
applyDrag();
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const applyDrag = () => {
|
|
696
|
+
if (!this.drag || !this.lastMove) return;
|
|
697
|
+
const room = this.trackLen - this.vpLen;
|
|
698
|
+
if (room <= 0) return;
|
|
699
|
+
const delta = this.lastMove[this.axis.client] - this.drag.pointer;
|
|
700
|
+
this.go(
|
|
701
|
+
this.drag.start + (delta * this.maxScroll) / room,
|
|
702
|
+
'drag',
|
|
703
|
+
false
|
|
704
|
+
);
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const end = () => {
|
|
708
|
+
if (!this.drag) return;
|
|
709
|
+
// Flush a move still waiting for its frame so the release position isn't lost.
|
|
710
|
+
if (this.dragRaf) {
|
|
711
|
+
cancelAnimationFrame(this.dragRaf);
|
|
712
|
+
this.dragRaf = 0;
|
|
713
|
+
applyDrag();
|
|
714
|
+
}
|
|
715
|
+
this.lastMove = null;
|
|
716
|
+
this.drag = null;
|
|
717
|
+
this.base.classList.remove('base--drag');
|
|
718
|
+
this.queueHide();
|
|
719
|
+
};
|
|
720
|
+
vp.addEventListener('pointerup', end);
|
|
721
|
+
vp.addEventListener('pointercancel', end);
|
|
722
|
+
vp.addEventListener('lostpointercapture', end);
|
|
723
|
+
|
|
724
|
+
track.addEventListener('pointerdown', e => {
|
|
725
|
+
if (e.target === vp || this.disabled || !this.target) return;
|
|
726
|
+
const r = track.getBoundingClientRect() as unknown as Record<
|
|
727
|
+
string,
|
|
728
|
+
number
|
|
729
|
+
>;
|
|
730
|
+
const p = e[this.axis.client] - r[this.axis.rectStart];
|
|
731
|
+
this.go(
|
|
732
|
+
(p / this.trackLen) * this.contentSize - this.clientSize / 2,
|
|
733
|
+
'click',
|
|
734
|
+
true
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
base.addEventListener(
|
|
739
|
+
'wheel',
|
|
740
|
+
e => {
|
|
741
|
+
if (this.disabled || !this.overflowing || !this.target) return;
|
|
742
|
+
let d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
|
|
743
|
+
if (e.deltaMode === 1) d *= 16;
|
|
744
|
+
else if (e.deltaMode === 2) d *= this.clientSize;
|
|
745
|
+
const el = this.target as unknown as Record<string, number>;
|
|
746
|
+
const cur = Math.abs(el[this.axis.pos]);
|
|
747
|
+
if ((d < 0 && cur <= 0) || (d > 0 && cur >= this.maxScroll - 1)) return;
|
|
748
|
+
e.preventDefault();
|
|
749
|
+
this.pending = null;
|
|
750
|
+
this.go(cur + d, 'wheel', false);
|
|
751
|
+
},
|
|
752
|
+
{ passive: false }
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
vp.addEventListener('keydown', e => {
|
|
756
|
+
if (this.disabled || !this.target) return;
|
|
757
|
+
let n: number | null = null;
|
|
758
|
+
switch (e.key) {
|
|
759
|
+
case 'ArrowRight':
|
|
760
|
+
case 'ArrowDown':
|
|
761
|
+
n = this.step(1);
|
|
762
|
+
break;
|
|
763
|
+
case 'ArrowLeft':
|
|
764
|
+
case 'ArrowUp':
|
|
765
|
+
n = this.step(-1);
|
|
766
|
+
break;
|
|
767
|
+
case 'PageDown':
|
|
768
|
+
n = this.cur() + this.clientSize;
|
|
769
|
+
break;
|
|
770
|
+
case 'PageUp':
|
|
771
|
+
n = this.cur() - this.clientSize;
|
|
772
|
+
break;
|
|
773
|
+
case 'Home':
|
|
774
|
+
n = 0;
|
|
775
|
+
break;
|
|
776
|
+
case 'End':
|
|
777
|
+
n = this.maxScroll;
|
|
778
|
+
break;
|
|
779
|
+
default:
|
|
780
|
+
n = null;
|
|
781
|
+
}
|
|
782
|
+
if (n !== null) {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
this.go(n, 'keyboard', true);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export default NileMinimap;
|
|
791
|
+
|
|
792
|
+
declare global {
|
|
793
|
+
interface HTMLElementTagNameMap {
|
|
794
|
+
'nile-minimap': NileMinimap;
|
|
795
|
+
}
|
|
796
|
+
}
|