@nexterajs/ui 0.0.1 → 0.1.0
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 +279 -34
- package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
- package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-core.mjs +835 -11
- package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-display.mjs +1142 -0
- package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-dropdowns.mjs +621 -69
- package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-file-uploader.mjs +576 -11
- package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-forms.mjs +2702 -452
- package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-grid.mjs +2350 -336
- package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-intl.mjs +117 -15
- package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
- package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-notify.mjs +353 -0
- package/fesm2022/nexterajs-ui-notify.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
- package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-pivot.mjs +191 -136
- package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-shell.mjs +542 -0
- package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
- package/fesm2022/nexterajs-ui-styling.mjs +568 -3
- package/fesm2022/nexterajs-ui-styling.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-tree-view.mjs +150 -18
- package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
- package/fesm2022/nexterajs-ui-validation.mjs +283 -97
- package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
- package/fesm2022/nexterajs-ui.mjs +6 -0
- package/fesm2022/nexterajs-ui.mjs.map +1 -1
- package/package.json +37 -2
- package/styles/_dropdown.scss +150 -1
- package/styles/_editor.scss +75 -21
- package/styles/_icons.scss +92 -0
- package/styles/_mixins.scss +104 -1
- package/styles/_scrollbar.scss +134 -0
- package/styles/_status.scss +21 -0
- package/styles/_toast.scss +243 -0
- package/styles/_tokens.scss +467 -2
- package/styles/_widget.scss +14 -3
- package/styles/index.scss +10 -4
- package/types/nexterajs-ui-charts.d.ts +446 -0
- package/types/nexterajs-ui-core.d.ts +515 -30
- package/types/nexterajs-ui-display.d.ts +720 -0
- package/types/nexterajs-ui-dropdowns.d.ts +318 -57
- package/types/nexterajs-ui-file-uploader.d.ts +240 -8
- package/types/nexterajs-ui-forms.d.ts +772 -43
- package/types/nexterajs-ui-grid.d.ts +878 -135
- package/types/nexterajs-ui-intl.d.ts +39 -4
- package/types/nexterajs-ui-navigation.d.ts +531 -0
- package/types/nexterajs-ui-notify.d.ts +149 -0
- package/types/nexterajs-ui-overlay.d.ts +703 -0
- package/types/nexterajs-ui-pivot.d.ts +91 -70
- package/types/nexterajs-ui-shell.d.ts +294 -0
- package/types/nexterajs-ui-styling.d.ts +163 -3
- package/types/nexterajs-ui-tree-view.d.ts +56 -4
- package/types/nexterajs-ui-validation.d.ts +110 -22
- package/types/nexterajs-ui.d.ts +6 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { FlexibleConnectedPositionStrategy } from '@angular/cdk/overlay';
|
|
2
|
+
import { DOCUMENT } from '@angular/common';
|
|
3
|
+
import * as i0 from '@angular/core';
|
|
4
|
+
import { inject, NgZone, Injectable, makeEnvironmentProviders, provideEnvironmentInitializer, ElementRef, DestroyRef, Input, Directive } from '@angular/core';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Marker class carried by every nxe surface — component hosts and CDK overlay panes alike.
|
|
5
8
|
*
|
|
6
|
-
* It is the nxe counterpart of DevExtreme's `.
|
|
9
|
+
* It is the nxe counterpart of DevExtreme's `.nxe-widget` base and is what gives a surface its
|
|
7
10
|
* font family/size/weight/line-height (see styling/_widget.scss).
|
|
8
11
|
*
|
|
9
12
|
* It matters most on overlays: a CDK pane is appended to `<body>`, so a portaled dropdown
|
|
@@ -31,7 +34,7 @@ function nxePane(...extra) {
|
|
|
31
34
|
*
|
|
32
35
|
* DevExtreme drew dropdowns flush against the editor — no border on the shared edge — and moved
|
|
33
36
|
* that seam to the other side when the popup flipped up
|
|
34
|
-
* (`.
|
|
37
|
+
* (`.nxe-overlay-content.nxe-dropdowneditor-overlay-flipped { border-top-width:1px; border-bottom-width:0 }`).
|
|
35
38
|
* CDK does not emit any equivalent hook, so panels need this to reproduce it.
|
|
36
39
|
*
|
|
37
40
|
* Returns the subscription; callers dispose it with the overlay.
|
|
@@ -49,11 +52,573 @@ function trackPaneFlip(overlayRef) {
|
|
|
49
52
|
});
|
|
50
53
|
}
|
|
51
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Migration aid: re-emit the old `dx-*` class names alongside the library's `nxe-*` ones.
|
|
57
|
+
*
|
|
58
|
+
* The library dropped the `dx-` prefix from its DOM. Applications that still style themselves
|
|
59
|
+
* with rules like `.dx-datagrid .dx-row > td { padding: 4px }` would silently lose that styling,
|
|
60
|
+
* and no stylesheet can fix it: CSS cannot alias a class, so a rule keyed on `.dx-datagrid` will
|
|
61
|
+
* never match an element that only says `nxe-datagrid`. The class has to be back on the element.
|
|
62
|
+
*
|
|
63
|
+
* So this mirrors them at runtime. Provide it once and unmigrated stylesheets keep working
|
|
64
|
+
* unchanged; provide nothing and the library costs nothing, which is the point of it being
|
|
65
|
+
* opt-in. `tools/dx-compat/rewrite-styles.mjs` rewrites an application's own stylesheets when it
|
|
66
|
+
* is ready to drop the flag — this is a bridge, not a destination.
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* bootstrapApplication(AppComponent, { providers: [provideNxeDxCompat()] });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
class NxeDxCompatService {
|
|
73
|
+
document = inject(DOCUMENT);
|
|
74
|
+
ngZone = inject(NgZone);
|
|
75
|
+
observer;
|
|
76
|
+
/** Our own writes retrigger the observer; this stops it chasing its own tail. */
|
|
77
|
+
mirroring = false;
|
|
78
|
+
start() {
|
|
79
|
+
if (this.observer || typeof MutationObserver === 'undefined' || !this.document.body) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.mirrorSubtree(this.document.body);
|
|
83
|
+
// Outside Angular: this reacts to the DOM the framework just wrote, and must not itself
|
|
84
|
+
// schedule change detection.
|
|
85
|
+
this.ngZone.runOutsideAngular(() => {
|
|
86
|
+
this.observer = new MutationObserver((records) => this.onMutations(records));
|
|
87
|
+
this.observer.observe(this.document.body, {
|
|
88
|
+
subtree: true,
|
|
89
|
+
childList: true,
|
|
90
|
+
attributes: true,
|
|
91
|
+
attributeFilter: ['class'],
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
stop() {
|
|
96
|
+
this.observer?.disconnect();
|
|
97
|
+
this.observer = undefined;
|
|
98
|
+
}
|
|
99
|
+
onMutations(records) {
|
|
100
|
+
if (this.mirroring) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.mirroring = true;
|
|
104
|
+
try {
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
if (record.type === 'attributes' && record.target instanceof Element) {
|
|
107
|
+
this.mirrorElement(record.target);
|
|
108
|
+
}
|
|
109
|
+
record.addedNodes.forEach((node) => {
|
|
110
|
+
if (node instanceof Element) {
|
|
111
|
+
this.mirrorSubtree(node);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
this.mirroring = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
mirrorSubtree(root) {
|
|
121
|
+
const wasMirroring = this.mirroring;
|
|
122
|
+
this.mirroring = true;
|
|
123
|
+
try {
|
|
124
|
+
this.mirrorElement(root);
|
|
125
|
+
root.querySelectorAll('*').forEach((el) => this.mirrorElement(el));
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this.mirroring = wasMirroring;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Keep `dx-x` present exactly while `nxe-x` is, so a removed state class is removed here too. */
|
|
132
|
+
mirrorElement(el) {
|
|
133
|
+
const list = el.classList;
|
|
134
|
+
const wanted = new Set();
|
|
135
|
+
for (const cls of Array.from(list)) {
|
|
136
|
+
if (cls.startsWith('nxe-')) {
|
|
137
|
+
wanted.add(`dx-${cls.slice(4)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!wanted.size && !Array.from(list).some((c) => c.startsWith('dx-'))) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (const cls of Array.from(list)) {
|
|
144
|
+
if (cls.startsWith('dx-') && !wanted.has(cls)) {
|
|
145
|
+
list.remove(cls);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const cls of wanted) {
|
|
149
|
+
if (!list.contains(cls)) {
|
|
150
|
+
list.add(cls);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDxCompatService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
155
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDxCompatService, providedIn: 'root' });
|
|
156
|
+
}
|
|
157
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeDxCompatService, decorators: [{
|
|
158
|
+
type: Injectable,
|
|
159
|
+
args: [{ providedIn: 'root' }]
|
|
160
|
+
}] });
|
|
161
|
+
/**
|
|
162
|
+
* Opt into the `dx-*` compatibility classes. See {@link NxeDxCompatService} for why this is a
|
|
163
|
+
* runtime mirror rather than a stylesheet.
|
|
164
|
+
*/
|
|
165
|
+
function provideNxeDxCompat() {
|
|
166
|
+
return makeEnvironmentProviders([
|
|
167
|
+
provideEnvironmentInitializer(() => inject(NxeDxCompatService).start()),
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const CLASS = {
|
|
172
|
+
host: 'nxe-scrollbar',
|
|
173
|
+
activeX: 'nxe-scrollbar--active-x',
|
|
174
|
+
activeY: 'nxe-scrollbar--active-y',
|
|
175
|
+
scrollingX: 'nxe-scrollbar--scrolling-x',
|
|
176
|
+
scrollingY: 'nxe-scrollbar--scrolling-y',
|
|
177
|
+
focus: 'nxe-scrollbar--focus',
|
|
178
|
+
clicking: 'nxe-scrollbar--clicking',
|
|
179
|
+
railX: 'nxe-scrollbar__rail-x',
|
|
180
|
+
railY: 'nxe-scrollbar__rail-y',
|
|
181
|
+
thumbX: 'nxe-scrollbar__thumb-x',
|
|
182
|
+
thumbY: 'nxe-scrollbar__thumb-y',
|
|
183
|
+
};
|
|
184
|
+
/** How long the `--scrolling-*` class lingers after the last scroll event, as the original did. */
|
|
185
|
+
const SCROLLING_CLASS_LINGER_MS = 1000;
|
|
186
|
+
/**
|
|
187
|
+
* `[nxeScrollbar]` — a container with custom overlay scrollbars.
|
|
188
|
+
*
|
|
189
|
+
* This replaces a Perfect Scrollbar wrapper, and reproduces its DOM and its rendering exactly:
|
|
190
|
+
* two absolutely-positioned rails, each holding a thumb, sized and offset from the host's own
|
|
191
|
+
* scroll geometry. The stylesheet (`@nexterajs/ui/styles/_scrollbar.scss`) is a value-for-value
|
|
192
|
+
* port, so applications that restyle the rails keep their appearance unchanged.
|
|
193
|
+
*
|
|
194
|
+
* **What is different, deliberately:** the original set `overflow: hidden` and reimplemented
|
|
195
|
+
* wheel, touch, keyboard and momentum scrolling in JavaScript. Here the element scrolls natively
|
|
196
|
+
* and only the native scrollbar is hidden — the rails are painted from the `scroll` event. That is
|
|
197
|
+
* less code and better behaviour on touch and trackpads, and it is invisible: a hidden native
|
|
198
|
+
* scrollbar takes no layout width, exactly like the overlay rails it replaces.
|
|
199
|
+
*
|
|
200
|
+
* The host carries `nxe-scrollbar` while enabled, which is the marker callers branch on.
|
|
201
|
+
*/
|
|
202
|
+
class NxeScrollbarDirective {
|
|
203
|
+
elementRef = inject(ElementRef);
|
|
204
|
+
zone = inject(NgZone);
|
|
205
|
+
destroyRef = inject(DestroyRef);
|
|
206
|
+
nxeScrollbar = true;
|
|
207
|
+
nxeScrollbarOptions = null;
|
|
208
|
+
options = {};
|
|
209
|
+
animation = null;
|
|
210
|
+
railX;
|
|
211
|
+
railY;
|
|
212
|
+
thumbX;
|
|
213
|
+
thumbY;
|
|
214
|
+
resizeObserver;
|
|
215
|
+
mutationObserver;
|
|
216
|
+
scrollingXTimer = null;
|
|
217
|
+
scrollingYTimer = null;
|
|
218
|
+
teardown = [];
|
|
219
|
+
initialised = false;
|
|
220
|
+
get element() {
|
|
221
|
+
return this.elementRef.nativeElement;
|
|
222
|
+
}
|
|
223
|
+
ngOnInit() {
|
|
224
|
+
if (this.nxeScrollbar)
|
|
225
|
+
this.init();
|
|
226
|
+
}
|
|
227
|
+
ngOnChanges(changes) {
|
|
228
|
+
if ('nxeScrollbarOptions' in changes) {
|
|
229
|
+
// A spread, not a deep merge: every option on this interface is a flat scalar.
|
|
230
|
+
this.options = { ...this.options, ...(changes['nxeScrollbarOptions'].currentValue ?? {}) };
|
|
231
|
+
if (this.initialised)
|
|
232
|
+
this.update();
|
|
233
|
+
}
|
|
234
|
+
if ('nxeScrollbar' in changes && !changes['nxeScrollbar'].firstChange) {
|
|
235
|
+
if (this.nxeScrollbar)
|
|
236
|
+
this.init();
|
|
237
|
+
else
|
|
238
|
+
this.dispose();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
ngOnDestroy() {
|
|
242
|
+
this.dispose();
|
|
243
|
+
}
|
|
244
|
+
isEnabled() {
|
|
245
|
+
return this.nxeScrollbar;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Recompute rail and thumb geometry. Call after changing content the directive cannot observe.
|
|
249
|
+
*
|
|
250
|
+
* This reproduces the original's arithmetic exactly, down to how it rounds — the requirement was
|
|
251
|
+
* no visual difference, and each of these is worth a pixel:
|
|
252
|
+
* - the container is measured from `getBoundingClientRect()` rounded, not from `clientHeight`;
|
|
253
|
+
* - sizes and offsets are **truncated** (the original ran them through `parseInt`), not rounded;
|
|
254
|
+
* - `scrollTop` is floored before it is used;
|
|
255
|
+
* - the thumb offset is clamped so it cannot overhang the end of the rail.
|
|
256
|
+
*/
|
|
257
|
+
update() {
|
|
258
|
+
if (!this.initialised)
|
|
259
|
+
return;
|
|
260
|
+
const el = this.element;
|
|
261
|
+
const rect = el.getBoundingClientRect();
|
|
262
|
+
const containerWidth = Math.round(rect.width);
|
|
263
|
+
const containerHeight = Math.round(rect.height);
|
|
264
|
+
const contentWidth = el.scrollWidth;
|
|
265
|
+
const contentHeight = el.scrollHeight;
|
|
266
|
+
const roundedScrollTop = Math.floor(el.scrollTop);
|
|
267
|
+
const activeX = this.options.suppressScrollX !== true && containerWidth < contentWidth;
|
|
268
|
+
const activeY = this.options.suppressScrollY !== true && containerHeight < contentHeight;
|
|
269
|
+
el.classList.toggle(CLASS.activeX, activeX);
|
|
270
|
+
el.classList.toggle(CLASS.activeY, activeY);
|
|
271
|
+
if (activeX && this.railX && this.thumbX) {
|
|
272
|
+
const railWidth = containerWidth;
|
|
273
|
+
const thumbWidth = this.clampThumb(trunc((railWidth * containerWidth) / contentWidth));
|
|
274
|
+
let left = trunc((el.scrollLeft * (railWidth - thumbWidth)) / (contentWidth - containerWidth));
|
|
275
|
+
if (left >= railWidth - thumbWidth)
|
|
276
|
+
left = railWidth - thumbWidth;
|
|
277
|
+
// The rails follow the scroll offset so they stay pinned to the visible edge: the host is
|
|
278
|
+
// the scrolling box, so an absolutely-positioned child would otherwise travel with content.
|
|
279
|
+
this.railX.style.left = `${el.scrollLeft}px`;
|
|
280
|
+
this.railX.style.bottom = `${-roundedScrollTop}px`;
|
|
281
|
+
this.railX.style.width = `${railWidth}px`;
|
|
282
|
+
this.thumbX.style.left = `${left}px`;
|
|
283
|
+
this.thumbX.style.width = `${thumbWidth}px`;
|
|
284
|
+
}
|
|
285
|
+
if (activeY && this.railY && this.thumbY) {
|
|
286
|
+
const railHeight = containerHeight;
|
|
287
|
+
const thumbHeight = this.clampThumb(trunc((railHeight * containerHeight) / contentHeight));
|
|
288
|
+
let top = trunc((roundedScrollTop * (railHeight - thumbHeight)) / (contentHeight - containerHeight));
|
|
289
|
+
if (top >= railHeight - thumbHeight)
|
|
290
|
+
top = railHeight - thumbHeight;
|
|
291
|
+
this.railY.style.top = `${roundedScrollTop}px`;
|
|
292
|
+
this.railY.style.right = `${-el.scrollLeft}px`;
|
|
293
|
+
this.railY.style.height = `${railHeight}px`;
|
|
294
|
+
this.thumbY.style.top = `${top}px`;
|
|
295
|
+
this.thumbY.style.height = `${thumbHeight}px`;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Kept for parity with the wrapper this replaces; teardown also happens in ngOnDestroy. */
|
|
299
|
+
destroy() {
|
|
300
|
+
this.dispose();
|
|
301
|
+
}
|
|
302
|
+
isScrollableX() {
|
|
303
|
+
return this.element.classList.contains(CLASS.activeX);
|
|
304
|
+
}
|
|
305
|
+
isScrollableY() {
|
|
306
|
+
return this.element.classList.contains(CLASS.activeY);
|
|
307
|
+
}
|
|
308
|
+
geometry(prefix = 'scroll') {
|
|
309
|
+
const el = this.element;
|
|
310
|
+
return {
|
|
311
|
+
x: el[`${prefix}Left`],
|
|
312
|
+
y: el[`${prefix}Top`],
|
|
313
|
+
w: el[`${prefix}Width`],
|
|
314
|
+
h: el[`${prefix}Height`],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** -1 at the start of an axis, 1 at its end, 0 anywhere between. */
|
|
318
|
+
position(absolute = false) {
|
|
319
|
+
const el = this.element;
|
|
320
|
+
if (absolute)
|
|
321
|
+
return { x: el.scrollLeft, y: el.scrollTop };
|
|
322
|
+
return {
|
|
323
|
+
x: reach(el.scrollLeft, el.scrollWidth - el.clientWidth),
|
|
324
|
+
y: reach(el.scrollTop, el.scrollHeight - el.clientHeight),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
scrollTo(x, y, speed) {
|
|
328
|
+
if (y == null && speed == null) {
|
|
329
|
+
this.animateScrolling('scrollTop', x, speed);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (x != null)
|
|
333
|
+
this.animateScrolling('scrollLeft', x, speed);
|
|
334
|
+
if (y != null)
|
|
335
|
+
this.animateScrolling('scrollTop', y, speed);
|
|
336
|
+
}
|
|
337
|
+
scrollToX(x, speed) {
|
|
338
|
+
this.animateScrolling('scrollLeft', x, speed);
|
|
339
|
+
}
|
|
340
|
+
scrollToY(y, speed) {
|
|
341
|
+
this.animateScrolling('scrollTop', y, speed);
|
|
342
|
+
}
|
|
343
|
+
scrollToTop(offset = 0, speed) {
|
|
344
|
+
this.animateScrolling('scrollTop', offset, speed);
|
|
345
|
+
}
|
|
346
|
+
scrollToBottom(offset = 0, speed) {
|
|
347
|
+
const top = this.element.scrollHeight - this.element.clientHeight;
|
|
348
|
+
this.animateScrolling('scrollTop', top - offset, speed);
|
|
349
|
+
}
|
|
350
|
+
scrollToLeft(offset = 0, speed) {
|
|
351
|
+
this.animateScrolling('scrollLeft', offset, speed);
|
|
352
|
+
}
|
|
353
|
+
scrollToRight(offset = 0, speed) {
|
|
354
|
+
const left = this.element.scrollWidth - this.element.clientWidth;
|
|
355
|
+
this.animateScrolling('scrollLeft', left - offset, speed);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Bring the first descendant matching `selector` into view.
|
|
359
|
+
*
|
|
360
|
+
* `ignoreVisible` skips the scroll when the target already sits inside the viewport less the
|
|
361
|
+
* offset, so an item that is merely low on screen is not yanked to the top on every change.
|
|
362
|
+
*/
|
|
363
|
+
scrollToElement(selector, offset = 0, ignoreVisible = false, speed) {
|
|
364
|
+
const target = this.element.querySelector(selector);
|
|
365
|
+
if (!target)
|
|
366
|
+
return;
|
|
367
|
+
const targetRect = target.getBoundingClientRect();
|
|
368
|
+
const hostRect = this.element.getBoundingClientRect();
|
|
369
|
+
if (this.isScrollableX()) {
|
|
370
|
+
const visible = ignoreVisible && targetRect.right <= hostRect.right - Math.abs(offset);
|
|
371
|
+
if (!visible) {
|
|
372
|
+
const position = targetRect.left - hostRect.left + this.element.scrollLeft;
|
|
373
|
+
this.animateScrolling('scrollLeft', position + offset, speed);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (this.isScrollableY()) {
|
|
377
|
+
const visible = ignoreVisible && targetRect.bottom <= hostRect.bottom - Math.abs(offset);
|
|
378
|
+
if (!visible) {
|
|
379
|
+
const position = targetRect.top - hostRect.top + this.element.scrollTop;
|
|
380
|
+
this.animateScrolling('scrollTop', position + offset, speed);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Move one axis to `value`, easing over `speed` ms when one is given.
|
|
386
|
+
*
|
|
387
|
+
* The easing bails as soon as the position stops matching what it last wrote, so a user who
|
|
388
|
+
* grabs the wheel mid-flight takes over instead of fighting the animation.
|
|
389
|
+
*/
|
|
390
|
+
animateScrolling(axis, value, speed) {
|
|
391
|
+
this.cancelAnimation();
|
|
392
|
+
const el = this.element;
|
|
393
|
+
if (!speed || typeof window === 'undefined') {
|
|
394
|
+
el[axis] = value;
|
|
395
|
+
this.update();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (value === el[axis])
|
|
399
|
+
return;
|
|
400
|
+
let scrollCount = 0;
|
|
401
|
+
let previousTimestamp = performance.now();
|
|
402
|
+
let previousValue = el[axis];
|
|
403
|
+
const half = (previousValue - value) / 2;
|
|
404
|
+
const step = (timestamp) => {
|
|
405
|
+
scrollCount += Math.PI / (speed / (timestamp - previousTimestamp));
|
|
406
|
+
if (el[axis] !== previousValue) {
|
|
407
|
+
this.animation = null;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (scrollCount >= Math.PI) {
|
|
411
|
+
el[axis] = value;
|
|
412
|
+
this.animation = null;
|
|
413
|
+
this.update();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
el[axis] = Math.round(value + half + half * Math.cos(scrollCount));
|
|
417
|
+
previousValue = el[axis];
|
|
418
|
+
previousTimestamp = timestamp;
|
|
419
|
+
this.animation = window.requestAnimationFrame(step);
|
|
420
|
+
};
|
|
421
|
+
this.animation = window.requestAnimationFrame(step);
|
|
422
|
+
}
|
|
423
|
+
// ---------------------------------------------------------------------------------------------
|
|
424
|
+
init() {
|
|
425
|
+
if (this.initialised)
|
|
426
|
+
return;
|
|
427
|
+
const el = this.element;
|
|
428
|
+
el.classList.add(CLASS.host);
|
|
429
|
+
this.railX = this.buildRail(CLASS.railX, CLASS.thumbX);
|
|
430
|
+
this.thumbX = this.railX.firstElementChild;
|
|
431
|
+
this.railY = this.buildRail(CLASS.railY, CLASS.thumbY);
|
|
432
|
+
this.thumbY = this.railY.firstElementChild;
|
|
433
|
+
el.appendChild(this.railX);
|
|
434
|
+
el.appendChild(this.railY);
|
|
435
|
+
// Scroll and resize fire far too often to be worth a change-detection pass each: nothing here
|
|
436
|
+
// touches component state, only inline styles on elements the directive owns.
|
|
437
|
+
this.zone.runOutsideAngular(() => {
|
|
438
|
+
this.on(el, 'scroll', () => this.onScroll());
|
|
439
|
+
this.on(el, 'focus', () => el.classList.add(CLASS.focus), true);
|
|
440
|
+
this.on(el, 'blur', () => el.classList.remove(CLASS.focus), true);
|
|
441
|
+
this.bindThumbDrag(this.thumbX, this.railX, 'x');
|
|
442
|
+
this.bindThumbDrag(this.thumbY, this.railY, 'y');
|
|
443
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
444
|
+
this.resizeObserver = new ResizeObserver(() => this.update());
|
|
445
|
+
this.resizeObserver.observe(el);
|
|
446
|
+
// Observing the first child catches content growing taller inside a fixed-height host,
|
|
447
|
+
// which resizing the host alone never reports.
|
|
448
|
+
if (el.firstElementChild)
|
|
449
|
+
this.resizeObserver.observe(el.firstElementChild);
|
|
450
|
+
}
|
|
451
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
452
|
+
this.mutationObserver = new MutationObserver(() => this.update());
|
|
453
|
+
this.mutationObserver.observe(el, { childList: true, subtree: true, characterData: true });
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
this.initialised = true;
|
|
457
|
+
this.update();
|
|
458
|
+
this.destroyRef.onDestroy(() => this.dispose());
|
|
459
|
+
}
|
|
460
|
+
buildRail(railClass, thumbClass) {
|
|
461
|
+
const rail = document.createElement('div');
|
|
462
|
+
rail.className = railClass;
|
|
463
|
+
const thumb = document.createElement('div');
|
|
464
|
+
thumb.className = thumbClass;
|
|
465
|
+
rail.appendChild(thumb);
|
|
466
|
+
return rail;
|
|
467
|
+
}
|
|
468
|
+
onScroll() {
|
|
469
|
+
this.update();
|
|
470
|
+
const el = this.element;
|
|
471
|
+
if (this.isScrollableX()) {
|
|
472
|
+
el.classList.add(CLASS.scrollingX);
|
|
473
|
+
if (this.scrollingXTimer)
|
|
474
|
+
clearTimeout(this.scrollingXTimer);
|
|
475
|
+
this.scrollingXTimer = setTimeout(() => el.classList.remove(CLASS.scrollingX), SCROLLING_CLASS_LINGER_MS);
|
|
476
|
+
}
|
|
477
|
+
if (this.isScrollableY()) {
|
|
478
|
+
el.classList.add(CLASS.scrollingY);
|
|
479
|
+
if (this.scrollingYTimer)
|
|
480
|
+
clearTimeout(this.scrollingYTimer);
|
|
481
|
+
this.scrollingYTimer = setTimeout(() => el.classList.remove(CLASS.scrollingY), SCROLLING_CLASS_LINGER_MS);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
bindThumbDrag(thumb, rail, axis) {
|
|
485
|
+
this.on(thumb, 'pointerdown', (event) => {
|
|
486
|
+
const pointerEvent = event;
|
|
487
|
+
pointerEvent.stopPropagation();
|
|
488
|
+
pointerEvent.preventDefault();
|
|
489
|
+
const el = this.element;
|
|
490
|
+
rail.classList.add(CLASS.clicking);
|
|
491
|
+
thumb.setPointerCapture(pointerEvent.pointerId);
|
|
492
|
+
const startPointer = axis === 'x' ? pointerEvent.clientX : pointerEvent.clientY;
|
|
493
|
+
const startScroll = axis === 'x' ? el.scrollLeft : el.scrollTop;
|
|
494
|
+
const railSize = axis === 'x' ? el.clientWidth : el.clientHeight;
|
|
495
|
+
const thumbSize = axis === 'x' ? thumb.offsetWidth : thumb.offsetHeight;
|
|
496
|
+
const travel = axis === 'x' ? el.scrollWidth - el.clientWidth : el.scrollHeight - el.clientHeight;
|
|
497
|
+
const onMove = (moveEvent) => {
|
|
498
|
+
const delta = (axis === 'x' ? moveEvent.clientX : moveEvent.clientY) - startPointer;
|
|
499
|
+
const trackable = railSize - thumbSize;
|
|
500
|
+
if (trackable <= 0)
|
|
501
|
+
return;
|
|
502
|
+
const next = startScroll + (delta * travel) / trackable;
|
|
503
|
+
if (axis === 'x')
|
|
504
|
+
el.scrollLeft = next;
|
|
505
|
+
else
|
|
506
|
+
el.scrollTop = next;
|
|
507
|
+
};
|
|
508
|
+
const onUp = (upEvent) => {
|
|
509
|
+
rail.classList.remove(CLASS.clicking);
|
|
510
|
+
thumb.releasePointerCapture(upEvent.pointerId);
|
|
511
|
+
thumb.removeEventListener('pointermove', onMove);
|
|
512
|
+
thumb.removeEventListener('pointerup', onUp);
|
|
513
|
+
thumb.removeEventListener('pointercancel', onUp);
|
|
514
|
+
};
|
|
515
|
+
thumb.addEventListener('pointermove', onMove);
|
|
516
|
+
thumb.addEventListener('pointerup', onUp);
|
|
517
|
+
thumb.addEventListener('pointercancel', onUp);
|
|
518
|
+
});
|
|
519
|
+
// A click on the rail itself jumps the thumb to that point, as the original did.
|
|
520
|
+
this.on(rail, 'pointerdown', (event) => {
|
|
521
|
+
const pointerEvent = event;
|
|
522
|
+
if (pointerEvent.target !== rail)
|
|
523
|
+
return;
|
|
524
|
+
const el = this.element;
|
|
525
|
+
const railRect = rail.getBoundingClientRect();
|
|
526
|
+
const thumbSize = axis === 'x' ? thumb.offsetWidth : thumb.offsetHeight;
|
|
527
|
+
const offset = axis === 'x'
|
|
528
|
+
? pointerEvent.clientX - railRect.left
|
|
529
|
+
: pointerEvent.clientY - railRect.top;
|
|
530
|
+
const railSize = axis === 'x' ? railRect.width : railRect.height;
|
|
531
|
+
const travel = axis === 'x' ? el.scrollWidth - el.clientWidth : el.scrollHeight - el.clientHeight;
|
|
532
|
+
const trackable = railSize - thumbSize;
|
|
533
|
+
if (trackable <= 0)
|
|
534
|
+
return;
|
|
535
|
+
const next = ((offset - thumbSize / 2) * travel) / trackable;
|
|
536
|
+
if (axis === 'x')
|
|
537
|
+
el.scrollLeft = next;
|
|
538
|
+
else
|
|
539
|
+
el.scrollTop = next;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/** Only the configured bounds, and only when set — the same shape the original's clamp had. */
|
|
543
|
+
clampThumb(size) {
|
|
544
|
+
const min = this.options.minScrollbarLength;
|
|
545
|
+
const max = this.options.maxScrollbarLength;
|
|
546
|
+
let result = size;
|
|
547
|
+
if (min)
|
|
548
|
+
result = Math.max(result, min);
|
|
549
|
+
if (max)
|
|
550
|
+
result = Math.min(result, max);
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
on(target, type, handler, capture = false) {
|
|
554
|
+
target.addEventListener(type, handler, capture);
|
|
555
|
+
this.teardown.push(() => target.removeEventListener(type, handler, capture));
|
|
556
|
+
}
|
|
557
|
+
dispose() {
|
|
558
|
+
if (!this.initialised)
|
|
559
|
+
return;
|
|
560
|
+
this.initialised = false;
|
|
561
|
+
this.cancelAnimation();
|
|
562
|
+
for (const off of this.teardown)
|
|
563
|
+
off();
|
|
564
|
+
this.teardown = [];
|
|
565
|
+
this.resizeObserver?.disconnect();
|
|
566
|
+
this.mutationObserver?.disconnect();
|
|
567
|
+
this.resizeObserver = undefined;
|
|
568
|
+
this.mutationObserver = undefined;
|
|
569
|
+
if (this.scrollingXTimer)
|
|
570
|
+
clearTimeout(this.scrollingXTimer);
|
|
571
|
+
if (this.scrollingYTimer)
|
|
572
|
+
clearTimeout(this.scrollingYTimer);
|
|
573
|
+
this.railX?.remove();
|
|
574
|
+
this.railY?.remove();
|
|
575
|
+
this.railX = this.railY = this.thumbX = this.thumbY = undefined;
|
|
576
|
+
this.element.classList.remove(CLASS.host, CLASS.activeX, CLASS.activeY, CLASS.scrollingX, CLASS.scrollingY, CLASS.focus);
|
|
577
|
+
}
|
|
578
|
+
cancelAnimation() {
|
|
579
|
+
if (this.animation !== null) {
|
|
580
|
+
window.cancelAnimationFrame(this.animation);
|
|
581
|
+
this.animation = null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollbarDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
585
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "22.1.5", type: NxeScrollbarDirective, isStandalone: true, selector: "[nxeScrollbar]", inputs: { nxeScrollbar: ["nxeScrollbar", "nxeScrollbar", coerceEnabled], nxeScrollbarOptions: "nxeScrollbarOptions" }, exportAs: ["nxeScrollbar"], usesOnChanges: true, ngImport: i0 });
|
|
586
|
+
}
|
|
587
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeScrollbarDirective, decorators: [{
|
|
588
|
+
type: Directive,
|
|
589
|
+
args: [{
|
|
590
|
+
selector: '[nxeScrollbar]',
|
|
591
|
+
exportAs: 'nxeScrollbar',
|
|
592
|
+
}]
|
|
593
|
+
}], propDecorators: { nxeScrollbar: [{
|
|
594
|
+
type: Input,
|
|
595
|
+
args: [{ transform: coerceEnabled }]
|
|
596
|
+
}], nxeScrollbarOptions: [{
|
|
597
|
+
type: Input
|
|
598
|
+
}] } });
|
|
599
|
+
/** `<div nxeScrollbar>` sets the empty string; treat that as on, like any boolean attribute. */
|
|
600
|
+
function coerceEnabled(value) {
|
|
601
|
+
return value !== false && value !== 'false' && value != null;
|
|
602
|
+
}
|
|
603
|
+
/** `parseInt`-style truncation toward zero, with NaN folded to 0 — what the original used. */
|
|
604
|
+
function trunc(value) {
|
|
605
|
+
return Math.trunc(value) || 0;
|
|
606
|
+
}
|
|
607
|
+
function reach(offset, max) {
|
|
608
|
+
if (max <= 0)
|
|
609
|
+
return 0;
|
|
610
|
+
if (offset <= 0)
|
|
611
|
+
return -1;
|
|
612
|
+
if (offset >= max)
|
|
613
|
+
return 1;
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
52
617
|
// @nexterajs/ui/styling — public API.
|
|
53
618
|
|
|
54
619
|
/**
|
|
55
620
|
* Generated bundle index. Do not edit.
|
|
56
621
|
*/
|
|
57
622
|
|
|
58
|
-
export { NXE_PANE_FLIPPED_CLASS, NXE_WIDGET_CLASS, nxePane, trackPaneFlip };
|
|
623
|
+
export { NXE_PANE_FLIPPED_CLASS, NXE_WIDGET_CLASS, NxeDxCompatService, NxeScrollbarDirective, nxePane, provideNxeDxCompat, trackPaneFlip };
|
|
59
624
|
//# sourceMappingURL=nexterajs-ui-styling.mjs.map
|