@besaitech/ng-design-system 0.0.52 → 0.0.53
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/fesm2022/besaitech-ng-design-system.mjs +3656 -140
- package/fesm2022/besaitech-ng-design-system.mjs.map +1 -1
- package/index.d.ts +899 -21
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, numberAttribute, output, signal, forwardRef, viewChild, viewChildren, afterRenderEffect, model, ElementRef, Injector, afterNextRender, TemplateRef,
|
|
2
|
+
import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, numberAttribute, output, signal, forwardRef, viewChild, viewChildren, afterRenderEffect, model, ElementRef, Directive, Injector, contentChild, untracked, afterNextRender, TemplateRef, linkedSignal, Injectable, contentChildren, DestroyRef } from '@angular/core';
|
|
3
3
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
4
4
|
import * as i1 from '@angular/forms';
|
|
5
5
|
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule } from '@angular/forms';
|
|
@@ -108,6 +108,22 @@ const SDS_ICONS = {
|
|
|
108
108
|
'search-x': '<path d="m13.5 8.5-5 5"/><path d="m8.5 8.5 5 5"/><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
109
109
|
command: '<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"/>',
|
|
110
110
|
sparkles: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
|
|
111
|
+
// Added for 0.0.53 (Lucide 24x24 stroke paths): status glyphs for sds-checklist-row / sds-score /
|
|
112
|
+
// sds-status-tile (distinct SHAPES per status, so status never rides on colour), the sds-menu trigger
|
|
113
|
+
// ellipses, the sds-sortable-list grip, sds-file-drop / sds-image-cropper glyphs, and common row actions.
|
|
114
|
+
'triangle-alert': '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
115
|
+
'octagon-x': '<path d="m15 9-6 6"/><path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"/><path d="m9 9 6 6"/>',
|
|
116
|
+
'ban': '<circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/>',
|
|
117
|
+
'ellipsis': '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
|
|
118
|
+
'ellipsis-vertical': '<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>',
|
|
119
|
+
'grip-vertical': '<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>',
|
|
120
|
+
'upload': '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>',
|
|
121
|
+
'image': '<rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>',
|
|
122
|
+
'zoom-in': '<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/>',
|
|
123
|
+
'zoom-out': '<circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/>',
|
|
124
|
+
'trash-2': '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>',
|
|
125
|
+
'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
|
126
|
+
'pencil': '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>',
|
|
111
127
|
// A question mark in a circle: the "may contain" mark on sds-tri-state-chip, and a generic help glyph.
|
|
112
128
|
'circle-help': '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
|
|
113
129
|
// Stock / inventory (the kitchen stock board, the sold-out toolkit). Original glyph: three stacked boxes.
|
|
@@ -314,7 +330,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
314
330
|
`, styles: [".sds-spinner{width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:9999px;animation:sds-spin .6s linear infinite}@keyframes sds-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
315
331
|
}], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
316
332
|
|
|
317
|
-
let nextId$
|
|
333
|
+
let nextId$l = 0;
|
|
318
334
|
const SIZES$a = {
|
|
319
335
|
sm: 'h-9 text-sm',
|
|
320
336
|
md: 'h-10 text-sm',
|
|
@@ -327,7 +343,7 @@ const SIZES$a = {
|
|
|
327
343
|
* sidebar search field).
|
|
328
344
|
*/
|
|
329
345
|
class SdsInput {
|
|
330
|
-
inputId = `sds-input-${nextId$
|
|
346
|
+
inputId = `sds-input-${nextId$l++}`;
|
|
331
347
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
332
348
|
/** Accessible name used when no visible [label] is provided. */
|
|
333
349
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -660,7 +676,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
660
676
|
`, styles: ["input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus,input:-webkit-autofill:active{transition:background-color 100000s ease-in-out 0s;-webkit-text-fill-color:currentColor;caret-color:currentColor}\n"] }]
|
|
661
677
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], autocomplete: [{ type: i0.Input, args: [{ isSignal: true, alias: "autocomplete", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }], trailingAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingAction", required: false }] }], trailingActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionLabel", required: false }] }], trailingActionPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionPressed", required: false }] }], trailingActionClick: [{ type: i0.Output, args: ["trailingActionClick"] }] } });
|
|
662
678
|
|
|
663
|
-
let nextId$
|
|
679
|
+
let nextId$k = 0;
|
|
664
680
|
const HEIGHTS = {
|
|
665
681
|
sm: 'h-9',
|
|
666
682
|
md: 'h-10',
|
|
@@ -683,7 +699,7 @@ const TEXT = {
|
|
|
683
699
|
* {@link min}/{@link max} on blur and on every spinner step; ArrowUp / ArrowDown also step it.
|
|
684
700
|
*/
|
|
685
701
|
class SdsInputNumber {
|
|
686
|
-
inputId = `sds-input-number-${nextId$
|
|
702
|
+
inputId = `sds-input-number-${nextId$k++}`;
|
|
687
703
|
fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
|
|
688
704
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
689
705
|
/** Accessible name used when no visible [label] is provided. */
|
|
@@ -856,6 +872,20 @@ class SdsInputNumber {
|
|
|
856
872
|
el.value = v === null ? '' : this.format(v);
|
|
857
873
|
});
|
|
858
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* Focus the field, painting the current value into it first (the live-format effect stands down while
|
|
877
|
+
* the field is focused, so a value written just before focusing would otherwise not show). With
|
|
878
|
+
* `select`, the text is selected — handy for an in-cell editor.
|
|
879
|
+
*/
|
|
880
|
+
focus(options = {}) {
|
|
881
|
+
const el = this.fieldRef()?.nativeElement;
|
|
882
|
+
if (!el)
|
|
883
|
+
return;
|
|
884
|
+
this.paintCommitted();
|
|
885
|
+
el.focus();
|
|
886
|
+
if (options.select)
|
|
887
|
+
el.select();
|
|
888
|
+
}
|
|
859
889
|
isActive(el) {
|
|
860
890
|
return typeof document !== 'undefined' && document.activeElement === el;
|
|
861
891
|
}
|
|
@@ -1390,7 +1420,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1390
1420
|
}]
|
|
1391
1421
|
}], ctorParameters: () => [], propDecorators: { fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], currencyDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyDisplay", required: false }] }], useGrouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "useGrouping", required: false }] }], minFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "minFractionDigits", required: false }] }], maxFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFractionDigits", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], showButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtons", required: false }] }], buttonLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLayout", required: false }] }], incrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementIcon", required: false }] }], decrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementIcon", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], incrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementLabel", required: false }] }], decrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementLabel", required: false }] }] } });
|
|
1392
1422
|
|
|
1393
|
-
let nextId$
|
|
1423
|
+
let nextId$j = 0;
|
|
1394
1424
|
const SIZES$9 = {
|
|
1395
1425
|
sm: 'min-h-9 text-sm',
|
|
1396
1426
|
md: 'min-h-10 text-sm',
|
|
@@ -1410,7 +1440,7 @@ const SIZES$9 = {
|
|
|
1410
1440
|
* `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
|
|
1411
1441
|
*/
|
|
1412
1442
|
class SdsLocalizedInput {
|
|
1413
|
-
inputId = `sds-localized-input-${nextId$
|
|
1443
|
+
inputId = `sds-localized-input-${nextId$j++}`;
|
|
1414
1444
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
1415
1445
|
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
1416
1446
|
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
@@ -1666,7 +1696,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1666
1696
|
}]
|
|
1667
1697
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], currentLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentLanguage", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], languageSwitcherLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "languageSwitcherLabel", required: false }] }], untranslatedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "untranslatedLabel", required: false }] }] } });
|
|
1668
1698
|
|
|
1669
|
-
let nextId$
|
|
1699
|
+
let nextId$i = 0;
|
|
1670
1700
|
const SIZES$8 = {
|
|
1671
1701
|
md: 'h-12 w-11 text-lg',
|
|
1672
1702
|
lg: 'h-14 w-12 text-xl',
|
|
@@ -1683,7 +1713,7 @@ const SIZES$8 = {
|
|
|
1683
1713
|
* first box and have it land correctly.
|
|
1684
1714
|
*/
|
|
1685
1715
|
class SdsOtpInput {
|
|
1686
|
-
groupId = `sds-otp-${nextId$
|
|
1716
|
+
groupId = `sds-otp-${nextId$i++}`;
|
|
1687
1717
|
/** How many boxes / characters the code has. */
|
|
1688
1718
|
length = input(6, ...(ngDevMode ? [{ debugName: "length", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
1689
1719
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
@@ -1983,7 +2013,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1983
2013
|
}]
|
|
1984
2014
|
}], ctorParameters: () => [], propDecorators: { length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], inputMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputMode", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], cellLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellLabel", required: false }] }], completed: [{ type: i0.Output, args: ["completed"] }], cells: [{ type: i0.ViewChildren, args: ['cell', { isSignal: true }] }] } });
|
|
1985
2015
|
|
|
1986
|
-
let nextId$
|
|
2016
|
+
let nextId$h = 0;
|
|
1987
2017
|
const SIZES$7 = {
|
|
1988
2018
|
sm: 'text-sm px-3 py-2',
|
|
1989
2019
|
md: 'text-sm px-3.5 py-2.5',
|
|
@@ -1995,7 +2025,7 @@ const SIZES$7 = {
|
|
|
1995
2025
|
* [(ngModel)].
|
|
1996
2026
|
*/
|
|
1997
2027
|
class SdsTextarea {
|
|
1998
|
-
fieldId = `sds-textarea-${nextId$
|
|
2028
|
+
fieldId = `sds-textarea-${nextId$h++}`;
|
|
1999
2029
|
field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
2000
2030
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2001
2031
|
/** Accessible name used when no visible [label] is provided. */
|
|
@@ -2248,7 +2278,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2248
2278
|
}]
|
|
2249
2279
|
}], ctorParameters: () => [], propDecorators: { field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], autoResize: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoResize", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], resize: [{ type: i0.Input, args: [{ isSignal: true, alias: "resize", required: false }] }] } });
|
|
2250
2280
|
|
|
2251
|
-
let nextId$
|
|
2281
|
+
let nextId$g = 0;
|
|
2252
2282
|
const SIZES$6 = {
|
|
2253
2283
|
sm: { track: 'h-5 w-9', knob: 'h-4 w-4', on: 'translate-x-4', icon: 11 },
|
|
2254
2284
|
md: { track: 'h-6 w-11', knob: 'h-5 w-5', on: 'translate-x-5', icon: 13 },
|
|
@@ -2261,7 +2291,7 @@ const SIZES$6 = {
|
|
|
2261
2291
|
* states.
|
|
2262
2292
|
*/
|
|
2263
2293
|
class SdsToggleSwitch {
|
|
2264
|
-
fieldId = `sds-toggle-switch-${nextId$
|
|
2294
|
+
fieldId = `sds-toggle-switch-${nextId$g++}`;
|
|
2265
2295
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2266
2296
|
/** Accessible name used when no visible [label] is provided. */
|
|
2267
2297
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -2505,7 +2535,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2505
2535
|
}]
|
|
2506
2536
|
}], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], trueValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "trueValue", required: false }] }], falseValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "falseValue", required: false }] }], onIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "onIcon", required: false }] }], offIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "offIcon", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }] } });
|
|
2507
2537
|
|
|
2508
|
-
let nextId$
|
|
2538
|
+
let nextId$f = 0;
|
|
2509
2539
|
/**
|
|
2510
2540
|
* Boolean checkbox. Implements ControlValueAccessor — use with reactive forms
|
|
2511
2541
|
* or `[(ngModel)]`. Renders a styled box (brand-filled with a check when on)
|
|
@@ -2517,7 +2547,7 @@ let nextId$a = 0;
|
|
|
2517
2547
|
* <sds-checkbox label="Fusion automatique" [(ngModel)]="autoMerge" />
|
|
2518
2548
|
*/
|
|
2519
2549
|
class SdsCheckbox {
|
|
2520
|
-
id = `sds-checkbox-${nextId$
|
|
2550
|
+
id = `sds-checkbox-${nextId$f++}`;
|
|
2521
2551
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2522
2552
|
/** Accessible name used when no visible [label] is provided. */
|
|
2523
2553
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -2651,7 +2681,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2651
2681
|
}]
|
|
2652
2682
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
|
|
2653
2683
|
|
|
2654
|
-
let nextId$
|
|
2684
|
+
let nextId$e = 0;
|
|
2655
2685
|
/**
|
|
2656
2686
|
* Mounted radios by group name. A pick updates the rest of its group, because a native radio says
|
|
2657
2687
|
* nothing when the browser unchecks it — without this a reactive form would leave the old choice drawn.
|
|
@@ -2677,7 +2707,7 @@ const groups = new Map();
|
|
|
2677
2707
|
* <sds-radio name="size" value="large" label="Grand" [(ngModel)]="size" />
|
|
2678
2708
|
*/
|
|
2679
2709
|
class SdsRadio {
|
|
2680
|
-
id = `sds-radio-${nextId$
|
|
2710
|
+
id = `sds-radio-${nextId$e++}`;
|
|
2681
2711
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2682
2712
|
/** Accessible name used when no visible [label] is provided. */
|
|
2683
2713
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -3163,7 +3193,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
3163
3193
|
|
|
3164
3194
|
/** Activation order: none → contains → may-contain → none. */
|
|
3165
3195
|
const ORDER = ['none', 'contains', 'may-contain'];
|
|
3166
|
-
let nextId$
|
|
3196
|
+
let nextId$d = 0;
|
|
3167
3197
|
/**
|
|
3168
3198
|
* A chip that cycles through three states — `none`, `contains`, `may-contain` — built for the 14-tile
|
|
3169
3199
|
* allergen declaration grid, but generic (any "no / yes / maybe" declaration).
|
|
@@ -3183,7 +3213,7 @@ let nextId$8 = 0;
|
|
|
3183
3213
|
* containsLabel="Contains" mayContainLabel="May contain" noneLabel="Not present" />
|
|
3184
3214
|
*/
|
|
3185
3215
|
class SdsTriStateChip {
|
|
3186
|
-
chipId = `sds-tri-state-chip-${nextId$
|
|
3216
|
+
chipId = `sds-tri-state-chip-${nextId$d++}`;
|
|
3187
3217
|
/** Current state. Two-way: `[(value)]`. */
|
|
3188
3218
|
value = model('none', ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
3189
3219
|
/** Visible name (e.g. the allergen). Required — the chip has no other accessible name. */
|
|
@@ -3314,96 +3344,2875 @@ class SdsTriStateChip {
|
|
|
3314
3344
|
<!-- The state is part of the accessible name ("Milk, may contain"), visible caption or not. -->
|
|
3315
3345
|
<span class="sr-only">, {{ stateText() }}</span>
|
|
3316
3346
|
|
|
3317
|
-
<!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
|
|
3318
|
-
state so the chip never shifts when cycled. -->
|
|
3319
|
-
<span
|
|
3320
|
-
class="grid shrink-0 place-items-center"
|
|
3321
|
-
[class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
|
|
3322
|
-
aria-hidden="true"
|
|
3347
|
+
<!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
|
|
3348
|
+
state so the chip never shifts when cycled. -->
|
|
3349
|
+
<span
|
|
3350
|
+
class="grid shrink-0 place-items-center"
|
|
3351
|
+
[class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
|
|
3352
|
+
aria-hidden="true"
|
|
3353
|
+
>
|
|
3354
|
+
@switch (value()) {
|
|
3355
|
+
@case ('contains') {
|
|
3356
|
+
<sds-icon name="check" [size]="size() === 'sm' ? 14 : 18" [strokeWidth]="3" />
|
|
3357
|
+
}
|
|
3358
|
+
@case ('may-contain') {
|
|
3359
|
+
<sds-icon name="circle-help" [size]="size() === 'sm' ? 15 : 19" />
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
</span>
|
|
3363
|
+
</button>
|
|
3364
|
+
|
|
3365
|
+
@if (hint()) {
|
|
3366
|
+
<span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
|
|
3367
|
+
}
|
|
3368
|
+
<span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
|
|
3369
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3370
|
+
}
|
|
3371
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTriStateChip, decorators: [{
|
|
3372
|
+
type: Component,
|
|
3373
|
+
args: [{
|
|
3374
|
+
selector: 'sds-tri-state-chip',
|
|
3375
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3376
|
+
imports: [SdsIcon],
|
|
3377
|
+
host: { class: 'inline-flex min-w-0' },
|
|
3378
|
+
template: `
|
|
3379
|
+
<button
|
|
3380
|
+
type="button"
|
|
3381
|
+
[id]="chipId"
|
|
3382
|
+
[class]="chipClass()"
|
|
3383
|
+
[disabled]="disabled()"
|
|
3384
|
+
[attr.data-state]="value()"
|
|
3385
|
+
[attr.aria-describedby]="hint() ? chipId + '-hint' : null"
|
|
3386
|
+
(click)="onClick($event)"
|
|
3387
|
+
(keydown)="onKeydown($event)"
|
|
3388
|
+
>
|
|
3389
|
+
@if (icon()) {
|
|
3390
|
+
<sds-icon [name]="icon()!" [size]="size() === 'sm' ? 18 : 22" class="shrink-0" />
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
<span class="flex min-w-0 flex-1 flex-col text-left leading-tight">
|
|
3394
|
+
<!-- Up to two lines, so "Cereals containing gluten" stays readable in a narrow grid cell. -->
|
|
3395
|
+
<span
|
|
3396
|
+
class="line-clamp-2 break-words font-semibold"
|
|
3397
|
+
[class]="size() === 'sm' ? 'text-xs' : 'text-sm'"
|
|
3398
|
+
>{{ label() }}</span
|
|
3399
|
+
>
|
|
3400
|
+
@if (showState()) {
|
|
3401
|
+
<span
|
|
3402
|
+
class="truncate text-[11px] font-medium"
|
|
3403
|
+
[class]="captionClass()"
|
|
3404
|
+
aria-hidden="true"
|
|
3405
|
+
>{{ stateText() }}</span
|
|
3406
|
+
>
|
|
3407
|
+
}
|
|
3408
|
+
</span>
|
|
3409
|
+
<!-- The state is part of the accessible name ("Milk, may contain"), visible caption or not. -->
|
|
3410
|
+
<span class="sr-only">, {{ stateText() }}</span>
|
|
3411
|
+
|
|
3412
|
+
<!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
|
|
3413
|
+
state so the chip never shifts when cycled. -->
|
|
3414
|
+
<span
|
|
3415
|
+
class="grid shrink-0 place-items-center"
|
|
3416
|
+
[class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
|
|
3417
|
+
aria-hidden="true"
|
|
3418
|
+
>
|
|
3419
|
+
@switch (value()) {
|
|
3420
|
+
@case ('contains') {
|
|
3421
|
+
<sds-icon name="check" [size]="size() === 'sm' ? 14 : 18" [strokeWidth]="3" />
|
|
3422
|
+
}
|
|
3423
|
+
@case ('may-contain') {
|
|
3424
|
+
<sds-icon name="circle-help" [size]="size() === 'sm' ? 15 : 19" />
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
</span>
|
|
3428
|
+
</button>
|
|
3429
|
+
|
|
3430
|
+
@if (hint()) {
|
|
3431
|
+
<span [id]="chipId + '-hint'" class="sr-only">{{ hint() }}</span>
|
|
3432
|
+
}
|
|
3433
|
+
<span class="sr-only" aria-live="polite" aria-atomic="true">{{ announcement() }}</span>
|
|
3434
|
+
`,
|
|
3435
|
+
}]
|
|
3436
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showState: [{ type: i0.Input, args: [{ isSignal: true, alias: "showState", required: false }] }], noneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noneLabel", required: false }] }], containsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "containsLabel", required: false }] }], mayContainLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "mayContainLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }] } });
|
|
3437
|
+
|
|
3438
|
+
let nextId$c = 0;
|
|
3439
|
+
/**
|
|
3440
|
+
* Segmented control: a small set of mutually exclusive options shown side by side (e.g. "Pick exactly /
|
|
3441
|
+
* at least / up to"). Radio-group semantics: `role="radiogroup"` with `role="radio"` segments, one tab
|
|
3442
|
+
* stop, and the arrow keys (with Home/End) move AND select, like native radios. Disabled segments are
|
|
3443
|
+
* skipped.
|
|
3444
|
+
*
|
|
3445
|
+
* The value is a two-way `model` — bind `[(value)]`. Give the group a name with [ariaLabel] (or
|
|
3446
|
+
* [ariaLabelledby]); with [iconOnly] each segment keeps its label as its accessible name.
|
|
3447
|
+
*
|
|
3448
|
+
* @example
|
|
3449
|
+
* <sds-segmented ariaLabel="Rule" [options]="[{ value: 'exactly', label: 'Exactly' }, { value: 'upTo', label: 'Up to' }]"
|
|
3450
|
+
* [(value)]="rule" />
|
|
3451
|
+
*/
|
|
3452
|
+
class SdsSegmented {
|
|
3453
|
+
host = inject(ElementRef);
|
|
3454
|
+
baseId = `sds-segmented-${nextId$c++}`;
|
|
3455
|
+
/** Options: `{ value, label, icon?, disabled? }` objects, or bare strings/numbers. */
|
|
3456
|
+
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
3457
|
+
/** Selected value. Two-way: `[(value)]`. */
|
|
3458
|
+
value = model(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
3459
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
3460
|
+
/** Accessible name of the group. */
|
|
3461
|
+
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
3462
|
+
/** Id of an element that names the group (alternative to [ariaLabel]). */
|
|
3463
|
+
ariaLabelledby = input('', ...(ngDevMode ? [{ debugName: "ariaLabelledby" }] : []));
|
|
3464
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3465
|
+
/** Stretch to the container width, segments sharing it equally. */
|
|
3466
|
+
fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3467
|
+
/** Show icons only; labels stay as accessible names and tooltips. */
|
|
3468
|
+
iconOnly = input(false, ...(ngDevMode ? [{ debugName: "iconOnly", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3469
|
+
segments = computed(() => this.options().map((o) => o !== null && typeof o === 'object'
|
|
3470
|
+
? { value: o.value, label: o.label, icon: o.icon ?? null, disabled: !!o.disabled }
|
|
3471
|
+
: { value: o, label: String(o), icon: null, disabled: false }), ...(ngDevMode ? [{ debugName: "segments" }] : []));
|
|
3472
|
+
selectedIndex = computed(() => this.segments().findIndex((s) => s.value === this.value()), ...(ngDevMode ? [{ debugName: "selectedIndex" }] : []));
|
|
3473
|
+
/** Roving tab stop: the selected segment, else the first enabled one. */
|
|
3474
|
+
tabStop = computed(() => {
|
|
3475
|
+
const sel = this.selectedIndex();
|
|
3476
|
+
if (sel >= 0 && !this.segments()[sel].disabled)
|
|
3477
|
+
return sel;
|
|
3478
|
+
return this.segments().findIndex((s) => !s.disabled);
|
|
3479
|
+
}, ...(ngDevMode ? [{ debugName: "tabStop" }] : []));
|
|
3480
|
+
isSelected(seg) {
|
|
3481
|
+
return seg.value === this.value();
|
|
3482
|
+
}
|
|
3483
|
+
segmentId(i) {
|
|
3484
|
+
return `${this.baseId}-${i}`;
|
|
3485
|
+
}
|
|
3486
|
+
segmentClass(seg) {
|
|
3487
|
+
const sm = this.size() === 'sm';
|
|
3488
|
+
const base = 'inline-flex min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-[calc(var(--radius-control)-2px)] ' +
|
|
3489
|
+
'font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
|
|
3490
|
+
'disabled:cursor-not-allowed disabled:opacity-50 ' +
|
|
3491
|
+
(sm ? 'h-7 px-2.5 text-xs' : 'h-8 px-3.5 text-sm') +
|
|
3492
|
+
(this.fullWidth() ? ' flex-1' : '') +
|
|
3493
|
+
(this.iconOnly() ? (sm ? ' w-8 px-0' : ' w-9 px-0') : '');
|
|
3494
|
+
// Selected: raised surface, ring and bold text — more than a tint, so it reads in greyscale too.
|
|
3495
|
+
return this.isSelected(seg)
|
|
3496
|
+
? `${base} bg-surface font-semibold text-ink shadow-sm ring-1 ring-border dark:bg-border`
|
|
3497
|
+
: `${base} cursor-pointer text-ink-soft hover:bg-surface/60 hover:text-ink`;
|
|
3498
|
+
}
|
|
3499
|
+
choose(i) {
|
|
3500
|
+
const seg = this.segments()[i];
|
|
3501
|
+
if (!seg || seg.disabled || this.disabled())
|
|
3502
|
+
return;
|
|
3503
|
+
this.value.set(seg.value);
|
|
3504
|
+
}
|
|
3505
|
+
onKeydown(event) {
|
|
3506
|
+
if (this.disabled())
|
|
3507
|
+
return;
|
|
3508
|
+
const segs = this.segments();
|
|
3509
|
+
if (!segs.length)
|
|
3510
|
+
return;
|
|
3511
|
+
const current = Number(event.target.dataset['index'] ?? this.tabStop());
|
|
3512
|
+
let next = null;
|
|
3513
|
+
switch (event.key) {
|
|
3514
|
+
case 'ArrowRight':
|
|
3515
|
+
case 'ArrowDown':
|
|
3516
|
+
next = this.step(current, 1);
|
|
3517
|
+
break;
|
|
3518
|
+
case 'ArrowLeft':
|
|
3519
|
+
case 'ArrowUp':
|
|
3520
|
+
next = this.step(current, -1);
|
|
3521
|
+
break;
|
|
3522
|
+
case 'Home':
|
|
3523
|
+
next = segs.findIndex((s) => !s.disabled);
|
|
3524
|
+
break;
|
|
3525
|
+
case 'End':
|
|
3526
|
+
for (let i = segs.length - 1; i >= 0; i--) {
|
|
3527
|
+
if (!segs[i].disabled) {
|
|
3528
|
+
next = i;
|
|
3529
|
+
break;
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
break;
|
|
3533
|
+
default:
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
event.preventDefault();
|
|
3537
|
+
if (next === null || next < 0)
|
|
3538
|
+
return;
|
|
3539
|
+
this.choose(next);
|
|
3540
|
+
this.host.nativeElement.querySelector(`#${this.segmentId(next)}`)?.focus();
|
|
3541
|
+
}
|
|
3542
|
+
step(from, delta) {
|
|
3543
|
+
const segs = this.segments();
|
|
3544
|
+
let i = from;
|
|
3545
|
+
for (let n = 0; n < segs.length; n++) {
|
|
3546
|
+
i = (i + delta + segs.length) % segs.length;
|
|
3547
|
+
if (!segs[i].disabled)
|
|
3548
|
+
return i;
|
|
3549
|
+
}
|
|
3550
|
+
return null;
|
|
3551
|
+
}
|
|
3552
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3553
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSegmented, isStandalone: true, selector: "sds-segmented", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, iconOnly: { classPropertyName: "iconOnly", publicName: "iconOnly", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { properties: { "class.w-full": "fullWidth()" }, classAttribute: "inline-flex max-w-full" }, ngImport: i0, template: `
|
|
3554
|
+
<div
|
|
3555
|
+
role="radiogroup"
|
|
3556
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
3557
|
+
[attr.aria-labelledby]="ariaLabelledby() || null"
|
|
3558
|
+
[attr.aria-disabled]="disabled() ? 'true' : null"
|
|
3559
|
+
class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
|
|
3560
|
+
[class.w-full]="fullWidth()"
|
|
3561
|
+
(keydown)="onKeydown($event)"
|
|
3562
|
+
>
|
|
3563
|
+
@for (seg of segments(); track $index; let i = $index) {
|
|
3564
|
+
<button
|
|
3565
|
+
type="button"
|
|
3566
|
+
role="radio"
|
|
3567
|
+
[id]="segmentId(i)"
|
|
3568
|
+
[attr.aria-checked]="isSelected(seg)"
|
|
3569
|
+
[attr.aria-label]="iconOnly() ? seg.label : null"
|
|
3570
|
+
[attr.title]="iconOnly() ? seg.label : null"
|
|
3571
|
+
[attr.tabindex]="i === tabStop() ? 0 : -1"
|
|
3572
|
+
[attr.data-index]="i"
|
|
3573
|
+
[disabled]="disabled() || seg.disabled"
|
|
3574
|
+
[class]="segmentClass(seg)"
|
|
3575
|
+
(click)="choose(i)"
|
|
3576
|
+
>
|
|
3577
|
+
@if (seg.icon) {
|
|
3578
|
+
<sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
|
|
3579
|
+
}
|
|
3580
|
+
@if (!iconOnly()) {
|
|
3581
|
+
<span class="truncate">{{ seg.label }}</span>
|
|
3582
|
+
}
|
|
3583
|
+
</button>
|
|
3584
|
+
}
|
|
3585
|
+
</div>
|
|
3586
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3587
|
+
}
|
|
3588
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSegmented, decorators: [{
|
|
3589
|
+
type: Component,
|
|
3590
|
+
args: [{
|
|
3591
|
+
selector: 'sds-segmented',
|
|
3592
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3593
|
+
imports: [SdsIcon],
|
|
3594
|
+
host: { class: 'inline-flex max-w-full', '[class.w-full]': 'fullWidth()' },
|
|
3595
|
+
template: `
|
|
3596
|
+
<div
|
|
3597
|
+
role="radiogroup"
|
|
3598
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
3599
|
+
[attr.aria-labelledby]="ariaLabelledby() || null"
|
|
3600
|
+
[attr.aria-disabled]="disabled() ? 'true' : null"
|
|
3601
|
+
class="flex max-w-full items-stretch gap-0.5 overflow-x-auto rounded-control border border-border bg-muted p-0.5"
|
|
3602
|
+
[class.w-full]="fullWidth()"
|
|
3603
|
+
(keydown)="onKeydown($event)"
|
|
3604
|
+
>
|
|
3605
|
+
@for (seg of segments(); track $index; let i = $index) {
|
|
3606
|
+
<button
|
|
3607
|
+
type="button"
|
|
3608
|
+
role="radio"
|
|
3609
|
+
[id]="segmentId(i)"
|
|
3610
|
+
[attr.aria-checked]="isSelected(seg)"
|
|
3611
|
+
[attr.aria-label]="iconOnly() ? seg.label : null"
|
|
3612
|
+
[attr.title]="iconOnly() ? seg.label : null"
|
|
3613
|
+
[attr.tabindex]="i === tabStop() ? 0 : -1"
|
|
3614
|
+
[attr.data-index]="i"
|
|
3615
|
+
[disabled]="disabled() || seg.disabled"
|
|
3616
|
+
[class]="segmentClass(seg)"
|
|
3617
|
+
(click)="choose(i)"
|
|
3618
|
+
>
|
|
3619
|
+
@if (seg.icon) {
|
|
3620
|
+
<sds-icon [name]="seg.icon" [size]="size() === 'sm' ? 15 : 17" class="shrink-0" />
|
|
3621
|
+
}
|
|
3622
|
+
@if (!iconOnly()) {
|
|
3623
|
+
<span class="truncate">{{ seg.label }}</span>
|
|
3624
|
+
}
|
|
3625
|
+
</button>
|
|
3626
|
+
}
|
|
3627
|
+
</div>
|
|
3628
|
+
`,
|
|
3629
|
+
}]
|
|
3630
|
+
}], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }] } });
|
|
3631
|
+
|
|
3632
|
+
/**
|
|
3633
|
+
* Each status has its own glyph SHAPE (circle-check, triangle, octagon, circle-i) as well as its own
|
|
3634
|
+
* tint, so the row still reads in greyscale or for a colour-blind reader. `label` is the English
|
|
3635
|
+
* default of the spoken status; override it with [statusLabel].
|
|
3636
|
+
*/
|
|
3637
|
+
const STATUS = {
|
|
3638
|
+
done: { icon: 'check-circle', tile: 'bg-accent-green-soft text-accent-green', label: 'Done' },
|
|
3639
|
+
warning: { icon: 'triangle-alert', tile: 'bg-accent-amber-soft text-accent-amber', label: 'Warning' },
|
|
3640
|
+
blocker: { icon: 'octagon-x', tile: 'bg-accent-red-soft text-accent-red', label: 'Blocking' },
|
|
3641
|
+
info: { icon: 'info', tile: 'bg-muted text-ink-soft', label: 'Info' },
|
|
3642
|
+
};
|
|
3643
|
+
/**
|
|
3644
|
+
* One line of a readiness checklist or health report: a status glyph, a title, an optional
|
|
3645
|
+
* description, an optional count pill and an action (a projected `[sdsChecklistAction]` element, or
|
|
3646
|
+
* the built-in button from [actionLabel] that emits `(action)`).
|
|
3647
|
+
*
|
|
3648
|
+
* The status is announced as text before the title ("Blocking: 3 items have no price"), never by
|
|
3649
|
+
* colour alone. Put rows in a list for screen readers: `<ul role="list">` with each row wrapped in
|
|
3650
|
+
* an `<li>`, or give the container `role="list"` and each row `role="listitem"`.
|
|
3651
|
+
*
|
|
3652
|
+
* @example
|
|
3653
|
+
* <sds-checklist-row status="blocker" title="3 items have no price" description="Customers can't order them."
|
|
3654
|
+
* [count]="3" actionLabel="Fix" (action)="openFilter('no-price')" />
|
|
3655
|
+
*/
|
|
3656
|
+
class SdsChecklistRow {
|
|
3657
|
+
status = input('info', ...(ngDevMode ? [{ debugName: "status" }] : []));
|
|
3658
|
+
title = input.required(...(ngDevMode ? [{ debugName: "title" }] : []));
|
|
3659
|
+
description = input('', ...(ngDevMode ? [{ debugName: "description" }] : []));
|
|
3660
|
+
/** Optional number of affected things, shown as a pill after the title. Null hides it. */
|
|
3661
|
+
count = input(null, ...(ngDevMode ? [{ debugName: "count" }] : []));
|
|
3662
|
+
/**
|
|
3663
|
+
* Accessible text for the count pill; `{count}` is replaced. Default `'{count} items'` — pass a
|
|
3664
|
+
* translated one.
|
|
3665
|
+
*/
|
|
3666
|
+
countLabel = input('{count} items', ...(ngDevMode ? [{ debugName: "countLabel" }] : []));
|
|
3667
|
+
/** Spoken status prefix. Defaults per status: Done · Warning · Blocking · Info (English). */
|
|
3668
|
+
statusLabel = input('', ...(ngDevMode ? [{ debugName: "statusLabel" }] : []));
|
|
3669
|
+
/** Label of the built-in action button. Empty hides it (project `[sdsChecklistAction]` instead). */
|
|
3670
|
+
actionLabel = input('', ...(ngDevMode ? [{ debugName: "actionLabel" }] : []));
|
|
3671
|
+
/**
|
|
3672
|
+
* Full accessible name for the action button when its visible label is terse ("Fix") — e.g.
|
|
3673
|
+
* "Fix: 3 items have no price".
|
|
3674
|
+
*/
|
|
3675
|
+
actionAriaLabel = input('', ...(ngDevMode ? [{ debugName: "actionAriaLabel" }] : []));
|
|
3676
|
+
/** Strike the title through when the row is done. */
|
|
3677
|
+
strikeDone = input(false, ...(ngDevMode ? [{ debugName: "strikeDone", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3678
|
+
/** Emitted when the built-in action button is pressed. */
|
|
3679
|
+
action = output();
|
|
3680
|
+
style = computed(() => STATUS[this.status()] ?? STATUS.info, ...(ngDevMode ? [{ debugName: "style" }] : []));
|
|
3681
|
+
resolvedStatusLabel = computed(() => this.statusLabel() || this.style().label, ...(ngDevMode ? [{ debugName: "resolvedStatusLabel" }] : []));
|
|
3682
|
+
countText = computed(() => this.countLabel().replace('{count}', String(this.count() ?? '')), ...(ngDevMode ? [{ debugName: "countText" }] : []));
|
|
3683
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChecklistRow, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3684
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsChecklistRow, isStandalone: true, selector: "sds-checklist-row", inputs: { status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, countLabel: { classPropertyName: "countLabel", publicName: "countLabel", isSignal: true, isRequired: false, transformFunction: null }, statusLabel: { classPropertyName: "statusLabel", publicName: "statusLabel", isSignal: true, isRequired: false, transformFunction: null }, actionLabel: { classPropertyName: "actionLabel", publicName: "actionLabel", isSignal: true, isRequired: false, transformFunction: null }, actionAriaLabel: { classPropertyName: "actionAriaLabel", publicName: "actionAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, strikeDone: { classPropertyName: "strikeDone", publicName: "strikeDone", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { action: "action" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
3685
|
+
<div
|
|
3686
|
+
class="flex items-start gap-3 rounded-control px-3 py-3"
|
|
3687
|
+
[class.opacity-80]="status() === 'done'"
|
|
3688
|
+
[attr.data-status]="status()"
|
|
3689
|
+
>
|
|
3690
|
+
<span
|
|
3691
|
+
class="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-full"
|
|
3692
|
+
[class]="style().tile"
|
|
3693
|
+
aria-hidden="true"
|
|
3694
|
+
>
|
|
3695
|
+
<sds-icon [name]="style().icon" [size]="18" />
|
|
3696
|
+
</span>
|
|
3697
|
+
|
|
3698
|
+
<div class="min-w-0 flex-1">
|
|
3699
|
+
<p class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm font-semibold text-ink">
|
|
3700
|
+
<span class="sr-only">{{ resolvedStatusLabel() }}:</span>
|
|
3701
|
+
<span [class.line-through]="status() === 'done' && strikeDone()">{{ title() }}</span>
|
|
3702
|
+
@if (count() !== null) {
|
|
3703
|
+
<span
|
|
3704
|
+
class="inline-flex min-w-6 items-center justify-center rounded-full border border-border bg-muted px-1.5 text-xs font-semibold tabular-nums text-ink-soft"
|
|
3705
|
+
aria-hidden="true"
|
|
3706
|
+
>{{ count() }}</span
|
|
3707
|
+
>
|
|
3708
|
+
<span class="sr-only">({{ countText() }})</span>
|
|
3709
|
+
}
|
|
3710
|
+
</p>
|
|
3711
|
+
@if (description()) {
|
|
3712
|
+
<p class="mt-0.5 text-sm text-ink-soft">{{ description() }}</p>
|
|
3713
|
+
}
|
|
3714
|
+
<ng-content />
|
|
3715
|
+
</div>
|
|
3716
|
+
|
|
3717
|
+
<div class="flex shrink-0 items-center gap-2 self-center">
|
|
3718
|
+
<ng-content select="[sdsChecklistAction]" />
|
|
3719
|
+
@if (actionLabel()) {
|
|
3720
|
+
<button
|
|
3721
|
+
type="button"
|
|
3722
|
+
class="inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3 text-sm font-semibold text-ink transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
3723
|
+
[attr.aria-label]="actionAriaLabel() || null"
|
|
3724
|
+
(click)="action.emit()"
|
|
3725
|
+
>
|
|
3726
|
+
{{ actionLabel() }}
|
|
3727
|
+
<sds-icon name="chevron-right" [size]="16" class="text-ink-faint" />
|
|
3728
|
+
</button>
|
|
3729
|
+
}
|
|
3730
|
+
</div>
|
|
3731
|
+
</div>
|
|
3732
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3733
|
+
}
|
|
3734
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsChecklistRow, decorators: [{
|
|
3735
|
+
type: Component,
|
|
3736
|
+
args: [{
|
|
3737
|
+
selector: 'sds-checklist-row',
|
|
3738
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3739
|
+
imports: [SdsIcon],
|
|
3740
|
+
host: { class: 'block' },
|
|
3741
|
+
template: `
|
|
3742
|
+
<div
|
|
3743
|
+
class="flex items-start gap-3 rounded-control px-3 py-3"
|
|
3744
|
+
[class.opacity-80]="status() === 'done'"
|
|
3745
|
+
[attr.data-status]="status()"
|
|
3746
|
+
>
|
|
3747
|
+
<span
|
|
3748
|
+
class="mt-0.5 grid h-8 w-8 shrink-0 place-items-center rounded-full"
|
|
3749
|
+
[class]="style().tile"
|
|
3750
|
+
aria-hidden="true"
|
|
3751
|
+
>
|
|
3752
|
+
<sds-icon [name]="style().icon" [size]="18" />
|
|
3753
|
+
</span>
|
|
3754
|
+
|
|
3755
|
+
<div class="min-w-0 flex-1">
|
|
3756
|
+
<p class="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm font-semibold text-ink">
|
|
3757
|
+
<span class="sr-only">{{ resolvedStatusLabel() }}:</span>
|
|
3758
|
+
<span [class.line-through]="status() === 'done' && strikeDone()">{{ title() }}</span>
|
|
3759
|
+
@if (count() !== null) {
|
|
3760
|
+
<span
|
|
3761
|
+
class="inline-flex min-w-6 items-center justify-center rounded-full border border-border bg-muted px-1.5 text-xs font-semibold tabular-nums text-ink-soft"
|
|
3762
|
+
aria-hidden="true"
|
|
3763
|
+
>{{ count() }}</span
|
|
3764
|
+
>
|
|
3765
|
+
<span class="sr-only">({{ countText() }})</span>
|
|
3766
|
+
}
|
|
3767
|
+
</p>
|
|
3768
|
+
@if (description()) {
|
|
3769
|
+
<p class="mt-0.5 text-sm text-ink-soft">{{ description() }}</p>
|
|
3770
|
+
}
|
|
3771
|
+
<ng-content />
|
|
3772
|
+
</div>
|
|
3773
|
+
|
|
3774
|
+
<div class="flex shrink-0 items-center gap-2 self-center">
|
|
3775
|
+
<ng-content select="[sdsChecklistAction]" />
|
|
3776
|
+
@if (actionLabel()) {
|
|
3777
|
+
<button
|
|
3778
|
+
type="button"
|
|
3779
|
+
class="inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3 text-sm font-semibold text-ink transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
3780
|
+
[attr.aria-label]="actionAriaLabel() || null"
|
|
3781
|
+
(click)="action.emit()"
|
|
3782
|
+
>
|
|
3783
|
+
{{ actionLabel() }}
|
|
3784
|
+
<sds-icon name="chevron-right" [size]="16" class="text-ink-faint" />
|
|
3785
|
+
</button>
|
|
3786
|
+
}
|
|
3787
|
+
</div>
|
|
3788
|
+
</div>
|
|
3789
|
+
`,
|
|
3790
|
+
}]
|
|
3791
|
+
}], propDecorators: { status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], countLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLabel", required: false }] }], statusLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "statusLabel", required: false }] }], actionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionLabel", required: false }] }], actionAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionAriaLabel", required: false }] }], strikeDone: [{ type: i0.Input, args: [{ isSignal: true, alias: "strikeDone", required: false }] }], action: [{ type: i0.Output, args: ["action"] }] } });
|
|
3792
|
+
|
|
3793
|
+
const TIERS = {
|
|
3794
|
+
good: { icon: 'check-circle', text: 'text-accent-green', stroke: 'stroke-success', fill: 'bg-success' },
|
|
3795
|
+
fair: { icon: 'triangle-alert', text: 'text-accent-amber', stroke: 'stroke-warning', fill: 'bg-warning' },
|
|
3796
|
+
poor: { icon: 'octagon-x', text: 'text-accent-red', stroke: 'stroke-danger', fill: 'bg-danger' },
|
|
3797
|
+
};
|
|
3798
|
+
const RING = {
|
|
3799
|
+
sm: { box: 48, stroke: 5, value: 'text-sm', max: 'text-[9px]' },
|
|
3800
|
+
md: { box: 72, stroke: 7, value: 'text-xl', max: 'text-[10px]' },
|
|
3801
|
+
lg: { box: 96, stroke: 9, value: 'text-3xl', max: 'text-xs' },
|
|
3802
|
+
};
|
|
3803
|
+
const BAR_H = { sm: 'h-1.5', md: 'h-2', lg: 'h-3' };
|
|
3804
|
+
/**
|
|
3805
|
+
* A 0–100 score as a ring or a bar, tinted by tier (poor · fair · good). The tier is also written out
|
|
3806
|
+
* in words with its own glyph, and the whole thing is a `role="meter"` with a spoken value text — so
|
|
3807
|
+
* the score is never conveyed by colour alone.
|
|
3808
|
+
*
|
|
3809
|
+
* @example
|
|
3810
|
+
* <sds-score [value]="64" label="Menu health" />
|
|
3811
|
+
* <sds-score [value]="health()" variant="bar" label="Menu health" goodLabel="Great" />
|
|
3812
|
+
*/
|
|
3813
|
+
class SdsScore {
|
|
3814
|
+
/** The score, 0–100 (clamped; non-numbers read as 0). */
|
|
3815
|
+
value = input(0, ...(ngDevMode ? [{ debugName: "value", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
3816
|
+
variant = input('ring', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
3817
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
3818
|
+
/** Visible caption and the meter's accessible name, e.g. "Menu health". */
|
|
3819
|
+
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
3820
|
+
/** Show the tier in words (with its glyph). */
|
|
3821
|
+
showTier = input(true, ...(ngDevMode ? [{ debugName: "showTier", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3822
|
+
/** Lowest value that counts as `fair`. */
|
|
3823
|
+
fairFrom = input(50, ...(ngDevMode ? [{ debugName: "fairFrom", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
3824
|
+
/** Lowest value that counts as `good`. */
|
|
3825
|
+
goodFrom = input(80, ...(ngDevMode ? [{ debugName: "goodFrom", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
3826
|
+
poorLabel = input('Needs work', ...(ngDevMode ? [{ debugName: "poorLabel" }] : []));
|
|
3827
|
+
fairLabel = input('Fair', ...(ngDevMode ? [{ debugName: "fairLabel" }] : []));
|
|
3828
|
+
goodLabel = input('Good', ...(ngDevMode ? [{ debugName: "goodLabel" }] : []));
|
|
3829
|
+
/** Spoken value; tokens `{value}` and `{tier}`. */
|
|
3830
|
+
valueTextTemplate = input('{value} out of 100, {tier}', ...(ngDevMode ? [{ debugName: "valueTextTemplate" }] : []));
|
|
3831
|
+
clamped = computed(() => {
|
|
3832
|
+
const v = this.value();
|
|
3833
|
+
return Number.isFinite(v) ? Math.round(Math.min(100, Math.max(0, v))) : 0;
|
|
3834
|
+
}, ...(ngDevMode ? [{ debugName: "clamped" }] : []));
|
|
3835
|
+
tier = computed(() => {
|
|
3836
|
+
const v = this.clamped();
|
|
3837
|
+
if (v >= this.goodFrom())
|
|
3838
|
+
return 'good';
|
|
3839
|
+
if (v >= this.fairFrom())
|
|
3840
|
+
return 'fair';
|
|
3841
|
+
return 'poor';
|
|
3842
|
+
}, ...(ngDevMode ? [{ debugName: "tier" }] : []));
|
|
3843
|
+
tierStyle = computed(() => TIERS[this.tier()], ...(ngDevMode ? [{ debugName: "tierStyle" }] : []));
|
|
3844
|
+
tierText = computed(() => this.tier() === 'good' ? this.goodLabel() : this.tier() === 'fair' ? this.fairLabel() : this.poorLabel(), ...(ngDevMode ? [{ debugName: "tierText" }] : []));
|
|
3845
|
+
valueText = computed(() => this.valueTextTemplate()
|
|
3846
|
+
.replace('{value}', String(this.clamped()))
|
|
3847
|
+
.replace('{tier}', this.tierText()), ...(ngDevMode ? [{ debugName: "valueText" }] : []));
|
|
3848
|
+
ring = computed(() => RING[this.size()], ...(ngDevMode ? [{ debugName: "ring" }] : []));
|
|
3849
|
+
radius = computed(() => (this.ring().box - this.ring().stroke) / 2, ...(ngDevMode ? [{ debugName: "radius" }] : []));
|
|
3850
|
+
circumference = computed(() => 2 * Math.PI * this.radius(), ...(ngDevMode ? [{ debugName: "circumference" }] : []));
|
|
3851
|
+
dashOffset = computed(() => this.circumference() * (1 - this.clamped() / 100), ...(ngDevMode ? [{ debugName: "dashOffset" }] : []));
|
|
3852
|
+
barHeight = computed(() => BAR_H[this.size()], ...(ngDevMode ? [{ debugName: "barHeight" }] : []));
|
|
3853
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsScore, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3854
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsScore, isStandalone: true, selector: "sds-score", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, showTier: { classPropertyName: "showTier", publicName: "showTier", isSignal: true, isRequired: false, transformFunction: null }, fairFrom: { classPropertyName: "fairFrom", publicName: "fairFrom", isSignal: true, isRequired: false, transformFunction: null }, goodFrom: { classPropertyName: "goodFrom", publicName: "goodFrom", isSignal: true, isRequired: false, transformFunction: null }, poorLabel: { classPropertyName: "poorLabel", publicName: "poorLabel", isSignal: true, isRequired: false, transformFunction: null }, fairLabel: { classPropertyName: "fairLabel", publicName: "fairLabel", isSignal: true, isRequired: false, transformFunction: null }, goodLabel: { classPropertyName: "goodLabel", publicName: "goodLabel", isSignal: true, isRequired: false, transformFunction: null }, valueTextTemplate: { classPropertyName: "valueTextTemplate", publicName: "valueTextTemplate", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
3855
|
+
<div
|
|
3856
|
+
role="meter"
|
|
3857
|
+
aria-valuemin="0"
|
|
3858
|
+
aria-valuemax="100"
|
|
3859
|
+
[attr.aria-valuenow]="clamped()"
|
|
3860
|
+
[attr.aria-valuetext]="valueText()"
|
|
3861
|
+
[attr.aria-label]="label() || null"
|
|
3862
|
+
[attr.data-tier]="tier()"
|
|
3863
|
+
>
|
|
3864
|
+
@if (variant() === 'ring') {
|
|
3865
|
+
<div class="flex items-center gap-3" aria-hidden="true">
|
|
3866
|
+
<div class="relative shrink-0" [style.width.px]="ring().box" [style.height.px]="ring().box">
|
|
3867
|
+
<svg [attr.viewBox]="'0 0 ' + ring().box + ' ' + ring().box" class="-rotate-90" width="100%" height="100%">
|
|
3868
|
+
<circle
|
|
3869
|
+
[attr.cx]="ring().box / 2"
|
|
3870
|
+
[attr.cy]="ring().box / 2"
|
|
3871
|
+
[attr.r]="radius()"
|
|
3872
|
+
fill="none"
|
|
3873
|
+
class="stroke-border"
|
|
3874
|
+
[attr.stroke-width]="ring().stroke"
|
|
3875
|
+
/>
|
|
3876
|
+
<circle
|
|
3877
|
+
[attr.cx]="ring().box / 2"
|
|
3878
|
+
[attr.cy]="ring().box / 2"
|
|
3879
|
+
[attr.r]="radius()"
|
|
3880
|
+
fill="none"
|
|
3881
|
+
stroke-linecap="round"
|
|
3882
|
+
class="transition-[stroke-dashoffset] duration-500"
|
|
3883
|
+
[class]="tierStyle().stroke"
|
|
3884
|
+
[attr.stroke-width]="ring().stroke"
|
|
3885
|
+
[attr.stroke-dasharray]="circumference()"
|
|
3886
|
+
[attr.stroke-dashoffset]="dashOffset()"
|
|
3887
|
+
/>
|
|
3888
|
+
</svg>
|
|
3889
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center leading-none">
|
|
3890
|
+
<span class="font-extrabold tabular-nums text-ink" [class]="ring().value">{{ clamped() }}</span>
|
|
3891
|
+
@if (size() !== 'sm') {
|
|
3892
|
+
<span class="mt-0.5 font-medium text-ink-faint" [class]="ring().max">/100</span>
|
|
3893
|
+
}
|
|
3894
|
+
</div>
|
|
3895
|
+
</div>
|
|
3896
|
+
@if (label() || showTier()) {
|
|
3897
|
+
<div class="min-w-0">
|
|
3898
|
+
@if (label()) {
|
|
3899
|
+
<p class="truncate text-sm font-semibold text-ink">{{ label() }}</p>
|
|
3900
|
+
}
|
|
3901
|
+
@if (showTier()) {
|
|
3902
|
+
<p class="mt-0.5 flex items-center gap-1 text-sm font-medium" [class]="tierStyle().text">
|
|
3903
|
+
<sds-icon [name]="tierStyle().icon" [size]="15" />
|
|
3904
|
+
{{ tierText() }}
|
|
3905
|
+
</p>
|
|
3906
|
+
}
|
|
3907
|
+
</div>
|
|
3908
|
+
}
|
|
3909
|
+
</div>
|
|
3910
|
+
} @else {
|
|
3911
|
+
<div aria-hidden="true">
|
|
3912
|
+
<div class="mb-1.5 flex items-baseline justify-between gap-3">
|
|
3913
|
+
<span class="truncate text-sm font-semibold text-ink">{{ label() }}</span>
|
|
3914
|
+
<span class="flex shrink-0 items-center gap-1.5 text-sm">
|
|
3915
|
+
<span class="font-bold tabular-nums text-ink">{{ clamped() }}<span class="text-ink-faint">/100</span></span>
|
|
3916
|
+
@if (showTier()) {
|
|
3917
|
+
<span class="flex items-center gap-1 font-medium" [class]="tierStyle().text">
|
|
3918
|
+
<sds-icon [name]="tierStyle().icon" [size]="14" />
|
|
3919
|
+
{{ tierText() }}
|
|
3920
|
+
</span>
|
|
3921
|
+
}
|
|
3922
|
+
</span>
|
|
3923
|
+
</div>
|
|
3924
|
+
<div class="w-full overflow-hidden rounded-full bg-border" [class]="barHeight()">
|
|
3925
|
+
<div
|
|
3926
|
+
class="h-full rounded-full transition-[width] duration-500"
|
|
3927
|
+
[class]="tierStyle().fill"
|
|
3928
|
+
[style.width.%]="clamped()"
|
|
3929
|
+
></div>
|
|
3930
|
+
</div>
|
|
3931
|
+
</div>
|
|
3932
|
+
}
|
|
3933
|
+
</div>
|
|
3934
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3935
|
+
}
|
|
3936
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsScore, decorators: [{
|
|
3937
|
+
type: Component,
|
|
3938
|
+
args: [{
|
|
3939
|
+
selector: 'sds-score',
|
|
3940
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3941
|
+
imports: [SdsIcon],
|
|
3942
|
+
host: { class: 'block' },
|
|
3943
|
+
template: `
|
|
3944
|
+
<div
|
|
3945
|
+
role="meter"
|
|
3946
|
+
aria-valuemin="0"
|
|
3947
|
+
aria-valuemax="100"
|
|
3948
|
+
[attr.aria-valuenow]="clamped()"
|
|
3949
|
+
[attr.aria-valuetext]="valueText()"
|
|
3950
|
+
[attr.aria-label]="label() || null"
|
|
3951
|
+
[attr.data-tier]="tier()"
|
|
3952
|
+
>
|
|
3953
|
+
@if (variant() === 'ring') {
|
|
3954
|
+
<div class="flex items-center gap-3" aria-hidden="true">
|
|
3955
|
+
<div class="relative shrink-0" [style.width.px]="ring().box" [style.height.px]="ring().box">
|
|
3956
|
+
<svg [attr.viewBox]="'0 0 ' + ring().box + ' ' + ring().box" class="-rotate-90" width="100%" height="100%">
|
|
3957
|
+
<circle
|
|
3958
|
+
[attr.cx]="ring().box / 2"
|
|
3959
|
+
[attr.cy]="ring().box / 2"
|
|
3960
|
+
[attr.r]="radius()"
|
|
3961
|
+
fill="none"
|
|
3962
|
+
class="stroke-border"
|
|
3963
|
+
[attr.stroke-width]="ring().stroke"
|
|
3964
|
+
/>
|
|
3965
|
+
<circle
|
|
3966
|
+
[attr.cx]="ring().box / 2"
|
|
3967
|
+
[attr.cy]="ring().box / 2"
|
|
3968
|
+
[attr.r]="radius()"
|
|
3969
|
+
fill="none"
|
|
3970
|
+
stroke-linecap="round"
|
|
3971
|
+
class="transition-[stroke-dashoffset] duration-500"
|
|
3972
|
+
[class]="tierStyle().stroke"
|
|
3973
|
+
[attr.stroke-width]="ring().stroke"
|
|
3974
|
+
[attr.stroke-dasharray]="circumference()"
|
|
3975
|
+
[attr.stroke-dashoffset]="dashOffset()"
|
|
3976
|
+
/>
|
|
3977
|
+
</svg>
|
|
3978
|
+
<div class="absolute inset-0 flex flex-col items-center justify-center leading-none">
|
|
3979
|
+
<span class="font-extrabold tabular-nums text-ink" [class]="ring().value">{{ clamped() }}</span>
|
|
3980
|
+
@if (size() !== 'sm') {
|
|
3981
|
+
<span class="mt-0.5 font-medium text-ink-faint" [class]="ring().max">/100</span>
|
|
3982
|
+
}
|
|
3983
|
+
</div>
|
|
3984
|
+
</div>
|
|
3985
|
+
@if (label() || showTier()) {
|
|
3986
|
+
<div class="min-w-0">
|
|
3987
|
+
@if (label()) {
|
|
3988
|
+
<p class="truncate text-sm font-semibold text-ink">{{ label() }}</p>
|
|
3989
|
+
}
|
|
3990
|
+
@if (showTier()) {
|
|
3991
|
+
<p class="mt-0.5 flex items-center gap-1 text-sm font-medium" [class]="tierStyle().text">
|
|
3992
|
+
<sds-icon [name]="tierStyle().icon" [size]="15" />
|
|
3993
|
+
{{ tierText() }}
|
|
3994
|
+
</p>
|
|
3995
|
+
}
|
|
3996
|
+
</div>
|
|
3997
|
+
}
|
|
3998
|
+
</div>
|
|
3999
|
+
} @else {
|
|
4000
|
+
<div aria-hidden="true">
|
|
4001
|
+
<div class="mb-1.5 flex items-baseline justify-between gap-3">
|
|
4002
|
+
<span class="truncate text-sm font-semibold text-ink">{{ label() }}</span>
|
|
4003
|
+
<span class="flex shrink-0 items-center gap-1.5 text-sm">
|
|
4004
|
+
<span class="font-bold tabular-nums text-ink">{{ clamped() }}<span class="text-ink-faint">/100</span></span>
|
|
4005
|
+
@if (showTier()) {
|
|
4006
|
+
<span class="flex items-center gap-1 font-medium" [class]="tierStyle().text">
|
|
4007
|
+
<sds-icon [name]="tierStyle().icon" [size]="14" />
|
|
4008
|
+
{{ tierText() }}
|
|
4009
|
+
</span>
|
|
4010
|
+
}
|
|
4011
|
+
</span>
|
|
4012
|
+
</div>
|
|
4013
|
+
<div class="w-full overflow-hidden rounded-full bg-border" [class]="barHeight()">
|
|
4014
|
+
<div
|
|
4015
|
+
class="h-full rounded-full transition-[width] duration-500"
|
|
4016
|
+
[class]="tierStyle().fill"
|
|
4017
|
+
[style.width.%]="clamped()"
|
|
4018
|
+
></div>
|
|
4019
|
+
</div>
|
|
4020
|
+
</div>
|
|
4021
|
+
}
|
|
4022
|
+
</div>
|
|
4023
|
+
`,
|
|
4024
|
+
}]
|
|
4025
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], showTier: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTier", required: false }] }], fairFrom: [{ type: i0.Input, args: [{ isSignal: true, alias: "fairFrom", required: false }] }], goodFrom: [{ type: i0.Input, args: [{ isSignal: true, alias: "goodFrom", required: false }] }], poorLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "poorLabel", required: false }] }], fairLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "fairLabel", required: false }] }], goodLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "goodLabel", required: false }] }], valueTextTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "valueTextTemplate", required: false }] }] } });
|
|
4026
|
+
|
|
4027
|
+
const STATES = {
|
|
4028
|
+
available: {
|
|
4029
|
+
icon: 'check-circle',
|
|
4030
|
+
frame: 'border-border bg-surface',
|
|
4031
|
+
badge: 'bg-accent-green-soft text-accent-green',
|
|
4032
|
+
},
|
|
4033
|
+
'sold-out': {
|
|
4034
|
+
icon: 'ban',
|
|
4035
|
+
frame: 'border-danger bg-accent-red-soft',
|
|
4036
|
+
badge: 'bg-danger text-white',
|
|
4037
|
+
},
|
|
4038
|
+
hidden: {
|
|
4039
|
+
icon: 'eye-off',
|
|
4040
|
+
frame: 'border-dashed border-ink-faint bg-muted',
|
|
4041
|
+
badge: 'bg-ink/10 text-ink-soft',
|
|
4042
|
+
},
|
|
4043
|
+
};
|
|
4044
|
+
/**
|
|
4045
|
+
* Touch-first square tile for a stock board: a big press target (the whole tile) with a title,
|
|
4046
|
+
* optional subtitle, optional photo and a state — available · sold out · hidden.
|
|
4047
|
+
*
|
|
4048
|
+
* The state is never colour-only: each has its own glyph and word in a badge (check / "Available",
|
|
4049
|
+
* a no-entry sign / "Sold out", a crossed eye / "Hidden"), sold-out tiles get a diagonal hatch and a
|
|
4050
|
+
* struck-through title, hidden tiles a dashed frame. Pressing gives a visible pressed state.
|
|
4051
|
+
*
|
|
4052
|
+
* The tile is one `<button>` that emits `(activate)`. Pass [selected] (true/false) to make it a toggle
|
|
4053
|
+
* for multi-select ("select all matches"): it then carries `aria-pressed` and a corner check. [busy]
|
|
4054
|
+
* shows a spinner and sets `aria-busy` while a save is in flight.
|
|
4055
|
+
*
|
|
4056
|
+
* @example
|
|
4057
|
+
* <sds-status-tile title="Bacon" subtitle="Extras" state="sold-out" note="Back at 22:00"
|
|
4058
|
+
* soldOutLabel="Sold out" (activate)="toggle(item)" />
|
|
4059
|
+
*/
|
|
4060
|
+
class SdsStatusTile {
|
|
4061
|
+
title = input.required(...(ngDevMode ? [{ debugName: "title" }] : []));
|
|
4062
|
+
subtitle = input('', ...(ngDevMode ? [{ debugName: "subtitle" }] : []));
|
|
4063
|
+
/** Optional photo URL, shown across the top of the tile. */
|
|
4064
|
+
image = input(null, ...(ngDevMode ? [{ debugName: "image" }] : []));
|
|
4065
|
+
/** Alt text for the photo. Empty (default) marks it decorative — the title already names the tile. */
|
|
4066
|
+
imageAlt = input('', ...(ngDevMode ? [{ debugName: "imageAlt" }] : []));
|
|
4067
|
+
state = input('available', ...(ngDevMode ? [{ debugName: "state" }] : []));
|
|
4068
|
+
/** Short extra line next to the state, e.g. "Back at 22:00". */
|
|
4069
|
+
note = input('', ...(ngDevMode ? [{ debugName: "note" }] : []));
|
|
4070
|
+
/** `null` (default): a plain action tile. `true`/`false`: a toggle for multi-select (`aria-pressed`). */
|
|
4071
|
+
selected = input(null, ...(ngDevMode ? [{ debugName: "selected" }] : []));
|
|
4072
|
+
/** A save is in flight: spinner + `aria-busy`. */
|
|
4073
|
+
busy = input(false, ...(ngDevMode ? [{ debugName: "busy", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
4074
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
4075
|
+
availableLabel = input('Available', ...(ngDevMode ? [{ debugName: "availableLabel" }] : []));
|
|
4076
|
+
soldOutLabel = input('Sold out', ...(ngDevMode ? [{ debugName: "soldOutLabel" }] : []));
|
|
4077
|
+
hiddenLabel = input('Hidden', ...(ngDevMode ? [{ debugName: "hiddenLabel" }] : []));
|
|
4078
|
+
/** The tile was pressed (click, tap, Enter or Space). */
|
|
4079
|
+
activate = output();
|
|
4080
|
+
style = computed(() => STATES[this.state()] ?? STATES.available, ...(ngDevMode ? [{ debugName: "style" }] : []));
|
|
4081
|
+
stateText = computed(() => {
|
|
4082
|
+
switch (this.state()) {
|
|
4083
|
+
case 'sold-out':
|
|
4084
|
+
return this.soldOutLabel();
|
|
4085
|
+
case 'hidden':
|
|
4086
|
+
return this.hiddenLabel();
|
|
4087
|
+
default:
|
|
4088
|
+
return this.availableLabel();
|
|
4089
|
+
}
|
|
4090
|
+
}, ...(ngDevMode ? [{ debugName: "stateText" }] : []));
|
|
4091
|
+
/** Diagonal hatch on sold-out tiles — a pattern cue on top of the colour. */
|
|
4092
|
+
hatch = computed(() => this.state() === 'sold-out'
|
|
4093
|
+
? 'repeating-linear-gradient(135deg, transparent 0 12px, color-mix(in srgb, var(--color-danger) 10%, transparent) 12px 24px)'
|
|
4094
|
+
: null, ...(ngDevMode ? [{ debugName: "hatch" }] : []));
|
|
4095
|
+
tileClass = computed(() => {
|
|
4096
|
+
const ring = this.selected()
|
|
4097
|
+
? ' ring-2 ring-brand-500 ring-offset-2 ring-offset-canvas dark:ring-ink'
|
|
4098
|
+
: '';
|
|
4099
|
+
return ('relative flex aspect-square w-full min-h-32 cursor-pointer select-none flex-col overflow-hidden ' +
|
|
4100
|
+
'rounded-card border-2 text-left shadow-sm transition-[transform,box-shadow] duration-100 ' +
|
|
4101
|
+
'hover:shadow-md active:scale-[0.97] active:shadow-none focus-visible:outline-none ' +
|
|
4102
|
+
'focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 ' +
|
|
4103
|
+
'focus-visible:ring-offset-canvas disabled:cursor-not-allowed disabled:opacity-60 ' +
|
|
4104
|
+
'touch-manipulation ' +
|
|
4105
|
+
this.style().frame +
|
|
4106
|
+
(this.state() === 'hidden' ? ' opacity-80' : '') +
|
|
4107
|
+
ring);
|
|
4108
|
+
}, ...(ngDevMode ? [{ debugName: "tileClass" }] : []));
|
|
4109
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsStatusTile, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4110
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsStatusTile, isStandalone: true, selector: "sds-status-tile", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, image: { classPropertyName: "image", publicName: "image", isSignal: true, isRequired: false, transformFunction: null }, imageAlt: { classPropertyName: "imageAlt", publicName: "imageAlt", isSignal: true, isRequired: false, transformFunction: null }, state: { classPropertyName: "state", publicName: "state", isSignal: true, isRequired: false, transformFunction: null }, note: { classPropertyName: "note", publicName: "note", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, busy: { classPropertyName: "busy", publicName: "busy", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, availableLabel: { classPropertyName: "availableLabel", publicName: "availableLabel", isSignal: true, isRequired: false, transformFunction: null }, soldOutLabel: { classPropertyName: "soldOutLabel", publicName: "soldOutLabel", isSignal: true, isRequired: false, transformFunction: null }, hiddenLabel: { classPropertyName: "hiddenLabel", publicName: "hiddenLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activate: "activate" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
4111
|
+
<button
|
|
4112
|
+
type="button"
|
|
4113
|
+
[class]="tileClass()"
|
|
4114
|
+
[disabled]="disabled()"
|
|
4115
|
+
[attr.aria-pressed]="selected() === null ? null : selected()"
|
|
4116
|
+
[attr.aria-busy]="busy() ? 'true' : null"
|
|
4117
|
+
[attr.data-state]="state()"
|
|
4118
|
+
[style.background-image]="hatch()"
|
|
4119
|
+
(click)="activate.emit()"
|
|
4120
|
+
>
|
|
4121
|
+
@if (image()) {
|
|
4122
|
+
<span class="relative block h-[45%] w-full shrink-0 overflow-hidden bg-muted">
|
|
4123
|
+
<img
|
|
4124
|
+
[src]="image()"
|
|
4125
|
+
[alt]="imageAlt()"
|
|
4126
|
+
class="h-full w-full object-cover"
|
|
4127
|
+
[class.grayscale]="state() !== 'available'"
|
|
4128
|
+
[class.opacity-60]="state() !== 'available'"
|
|
4129
|
+
loading="lazy"
|
|
4130
|
+
draggable="false"
|
|
4131
|
+
/>
|
|
4132
|
+
</span>
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
<span class="flex min-h-0 w-full flex-1 flex-col gap-1 p-3 text-left">
|
|
4136
|
+
<span
|
|
4137
|
+
class="line-clamp-2 break-words font-bold leading-tight text-ink"
|
|
4138
|
+
[class]="image() ? 'text-base' : 'text-lg'"
|
|
4139
|
+
[class.line-through]="state() === 'sold-out'"
|
|
4140
|
+
[class.decoration-2]="state() === 'sold-out'"
|
|
4141
|
+
>{{ title() }}</span
|
|
4142
|
+
>
|
|
4143
|
+
@if (subtitle()) {
|
|
4144
|
+
<span class="truncate text-xs text-ink-soft">{{ subtitle() }}</span>
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
<span class="mt-auto flex flex-wrap items-center gap-1.5 pt-1">
|
|
4148
|
+
<span
|
|
4149
|
+
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-bold"
|
|
4150
|
+
[class]="style().badge"
|
|
4151
|
+
>
|
|
4152
|
+
<sds-icon [name]="style().icon" [size]="14" [strokeWidth]="2.5" />
|
|
4153
|
+
{{ stateText() }}
|
|
4154
|
+
</span>
|
|
4155
|
+
@if (note()) {
|
|
4156
|
+
<span class="truncate text-xs font-medium text-ink-soft">{{ note() }}</span>
|
|
4157
|
+
}
|
|
4158
|
+
</span>
|
|
4159
|
+
</span>
|
|
4160
|
+
|
|
4161
|
+
@if (selected()) {
|
|
4162
|
+
<span
|
|
4163
|
+
class="absolute right-2 top-2 grid h-7 w-7 place-items-center rounded-full bg-brand-600 text-white shadow ring-2 ring-surface dark:bg-ink dark:text-surface"
|
|
4164
|
+
aria-hidden="true"
|
|
4165
|
+
>
|
|
4166
|
+
<sds-icon name="check" [size]="16" [strokeWidth]="3" />
|
|
4167
|
+
</span>
|
|
4168
|
+
} @else if (selected() === false) {
|
|
4169
|
+
<span
|
|
4170
|
+
class="absolute right-2 top-2 h-7 w-7 rounded-full border-2 border-ink-faint bg-surface/80"
|
|
4171
|
+
aria-hidden="true"
|
|
4172
|
+
></span>
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
@if (busy()) {
|
|
4176
|
+
<span class="absolute inset-0 grid place-items-center bg-surface/60" aria-hidden="true">
|
|
4177
|
+
<span class="h-7 w-7 animate-spin rounded-full border-2 border-brand-200 border-t-brand-600"></span>
|
|
4178
|
+
</span>
|
|
4179
|
+
}
|
|
4180
|
+
</button>
|
|
4181
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4182
|
+
}
|
|
4183
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsStatusTile, decorators: [{
|
|
4184
|
+
type: Component,
|
|
4185
|
+
args: [{
|
|
4186
|
+
selector: 'sds-status-tile',
|
|
4187
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4188
|
+
imports: [SdsIcon],
|
|
4189
|
+
host: { class: 'block' },
|
|
4190
|
+
template: `
|
|
4191
|
+
<button
|
|
4192
|
+
type="button"
|
|
4193
|
+
[class]="tileClass()"
|
|
4194
|
+
[disabled]="disabled()"
|
|
4195
|
+
[attr.aria-pressed]="selected() === null ? null : selected()"
|
|
4196
|
+
[attr.aria-busy]="busy() ? 'true' : null"
|
|
4197
|
+
[attr.data-state]="state()"
|
|
4198
|
+
[style.background-image]="hatch()"
|
|
4199
|
+
(click)="activate.emit()"
|
|
4200
|
+
>
|
|
4201
|
+
@if (image()) {
|
|
4202
|
+
<span class="relative block h-[45%] w-full shrink-0 overflow-hidden bg-muted">
|
|
4203
|
+
<img
|
|
4204
|
+
[src]="image()"
|
|
4205
|
+
[alt]="imageAlt()"
|
|
4206
|
+
class="h-full w-full object-cover"
|
|
4207
|
+
[class.grayscale]="state() !== 'available'"
|
|
4208
|
+
[class.opacity-60]="state() !== 'available'"
|
|
4209
|
+
loading="lazy"
|
|
4210
|
+
draggable="false"
|
|
4211
|
+
/>
|
|
4212
|
+
</span>
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
<span class="flex min-h-0 w-full flex-1 flex-col gap-1 p-3 text-left">
|
|
4216
|
+
<span
|
|
4217
|
+
class="line-clamp-2 break-words font-bold leading-tight text-ink"
|
|
4218
|
+
[class]="image() ? 'text-base' : 'text-lg'"
|
|
4219
|
+
[class.line-through]="state() === 'sold-out'"
|
|
4220
|
+
[class.decoration-2]="state() === 'sold-out'"
|
|
4221
|
+
>{{ title() }}</span
|
|
4222
|
+
>
|
|
4223
|
+
@if (subtitle()) {
|
|
4224
|
+
<span class="truncate text-xs text-ink-soft">{{ subtitle() }}</span>
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
<span class="mt-auto flex flex-wrap items-center gap-1.5 pt-1">
|
|
4228
|
+
<span
|
|
4229
|
+
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-bold"
|
|
4230
|
+
[class]="style().badge"
|
|
4231
|
+
>
|
|
4232
|
+
<sds-icon [name]="style().icon" [size]="14" [strokeWidth]="2.5" />
|
|
4233
|
+
{{ stateText() }}
|
|
4234
|
+
</span>
|
|
4235
|
+
@if (note()) {
|
|
4236
|
+
<span class="truncate text-xs font-medium text-ink-soft">{{ note() }}</span>
|
|
4237
|
+
}
|
|
4238
|
+
</span>
|
|
4239
|
+
</span>
|
|
4240
|
+
|
|
4241
|
+
@if (selected()) {
|
|
4242
|
+
<span
|
|
4243
|
+
class="absolute right-2 top-2 grid h-7 w-7 place-items-center rounded-full bg-brand-600 text-white shadow ring-2 ring-surface dark:bg-ink dark:text-surface"
|
|
4244
|
+
aria-hidden="true"
|
|
4245
|
+
>
|
|
4246
|
+
<sds-icon name="check" [size]="16" [strokeWidth]="3" />
|
|
4247
|
+
</span>
|
|
4248
|
+
} @else if (selected() === false) {
|
|
4249
|
+
<span
|
|
4250
|
+
class="absolute right-2 top-2 h-7 w-7 rounded-full border-2 border-ink-faint bg-surface/80"
|
|
4251
|
+
aria-hidden="true"
|
|
4252
|
+
></span>
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
@if (busy()) {
|
|
4256
|
+
<span class="absolute inset-0 grid place-items-center bg-surface/60" aria-hidden="true">
|
|
4257
|
+
<span class="h-7 w-7 animate-spin rounded-full border-2 border-brand-200 border-t-brand-600"></span>
|
|
4258
|
+
</span>
|
|
4259
|
+
}
|
|
4260
|
+
</button>
|
|
4261
|
+
`,
|
|
4262
|
+
}]
|
|
4263
|
+
}], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], image: [{ type: i0.Input, args: [{ isSignal: true, alias: "image", required: false }] }], imageAlt: [{ type: i0.Input, args: [{ isSignal: true, alias: "imageAlt", required: false }] }], state: [{ type: i0.Input, args: [{ isSignal: true, alias: "state", required: false }] }], note: [{ type: i0.Input, args: [{ isSignal: true, alias: "note", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], busy: [{ type: i0.Input, args: [{ isSignal: true, alias: "busy", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], availableLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "availableLabel", required: false }] }], soldOutLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "soldOutLabel", required: false }] }], hiddenLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenLabel", required: false }] }], activate: [{ type: i0.Output, args: ["activate"] }] } });
|
|
4264
|
+
|
|
4265
|
+
let nextId$b = 0;
|
|
4266
|
+
/**
|
|
4267
|
+
* Marks the element inside `<sds-menu>` that opens it. It may be a native button or a component that
|
|
4268
|
+
* renders one (e.g. `<sds-button sdsMenuTrigger>`): the menu wires `aria-haspopup`, `aria-expanded` and
|
|
4269
|
+
* `aria-controls` onto the first focusable element it finds there.
|
|
4270
|
+
*/
|
|
4271
|
+
class SdsMenuTrigger {
|
|
4272
|
+
el = inject(ElementRef);
|
|
4273
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4274
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsMenuTrigger, isStandalone: true, selector: "[sdsMenuTrigger]", ngImport: i0 });
|
|
4275
|
+
}
|
|
4276
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenuTrigger, decorators: [{
|
|
4277
|
+
type: Directive,
|
|
4278
|
+
args: [{
|
|
4279
|
+
selector: '[sdsMenuTrigger]',
|
|
4280
|
+
standalone: true,
|
|
4281
|
+
}]
|
|
4282
|
+
}] });
|
|
4283
|
+
/**
|
|
4284
|
+
* Action menu (menu button + popover). The panel is appended to `<body>` and positioned against the
|
|
4285
|
+
* trigger, flipping above it when there is no room below — so it is never clipped by a drawer, a dialog
|
|
4286
|
+
* or a scrolling table, and it sits above them (z 1100).
|
|
4287
|
+
*
|
|
4288
|
+
* Items come from [items]: actions (`{ label, description?, icon?, danger?, disabled? }`) and
|
|
4289
|
+
* `{ separator: true }` dividers. Activating one emits `(select)` with it and closes the menu.
|
|
4290
|
+
*
|
|
4291
|
+
* Trigger: project your own element marked `sdsMenuTrigger`, or let the menu render a compact icon
|
|
4292
|
+
* button (`triggerIcon`, default a horizontal ellipsis) named by [triggerLabel].
|
|
4293
|
+
*
|
|
4294
|
+
* Keyboard (WAI-ARIA menu button): Enter / Space / ArrowDown open on the first item, ArrowUp on the
|
|
4295
|
+
* last; ArrowUp/Down move (wrapping, skipping disabled items), Home/End jump, a letter jumps to the next
|
|
4296
|
+
* item starting with it, Enter/Space activate. **Escape closes only the menu** — the key event is
|
|
4297
|
+
* stopped, so an enclosing drawer or dialog stays open — and focus returns to the trigger. Tab closes it
|
|
4298
|
+
* too. A click outside closes it.
|
|
4299
|
+
*
|
|
4300
|
+
* @example
|
|
4301
|
+
* <sds-menu [items]="[{ id: 'sold', label: 'Mark sold out', icon: 'ban' }, { separator: true },
|
|
4302
|
+
* { id: 'del', label: 'Delete', icon: 'trash-2', danger: true }]"
|
|
4303
|
+
* triggerLabel="Item actions" (select)="onAction($event)" />
|
|
4304
|
+
*
|
|
4305
|
+
* @example
|
|
4306
|
+
* <sds-menu [items]="durations" (select)="snooze($event)">
|
|
4307
|
+
* <sds-button sdsMenuTrigger variant="secondary">Mark sold out</sds-button>
|
|
4308
|
+
* </sds-menu>
|
|
4309
|
+
*/
|
|
4310
|
+
class SdsMenu {
|
|
4311
|
+
host = inject(ElementRef);
|
|
4312
|
+
injector = inject(Injector);
|
|
4313
|
+
menuId = `sds-menu-${nextId$b++}`;
|
|
4314
|
+
triggerId = `${this.menuId}-trigger`;
|
|
4315
|
+
/** Actions and separators, in order. */
|
|
4316
|
+
items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
4317
|
+
/** Open state. Two-way: `[(open)]` — set it to open the menu from code. */
|
|
4318
|
+
open = model(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
|
|
4319
|
+
/** Which edge of the trigger the panel lines up with. */
|
|
4320
|
+
align = input('start', ...(ngDevMode ? [{ debugName: "align" }] : []));
|
|
4321
|
+
/** Accessible name of the built-in icon trigger (required when there is no [triggerText]). */
|
|
4322
|
+
triggerLabel = input('More actions', ...(ngDevMode ? [{ debugName: "triggerLabel" }] : []));
|
|
4323
|
+
/** Icon of the built-in trigger. */
|
|
4324
|
+
triggerIcon = input('ellipsis', ...(ngDevMode ? [{ debugName: "triggerIcon" }] : []));
|
|
4325
|
+
/** Optional visible text on the built-in trigger (then it is its name). */
|
|
4326
|
+
triggerText = input('', ...(ngDevMode ? [{ debugName: "triggerText" }] : []));
|
|
4327
|
+
/** Accessible name of the menu itself; defaults to the trigger's name. */
|
|
4328
|
+
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
4329
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
4330
|
+
/** Keep the menu open after an item is activated (e.g. toggles). */
|
|
4331
|
+
keepOpen = input(false, ...(ngDevMode ? [{ debugName: "keepOpen", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
4332
|
+
/** An action was activated. */
|
|
4333
|
+
select = output();
|
|
4334
|
+
projectedTrigger = contentChild(SdsMenuTrigger, ...(ngDevMode ? [{ debugName: "projectedTrigger" }] : []));
|
|
4335
|
+
panelEl = null;
|
|
4336
|
+
/** Which item gets focus when the panel opens: 'first' | 'last' | 'none'. */
|
|
4337
|
+
focusOnOpen = 'first';
|
|
4338
|
+
typeBuffer = '';
|
|
4339
|
+
typeTimer;
|
|
4340
|
+
enabledIndexes = computed(() => this.items()
|
|
4341
|
+
.map((e, i) => (this.isSeparator(e) || e.disabled ? -1 : i))
|
|
4342
|
+
.filter((i) => i >= 0), ...(ngDevMode ? [{ debugName: "enabledIndexes" }] : []));
|
|
4343
|
+
constructor() {
|
|
4344
|
+
// Wire ARIA onto a projected trigger's focusable element (the built-in one binds it in the template).
|
|
4345
|
+
effect(() => {
|
|
4346
|
+
const isOpen = this.open();
|
|
4347
|
+
const trigger = this.projectedTrigger();
|
|
4348
|
+
if (!trigger)
|
|
4349
|
+
return;
|
|
4350
|
+
const el = this.findFocusable(trigger.el.nativeElement);
|
|
4351
|
+
if (!el)
|
|
4352
|
+
return;
|
|
4353
|
+
if (!el.id)
|
|
4354
|
+
el.id = this.triggerId;
|
|
4355
|
+
el.setAttribute('aria-haspopup', 'menu');
|
|
4356
|
+
el.setAttribute('aria-expanded', String(isOpen));
|
|
4357
|
+
if (isOpen)
|
|
4358
|
+
el.setAttribute('aria-controls', this.menuId);
|
|
4359
|
+
else
|
|
4360
|
+
el.removeAttribute('aria-controls');
|
|
4361
|
+
});
|
|
4362
|
+
// Open/close side effects: move the panel to <body>, position it, focus an item; clean up on close.
|
|
4363
|
+
effect(() => {
|
|
4364
|
+
if (this.open()) {
|
|
4365
|
+
untracked(() => afterNextRender(() => this.afterOpen(), { injector: this.injector }));
|
|
4366
|
+
}
|
|
4367
|
+
else {
|
|
4368
|
+
untracked(() => this.teardown());
|
|
4369
|
+
}
|
|
4370
|
+
});
|
|
4371
|
+
}
|
|
4372
|
+
ngOnDestroy() {
|
|
4373
|
+
clearTimeout(this.typeTimer);
|
|
4374
|
+
this.teardown();
|
|
4375
|
+
}
|
|
4376
|
+
// --- helpers ----------------------------------------------------------------
|
|
4377
|
+
isSeparator(entry) {
|
|
4378
|
+
return entry.separator === true;
|
|
4379
|
+
}
|
|
4380
|
+
descId(i) {
|
|
4381
|
+
return `${this.menuId}-desc-${i}`;
|
|
4382
|
+
}
|
|
4383
|
+
itemClass(entry) {
|
|
4384
|
+
const base = 'flex w-full items-start gap-3 px-3.5 py-2 text-left transition-colors outline-none ' +
|
|
4385
|
+
'focus-visible:ring-2 focus-visible:ring-inset ';
|
|
4386
|
+
if (entry.disabled)
|
|
4387
|
+
return `${base} cursor-not-allowed text-ink-faint opacity-60`;
|
|
4388
|
+
return entry.danger
|
|
4389
|
+
? `${base} cursor-pointer text-danger hover:bg-danger/10 focus:bg-danger/10 focus-visible:ring-danger/40`
|
|
4390
|
+
: `${base} cursor-pointer text-ink hover:bg-muted focus:bg-muted focus-visible:ring-brand-500/40`;
|
|
4391
|
+
}
|
|
4392
|
+
findFocusable(root) {
|
|
4393
|
+
if (root.matches('button, a[href], [tabindex]'))
|
|
4394
|
+
return root;
|
|
4395
|
+
return root.querySelector('button, a[href], [tabindex]:not([tabindex="-1"])');
|
|
4396
|
+
}
|
|
4397
|
+
triggerEl() {
|
|
4398
|
+
const projected = this.projectedTrigger();
|
|
4399
|
+
if (projected)
|
|
4400
|
+
return this.findFocusable(projected.el.nativeElement);
|
|
4401
|
+
return this.host.nativeElement.querySelector(`#${this.triggerId}`);
|
|
4402
|
+
}
|
|
4403
|
+
isTriggerDisabled() {
|
|
4404
|
+
const el = this.triggerEl();
|
|
4405
|
+
return this.disabled() || !!el?.disabled;
|
|
4406
|
+
}
|
|
4407
|
+
// --- open / close ---------------------------------------------------------
|
|
4408
|
+
show(focus) {
|
|
4409
|
+
if (this.isTriggerDisabled())
|
|
4410
|
+
return;
|
|
4411
|
+
this.focusOnOpen = focus;
|
|
4412
|
+
if (this.open()) {
|
|
4413
|
+
this.focusItemAt(focus);
|
|
4414
|
+
return;
|
|
4415
|
+
}
|
|
4416
|
+
this.open.set(true);
|
|
4417
|
+
}
|
|
4418
|
+
/** Close the menu; with `returnFocus`, focus goes back to the trigger. */
|
|
4419
|
+
close(returnFocus = false) {
|
|
4420
|
+
if (!this.open())
|
|
4421
|
+
return;
|
|
4422
|
+
this.open.set(false);
|
|
4423
|
+
this.teardown();
|
|
4424
|
+
if (returnFocus)
|
|
4425
|
+
this.triggerEl()?.focus();
|
|
4426
|
+
}
|
|
4427
|
+
afterOpen() {
|
|
4428
|
+
if (!this.open())
|
|
4429
|
+
return;
|
|
4430
|
+
this.panelEl = this.host.nativeElement.querySelector('[data-sds-menu-panel]');
|
|
4431
|
+
if (!this.panelEl)
|
|
4432
|
+
return;
|
|
4433
|
+
if (this.panelEl.parentElement !== document.body)
|
|
4434
|
+
document.body.appendChild(this.panelEl);
|
|
4435
|
+
// Name the menu after its trigger unless an explicit [ariaLabel] was given.
|
|
4436
|
+
const trigger = this.triggerEl();
|
|
4437
|
+
if (!this.ariaLabel() && trigger?.id)
|
|
4438
|
+
this.panelEl.setAttribute('aria-labelledby', trigger.id);
|
|
4439
|
+
this.position();
|
|
4440
|
+
this.addReflowListeners();
|
|
4441
|
+
this.focusItemAt(this.focusOnOpen);
|
|
4442
|
+
}
|
|
4443
|
+
teardown() {
|
|
4444
|
+
this.removeReflowListeners();
|
|
4445
|
+
this.panelEl?.remove();
|
|
4446
|
+
this.panelEl = null;
|
|
4447
|
+
}
|
|
4448
|
+
// --- trigger events -------------------------------------------------------
|
|
4449
|
+
onTriggerClick(event) {
|
|
4450
|
+
if (!event.target.closest('button, a, [tabindex]'))
|
|
4451
|
+
return;
|
|
4452
|
+
if (this.open())
|
|
4453
|
+
this.close(false);
|
|
4454
|
+
else
|
|
4455
|
+
this.show('first');
|
|
4456
|
+
}
|
|
4457
|
+
onTriggerKeydown(event) {
|
|
4458
|
+
if (event.key === 'ArrowDown') {
|
|
4459
|
+
event.preventDefault();
|
|
4460
|
+
this.show('first');
|
|
4461
|
+
}
|
|
4462
|
+
else if (event.key === 'ArrowUp') {
|
|
4463
|
+
event.preventDefault();
|
|
4464
|
+
this.show('last');
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
// --- menu keyboard --------------------------------------------------------
|
|
4468
|
+
onMenuKeydown(event) {
|
|
4469
|
+
const idx = this.enabledIndexes();
|
|
4470
|
+
const current = Number(document.activeElement?.dataset?.['index'] ?? -1);
|
|
4471
|
+
const pos = idx.indexOf(current);
|
|
4472
|
+
switch (event.key) {
|
|
4473
|
+
case 'ArrowDown':
|
|
4474
|
+
event.preventDefault();
|
|
4475
|
+
if (idx.length)
|
|
4476
|
+
this.focusIndex(idx[(pos + 1 + idx.length) % idx.length]);
|
|
4477
|
+
return;
|
|
4478
|
+
case 'ArrowUp':
|
|
4479
|
+
event.preventDefault();
|
|
4480
|
+
if (idx.length)
|
|
4481
|
+
this.focusIndex(idx[pos <= 0 ? idx.length - 1 : pos - 1]);
|
|
4482
|
+
return;
|
|
4483
|
+
case 'Home':
|
|
4484
|
+
event.preventDefault();
|
|
4485
|
+
this.focusItemAt('first');
|
|
4486
|
+
return;
|
|
4487
|
+
case 'End':
|
|
4488
|
+
event.preventDefault();
|
|
4489
|
+
this.focusItemAt('last');
|
|
4490
|
+
return;
|
|
4491
|
+
case 'Escape':
|
|
4492
|
+
// Close ONLY the menu: stop the event so a drawer/dialog listening for Escape stays open.
|
|
4493
|
+
event.preventDefault();
|
|
4494
|
+
event.stopPropagation();
|
|
4495
|
+
event.stopImmediatePropagation();
|
|
4496
|
+
this.close(true);
|
|
4497
|
+
return;
|
|
4498
|
+
case 'Tab':
|
|
4499
|
+
event.preventDefault();
|
|
4500
|
+
this.close(true);
|
|
4501
|
+
return;
|
|
4502
|
+
case 'Enter':
|
|
4503
|
+
case ' ': {
|
|
4504
|
+
event.preventDefault();
|
|
4505
|
+
const entry = this.items()[current];
|
|
4506
|
+
if (entry && !this.isSeparator(entry))
|
|
4507
|
+
this.activate(entry);
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
default:
|
|
4511
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
4512
|
+
this.typeAhead(event.key, current);
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
typeAhead(char, current) {
|
|
4517
|
+
clearTimeout(this.typeTimer);
|
|
4518
|
+
this.typeBuffer += char.toLowerCase();
|
|
4519
|
+
// "d", "d", "d" cycles through the items starting with d; "de" narrows to "Delete".
|
|
4520
|
+
const repeated = [...this.typeBuffer].every((c) => c === this.typeBuffer[0]);
|
|
4521
|
+
const query = repeated ? this.typeBuffer[0] : this.typeBuffer;
|
|
4522
|
+
const items = this.items();
|
|
4523
|
+
const idx = this.enabledIndexes();
|
|
4524
|
+
const start = Math.max(0, idx.indexOf(current));
|
|
4525
|
+
const from = query.length === 1 ? start + 1 : start;
|
|
4526
|
+
const ordered = [...idx.slice(from), ...idx.slice(0, from)];
|
|
4527
|
+
const hit = ordered.find((i) => items[i].label.toLowerCase().startsWith(query));
|
|
4528
|
+
if (hit !== undefined)
|
|
4529
|
+
this.focusIndex(hit);
|
|
4530
|
+
this.typeTimer = setTimeout(() => (this.typeBuffer = ''), 600);
|
|
4531
|
+
}
|
|
4532
|
+
focusIndex(i) {
|
|
4533
|
+
this.panelEl?.querySelector(`[data-index="${i}"]`)?.focus();
|
|
4534
|
+
}
|
|
4535
|
+
focusItemAt(where) {
|
|
4536
|
+
const idx = this.enabledIndexes();
|
|
4537
|
+
if (!idx.length) {
|
|
4538
|
+
this.panelEl?.focus();
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
this.focusIndex(where === 'first' ? idx[0] : idx[idx.length - 1]);
|
|
4542
|
+
}
|
|
4543
|
+
activate(entry, event) {
|
|
4544
|
+
event?.stopPropagation();
|
|
4545
|
+
if (entry.disabled)
|
|
4546
|
+
return;
|
|
4547
|
+
this.select.emit(entry);
|
|
4548
|
+
if (!this.keepOpen())
|
|
4549
|
+
this.close(true);
|
|
4550
|
+
}
|
|
4551
|
+
// --- outside click ----------------------------------------------------------
|
|
4552
|
+
onDocumentClick(event) {
|
|
4553
|
+
if (!this.open())
|
|
4554
|
+
return;
|
|
4555
|
+
const target = event.target;
|
|
4556
|
+
if (this.host.nativeElement.contains(target))
|
|
4557
|
+
return;
|
|
4558
|
+
if (this.panelEl?.contains(target))
|
|
4559
|
+
return;
|
|
4560
|
+
this.close(false);
|
|
4561
|
+
}
|
|
4562
|
+
// --- positioning (body-appended, mirrors sds-select) ------------------------
|
|
4563
|
+
reflow = () => this.position();
|
|
4564
|
+
listening = false;
|
|
4565
|
+
addReflowListeners() {
|
|
4566
|
+
if (this.listening)
|
|
4567
|
+
return;
|
|
4568
|
+
window.addEventListener('scroll', this.reflow, true);
|
|
4569
|
+
window.addEventListener('resize', this.reflow);
|
|
4570
|
+
this.listening = true;
|
|
4571
|
+
}
|
|
4572
|
+
removeReflowListeners() {
|
|
4573
|
+
if (!this.listening)
|
|
4574
|
+
return;
|
|
4575
|
+
window.removeEventListener('scroll', this.reflow, true);
|
|
4576
|
+
window.removeEventListener('resize', this.reflow);
|
|
4577
|
+
this.listening = false;
|
|
4578
|
+
}
|
|
4579
|
+
position() {
|
|
4580
|
+
const trigger = this.triggerEl();
|
|
4581
|
+
const panel = this.panelEl;
|
|
4582
|
+
if (!trigger || !panel)
|
|
4583
|
+
return;
|
|
4584
|
+
const r = trigger.getBoundingClientRect();
|
|
4585
|
+
const pw = panel.offsetWidth;
|
|
4586
|
+
const ph = panel.offsetHeight;
|
|
4587
|
+
const gap = 4;
|
|
4588
|
+
const margin = 8;
|
|
4589
|
+
let top = r.bottom + gap;
|
|
4590
|
+
if (top + ph > window.innerHeight - margin && r.top - gap - ph > margin) {
|
|
4591
|
+
top = r.top - gap - ph;
|
|
4592
|
+
}
|
|
4593
|
+
top = Math.max(margin, Math.min(top, window.innerHeight - margin - ph));
|
|
4594
|
+
let left = this.align() === 'end' ? r.right - pw : r.left;
|
|
4595
|
+
left = Math.max(margin, Math.min(left, window.innerWidth - margin - pw));
|
|
4596
|
+
panel.style.top = `${Math.round(top)}px`;
|
|
4597
|
+
panel.style.left = `${Math.round(left)}px`;
|
|
4598
|
+
}
|
|
4599
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4600
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsMenu, isStandalone: true, selector: "sds-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, triggerLabel: { classPropertyName: "triggerLabel", publicName: "triggerLabel", isSignal: true, isRequired: false, transformFunction: null }, triggerIcon: { classPropertyName: "triggerIcon", publicName: "triggerIcon", isSignal: true, isRequired: false, transformFunction: null }, triggerText: { classPropertyName: "triggerText", publicName: "triggerText", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, keepOpen: { classPropertyName: "keepOpen", publicName: "keepOpen", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange", select: "select" }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "relative inline-flex" }, queries: [{ propertyName: "projectedTrigger", first: true, predicate: SdsMenuTrigger, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
4601
|
+
<span
|
|
4602
|
+
#slot
|
|
4603
|
+
class="inline-flex"
|
|
4604
|
+
(click)="onTriggerClick($event)"
|
|
4605
|
+
(keydown)="onTriggerKeydown($event)"
|
|
4606
|
+
>
|
|
4607
|
+
<ng-content select="[sdsMenuTrigger]" />
|
|
4608
|
+
@if (!projectedTrigger()) {
|
|
4609
|
+
<button
|
|
4610
|
+
type="button"
|
|
4611
|
+
[id]="triggerId"
|
|
4612
|
+
class="inline-flex h-9 min-w-9 items-center justify-center gap-1.5 rounded-control px-2 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
4613
|
+
[class.bg-muted]="open()"
|
|
4614
|
+
[attr.aria-label]="triggerText() ? null : triggerLabel()"
|
|
4615
|
+
[disabled]="disabled()"
|
|
4616
|
+
aria-haspopup="menu"
|
|
4617
|
+
[attr.aria-expanded]="open()"
|
|
4618
|
+
[attr.aria-controls]="open() ? menuId : null"
|
|
4619
|
+
>
|
|
4620
|
+
<sds-icon [name]="triggerIcon()" [size]="18" />
|
|
4621
|
+
@if (triggerText()) {
|
|
4622
|
+
<span>{{ triggerText() }}</span>
|
|
4623
|
+
}
|
|
4624
|
+
</button>
|
|
4625
|
+
}
|
|
4626
|
+
</span>
|
|
4627
|
+
|
|
4628
|
+
@if (open()) {
|
|
4629
|
+
<div
|
|
4630
|
+
data-sds-menu-panel
|
|
4631
|
+
role="menu"
|
|
4632
|
+
[id]="menuId"
|
|
4633
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
4634
|
+
class="fixed z-[1100] min-w-52 max-w-80 overflow-y-auto rounded-card border border-border bg-surface py-1.5 shadow-xl outline-none animate-sds-menu"
|
|
4635
|
+
[style.max-height]="'min(24rem, calc(100vh - 16px))'"
|
|
4636
|
+
(keydown)="onMenuKeydown($event)"
|
|
4637
|
+
>
|
|
4638
|
+
@for (entry of items(); track $index; let i = $index) {
|
|
4639
|
+
@if (isSeparator(entry)) {
|
|
4640
|
+
<div role="separator" class="my-1.5 border-t border-border"></div>
|
|
4641
|
+
} @else {
|
|
4642
|
+
<button
|
|
4643
|
+
type="button"
|
|
4644
|
+
role="menuitem"
|
|
4645
|
+
tabindex="-1"
|
|
4646
|
+
[attr.data-index]="i"
|
|
4647
|
+
[attr.aria-disabled]="entry.disabled ? 'true' : null"
|
|
4648
|
+
[attr.aria-describedby]="entry.description ? descId(i) : null"
|
|
4649
|
+
[class]="itemClass(entry)"
|
|
4650
|
+
(click)="activate(entry, $event)"
|
|
4651
|
+
(mouseenter)="entry.disabled ? null : focusIndex(i)"
|
|
4652
|
+
>
|
|
4653
|
+
@if (entry.icon) {
|
|
4654
|
+
<sds-icon [name]="entry.icon" [size]="18" class="mt-px shrink-0" />
|
|
4655
|
+
}
|
|
4656
|
+
<span class="min-w-0 flex-1 text-left">
|
|
4657
|
+
<span class="block truncate text-sm font-medium">{{ entry.label }}</span>
|
|
4658
|
+
@if (entry.description) {
|
|
4659
|
+
<span
|
|
4660
|
+
[id]="descId(i)"
|
|
4661
|
+
class="mt-0.5 block text-xs"
|
|
4662
|
+
[class]="entry.danger ? 'text-danger/80' : 'text-ink-faint'"
|
|
4663
|
+
>{{ entry.description }}</span
|
|
4664
|
+
>
|
|
4665
|
+
}
|
|
4666
|
+
</span>
|
|
4667
|
+
</button>
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
</div>
|
|
4671
|
+
}
|
|
4672
|
+
`, isInline: true, styles: [".animate-sds-menu{animation:sds-menu-pop .12s ease-out}@keyframes sds-menu-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4673
|
+
}
|
|
4674
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenu, decorators: [{
|
|
4675
|
+
type: Component,
|
|
4676
|
+
args: [{ selector: 'sds-menu', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsIcon], host: {
|
|
4677
|
+
class: 'relative inline-flex',
|
|
4678
|
+
'(document:click)': 'onDocumentClick($event)',
|
|
4679
|
+
}, template: `
|
|
4680
|
+
<span
|
|
4681
|
+
#slot
|
|
4682
|
+
class="inline-flex"
|
|
4683
|
+
(click)="onTriggerClick($event)"
|
|
4684
|
+
(keydown)="onTriggerKeydown($event)"
|
|
4685
|
+
>
|
|
4686
|
+
<ng-content select="[sdsMenuTrigger]" />
|
|
4687
|
+
@if (!projectedTrigger()) {
|
|
4688
|
+
<button
|
|
4689
|
+
type="button"
|
|
4690
|
+
[id]="triggerId"
|
|
4691
|
+
class="inline-flex h-9 min-w-9 items-center justify-center gap-1.5 rounded-control px-2 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
4692
|
+
[class.bg-muted]="open()"
|
|
4693
|
+
[attr.aria-label]="triggerText() ? null : triggerLabel()"
|
|
4694
|
+
[disabled]="disabled()"
|
|
4695
|
+
aria-haspopup="menu"
|
|
4696
|
+
[attr.aria-expanded]="open()"
|
|
4697
|
+
[attr.aria-controls]="open() ? menuId : null"
|
|
4698
|
+
>
|
|
4699
|
+
<sds-icon [name]="triggerIcon()" [size]="18" />
|
|
4700
|
+
@if (triggerText()) {
|
|
4701
|
+
<span>{{ triggerText() }}</span>
|
|
4702
|
+
}
|
|
4703
|
+
</button>
|
|
4704
|
+
}
|
|
4705
|
+
</span>
|
|
4706
|
+
|
|
4707
|
+
@if (open()) {
|
|
4708
|
+
<div
|
|
4709
|
+
data-sds-menu-panel
|
|
4710
|
+
role="menu"
|
|
4711
|
+
[id]="menuId"
|
|
4712
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
4713
|
+
class="fixed z-[1100] min-w-52 max-w-80 overflow-y-auto rounded-card border border-border bg-surface py-1.5 shadow-xl outline-none animate-sds-menu"
|
|
4714
|
+
[style.max-height]="'min(24rem, calc(100vh - 16px))'"
|
|
4715
|
+
(keydown)="onMenuKeydown($event)"
|
|
4716
|
+
>
|
|
4717
|
+
@for (entry of items(); track $index; let i = $index) {
|
|
4718
|
+
@if (isSeparator(entry)) {
|
|
4719
|
+
<div role="separator" class="my-1.5 border-t border-border"></div>
|
|
4720
|
+
} @else {
|
|
4721
|
+
<button
|
|
4722
|
+
type="button"
|
|
4723
|
+
role="menuitem"
|
|
4724
|
+
tabindex="-1"
|
|
4725
|
+
[attr.data-index]="i"
|
|
4726
|
+
[attr.aria-disabled]="entry.disabled ? 'true' : null"
|
|
4727
|
+
[attr.aria-describedby]="entry.description ? descId(i) : null"
|
|
4728
|
+
[class]="itemClass(entry)"
|
|
4729
|
+
(click)="activate(entry, $event)"
|
|
4730
|
+
(mouseenter)="entry.disabled ? null : focusIndex(i)"
|
|
4731
|
+
>
|
|
4732
|
+
@if (entry.icon) {
|
|
4733
|
+
<sds-icon [name]="entry.icon" [size]="18" class="mt-px shrink-0" />
|
|
4734
|
+
}
|
|
4735
|
+
<span class="min-w-0 flex-1 text-left">
|
|
4736
|
+
<span class="block truncate text-sm font-medium">{{ entry.label }}</span>
|
|
4737
|
+
@if (entry.description) {
|
|
4738
|
+
<span
|
|
4739
|
+
[id]="descId(i)"
|
|
4740
|
+
class="mt-0.5 block text-xs"
|
|
4741
|
+
[class]="entry.danger ? 'text-danger/80' : 'text-ink-faint'"
|
|
4742
|
+
>{{ entry.description }}</span
|
|
4743
|
+
>
|
|
4744
|
+
}
|
|
4745
|
+
</span>
|
|
4746
|
+
</button>
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
</div>
|
|
4750
|
+
}
|
|
4751
|
+
`, styles: [".animate-sds-menu{animation:sds-menu-pop .12s ease-out}@keyframes sds-menu-pop{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"] }]
|
|
4752
|
+
}], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], triggerLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerLabel", required: false }] }], triggerIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerIcon", required: false }] }], triggerText: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerText", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], keepOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepOpen", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], projectedTrigger: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsMenuTrigger), { isSignal: true }] }] } });
|
|
4753
|
+
|
|
4754
|
+
/**
|
|
4755
|
+
* Selection toolbar for bulk editing: "N selected", the actions the app projects, and a clear-selection
|
|
4756
|
+
* button. It shows only while something is selected (`count > 0`) and sticks to the bottom of its
|
|
4757
|
+
* scroll container by default, so it stays in reach while the list scrolls.
|
|
4758
|
+
*
|
|
4759
|
+
* A `role="toolbar"` named by [ariaLabel]; the count sits in a polite live region that exists even
|
|
4760
|
+
* while the bar is hidden, so "3 selected" is announced as the selection changes. Every visible and
|
|
4761
|
+
* spoken string is an input (English defaults).
|
|
4762
|
+
*
|
|
4763
|
+
* @example
|
|
4764
|
+
* <sds-bulk-bar [count]="selected().length" countLabel="{count} selected" (clear)="selected.set([])">
|
|
4765
|
+
* <sds-button size="sm" variant="secondary" (click)="bulkPrice()">Change price</sds-button>
|
|
4766
|
+
* <sds-button size="sm" variant="secondary" (click)="bulkSoldOut()">Mark sold out</sds-button>
|
|
4767
|
+
* </sds-bulk-bar>
|
|
4768
|
+
*/
|
|
4769
|
+
class SdsBulkBar {
|
|
4770
|
+
/** Number of selected things. The bar is hidden at 0. */
|
|
4771
|
+
count = input(0, ...(ngDevMode ? [{ debugName: "count", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
4772
|
+
/** "{count} selected" — `{count}` is replaced. Pass a translated string. */
|
|
4773
|
+
countLabel = input('{count} selected', ...(ngDevMode ? [{ debugName: "countLabel" }] : []));
|
|
4774
|
+
/** Text of the clear-selection button. */
|
|
4775
|
+
clearLabel = input('Clear selection', ...(ngDevMode ? [{ debugName: "clearLabel" }] : []));
|
|
4776
|
+
/** Accessible name of the toolbar. */
|
|
4777
|
+
ariaLabel = input('Bulk actions', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
4778
|
+
position = input('bottom', ...(ngDevMode ? [{ debugName: "position" }] : []));
|
|
4779
|
+
/** The clear-selection button was pressed. */
|
|
4780
|
+
clear = output();
|
|
4781
|
+
countText = computed(() => this.countLabel().replace('{count}', String(this.count())), ...(ngDevMode ? [{ debugName: "countText" }] : []));
|
|
4782
|
+
/** The label without the number, shown next to the count pill ("selected"). */
|
|
4783
|
+
countWords = computed(() => this.countLabel().replace('{count}', '').replace(/\s+/g, ' ').trim(), ...(ngDevMode ? [{ debugName: "countWords" }] : []));
|
|
4784
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsBulkBar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4785
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsBulkBar, isStandalone: true, selector: "sds-bulk-bar", inputs: { count: { classPropertyName: "count", publicName: "count", isSignal: true, isRequired: false, transformFunction: null }, countLabel: { classPropertyName: "countLabel", publicName: "countLabel", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { clear: "clear" }, host: { properties: { "class.sticky": "position() !== 'static' && count() > 0", "class.bottom-3": "position() === 'bottom'", "class.top-3": "position() === 'top'", "class.z-30": "position() !== 'static'" }, classAttribute: "block" }, ngImport: i0, template: `
|
|
4786
|
+
<!-- Always present so screen readers hear the count change, including back to zero. -->
|
|
4787
|
+
<span class="sr-only" aria-live="polite" aria-atomic="true">{{ count() > 0 ? countText() : '' }}</span>
|
|
4788
|
+
|
|
4789
|
+
@if (count() > 0) {
|
|
4790
|
+
<div
|
|
4791
|
+
role="toolbar"
|
|
4792
|
+
[attr.aria-label]="ariaLabel()"
|
|
4793
|
+
class="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-card border border-border bg-surface px-3 py-2 shadow-xl animate-sds-bulk"
|
|
4794
|
+
>
|
|
4795
|
+
<span class="inline-flex items-center gap-2 pr-1 text-sm font-semibold text-ink">
|
|
4796
|
+
<span
|
|
4797
|
+
class="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-brand-600 px-1.5 text-xs font-bold tabular-nums text-white dark:bg-ink dark:text-surface"
|
|
4798
|
+
>{{ count() }}</span
|
|
4799
|
+
>
|
|
4800
|
+
{{ countWords() }}
|
|
4801
|
+
</span>
|
|
4802
|
+
|
|
4803
|
+
<span class="hidden h-6 w-px bg-border sm:block" aria-hidden="true"></span>
|
|
4804
|
+
|
|
4805
|
+
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
|
4806
|
+
<ng-content />
|
|
4807
|
+
</div>
|
|
4808
|
+
|
|
4809
|
+
<button
|
|
4810
|
+
type="button"
|
|
4811
|
+
class="ml-auto inline-flex h-9 items-center gap-1.5 rounded-control px-2.5 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
4812
|
+
(click)="clear.emit()"
|
|
4813
|
+
>
|
|
4814
|
+
<sds-icon name="x" [size]="16" />
|
|
4815
|
+
<span>{{ clearLabel() }}</span>
|
|
4816
|
+
</button>
|
|
4817
|
+
</div>
|
|
4818
|
+
}
|
|
4819
|
+
`, isInline: true, styles: [".animate-sds-bulk{animation:sds-bulk-in .16s ease-out}@keyframes sds-bulk-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-bulk{animation:none}}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4820
|
+
}
|
|
4821
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsBulkBar, decorators: [{
|
|
4822
|
+
type: Component,
|
|
4823
|
+
args: [{ selector: 'sds-bulk-bar', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsIcon], host: {
|
|
4824
|
+
class: 'block',
|
|
4825
|
+
'[class.sticky]': "position() !== 'static' && count() > 0",
|
|
4826
|
+
'[class.bottom-3]': "position() === 'bottom'",
|
|
4827
|
+
'[class.top-3]': "position() === 'top'",
|
|
4828
|
+
'[class.z-30]': "position() !== 'static'",
|
|
4829
|
+
}, template: `
|
|
4830
|
+
<!-- Always present so screen readers hear the count change, including back to zero. -->
|
|
4831
|
+
<span class="sr-only" aria-live="polite" aria-atomic="true">{{ count() > 0 ? countText() : '' }}</span>
|
|
4832
|
+
|
|
4833
|
+
@if (count() > 0) {
|
|
4834
|
+
<div
|
|
4835
|
+
role="toolbar"
|
|
4836
|
+
[attr.aria-label]="ariaLabel()"
|
|
4837
|
+
class="flex flex-wrap items-center gap-x-3 gap-y-2 rounded-card border border-border bg-surface px-3 py-2 shadow-xl animate-sds-bulk"
|
|
4838
|
+
>
|
|
4839
|
+
<span class="inline-flex items-center gap-2 pr-1 text-sm font-semibold text-ink">
|
|
4840
|
+
<span
|
|
4841
|
+
class="inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-brand-600 px-1.5 text-xs font-bold tabular-nums text-white dark:bg-ink dark:text-surface"
|
|
4842
|
+
>{{ count() }}</span
|
|
4843
|
+
>
|
|
4844
|
+
{{ countWords() }}
|
|
4845
|
+
</span>
|
|
4846
|
+
|
|
4847
|
+
<span class="hidden h-6 w-px bg-border sm:block" aria-hidden="true"></span>
|
|
4848
|
+
|
|
4849
|
+
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
|
4850
|
+
<ng-content />
|
|
4851
|
+
</div>
|
|
4852
|
+
|
|
4853
|
+
<button
|
|
4854
|
+
type="button"
|
|
4855
|
+
class="ml-auto inline-flex h-9 items-center gap-1.5 rounded-control px-2.5 text-sm font-medium text-ink-soft transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500"
|
|
4856
|
+
(click)="clear.emit()"
|
|
4857
|
+
>
|
|
4858
|
+
<sds-icon name="x" [size]="16" />
|
|
4859
|
+
<span>{{ clearLabel() }}</span>
|
|
4860
|
+
</button>
|
|
4861
|
+
</div>
|
|
4862
|
+
}
|
|
4863
|
+
`, styles: [".animate-sds-bulk{animation:sds-bulk-in .16s ease-out}@keyframes sds-bulk-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-bulk{animation:none}}\n"] }]
|
|
4864
|
+
}], propDecorators: { count: [{ type: i0.Input, args: [{ isSignal: true, alias: "count", required: false }] }], countLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "countLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], clear: [{ type: i0.Output, args: ["clear"] }] } });
|
|
4865
|
+
|
|
4866
|
+
/**
|
|
4867
|
+
* Template for one row's content inside `<sds-sortable-list>`.
|
|
4868
|
+
*
|
|
4869
|
+
* @example
|
|
4870
|
+
* <ng-template sdsSortableItem let-item let-i="index">{{ item.name }}</ng-template>
|
|
4871
|
+
*/
|
|
4872
|
+
class SdsSortableItem {
|
|
4873
|
+
template = inject(TemplateRef);
|
|
4874
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
4875
|
+
return true;
|
|
4876
|
+
}
|
|
4877
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4878
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsSortableItem, isStandalone: true, selector: "ng-template[sdsSortableItem]", ngImport: i0 });
|
|
4879
|
+
}
|
|
4880
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableItem, decorators: [{
|
|
4881
|
+
type: Directive,
|
|
4882
|
+
args: [{
|
|
4883
|
+
selector: 'ng-template[sdsSortableItem]',
|
|
4884
|
+
standalone: true,
|
|
4885
|
+
}]
|
|
4886
|
+
}] });
|
|
4887
|
+
/**
|
|
4888
|
+
* Optional template for the drag handle's content (replaces the default grip icon). The handle itself
|
|
4889
|
+
* stays a library-owned button, so it keeps its accessible name and keyboard behaviour.
|
|
4890
|
+
*/
|
|
4891
|
+
class SdsSortableHandle {
|
|
4892
|
+
template = inject(TemplateRef);
|
|
4893
|
+
static ngTemplateContextGuard(_dir, _ctx) {
|
|
4894
|
+
return true;
|
|
4895
|
+
}
|
|
4896
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4897
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsSortableHandle, isStandalone: true, selector: "ng-template[sdsSortableHandle]", ngImport: i0 });
|
|
4898
|
+
}
|
|
4899
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableHandle, decorators: [{
|
|
4900
|
+
type: Directive,
|
|
4901
|
+
args: [{
|
|
4902
|
+
selector: 'ng-template[sdsSortableHandle]',
|
|
4903
|
+
standalone: true,
|
|
4904
|
+
}]
|
|
4905
|
+
}] });
|
|
4906
|
+
let nextId$a = 0;
|
|
4907
|
+
/** Move one entry of `list` from `from` to `to`, returning a new array. */
|
|
4908
|
+
function moveItem(list, from, to) {
|
|
4909
|
+
const next = [...list];
|
|
4910
|
+
const [moved] = next.splice(from, 1);
|
|
4911
|
+
next.splice(to, 0, moved);
|
|
4912
|
+
return next;
|
|
4913
|
+
}
|
|
4914
|
+
/**
|
|
4915
|
+
* Accessible reorderable list, with no `@angular/cdk` dependency.
|
|
4916
|
+
*
|
|
4917
|
+
* - **Pointer** (mouse, pen, touch): drag an item by its handle. The handle has `touch-action: none`, so
|
|
4918
|
+
* a finger drags instead of scrolling the page; the rest of the row still scrolls normally.
|
|
4919
|
+
* - **Keyboard**: focus a handle, **Space/Enter** picks the item up, **↑/↓** (and Home/End) move it,
|
|
4920
|
+
* **Space/Enter** drops it, **Escape** cancels (the event is stopped, so an enclosing drawer stays open).
|
|
4921
|
+
* Moving focus away also cancels.
|
|
4922
|
+
* - **Screen readers**: every handle is named ("Reorder Burgers") and described by the instructions; an
|
|
4923
|
+
* assertive live region announces pick-up, each move ("moved to position 3 of 8"), the drop and a cancel.
|
|
4924
|
+
*
|
|
4925
|
+
* Nothing is committed until the drop: while dragging, the other rows slide to make room. The drop
|
|
4926
|
+
* updates the two-way `[(items)]` model and emits `(reorder)` with the old/new index and the new array.
|
|
4927
|
+
* Items for which [itemDisabled] returns true cannot be picked up (they still shift when others pass).
|
|
4928
|
+
*
|
|
4929
|
+
* Row content comes from `<ng-template sdsSortableItem let-item>`; without one the item's label is shown.
|
|
4930
|
+
* All spoken text is set through inputs (English defaults, `{item}` `{position}` `{total}` tokens).
|
|
4931
|
+
*
|
|
4932
|
+
* @example
|
|
4933
|
+
* <sds-sortable-list [(items)]="categories" dataKey="id" labelField="name" ariaLabel="Categories"
|
|
4934
|
+
* (reorder)="save($event)">
|
|
4935
|
+
* <ng-template sdsSortableItem let-cat>{{ cat.name }} <span>{{ cat.count }} items</span></ng-template>
|
|
4936
|
+
* </sds-sortable-list>
|
|
4937
|
+
*/
|
|
4938
|
+
class SdsSortableList {
|
|
4939
|
+
host = inject(ElementRef);
|
|
4940
|
+
injector = inject(Injector);
|
|
4941
|
+
instructionsId = `sds-sortable-${nextId$a++}-help`;
|
|
4942
|
+
/** The items, in order. Two-way: `[(items)]` receives the new order on drop. */
|
|
4943
|
+
items = model([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
4944
|
+
/** Property used as a stable identity for rows (strongly recommended). */
|
|
4945
|
+
dataKey = input('', ...(ngDevMode ? [{ debugName: "dataKey" }] : []));
|
|
4946
|
+
/** Property holding the item's name, for the default row, the handle name and announcements. */
|
|
4947
|
+
labelField = input('label', ...(ngDevMode ? [{ debugName: "labelField" }] : []));
|
|
4948
|
+
/** Function returning the item's name; takes precedence over [labelField]. */
|
|
4949
|
+
itemLabel = input(null, ...(ngDevMode ? [{ debugName: "itemLabel" }] : []));
|
|
4950
|
+
/** Items for which this returns true cannot be picked up. */
|
|
4951
|
+
itemDisabled = input(null, ...(ngDevMode ? [{ debugName: "itemDisabled" }] : []));
|
|
4952
|
+
/** Disable reordering for the whole list. */
|
|
4953
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
4954
|
+
/** `card` (default): bordered rows with a gap. `plain`: rows separated by dividers. */
|
|
4955
|
+
appearance = input('card', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
|
|
4956
|
+
/** Accessible name of the list. */
|
|
4957
|
+
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
4958
|
+
/** Name of each handle; `{item}` is the item's label. */
|
|
4959
|
+
handleLabel = input('Reorder {item}', ...(ngDevMode ? [{ debugName: "handleLabel" }] : []));
|
|
4960
|
+
/** Read after a handle's name (aria-describedby). */
|
|
4961
|
+
instructions = input('Press Space or Enter to pick up. Use the arrow keys to move, Space or Enter to drop, Escape to cancel.', ...(ngDevMode ? [{ debugName: "instructions" }] : []));
|
|
4962
|
+
pickedUpMessage = input('{item} picked up. Position {position} of {total}.', ...(ngDevMode ? [{ debugName: "pickedUpMessage" }] : []));
|
|
4963
|
+
movedMessage = input('{item} moved to position {position} of {total}.', ...(ngDevMode ? [{ debugName: "movedMessage" }] : []));
|
|
4964
|
+
droppedMessage = input('{item} dropped at position {position} of {total}.', ...(ngDevMode ? [{ debugName: "droppedMessage" }] : []));
|
|
4965
|
+
cancelledMessage = input('Reorder cancelled. {item} is back at position {position} of {total}.', ...(ngDevMode ? [{ debugName: "cancelledMessage" }] : []));
|
|
4966
|
+
/** A move was completed. */
|
|
4967
|
+
reorder = output();
|
|
4968
|
+
itemTpl = contentChild(SdsSortableItem, ...(ngDevMode ? [{ debugName: "itemTpl" }] : []));
|
|
4969
|
+
handleTpl = contentChild(SdsSortableHandle, ...(ngDevMode ? [{ debugName: "handleTpl" }] : []));
|
|
4970
|
+
/** Rendered order: follows [items] until a drop commits a new one. */
|
|
4971
|
+
working = linkedSignal(() => [...this.items()], ...(ngDevMode ? [{ debugName: "working" }] : []));
|
|
4972
|
+
// --- drag state -------------------------------------------------------------
|
|
4973
|
+
/** Index (in `working`) of the item being moved, or null. */
|
|
4974
|
+
active = signal(null, ...(ngDevMode ? [{ debugName: "active" }] : []));
|
|
4975
|
+
/** Where it would land if dropped now. */
|
|
4976
|
+
target = signal(null, ...(ngDevMode ? [{ debugName: "target" }] : []));
|
|
4977
|
+
mode = signal(null, ...(ngDevMode ? [{ debugName: "mode" }] : []));
|
|
4978
|
+
/** Pointer-mode vertical offset of the dragged row. */
|
|
4979
|
+
pointerDy = signal(0, ...(ngDevMode ? [{ debugName: "pointerDy" }] : []));
|
|
4980
|
+
/** No transitions for one frame while a drop re-renders the new order in place. */
|
|
4981
|
+
settling = signal(false, ...(ngDevMode ? [{ debugName: "settling" }] : []));
|
|
4982
|
+
liveMessage = signal('', ...(ngDevMode ? [{ debugName: "liveMessage" }] : []));
|
|
4983
|
+
/** Row boxes measured at drag start (top/height), plus the gap between rows. */
|
|
4984
|
+
rects = [];
|
|
4985
|
+
gap = 0;
|
|
4986
|
+
pointerId = null;
|
|
4987
|
+
startY = 0;
|
|
4988
|
+
/** Per-row offsets that open a gap where the moving row will land. */
|
|
4989
|
+
shifts = computed(() => {
|
|
4990
|
+
const a = this.active();
|
|
4991
|
+
const t = this.target();
|
|
4992
|
+
const n = this.working().length;
|
|
4993
|
+
const out = new Array(n).fill(0);
|
|
4994
|
+
if (a === null || t === null || !this.rects[a])
|
|
4995
|
+
return out;
|
|
4996
|
+
const step = this.rects[a].height + this.gap;
|
|
4997
|
+
for (let j = 0; j < n; j++) {
|
|
4998
|
+
if (j === a)
|
|
4999
|
+
continue;
|
|
5000
|
+
if (a < t && j > a && j <= t)
|
|
5001
|
+
out[j] = -step;
|
|
5002
|
+
else if (t < a && j >= t && j < a)
|
|
5003
|
+
out[j] = step;
|
|
5004
|
+
}
|
|
5005
|
+
return out;
|
|
5006
|
+
}, ...(ngDevMode ? [{ debugName: "shifts" }] : []));
|
|
5007
|
+
/** Offset of the moving row: follows the pointer, or jumps slot to slot with the keyboard. */
|
|
5008
|
+
activeOffset = computed(() => {
|
|
5009
|
+
const a = this.active();
|
|
5010
|
+
const t = this.target();
|
|
5011
|
+
if (a === null || t === null)
|
|
5012
|
+
return 0;
|
|
5013
|
+
if (this.mode() === 'pointer')
|
|
5014
|
+
return this.pointerDy();
|
|
5015
|
+
let d = 0;
|
|
5016
|
+
if (t > a)
|
|
5017
|
+
for (let j = a + 1; j <= t; j++)
|
|
5018
|
+
d += (this.rects[j]?.height ?? 0) + this.gap;
|
|
5019
|
+
else
|
|
5020
|
+
for (let j = t; j < a; j++)
|
|
5021
|
+
d -= (this.rects[j]?.height ?? 0) + this.gap;
|
|
5022
|
+
return d;
|
|
5023
|
+
}, ...(ngDevMode ? [{ debugName: "activeOffset" }] : []));
|
|
5024
|
+
// --- rendering helpers ------------------------------------------------------
|
|
5025
|
+
trackKey(item, index) {
|
|
5026
|
+
const k = this.dataKey();
|
|
5027
|
+
if (k && item && typeof item === 'object')
|
|
5028
|
+
return item[k] ?? index;
|
|
5029
|
+
return item ?? index;
|
|
5030
|
+
}
|
|
5031
|
+
labelOf(item) {
|
|
5032
|
+
const fn = this.itemLabel();
|
|
5033
|
+
if (fn)
|
|
5034
|
+
return fn(item);
|
|
5035
|
+
if (item && typeof item === 'object') {
|
|
5036
|
+
const v = item[this.labelField()];
|
|
5037
|
+
return v == null ? '' : String(v);
|
|
5038
|
+
}
|
|
5039
|
+
return String(item);
|
|
5040
|
+
}
|
|
5041
|
+
isItemDisabled(item) {
|
|
5042
|
+
return this.itemDisabled()?.(item) ?? false;
|
|
5043
|
+
}
|
|
5044
|
+
context(item, index) {
|
|
5045
|
+
return { $implicit: item, item, index, grabbed: this.active() === index };
|
|
5046
|
+
}
|
|
5047
|
+
handleText(item) {
|
|
5048
|
+
return this.handleLabel().replace('{item}', this.labelOf(item));
|
|
5049
|
+
}
|
|
5050
|
+
transformFor(i) {
|
|
5051
|
+
if (this.active() === null)
|
|
5052
|
+
return null;
|
|
5053
|
+
const y = i === this.active() ? this.activeOffset() : this.shifts()[i];
|
|
5054
|
+
return y ? `translate3d(0, ${y}px, 0)` : null;
|
|
5055
|
+
}
|
|
5056
|
+
rowClass(i) {
|
|
5057
|
+
const isActive = this.active() === i;
|
|
5058
|
+
const moving = this.active() !== null;
|
|
5059
|
+
const card = this.appearance() === 'card';
|
|
5060
|
+
const parts = ['relative flex items-center gap-2'];
|
|
5061
|
+
parts.push(card ? 'rounded-control border border-border bg-surface px-2 py-2' : 'bg-surface px-1 py-2');
|
|
5062
|
+
// Neighbours glide; the dragged row tracks the pointer 1:1 (no easing), and nothing animates
|
|
5063
|
+
// during the settle frame after a drop.
|
|
5064
|
+
if (moving && !this.settling() && !(isActive && this.mode() === 'pointer')) {
|
|
5065
|
+
parts.push('transition-transform duration-150 ease-out');
|
|
5066
|
+
}
|
|
5067
|
+
if (isActive)
|
|
5068
|
+
parts.push('z-10 shadow-lg ring-2 ring-brand-500');
|
|
5069
|
+
return parts.join(' ');
|
|
5070
|
+
}
|
|
5071
|
+
handleClass(item) {
|
|
5072
|
+
const base = 'grid h-9 w-8 shrink-0 place-items-center rounded-control text-ink-faint transition-colors ' +
|
|
5073
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 ' +
|
|
5074
|
+
'disabled:cursor-not-allowed disabled:opacity-40 touch-none select-none';
|
|
5075
|
+
return this.disabled() || this.isItemDisabled(item)
|
|
5076
|
+
? base
|
|
5077
|
+
: `${base} cursor-grab hover:bg-muted hover:text-ink active:cursor-grabbing`;
|
|
5078
|
+
}
|
|
5079
|
+
// --- keyboard ---------------------------------------------------------------
|
|
5080
|
+
onHandleKeydown(event, i) {
|
|
5081
|
+
if (this.mode() === 'pointer')
|
|
5082
|
+
return;
|
|
5083
|
+
const a = this.active();
|
|
5084
|
+
if (a === null) {
|
|
5085
|
+
if (event.key === ' ' || event.key === 'Enter') {
|
|
5086
|
+
event.preventDefault();
|
|
5087
|
+
this.begin(i, 'keyboard');
|
|
5088
|
+
this.announce(this.pickedUpMessage(), i);
|
|
5089
|
+
}
|
|
5090
|
+
return;
|
|
5091
|
+
}
|
|
5092
|
+
const last = this.working().length - 1;
|
|
5093
|
+
const t = this.target() ?? a;
|
|
5094
|
+
switch (event.key) {
|
|
5095
|
+
case 'ArrowUp':
|
|
5096
|
+
case 'ArrowLeft':
|
|
5097
|
+
event.preventDefault();
|
|
5098
|
+
this.keyMove(Math.max(0, t - 1));
|
|
5099
|
+
return;
|
|
5100
|
+
case 'ArrowDown':
|
|
5101
|
+
case 'ArrowRight':
|
|
5102
|
+
event.preventDefault();
|
|
5103
|
+
this.keyMove(Math.min(last, t + 1));
|
|
5104
|
+
return;
|
|
5105
|
+
case 'Home':
|
|
5106
|
+
event.preventDefault();
|
|
5107
|
+
this.keyMove(0);
|
|
5108
|
+
return;
|
|
5109
|
+
case 'End':
|
|
5110
|
+
event.preventDefault();
|
|
5111
|
+
this.keyMove(last);
|
|
5112
|
+
return;
|
|
5113
|
+
case ' ':
|
|
5114
|
+
case 'Enter':
|
|
5115
|
+
event.preventDefault();
|
|
5116
|
+
this.commit();
|
|
5117
|
+
return;
|
|
5118
|
+
case 'Escape':
|
|
5119
|
+
event.preventDefault();
|
|
5120
|
+
event.stopPropagation();
|
|
5121
|
+
this.cancel();
|
|
5122
|
+
return;
|
|
5123
|
+
case 'Tab':
|
|
5124
|
+
this.cancel();
|
|
5125
|
+
return;
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
keyMove(to) {
|
|
5129
|
+
if (to === this.target())
|
|
5130
|
+
return;
|
|
5131
|
+
this.target.set(to);
|
|
5132
|
+
this.announce(this.movedMessage(), to);
|
|
5133
|
+
}
|
|
5134
|
+
/** Focus leaving the list while an item is picked up cancels the move. */
|
|
5135
|
+
onFocusOut(event) {
|
|
5136
|
+
if (this.mode() !== 'keyboard')
|
|
5137
|
+
return;
|
|
5138
|
+
const next = event.relatedTarget;
|
|
5139
|
+
if (next && this.host.nativeElement.contains(next))
|
|
5140
|
+
return;
|
|
5141
|
+
this.cancel();
|
|
5142
|
+
}
|
|
5143
|
+
// --- pointer ------------------------------------------------------------------
|
|
5144
|
+
onPointerDown(event, i) {
|
|
5145
|
+
if (this.active() !== null || event.button !== 0)
|
|
5146
|
+
return;
|
|
5147
|
+
const item = this.working()[i];
|
|
5148
|
+
if (this.disabled() || this.isItemDisabled(item))
|
|
5149
|
+
return;
|
|
5150
|
+
event.preventDefault();
|
|
5151
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
5152
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
5153
|
+
this.pointerId = event.pointerId;
|
|
5154
|
+
this.startY = event.clientY;
|
|
5155
|
+
this.pointerDy.set(0);
|
|
5156
|
+
this.begin(i, 'pointer');
|
|
5157
|
+
}
|
|
5158
|
+
onPointerMove(event) {
|
|
5159
|
+
if (this.mode() !== 'pointer' || event.pointerId !== this.pointerId)
|
|
5160
|
+
return;
|
|
5161
|
+
const a = this.active();
|
|
5162
|
+
if (a === null)
|
|
5163
|
+
return;
|
|
5164
|
+
const dy = event.clientY - this.startY;
|
|
5165
|
+
this.pointerDy.set(dy);
|
|
5166
|
+
const center = this.rects[a].top + this.rects[a].height / 2 + dy;
|
|
5167
|
+
let t = 0;
|
|
5168
|
+
this.rects.forEach((r, j) => {
|
|
5169
|
+
if (j !== a && r.top + r.height / 2 < center)
|
|
5170
|
+
t++;
|
|
5171
|
+
});
|
|
5172
|
+
this.target.set(t);
|
|
5173
|
+
}
|
|
5174
|
+
onPointerUp(event) {
|
|
5175
|
+
if (this.mode() !== 'pointer' || event.pointerId !== this.pointerId)
|
|
5176
|
+
return;
|
|
5177
|
+
this.commit();
|
|
5178
|
+
}
|
|
5179
|
+
onPointerCancel() {
|
|
5180
|
+
if (this.mode() === 'pointer')
|
|
5181
|
+
this.cancel(false);
|
|
5182
|
+
}
|
|
5183
|
+
// --- lifecycle of a move -------------------------------------------------------
|
|
5184
|
+
begin(i, mode) {
|
|
5185
|
+
this.measure();
|
|
5186
|
+
this.active.set(i);
|
|
5187
|
+
this.target.set(i);
|
|
5188
|
+
this.mode.set(mode);
|
|
5189
|
+
}
|
|
5190
|
+
measure() {
|
|
5191
|
+
const rows = Array.from(this.host.nativeElement.querySelectorAll('li[data-sortable-index]'));
|
|
5192
|
+
this.rects = rows.map((r) => {
|
|
5193
|
+
const b = r.getBoundingClientRect();
|
|
5194
|
+
return { top: b.top, height: b.height };
|
|
5195
|
+
});
|
|
5196
|
+
this.gap =
|
|
5197
|
+
this.rects.length > 1
|
|
5198
|
+
? Math.max(0, this.rects[1].top - (this.rects[0].top + this.rects[0].height))
|
|
5199
|
+
: 0;
|
|
5200
|
+
}
|
|
5201
|
+
commit() {
|
|
5202
|
+
const from = this.active();
|
|
5203
|
+
const to = this.target();
|
|
5204
|
+
const mode = this.mode();
|
|
5205
|
+
if (from === null || to === null)
|
|
5206
|
+
return this.reset();
|
|
5207
|
+
const item = this.working()[from];
|
|
5208
|
+
if (to === from) {
|
|
5209
|
+
this.reset();
|
|
5210
|
+
this.announce(this.droppedMessage(), to, item);
|
|
5211
|
+
if (mode === 'keyboard')
|
|
5212
|
+
this.focusHandle(to);
|
|
5213
|
+
return;
|
|
5214
|
+
}
|
|
5215
|
+
const next = moveItem(this.working(), from, to);
|
|
5216
|
+
// Swap order and clear the offsets in the same frame, with transitions off, so rows don't jump.
|
|
5217
|
+
this.settling.set(true);
|
|
5218
|
+
this.reset();
|
|
5219
|
+
this.working.set(next);
|
|
5220
|
+
this.items.set(next);
|
|
5221
|
+
this.reorder.emit({ previousIndex: from, currentIndex: to, item, items: next });
|
|
5222
|
+
this.announce(this.droppedMessage(), to, item);
|
|
5223
|
+
this.focusHandle(to);
|
|
5224
|
+
}
|
|
5225
|
+
/** Nothing was moved in the DOM while dragging, so focus is still where it should be. */
|
|
5226
|
+
cancel(announce = true) {
|
|
5227
|
+
const from = this.active();
|
|
5228
|
+
if (from === null)
|
|
5229
|
+
return;
|
|
5230
|
+
this.reset();
|
|
5231
|
+
if (announce)
|
|
5232
|
+
this.announce(this.cancelledMessage(), from);
|
|
5233
|
+
}
|
|
5234
|
+
reset() {
|
|
5235
|
+
this.active.set(null);
|
|
5236
|
+
this.target.set(null);
|
|
5237
|
+
this.mode.set(null);
|
|
5238
|
+
this.pointerDy.set(0);
|
|
5239
|
+
this.pointerId = null;
|
|
5240
|
+
}
|
|
5241
|
+
focusHandle(index) {
|
|
5242
|
+
afterNextRender(() => {
|
|
5243
|
+
this.settling.set(false);
|
|
5244
|
+
this.host.nativeElement
|
|
5245
|
+
.querySelector(`[data-sortable-handle="${index}"]`)
|
|
5246
|
+
?.focus({ preventScroll: false });
|
|
5247
|
+
}, { injector: this.injector });
|
|
5248
|
+
}
|
|
5249
|
+
announce(template, position, item) {
|
|
5250
|
+
const it = item ?? this.working()[this.active() ?? position];
|
|
5251
|
+
const text = template
|
|
5252
|
+
.replace('{item}', it === undefined ? '' : this.labelOf(it))
|
|
5253
|
+
.replace('{position}', String(position + 1))
|
|
5254
|
+
.replace('{total}', String(this.working().length));
|
|
5255
|
+
// Re-set even when unchanged, so a repeated message is re-announced.
|
|
5256
|
+
this.liveMessage.set('');
|
|
5257
|
+
queueMicrotask(() => this.liveMessage.set(text));
|
|
5258
|
+
}
|
|
5259
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5260
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSortableList, isStandalone: true, selector: "sds-sortable-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, labelField: { classPropertyName: "labelField", publicName: "labelField", isSignal: true, isRequired: false, transformFunction: null }, itemLabel: { classPropertyName: "itemLabel", publicName: "itemLabel", isSignal: true, isRequired: false, transformFunction: null }, itemDisabled: { classPropertyName: "itemDisabled", publicName: "itemDisabled", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, handleLabel: { classPropertyName: "handleLabel", publicName: "handleLabel", isSignal: true, isRequired: false, transformFunction: null }, instructions: { classPropertyName: "instructions", publicName: "instructions", isSignal: true, isRequired: false, transformFunction: null }, pickedUpMessage: { classPropertyName: "pickedUpMessage", publicName: "pickedUpMessage", isSignal: true, isRequired: false, transformFunction: null }, movedMessage: { classPropertyName: "movedMessage", publicName: "movedMessage", isSignal: true, isRequired: false, transformFunction: null }, droppedMessage: { classPropertyName: "droppedMessage", publicName: "droppedMessage", isSignal: true, isRequired: false, transformFunction: null }, cancelledMessage: { classPropertyName: "cancelledMessage", publicName: "cancelledMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { items: "itemsChange", reorder: "reorder" }, host: { classAttribute: "block" }, queries: [{ propertyName: "itemTpl", first: true, predicate: SdsSortableItem, descendants: true, isSignal: true }, { propertyName: "handleTpl", first: true, predicate: SdsSortableHandle, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
5261
|
+
<ul
|
|
5262
|
+
role="list"
|
|
5263
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
5264
|
+
class="flex flex-col"
|
|
5265
|
+
[class.gap-2]="appearance() === 'card'"
|
|
5266
|
+
[class.divide-y]="appearance() === 'plain'"
|
|
5267
|
+
[class.divide-border]="appearance() === 'plain'"
|
|
5268
|
+
(focusout)="onFocusOut($event)"
|
|
5269
|
+
>
|
|
5270
|
+
@for (item of working(); track trackKey(item, $index); let i = $index) {
|
|
5271
|
+
<li
|
|
5272
|
+
[attr.data-sortable-index]="i"
|
|
5273
|
+
[class]="rowClass(i)"
|
|
5274
|
+
[style.transform]="transformFor(i)"
|
|
5275
|
+
>
|
|
5276
|
+
<button
|
|
5277
|
+
type="button"
|
|
5278
|
+
[class]="handleClass(item)"
|
|
5279
|
+
[attr.aria-label]="handleText(item)"
|
|
5280
|
+
[attr.aria-describedby]="instructionsId"
|
|
5281
|
+
[attr.aria-pressed]="mode() === 'keyboard' && active() === i ? 'true' : 'false'"
|
|
5282
|
+
[attr.data-sortable-handle]="i"
|
|
5283
|
+
[disabled]="disabled() || isItemDisabled(item)"
|
|
5284
|
+
(keydown)="onHandleKeydown($event, i)"
|
|
5285
|
+
(pointerdown)="onPointerDown($event, i)"
|
|
5286
|
+
(pointermove)="onPointerMove($event)"
|
|
5287
|
+
(pointerup)="onPointerUp($event)"
|
|
5288
|
+
(pointercancel)="onPointerCancel()"
|
|
5289
|
+
>
|
|
5290
|
+
@if (handleTpl()) {
|
|
5291
|
+
<ng-container
|
|
5292
|
+
[ngTemplateOutlet]="handleTpl()!.template"
|
|
5293
|
+
[ngTemplateOutletContext]="context(item, i)"
|
|
5294
|
+
/>
|
|
5295
|
+
} @else {
|
|
5296
|
+
<sds-icon name="grip-vertical" [size]="18" />
|
|
5297
|
+
}
|
|
5298
|
+
</button>
|
|
5299
|
+
|
|
5300
|
+
<div class="min-w-0 flex-1">
|
|
5301
|
+
@if (itemTpl()) {
|
|
5302
|
+
<ng-container
|
|
5303
|
+
[ngTemplateOutlet]="itemTpl()!.template"
|
|
5304
|
+
[ngTemplateOutletContext]="context(item, i)"
|
|
5305
|
+
/>
|
|
5306
|
+
} @else {
|
|
5307
|
+
<span class="text-sm text-ink">{{ labelOf(item) }}</span>
|
|
5308
|
+
}
|
|
5309
|
+
</div>
|
|
5310
|
+
</li>
|
|
5311
|
+
}
|
|
5312
|
+
</ul>
|
|
5313
|
+
|
|
5314
|
+
<p [id]="instructionsId" class="sr-only">{{ instructions() }}</p>
|
|
5315
|
+
<div class="sr-only" aria-live="assertive" aria-atomic="true">{{ liveMessage() }}</div>
|
|
5316
|
+
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5317
|
+
}
|
|
5318
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSortableList, decorators: [{
|
|
5319
|
+
type: Component,
|
|
5320
|
+
args: [{
|
|
5321
|
+
selector: 'sds-sortable-list',
|
|
5322
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5323
|
+
imports: [NgTemplateOutlet, SdsIcon],
|
|
5324
|
+
host: { class: 'block' },
|
|
5325
|
+
template: `
|
|
5326
|
+
<ul
|
|
5327
|
+
role="list"
|
|
5328
|
+
[attr.aria-label]="ariaLabel() || null"
|
|
5329
|
+
class="flex flex-col"
|
|
5330
|
+
[class.gap-2]="appearance() === 'card'"
|
|
5331
|
+
[class.divide-y]="appearance() === 'plain'"
|
|
5332
|
+
[class.divide-border]="appearance() === 'plain'"
|
|
5333
|
+
(focusout)="onFocusOut($event)"
|
|
5334
|
+
>
|
|
5335
|
+
@for (item of working(); track trackKey(item, $index); let i = $index) {
|
|
5336
|
+
<li
|
|
5337
|
+
[attr.data-sortable-index]="i"
|
|
5338
|
+
[class]="rowClass(i)"
|
|
5339
|
+
[style.transform]="transformFor(i)"
|
|
5340
|
+
>
|
|
5341
|
+
<button
|
|
5342
|
+
type="button"
|
|
5343
|
+
[class]="handleClass(item)"
|
|
5344
|
+
[attr.aria-label]="handleText(item)"
|
|
5345
|
+
[attr.aria-describedby]="instructionsId"
|
|
5346
|
+
[attr.aria-pressed]="mode() === 'keyboard' && active() === i ? 'true' : 'false'"
|
|
5347
|
+
[attr.data-sortable-handle]="i"
|
|
5348
|
+
[disabled]="disabled() || isItemDisabled(item)"
|
|
5349
|
+
(keydown)="onHandleKeydown($event, i)"
|
|
5350
|
+
(pointerdown)="onPointerDown($event, i)"
|
|
5351
|
+
(pointermove)="onPointerMove($event)"
|
|
5352
|
+
(pointerup)="onPointerUp($event)"
|
|
5353
|
+
(pointercancel)="onPointerCancel()"
|
|
5354
|
+
>
|
|
5355
|
+
@if (handleTpl()) {
|
|
5356
|
+
<ng-container
|
|
5357
|
+
[ngTemplateOutlet]="handleTpl()!.template"
|
|
5358
|
+
[ngTemplateOutletContext]="context(item, i)"
|
|
5359
|
+
/>
|
|
5360
|
+
} @else {
|
|
5361
|
+
<sds-icon name="grip-vertical" [size]="18" />
|
|
5362
|
+
}
|
|
5363
|
+
</button>
|
|
5364
|
+
|
|
5365
|
+
<div class="min-w-0 flex-1">
|
|
5366
|
+
@if (itemTpl()) {
|
|
5367
|
+
<ng-container
|
|
5368
|
+
[ngTemplateOutlet]="itemTpl()!.template"
|
|
5369
|
+
[ngTemplateOutletContext]="context(item, i)"
|
|
5370
|
+
/>
|
|
5371
|
+
} @else {
|
|
5372
|
+
<span class="text-sm text-ink">{{ labelOf(item) }}</span>
|
|
5373
|
+
}
|
|
5374
|
+
</div>
|
|
5375
|
+
</li>
|
|
5376
|
+
}
|
|
5377
|
+
</ul>
|
|
5378
|
+
|
|
5379
|
+
<p [id]="instructionsId" class="sr-only">{{ instructions() }}</p>
|
|
5380
|
+
<div class="sr-only" aria-live="assertive" aria-atomic="true">{{ liveMessage() }}</div>
|
|
5381
|
+
`,
|
|
5382
|
+
}]
|
|
5383
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }, { type: i0.Output, args: ["itemsChange"] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], labelField: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelField", required: false }] }], itemLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemLabel", required: false }] }], itemDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemDisabled", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], handleLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "handleLabel", required: false }] }], instructions: [{ type: i0.Input, args: [{ isSignal: true, alias: "instructions", required: false }] }], pickedUpMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickedUpMessage", required: false }] }], movedMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "movedMessage", required: false }] }], droppedMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "droppedMessage", required: false }] }], cancelledMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "cancelledMessage", required: false }] }], reorder: [{ type: i0.Output, args: ["reorder"] }], itemTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSortableItem), { isSignal: true }] }], handleTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSortableHandle), { isSignal: true }] }] } });
|
|
5384
|
+
|
|
5385
|
+
let nextId$9 = 0;
|
|
5386
|
+
/** "1.5 MB" style size (decimal units, like file managers show them), for messages. */
|
|
5387
|
+
function sdsFormatBytes(bytes) {
|
|
5388
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
5389
|
+
return '';
|
|
5390
|
+
if (bytes < 1000)
|
|
5391
|
+
return `${bytes} B`;
|
|
5392
|
+
const units = ['kB', 'MB', 'GB'];
|
|
5393
|
+
let v = bytes / 1000;
|
|
5394
|
+
let u = 0;
|
|
5395
|
+
while (v >= 1000 && u < units.length - 1) {
|
|
5396
|
+
v /= 1000;
|
|
5397
|
+
u++;
|
|
5398
|
+
}
|
|
5399
|
+
return `${v >= 10 || Number.isInteger(v) ? Math.round(v) : v.toFixed(1)} ${units[u]}`;
|
|
5400
|
+
}
|
|
5401
|
+
/** Does `file` match an `accept` string (".jpg,image/png,image/*")? Empty accepts everything. */
|
|
5402
|
+
function sdsFileMatchesAccept(file, accept) {
|
|
5403
|
+
const rules = accept
|
|
5404
|
+
.split(',')
|
|
5405
|
+
.map((r) => r.trim().toLowerCase())
|
|
5406
|
+
.filter(Boolean);
|
|
5407
|
+
if (!rules.length)
|
|
5408
|
+
return true;
|
|
5409
|
+
const name = file.name.toLowerCase();
|
|
5410
|
+
const type = (file.type || '').toLowerCase();
|
|
5411
|
+
return rules.some((rule) => {
|
|
5412
|
+
if (rule.startsWith('.'))
|
|
5413
|
+
return name.endsWith(rule);
|
|
5414
|
+
if (rule.endsWith('/*'))
|
|
5415
|
+
return type.startsWith(rule.slice(0, -1));
|
|
5416
|
+
return type === rule;
|
|
5417
|
+
});
|
|
5418
|
+
}
|
|
5419
|
+
/**
|
|
5420
|
+
* Drop zone plus a "Browse" button for picking files. Filters by [accept] (same syntax as the native
|
|
5421
|
+
* input: `.csv`, `image/*`, `image/png`), [maxSize] (bytes) and [maxFiles]; emits the accepted files as
|
|
5422
|
+
* `(files)` and the rest as `(rejected)` with a reason and a readable message, which is also shown
|
|
5423
|
+
* under the zone and announced.
|
|
5424
|
+
*
|
|
5425
|
+
* Keyboard and screen-reader users use the native Browse button (the zone itself is a pointer
|
|
5426
|
+
* affordance); clicking anywhere on the zone opens the picker too. All text is set through inputs with
|
|
5427
|
+
* English defaults.
|
|
5428
|
+
*
|
|
5429
|
+
* @example
|
|
5430
|
+
* <sds-file-drop accept="image/jpeg,image/png,image/webp" multiple [maxSize]="10_000_000"
|
|
5431
|
+
* label="Drop photos here" browseLabel="Choose photos"
|
|
5432
|
+
* (files)="upload($event)" (rejected)="onRejected($event)" />
|
|
5433
|
+
*/
|
|
5434
|
+
class SdsFileDrop {
|
|
5435
|
+
zoneId = `sds-file-drop-${nextId$9++}`;
|
|
5436
|
+
fileInput = viewChild('fileInput', ...(ngDevMode ? [{ debugName: "fileInput" }] : []));
|
|
5437
|
+
/** Accepted types, native `accept` syntax. Empty = anything. */
|
|
5438
|
+
accept = input('', ...(ngDevMode ? [{ debugName: "accept" }] : []));
|
|
5439
|
+
multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
5440
|
+
/** Largest accepted file in bytes. Null = no limit. */
|
|
5441
|
+
maxSize = input(null, ...(ngDevMode ? [{ debugName: "maxSize" }] : []));
|
|
5442
|
+
/** Most files accepted in one pick/drop. Null = no limit (1 when not [multiple]). */
|
|
5443
|
+
maxFiles = input(null, ...(ngDevMode ? [{ debugName: "maxFiles" }] : []));
|
|
5444
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
5445
|
+
/** Icon in the zone at rest. */
|
|
5446
|
+
icon = input('upload', ...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
5447
|
+
label = input('Drag and drop files here', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
5448
|
+
/** Shown while files are dragged over the zone. */
|
|
5449
|
+
dropLabel = input('Drop to add', ...(ngDevMode ? [{ debugName: "dropLabel" }] : []));
|
|
5450
|
+
browseLabel = input('Browse files', ...(ngDevMode ? [{ debugName: "browseLabel" }] : []));
|
|
5451
|
+
/** Extra line under the label, e.g. "JPEG or PNG, up to 10 MB". */
|
|
5452
|
+
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
5453
|
+
/** Tokens: `{name}`. */
|
|
5454
|
+
wrongTypeMessage = input('{name}: this file type is not accepted.', ...(ngDevMode ? [{ debugName: "wrongTypeMessage" }] : []));
|
|
5455
|
+
/** Tokens: `{name}`, `{max}` (formatted size). */
|
|
5456
|
+
tooLargeMessage = input('{name}: larger than {max}.', ...(ngDevMode ? [{ debugName: "tooLargeMessage" }] : []));
|
|
5457
|
+
/** Tokens: `{name}`, `{max}` (file count). */
|
|
5458
|
+
tooManyMessage = input('{name}: only {max} file(s) can be added at once.', ...(ngDevMode ? [{ debugName: "tooManyMessage" }] : []));
|
|
5459
|
+
/** Files that passed every check, in pick/drop order. */
|
|
5460
|
+
files = output();
|
|
5461
|
+
/** Files that were turned away, with the reason. */
|
|
5462
|
+
rejected = output();
|
|
5463
|
+
dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : []));
|
|
5464
|
+
errors = signal([], ...(ngDevMode ? [{ debugName: "errors" }] : []));
|
|
5465
|
+
dragDepth = 0;
|
|
5466
|
+
describedBy = computed(() => {
|
|
5467
|
+
const ids = [`${this.zoneId}-label`];
|
|
5468
|
+
if (this.hint())
|
|
5469
|
+
ids.push(`${this.zoneId}-hint`);
|
|
5470
|
+
if (this.errors().length)
|
|
5471
|
+
ids.push(`${this.zoneId}-errors`);
|
|
5472
|
+
return ids.join(' ');
|
|
5473
|
+
}, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
|
|
5474
|
+
zoneClass = computed(() => {
|
|
5475
|
+
const base = 'flex flex-col items-center justify-center gap-2 rounded-card border-2 border-dashed px-6 py-8 ' +
|
|
5476
|
+
'text-center transition-colors';
|
|
5477
|
+
if (this.disabled())
|
|
5478
|
+
return `${base} cursor-not-allowed border-border bg-muted opacity-60`;
|
|
5479
|
+
return this.dragging()
|
|
5480
|
+
? `${base} cursor-copy border-brand-500 bg-brand-50 dark:bg-muted`
|
|
5481
|
+
: `${base} cursor-pointer border-border bg-muted/50 hover:border-ink-faint`;
|
|
5482
|
+
}, ...(ngDevMode ? [{ debugName: "zoneClass" }] : []));
|
|
5483
|
+
/** Open the native file picker. */
|
|
5484
|
+
browse(event) {
|
|
5485
|
+
event?.stopPropagation();
|
|
5486
|
+
if (this.disabled())
|
|
5487
|
+
return;
|
|
5488
|
+
this.fileInput()?.nativeElement.click();
|
|
5489
|
+
}
|
|
5490
|
+
onZoneClick(event) {
|
|
5491
|
+
if (event.target.closest('button, input'))
|
|
5492
|
+
return;
|
|
5493
|
+
this.browse();
|
|
5494
|
+
}
|
|
5495
|
+
onDragEnter(event) {
|
|
5496
|
+
if (this.disabled() || !this.hasFiles(event))
|
|
5497
|
+
return;
|
|
5498
|
+
event.preventDefault();
|
|
5499
|
+
this.dragDepth++;
|
|
5500
|
+
this.dragging.set(true);
|
|
5501
|
+
}
|
|
5502
|
+
onDragOver(event) {
|
|
5503
|
+
if (this.disabled() || !this.hasFiles(event))
|
|
5504
|
+
return;
|
|
5505
|
+
event.preventDefault();
|
|
5506
|
+
if (event.dataTransfer)
|
|
5507
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
5508
|
+
}
|
|
5509
|
+
onDragLeave(event) {
|
|
5510
|
+
if (this.disabled() || !this.hasFiles(event))
|
|
5511
|
+
return;
|
|
5512
|
+
this.dragDepth = Math.max(0, this.dragDepth - 1);
|
|
5513
|
+
if (this.dragDepth === 0)
|
|
5514
|
+
this.dragging.set(false);
|
|
5515
|
+
}
|
|
5516
|
+
onDrop(event) {
|
|
5517
|
+
if (this.disabled())
|
|
5518
|
+
return;
|
|
5519
|
+
event.preventDefault();
|
|
5520
|
+
this.dragDepth = 0;
|
|
5521
|
+
this.dragging.set(false);
|
|
5522
|
+
this.handle(Array.from(event.dataTransfer?.files ?? []));
|
|
5523
|
+
}
|
|
5524
|
+
onInputChange(event) {
|
|
5525
|
+
const el = event.target;
|
|
5526
|
+
this.handle(Array.from(el.files ?? []));
|
|
5527
|
+
// Reset so picking the same file again still fires `change`.
|
|
5528
|
+
el.value = '';
|
|
5529
|
+
}
|
|
5530
|
+
hasFiles(event) {
|
|
5531
|
+
return Array.from(event.dataTransfer?.types ?? []).includes('Files');
|
|
5532
|
+
}
|
|
5533
|
+
handle(list) {
|
|
5534
|
+
if (!list.length)
|
|
5535
|
+
return;
|
|
5536
|
+
const accepted = [];
|
|
5537
|
+
const rejected = [];
|
|
5538
|
+
const max = this.maxFiles() ?? (this.multiple() ? null : 1);
|
|
5539
|
+
const maxSize = this.maxSize();
|
|
5540
|
+
for (const file of list) {
|
|
5541
|
+
if (!sdsFileMatchesAccept(file, this.accept())) {
|
|
5542
|
+
rejected.push({ file, reason: 'type', message: this.render(this.wrongTypeMessage(), file) });
|
|
5543
|
+
}
|
|
5544
|
+
else if (maxSize !== null && file.size > maxSize) {
|
|
5545
|
+
rejected.push({
|
|
5546
|
+
file,
|
|
5547
|
+
reason: 'size',
|
|
5548
|
+
message: this.render(this.tooLargeMessage(), file, sdsFormatBytes(maxSize)),
|
|
5549
|
+
});
|
|
5550
|
+
}
|
|
5551
|
+
else if (max !== null && accepted.length >= max) {
|
|
5552
|
+
rejected.push({
|
|
5553
|
+
file,
|
|
5554
|
+
reason: 'count',
|
|
5555
|
+
message: this.render(this.tooManyMessage(), file, String(max)),
|
|
5556
|
+
});
|
|
5557
|
+
}
|
|
5558
|
+
else {
|
|
5559
|
+
accepted.push(file);
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
this.errors.set(rejected.map((r) => r.message));
|
|
5563
|
+
if (accepted.length)
|
|
5564
|
+
this.files.emit(accepted);
|
|
5565
|
+
if (rejected.length)
|
|
5566
|
+
this.rejected.emit(rejected);
|
|
5567
|
+
}
|
|
5568
|
+
render(template, file, max = '') {
|
|
5569
|
+
return template.replace('{name}', file.name).replace('{max}', max);
|
|
5570
|
+
}
|
|
5571
|
+
/** Clear the error messages under the zone. */
|
|
5572
|
+
clearErrors() {
|
|
5573
|
+
this.errors.set([]);
|
|
5574
|
+
}
|
|
5575
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsFileDrop, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5576
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsFileDrop, isStandalone: true, selector: "sds-file-drop", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, dropLabel: { classPropertyName: "dropLabel", publicName: "dropLabel", isSignal: true, isRequired: false, transformFunction: null }, browseLabel: { classPropertyName: "browseLabel", publicName: "browseLabel", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, wrongTypeMessage: { classPropertyName: "wrongTypeMessage", publicName: "wrongTypeMessage", isSignal: true, isRequired: false, transformFunction: null }, tooLargeMessage: { classPropertyName: "tooLargeMessage", publicName: "tooLargeMessage", isSignal: true, isRequired: false, transformFunction: null }, tooManyMessage: { classPropertyName: "tooManyMessage", publicName: "tooManyMessage", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { files: "files", rejected: "rejected" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
5577
|
+
<div
|
|
5578
|
+
[class]="zoneClass()"
|
|
5579
|
+
[attr.aria-disabled]="disabled() ? 'true' : null"
|
|
5580
|
+
[attr.data-dragging]="dragging() ? 'true' : null"
|
|
5581
|
+
(click)="onZoneClick($event)"
|
|
5582
|
+
(dragenter)="onDragEnter($event)"
|
|
5583
|
+
(dragover)="onDragOver($event)"
|
|
5584
|
+
(dragleave)="onDragLeave($event)"
|
|
5585
|
+
(drop)="onDrop($event)"
|
|
5586
|
+
>
|
|
5587
|
+
<span
|
|
5588
|
+
class="grid h-11 w-11 place-items-center rounded-full bg-surface text-ink-soft shadow-sm ring-1 ring-border"
|
|
5589
|
+
aria-hidden="true"
|
|
5590
|
+
>
|
|
5591
|
+
<sds-icon [name]="dragging() ? 'upload' : icon()" [size]="22" />
|
|
5592
|
+
</span>
|
|
5593
|
+
<p class="text-sm font-semibold text-ink" [id]="zoneId + '-label'">
|
|
5594
|
+
{{ dragging() ? dropLabel() : label() }}
|
|
5595
|
+
</p>
|
|
5596
|
+
@if (hint()) {
|
|
5597
|
+
<p class="text-xs text-ink-faint" [id]="zoneId + '-hint'">{{ hint() }}</p>
|
|
5598
|
+
}
|
|
5599
|
+
<button
|
|
5600
|
+
type="button"
|
|
5601
|
+
class="mt-1 inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3.5 text-sm font-semibold text-ink shadow-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-60"
|
|
5602
|
+
[disabled]="disabled()"
|
|
5603
|
+
[attr.aria-describedby]="describedBy()"
|
|
5604
|
+
(click)="browse($event)"
|
|
5605
|
+
>
|
|
5606
|
+
<sds-icon name="upload" [size]="16" />
|
|
5607
|
+
{{ browseLabel() }}
|
|
5608
|
+
</button>
|
|
5609
|
+
<input
|
|
5610
|
+
#fileInput
|
|
5611
|
+
type="file"
|
|
5612
|
+
class="sr-only"
|
|
5613
|
+
tabindex="-1"
|
|
5614
|
+
aria-hidden="true"
|
|
5615
|
+
[accept]="accept()"
|
|
5616
|
+
[multiple]="multiple()"
|
|
5617
|
+
[disabled]="disabled()"
|
|
5618
|
+
(change)="onInputChange($event)"
|
|
5619
|
+
/>
|
|
5620
|
+
</div>
|
|
5621
|
+
|
|
5622
|
+
<div aria-live="polite" aria-atomic="true">
|
|
5623
|
+
@if (errors().length) {
|
|
5624
|
+
<ul [id]="zoneId + '-errors'" class="mt-2 space-y-1">
|
|
5625
|
+
@for (err of errors(); track $index) {
|
|
5626
|
+
<li class="flex items-start gap-1.5 text-xs font-medium text-danger">
|
|
5627
|
+
<sds-icon name="alert-circle" [size]="14" class="mt-px shrink-0" />
|
|
5628
|
+
<span>{{ err }}</span>
|
|
5629
|
+
</li>
|
|
5630
|
+
}
|
|
5631
|
+
</ul>
|
|
5632
|
+
}
|
|
5633
|
+
</div>
|
|
5634
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5635
|
+
}
|
|
5636
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsFileDrop, decorators: [{
|
|
5637
|
+
type: Component,
|
|
5638
|
+
args: [{
|
|
5639
|
+
selector: 'sds-file-drop',
|
|
5640
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
5641
|
+
imports: [SdsIcon],
|
|
5642
|
+
host: { class: 'block' },
|
|
5643
|
+
template: `
|
|
5644
|
+
<div
|
|
5645
|
+
[class]="zoneClass()"
|
|
5646
|
+
[attr.aria-disabled]="disabled() ? 'true' : null"
|
|
5647
|
+
[attr.data-dragging]="dragging() ? 'true' : null"
|
|
5648
|
+
(click)="onZoneClick($event)"
|
|
5649
|
+
(dragenter)="onDragEnter($event)"
|
|
5650
|
+
(dragover)="onDragOver($event)"
|
|
5651
|
+
(dragleave)="onDragLeave($event)"
|
|
5652
|
+
(drop)="onDrop($event)"
|
|
5653
|
+
>
|
|
5654
|
+
<span
|
|
5655
|
+
class="grid h-11 w-11 place-items-center rounded-full bg-surface text-ink-soft shadow-sm ring-1 ring-border"
|
|
5656
|
+
aria-hidden="true"
|
|
5657
|
+
>
|
|
5658
|
+
<sds-icon [name]="dragging() ? 'upload' : icon()" [size]="22" />
|
|
5659
|
+
</span>
|
|
5660
|
+
<p class="text-sm font-semibold text-ink" [id]="zoneId + '-label'">
|
|
5661
|
+
{{ dragging() ? dropLabel() : label() }}
|
|
5662
|
+
</p>
|
|
5663
|
+
@if (hint()) {
|
|
5664
|
+
<p class="text-xs text-ink-faint" [id]="zoneId + '-hint'">{{ hint() }}</p>
|
|
5665
|
+
}
|
|
5666
|
+
<button
|
|
5667
|
+
type="button"
|
|
5668
|
+
class="mt-1 inline-flex h-9 items-center gap-1.5 rounded-control border border-border bg-surface px-3.5 text-sm font-semibold text-ink shadow-sm transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:cursor-not-allowed disabled:opacity-60"
|
|
5669
|
+
[disabled]="disabled()"
|
|
5670
|
+
[attr.aria-describedby]="describedBy()"
|
|
5671
|
+
(click)="browse($event)"
|
|
5672
|
+
>
|
|
5673
|
+
<sds-icon name="upload" [size]="16" />
|
|
5674
|
+
{{ browseLabel() }}
|
|
5675
|
+
</button>
|
|
5676
|
+
<input
|
|
5677
|
+
#fileInput
|
|
5678
|
+
type="file"
|
|
5679
|
+
class="sr-only"
|
|
5680
|
+
tabindex="-1"
|
|
5681
|
+
aria-hidden="true"
|
|
5682
|
+
[accept]="accept()"
|
|
5683
|
+
[multiple]="multiple()"
|
|
5684
|
+
[disabled]="disabled()"
|
|
5685
|
+
(change)="onInputChange($event)"
|
|
5686
|
+
/>
|
|
5687
|
+
</div>
|
|
5688
|
+
|
|
5689
|
+
<div aria-live="polite" aria-atomic="true">
|
|
5690
|
+
@if (errors().length) {
|
|
5691
|
+
<ul [id]="zoneId + '-errors'" class="mt-2 space-y-1">
|
|
5692
|
+
@for (err of errors(); track $index) {
|
|
5693
|
+
<li class="flex items-start gap-1.5 text-xs font-medium text-danger">
|
|
5694
|
+
<sds-icon name="alert-circle" [size]="14" class="mt-px shrink-0" />
|
|
5695
|
+
<span>{{ err }}</span>
|
|
5696
|
+
</li>
|
|
5697
|
+
}
|
|
5698
|
+
</ul>
|
|
5699
|
+
}
|
|
5700
|
+
</div>
|
|
5701
|
+
`,
|
|
5702
|
+
}]
|
|
5703
|
+
}], propDecorators: { fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], dropLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dropLabel", required: false }] }], browseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "browseLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], wrongTypeMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrongTypeMessage", required: false }] }], tooLargeMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooLargeMessage", required: false }] }], tooManyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooManyMessage", required: false }] }], files: [{ type: i0.Output, args: ["files"] }], rejected: [{ type: i0.Output, args: ["rejected"] }] } });
|
|
5704
|
+
|
|
5705
|
+
let nextId$8 = 0;
|
|
5706
|
+
/**
|
|
5707
|
+
* Crops an image to a FIXED aspect ratio (e.g. the shop's card ratio): the frame is fixed and the image
|
|
5708
|
+
* moves and zooms behind it, always covering the frame, so every crop is valid.
|
|
5709
|
+
*
|
|
5710
|
+
* - Pointer: drag to move; wheel or pinch to zoom around the pointer; a zoom slider and ± buttons.
|
|
5711
|
+
* - Keyboard (focus the frame): arrow keys nudge the image (Shift = 10×), `+`/`-` zoom, `0` resets.
|
|
5712
|
+
* - `(cropChange)` emits the crop in the source image's natural pixels after every change;
|
|
5713
|
+
* {@link toBlob} renders it through a canvas (optionally resized) for upload.
|
|
5714
|
+
*
|
|
5715
|
+
* `src` takes a URL or a Blob/File (an object URL is managed for you). For a cross-origin URL the server
|
|
5716
|
+
* must send CORS headers and [crossOrigin] must be set, or the canvas is tainted and toBlob() rejects.
|
|
5717
|
+
*
|
|
5718
|
+
* @example
|
|
5719
|
+
* <sds-image-cropper [src]="file" [aspectRatio]="4 / 3" (cropChange)="crop = $event" #cropper />
|
|
5720
|
+
* <sds-button (click)="upload(await cropper.toBlob({ width: 1600 }))">Save</sds-button>
|
|
5721
|
+
*/
|
|
5722
|
+
class SdsImageCropper {
|
|
5723
|
+
helpId = `sds-image-cropper-${nextId$8++}-help`;
|
|
5724
|
+
viewport = viewChild('viewport', ...(ngDevMode ? [{ debugName: "viewport" }] : []));
|
|
5725
|
+
/** Image to crop: a URL, or a Blob/File. */
|
|
5726
|
+
src = input(null, ...(ngDevMode ? [{ debugName: "src" }] : []));
|
|
5727
|
+
/** Frame width ÷ height, e.g. `1`, `4 / 3`, `16 / 9`. */
|
|
5728
|
+
aspectRatio = input(1, ...(ngDevMode ? [{ debugName: "aspectRatio", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
5729
|
+
/** Largest zoom factor relative to "cover". */
|
|
5730
|
+
maxZoom = input(4, ...(ngDevMode ? [{ debugName: "maxZoom", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
5731
|
+
/** Zoom change per button press / key press. */
|
|
5732
|
+
zoomStep = input(0.1, ...(ngDevMode ? [{ debugName: "zoomStep", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
5733
|
+
/** Visual mask only (the output is always the rectangle). */
|
|
5734
|
+
shape = input('rect', ...(ngDevMode ? [{ debugName: "shape" }] : []));
|
|
5735
|
+
/** `crossorigin` for URL sources, needed for toBlob() on a cross-origin image. */
|
|
5736
|
+
crossOrigin = input(null, ...(ngDevMode ? [{ debugName: "crossOrigin" }] : []));
|
|
5737
|
+
label = input('Crop area', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
5738
|
+
roleDescription = input('image cropper', ...(ngDevMode ? [{ debugName: "roleDescription" }] : []));
|
|
5739
|
+
instructions = input('Drag, or use the arrow keys, to move the image. Use plus and minus to zoom, 0 to reset.', ...(ngDevMode ? [{ debugName: "instructions" }] : []));
|
|
5740
|
+
zoomLabel = input('Zoom', ...(ngDevMode ? [{ debugName: "zoomLabel" }] : []));
|
|
5741
|
+
zoomInLabel = input('Zoom in', ...(ngDevMode ? [{ debugName: "zoomInLabel" }] : []));
|
|
5742
|
+
zoomOutLabel = input('Zoom out', ...(ngDevMode ? [{ debugName: "zoomOutLabel" }] : []));
|
|
5743
|
+
/** The crop, in natural pixels, after every change. */
|
|
5744
|
+
cropChange = output();
|
|
5745
|
+
/** The image loaded; natural size. */
|
|
5746
|
+
imageLoad = output();
|
|
5747
|
+
/** The image failed to load. */
|
|
5748
|
+
imageError = output();
|
|
5749
|
+
objectUrl = signal(null, ...(ngDevMode ? [{ debugName: "objectUrl" }] : []));
|
|
5750
|
+
ownedUrl = null;
|
|
5751
|
+
imgEl = null;
|
|
5752
|
+
loaded = signal(false, ...(ngDevMode ? [{ debugName: "loaded" }] : []));
|
|
5753
|
+
naturalW = signal(0, ...(ngDevMode ? [{ debugName: "naturalW" }] : []));
|
|
5754
|
+
naturalH = signal(0, ...(ngDevMode ? [{ debugName: "naturalH" }] : []));
|
|
5755
|
+
/** Frame size in CSS px (tracked with a ResizeObserver). */
|
|
5756
|
+
frameW = signal(0, ...(ngDevMode ? [{ debugName: "frameW" }] : []));
|
|
5757
|
+
frameH = signal(0, ...(ngDevMode ? [{ debugName: "frameH" }] : []));
|
|
5758
|
+
zoom = signal(1, ...(ngDevMode ? [{ debugName: "zoom" }] : []));
|
|
5759
|
+
tx = signal(0, ...(ngDevMode ? [{ debugName: "tx" }] : []));
|
|
5760
|
+
ty = signal(0, ...(ngDevMode ? [{ debugName: "ty" }] : []));
|
|
5761
|
+
dragging = signal(false, ...(ngDevMode ? [{ debugName: "dragging" }] : []));
|
|
5762
|
+
/** Scale that makes the image just cover the frame (zoom 1). */
|
|
5763
|
+
coverScale = computed(() => {
|
|
5764
|
+
const w = this.naturalW();
|
|
5765
|
+
const h = this.naturalH();
|
|
5766
|
+
if (!w || !h || !this.frameW() || !this.frameH())
|
|
5767
|
+
return 1;
|
|
5768
|
+
return Math.max(this.frameW() / w, this.frameH() / h);
|
|
5769
|
+
}, ...(ngDevMode ? [{ debugName: "coverScale" }] : []));
|
|
5770
|
+
scale = computed(() => this.coverScale() * this.zoom(), ...(ngDevMode ? [{ debugName: "scale" }] : []));
|
|
5771
|
+
displayW = computed(() => this.naturalW() * this.scale(), ...(ngDevMode ? [{ debugName: "displayW" }] : []));
|
|
5772
|
+
displayH = computed(() => this.naturalH() * this.scale(), ...(ngDevMode ? [{ debugName: "displayH" }] : []));
|
|
5773
|
+
zoomText = computed(() => `${Math.round(this.zoom() * 100)}%`, ...(ngDevMode ? [{ debugName: "zoomText" }] : []));
|
|
5774
|
+
/** The current crop in natural pixels. */
|
|
5775
|
+
crop = computed(() => {
|
|
5776
|
+
const s = this.scale();
|
|
5777
|
+
if (!this.loaded() || !s)
|
|
5778
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
5779
|
+
const x = -this.tx() / s;
|
|
5780
|
+
const y = -this.ty() / s;
|
|
5781
|
+
const width = Math.min(this.naturalW(), this.frameW() / s);
|
|
5782
|
+
const height = Math.min(this.naturalH(), this.frameH() / s);
|
|
5783
|
+
return {
|
|
5784
|
+
x: Math.max(0, Math.round(x)),
|
|
5785
|
+
y: Math.max(0, Math.round(y)),
|
|
5786
|
+
width: Math.round(width),
|
|
5787
|
+
height: Math.round(height),
|
|
5788
|
+
};
|
|
5789
|
+
}, ...(ngDevMode ? [{ debugName: "crop" }] : []));
|
|
5790
|
+
resizeObserver = null;
|
|
5791
|
+
pointers = new Map();
|
|
5792
|
+
lastPinch = 0;
|
|
5793
|
+
constructor() {
|
|
5794
|
+
// Source → object URL (for Blobs), reset state.
|
|
5795
|
+
effect(() => {
|
|
5796
|
+
const src = this.src();
|
|
5797
|
+
untracked(() => {
|
|
5798
|
+
this.revoke();
|
|
5799
|
+
this.loaded.set(false);
|
|
5800
|
+
if (src instanceof Blob) {
|
|
5801
|
+
this.ownedUrl = URL.createObjectURL(src);
|
|
5802
|
+
this.objectUrl.set(this.ownedUrl);
|
|
5803
|
+
}
|
|
5804
|
+
else {
|
|
5805
|
+
this.objectUrl.set(src || null);
|
|
5806
|
+
}
|
|
5807
|
+
});
|
|
5808
|
+
});
|
|
5809
|
+
// Track the frame size; keep the image covering it (and the crop centre stable) on resize.
|
|
5810
|
+
effect((onCleanup) => {
|
|
5811
|
+
const el = this.viewport()?.nativeElement;
|
|
5812
|
+
if (!el || typeof ResizeObserver === 'undefined')
|
|
5813
|
+
return;
|
|
5814
|
+
const ro = new ResizeObserver(() => this.measureFrame());
|
|
5815
|
+
ro.observe(el);
|
|
5816
|
+
this.resizeObserver = ro;
|
|
5817
|
+
onCleanup(() => ro.disconnect());
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5820
|
+
ngOnDestroy() {
|
|
5821
|
+
this.resizeObserver?.disconnect();
|
|
5822
|
+
this.revoke();
|
|
5823
|
+
}
|
|
5824
|
+
// --- loading -------------------------------------------------------------------
|
|
5825
|
+
onLoad(event) {
|
|
5826
|
+
const img = event.target;
|
|
5827
|
+
this.imgEl = img;
|
|
5828
|
+
this.naturalW.set(img.naturalWidth);
|
|
5829
|
+
this.naturalH.set(img.naturalHeight);
|
|
5830
|
+
this.measureFrame();
|
|
5831
|
+
this.loaded.set(true);
|
|
5832
|
+
this.reset();
|
|
5833
|
+
this.imageLoad.emit({ width: img.naturalWidth, height: img.naturalHeight });
|
|
5834
|
+
}
|
|
5835
|
+
onError() {
|
|
5836
|
+
this.loaded.set(false);
|
|
5837
|
+
this.imageError.emit();
|
|
5838
|
+
}
|
|
5839
|
+
revoke() {
|
|
5840
|
+
if (this.ownedUrl)
|
|
5841
|
+
URL.revokeObjectURL(this.ownedUrl);
|
|
5842
|
+
this.ownedUrl = null;
|
|
5843
|
+
}
|
|
5844
|
+
measureFrame() {
|
|
5845
|
+
const el = this.viewport()?.nativeElement;
|
|
5846
|
+
if (!el)
|
|
5847
|
+
return;
|
|
5848
|
+
const prevW = this.frameW();
|
|
5849
|
+
const w = el.clientWidth;
|
|
5850
|
+
const h = el.clientHeight || w / (this.aspectRatio() || 1);
|
|
5851
|
+
if (w === prevW && h === this.frameH())
|
|
5852
|
+
return;
|
|
5853
|
+
// Keep the same point of the image under the frame centre across a resize.
|
|
5854
|
+
const s = this.scale();
|
|
5855
|
+
const cx = s ? (this.frameW() / 2 - this.tx()) / s : 0;
|
|
5856
|
+
const cy = s ? (this.frameH() / 2 - this.ty()) / s : 0;
|
|
5857
|
+
this.frameW.set(w);
|
|
5858
|
+
this.frameH.set(h);
|
|
5859
|
+
if (this.loaded() && prevW) {
|
|
5860
|
+
const ns = this.scale();
|
|
5861
|
+
this.setOffset(w / 2 - cx * ns, h / 2 - cy * ns);
|
|
5862
|
+
this.emit();
|
|
5863
|
+
}
|
|
5864
|
+
}
|
|
5865
|
+
// --- geometry ------------------------------------------------------------------
|
|
5866
|
+
/** Centre the image at zoom 1. */
|
|
5867
|
+
reset() {
|
|
5868
|
+
this.zoom.set(1);
|
|
5869
|
+
this.setOffset((this.frameW() - this.displayW()) / 2, (this.frameH() - this.displayH()) / 2);
|
|
5870
|
+
this.emit();
|
|
5871
|
+
}
|
|
5872
|
+
setOffset(x, y) {
|
|
5873
|
+
const minX = Math.min(0, this.frameW() - this.displayW());
|
|
5874
|
+
const minY = Math.min(0, this.frameH() - this.displayH());
|
|
5875
|
+
this.tx.set(Math.min(0, Math.max(minX, x)));
|
|
5876
|
+
this.ty.set(Math.min(0, Math.max(minY, y)));
|
|
5877
|
+
}
|
|
5878
|
+
/** Zoom to `next`, keeping the frame point (px, py) fixed (defaults to the centre). */
|
|
5879
|
+
zoomTo(next, px = this.frameW() / 2, py = this.frameH() / 2) {
|
|
5880
|
+
const z = Math.min(this.maxZoom(), Math.max(1, next));
|
|
5881
|
+
const s = this.scale();
|
|
5882
|
+
const ix = (px - this.tx()) / s;
|
|
5883
|
+
const iy = (py - this.ty()) / s;
|
|
5884
|
+
this.zoom.set(z);
|
|
5885
|
+
const ns = this.scale();
|
|
5886
|
+
this.setOffset(px - ix * ns, py - iy * ns);
|
|
5887
|
+
this.emit();
|
|
5888
|
+
}
|
|
5889
|
+
/** Zoom in (+) or out (−) by `delta`. */
|
|
5890
|
+
zoomBy(delta) {
|
|
5891
|
+
if (!this.loaded())
|
|
5892
|
+
return;
|
|
5893
|
+
this.zoomTo(this.zoom() + delta);
|
|
5894
|
+
}
|
|
5895
|
+
emit() {
|
|
5896
|
+
if (this.loaded())
|
|
5897
|
+
this.cropChange.emit(this.crop());
|
|
5898
|
+
}
|
|
5899
|
+
// --- pointer ---------------------------------------------------------------------
|
|
5900
|
+
onPointerDown(event) {
|
|
5901
|
+
if (!this.loaded())
|
|
5902
|
+
return;
|
|
5903
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
5904
|
+
this.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
5905
|
+
this.dragging.set(true);
|
|
5906
|
+
if (this.pointers.size === 2)
|
|
5907
|
+
this.lastPinch = this.pinchDistance();
|
|
5908
|
+
}
|
|
5909
|
+
onPointerMove(event) {
|
|
5910
|
+
const prev = this.pointers.get(event.pointerId);
|
|
5911
|
+
if (!prev)
|
|
5912
|
+
return;
|
|
5913
|
+
const cur = { x: event.clientX, y: event.clientY };
|
|
5914
|
+
this.pointers.set(event.pointerId, cur);
|
|
5915
|
+
if (this.pointers.size >= 2) {
|
|
5916
|
+
const d = this.pinchDistance();
|
|
5917
|
+
if (this.lastPinch > 0) {
|
|
5918
|
+
const rect = this.viewport().nativeElement.getBoundingClientRect();
|
|
5919
|
+
const pts = [...this.pointers.values()];
|
|
5920
|
+
const mx = (pts[0].x + pts[1].x) / 2 - rect.left;
|
|
5921
|
+
const my = (pts[0].y + pts[1].y) / 2 - rect.top;
|
|
5922
|
+
this.zoomTo(this.zoom() * (d / this.lastPinch), mx, my);
|
|
5923
|
+
}
|
|
5924
|
+
this.lastPinch = d;
|
|
5925
|
+
return;
|
|
5926
|
+
}
|
|
5927
|
+
this.setOffset(this.tx() + (cur.x - prev.x), this.ty() + (cur.y - prev.y));
|
|
5928
|
+
}
|
|
5929
|
+
onPointerUp(event) {
|
|
5930
|
+
if (!this.pointers.delete(event.pointerId))
|
|
5931
|
+
return;
|
|
5932
|
+
if (this.pointers.size < 2)
|
|
5933
|
+
this.lastPinch = 0;
|
|
5934
|
+
if (this.pointers.size === 0) {
|
|
5935
|
+
this.dragging.set(false);
|
|
5936
|
+
this.emit();
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
pinchDistance() {
|
|
5940
|
+
const [a, b] = [...this.pointers.values()];
|
|
5941
|
+
return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : 0;
|
|
5942
|
+
}
|
|
5943
|
+
onWheel(event) {
|
|
5944
|
+
if (!this.loaded())
|
|
5945
|
+
return;
|
|
5946
|
+
event.preventDefault();
|
|
5947
|
+
const rect = this.viewport().nativeElement.getBoundingClientRect();
|
|
5948
|
+
const factor = Math.exp(-event.deltaY * 0.0015);
|
|
5949
|
+
this.zoomTo(this.zoom() * factor, event.clientX - rect.left, event.clientY - rect.top);
|
|
5950
|
+
}
|
|
5951
|
+
onSlider(event) {
|
|
5952
|
+
this.zoomTo(Number(event.target.value));
|
|
5953
|
+
}
|
|
5954
|
+
// --- keyboard ---------------------------------------------------------------------
|
|
5955
|
+
onKeydown(event) {
|
|
5956
|
+
if (!this.loaded())
|
|
5957
|
+
return;
|
|
5958
|
+
const step = event.shiftKey ? 10 : 1;
|
|
5959
|
+
let handled = true;
|
|
5960
|
+
switch (event.key) {
|
|
5961
|
+
// Arrow keys move the IMAGE in that direction under the fixed frame.
|
|
5962
|
+
case 'ArrowLeft':
|
|
5963
|
+
this.setOffset(this.tx() - step, this.ty());
|
|
5964
|
+
break;
|
|
5965
|
+
case 'ArrowRight':
|
|
5966
|
+
this.setOffset(this.tx() + step, this.ty());
|
|
5967
|
+
break;
|
|
5968
|
+
case 'ArrowUp':
|
|
5969
|
+
this.setOffset(this.tx(), this.ty() - step);
|
|
5970
|
+
break;
|
|
5971
|
+
case 'ArrowDown':
|
|
5972
|
+
this.setOffset(this.tx(), this.ty() + step);
|
|
5973
|
+
break;
|
|
5974
|
+
case '+':
|
|
5975
|
+
case '=':
|
|
5976
|
+
this.zoomBy(this.zoomStep());
|
|
5977
|
+
return event.preventDefault();
|
|
5978
|
+
case '-':
|
|
5979
|
+
case '_':
|
|
5980
|
+
this.zoomBy(-this.zoomStep());
|
|
5981
|
+
return event.preventDefault();
|
|
5982
|
+
case '0':
|
|
5983
|
+
this.reset();
|
|
5984
|
+
return event.preventDefault();
|
|
5985
|
+
default:
|
|
5986
|
+
handled = false;
|
|
5987
|
+
}
|
|
5988
|
+
if (handled) {
|
|
5989
|
+
event.preventDefault();
|
|
5990
|
+
this.emit();
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
// --- output -------------------------------------------------------------------------
|
|
5994
|
+
/**
|
|
5995
|
+
* Render the current crop to a canvas (natural resolution, or scaled to `width`). Returns null
|
|
5996
|
+
* before an image has loaded.
|
|
5997
|
+
*/
|
|
5998
|
+
toCanvas(width) {
|
|
5999
|
+
const img = this.imgEl;
|
|
6000
|
+
const c = this.crop();
|
|
6001
|
+
if (!img || !this.loaded() || !c.width || !c.height)
|
|
6002
|
+
return null;
|
|
6003
|
+
const outW = Math.max(1, Math.round(width ?? c.width));
|
|
6004
|
+
const outH = Math.max(1, Math.round(outW / (this.aspectRatio() || c.width / c.height)));
|
|
6005
|
+
const canvas = document.createElement('canvas');
|
|
6006
|
+
canvas.width = outW;
|
|
6007
|
+
canvas.height = outH;
|
|
6008
|
+
const ctx = canvas.getContext('2d');
|
|
6009
|
+
if (!ctx)
|
|
6010
|
+
return null;
|
|
6011
|
+
ctx.imageSmoothingQuality = 'high';
|
|
6012
|
+
ctx.drawImage(img, c.x, c.y, c.width, c.height, 0, 0, outW, outH);
|
|
6013
|
+
return canvas;
|
|
6014
|
+
}
|
|
6015
|
+
/** Encode the current crop as a Blob (JPEG 0.9 by default). */
|
|
6016
|
+
toBlob(options = {}) {
|
|
6017
|
+
const canvas = this.toCanvas(options.width);
|
|
6018
|
+
if (!canvas)
|
|
6019
|
+
return Promise.resolve(null);
|
|
6020
|
+
return new Promise((resolve, reject) => {
|
|
6021
|
+
try {
|
|
6022
|
+
canvas.toBlob((b) => resolve(b), options.type ?? 'image/jpeg', options.quality ?? 0.9);
|
|
6023
|
+
}
|
|
6024
|
+
catch (e) {
|
|
6025
|
+
reject(e);
|
|
6026
|
+
}
|
|
6027
|
+
});
|
|
6028
|
+
}
|
|
6029
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsImageCropper, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6030
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsImageCropper, isStandalone: true, selector: "sds-image-cropper", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, zoomStep: { classPropertyName: "zoomStep", publicName: "zoomStep", isSignal: true, isRequired: false, transformFunction: null }, shape: { classPropertyName: "shape", publicName: "shape", isSignal: true, isRequired: false, transformFunction: null }, crossOrigin: { classPropertyName: "crossOrigin", publicName: "crossOrigin", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, roleDescription: { classPropertyName: "roleDescription", publicName: "roleDescription", isSignal: true, isRequired: false, transformFunction: null }, instructions: { classPropertyName: "instructions", publicName: "instructions", isSignal: true, isRequired: false, transformFunction: null }, zoomLabel: { classPropertyName: "zoomLabel", publicName: "zoomLabel", isSignal: true, isRequired: false, transformFunction: null }, zoomInLabel: { classPropertyName: "zoomInLabel", publicName: "zoomInLabel", isSignal: true, isRequired: false, transformFunction: null }, zoomOutLabel: { classPropertyName: "zoomOutLabel", publicName: "zoomOutLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropChange: "cropChange", imageLoad: "imageLoad", imageError: "imageError" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
6031
|
+
<div
|
|
6032
|
+
#viewport
|
|
6033
|
+
class="relative w-full touch-none select-none overflow-hidden rounded-card bg-ink/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
|
|
6034
|
+
[class.cursor-grab]="loaded() && !dragging()"
|
|
6035
|
+
[class.cursor-grabbing]="dragging()"
|
|
6036
|
+
[style.aspect-ratio]="aspectRatio()"
|
|
6037
|
+
role="application"
|
|
6038
|
+
[attr.aria-roledescription]="roleDescription()"
|
|
6039
|
+
[attr.aria-label]="label()"
|
|
6040
|
+
[attr.aria-describedby]="helpId"
|
|
6041
|
+
tabindex="0"
|
|
6042
|
+
(pointerdown)="onPointerDown($event)"
|
|
6043
|
+
(pointermove)="onPointerMove($event)"
|
|
6044
|
+
(pointerup)="onPointerUp($event)"
|
|
6045
|
+
(pointercancel)="onPointerUp($event)"
|
|
6046
|
+
(wheel)="onWheel($event)"
|
|
6047
|
+
(keydown)="onKeydown($event)"
|
|
6048
|
+
>
|
|
6049
|
+
@if (objectUrl()) {
|
|
6050
|
+
<img
|
|
6051
|
+
[src]="objectUrl()"
|
|
6052
|
+
[attr.crossorigin]="crossOrigin()"
|
|
6053
|
+
alt=""
|
|
6054
|
+
draggable="false"
|
|
6055
|
+
class="pointer-events-none absolute left-0 top-0 max-w-none origin-top-left"
|
|
6056
|
+
[class.invisible]="!loaded()"
|
|
6057
|
+
[style.width.px]="displayW()"
|
|
6058
|
+
[style.height.px]="displayH()"
|
|
6059
|
+
[style.transform]="'translate3d(' + tx() + 'px,' + ty() + 'px,0)'"
|
|
6060
|
+
(load)="onLoad($event)"
|
|
6061
|
+
(error)="onError()"
|
|
6062
|
+
/>
|
|
6063
|
+
}
|
|
6064
|
+
@if (shape() === 'round') {
|
|
6065
|
+
<div
|
|
6066
|
+
class="pointer-events-none absolute inset-0 rounded-full shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]"
|
|
6067
|
+
aria-hidden="true"
|
|
6068
|
+
></div>
|
|
6069
|
+
}
|
|
6070
|
+
<!-- Rule-of-thirds guides. -->
|
|
6071
|
+
<div class="pointer-events-none absolute inset-0" aria-hidden="true">
|
|
6072
|
+
<div class="absolute inset-y-0 left-1/3 w-px bg-white/30"></div>
|
|
6073
|
+
<div class="absolute inset-y-0 left-2/3 w-px bg-white/30"></div>
|
|
6074
|
+
<div class="absolute inset-x-0 top-1/3 h-px bg-white/30"></div>
|
|
6075
|
+
<div class="absolute inset-x-0 top-2/3 h-px bg-white/30"></div>
|
|
6076
|
+
</div>
|
|
6077
|
+
@if (!loaded()) {
|
|
6078
|
+
<div class="absolute inset-0 grid place-items-center text-white/70" aria-hidden="true">
|
|
6079
|
+
<sds-icon name="image" [size]="32" />
|
|
6080
|
+
</div>
|
|
6081
|
+
}
|
|
6082
|
+
</div>
|
|
6083
|
+
<p [id]="helpId" class="sr-only">{{ instructions() }}</p>
|
|
6084
|
+
|
|
6085
|
+
<div class="mt-3 flex items-center gap-2">
|
|
6086
|
+
<button
|
|
6087
|
+
type="button"
|
|
6088
|
+
class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
|
|
6089
|
+
[attr.aria-label]="zoomOutLabel()"
|
|
6090
|
+
[disabled]="!loaded() || zoom() <= 1"
|
|
6091
|
+
(click)="zoomBy(-zoomStep())"
|
|
3323
6092
|
>
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
6093
|
+
<sds-icon name="zoom-out" [size]="18" />
|
|
6094
|
+
</button>
|
|
6095
|
+
<input
|
|
6096
|
+
type="range"
|
|
6097
|
+
class="h-2 min-w-0 flex-1 cursor-pointer accent-brand-600"
|
|
6098
|
+
min="1"
|
|
6099
|
+
[max]="maxZoom()"
|
|
6100
|
+
step="0.01"
|
|
6101
|
+
[value]="zoom()"
|
|
6102
|
+
[disabled]="!loaded()"
|
|
6103
|
+
[attr.aria-label]="zoomLabel()"
|
|
6104
|
+
[attr.aria-valuetext]="zoomText()"
|
|
6105
|
+
(input)="onSlider($event)"
|
|
6106
|
+
/>
|
|
6107
|
+
<button
|
|
6108
|
+
type="button"
|
|
6109
|
+
class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
|
|
6110
|
+
[attr.aria-label]="zoomInLabel()"
|
|
6111
|
+
[disabled]="!loaded() || zoom() >= maxZoom()"
|
|
6112
|
+
(click)="zoomBy(zoomStep())"
|
|
6113
|
+
>
|
|
6114
|
+
<sds-icon name="zoom-in" [size]="18" />
|
|
6115
|
+
</button>
|
|
6116
|
+
</div>
|
|
3339
6117
|
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3340
6118
|
}
|
|
3341
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type:
|
|
6119
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsImageCropper, decorators: [{
|
|
3342
6120
|
type: Component,
|
|
3343
6121
|
args: [{
|
|
3344
|
-
selector: 'sds-
|
|
6122
|
+
selector: 'sds-image-cropper',
|
|
3345
6123
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3346
6124
|
imports: [SdsIcon],
|
|
3347
|
-
host: { class: '
|
|
6125
|
+
host: { class: 'block' },
|
|
3348
6126
|
template: `
|
|
3349
|
-
<
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
[class]="
|
|
3353
|
-
[
|
|
3354
|
-
[
|
|
3355
|
-
|
|
3356
|
-
|
|
6127
|
+
<div
|
|
6128
|
+
#viewport
|
|
6129
|
+
class="relative w-full touch-none select-none overflow-hidden rounded-card bg-ink/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
|
|
6130
|
+
[class.cursor-grab]="loaded() && !dragging()"
|
|
6131
|
+
[class.cursor-grabbing]="dragging()"
|
|
6132
|
+
[style.aspect-ratio]="aspectRatio()"
|
|
6133
|
+
role="application"
|
|
6134
|
+
[attr.aria-roledescription]="roleDescription()"
|
|
6135
|
+
[attr.aria-label]="label()"
|
|
6136
|
+
[attr.aria-describedby]="helpId"
|
|
6137
|
+
tabindex="0"
|
|
6138
|
+
(pointerdown)="onPointerDown($event)"
|
|
6139
|
+
(pointermove)="onPointerMove($event)"
|
|
6140
|
+
(pointerup)="onPointerUp($event)"
|
|
6141
|
+
(pointercancel)="onPointerUp($event)"
|
|
6142
|
+
(wheel)="onWheel($event)"
|
|
3357
6143
|
(keydown)="onKeydown($event)"
|
|
3358
6144
|
>
|
|
3359
|
-
@if (
|
|
3360
|
-
<
|
|
6145
|
+
@if (objectUrl()) {
|
|
6146
|
+
<img
|
|
6147
|
+
[src]="objectUrl()"
|
|
6148
|
+
[attr.crossorigin]="crossOrigin()"
|
|
6149
|
+
alt=""
|
|
6150
|
+
draggable="false"
|
|
6151
|
+
class="pointer-events-none absolute left-0 top-0 max-w-none origin-top-left"
|
|
6152
|
+
[class.invisible]="!loaded()"
|
|
6153
|
+
[style.width.px]="displayW()"
|
|
6154
|
+
[style.height.px]="displayH()"
|
|
6155
|
+
[style.transform]="'translate3d(' + tx() + 'px,' + ty() + 'px,0)'"
|
|
6156
|
+
(load)="onLoad($event)"
|
|
6157
|
+
(error)="onError()"
|
|
6158
|
+
/>
|
|
3361
6159
|
}
|
|
6160
|
+
@if (shape() === 'round') {
|
|
6161
|
+
<div
|
|
6162
|
+
class="pointer-events-none absolute inset-0 rounded-full shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]"
|
|
6163
|
+
aria-hidden="true"
|
|
6164
|
+
></div>
|
|
6165
|
+
}
|
|
6166
|
+
<!-- Rule-of-thirds guides. -->
|
|
6167
|
+
<div class="pointer-events-none absolute inset-0" aria-hidden="true">
|
|
6168
|
+
<div class="absolute inset-y-0 left-1/3 w-px bg-white/30"></div>
|
|
6169
|
+
<div class="absolute inset-y-0 left-2/3 w-px bg-white/30"></div>
|
|
6170
|
+
<div class="absolute inset-x-0 top-1/3 h-px bg-white/30"></div>
|
|
6171
|
+
<div class="absolute inset-x-0 top-2/3 h-px bg-white/30"></div>
|
|
6172
|
+
</div>
|
|
6173
|
+
@if (!loaded()) {
|
|
6174
|
+
<div class="absolute inset-0 grid place-items-center text-white/70" aria-hidden="true">
|
|
6175
|
+
<sds-icon name="image" [size]="32" />
|
|
6176
|
+
</div>
|
|
6177
|
+
}
|
|
6178
|
+
</div>
|
|
6179
|
+
<p [id]="helpId" class="sr-only">{{ instructions() }}</p>
|
|
3362
6180
|
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
@if (showState()) {
|
|
3371
|
-
<span
|
|
3372
|
-
class="truncate text-[11px] font-medium"
|
|
3373
|
-
[class]="captionClass()"
|
|
3374
|
-
aria-hidden="true"
|
|
3375
|
-
>{{ stateText() }}</span
|
|
3376
|
-
>
|
|
3377
|
-
}
|
|
3378
|
-
</span>
|
|
3379
|
-
<!-- The state is part of the accessible name ("Milk, may contain"), visible caption or not. -->
|
|
3380
|
-
<span class="sr-only">, {{ stateText() }}</span>
|
|
3381
|
-
|
|
3382
|
-
<!-- Non-colour mark: nothing / a check / a question mark. The slot keeps its width in every
|
|
3383
|
-
state so the chip never shifts when cycled. -->
|
|
3384
|
-
<span
|
|
3385
|
-
class="grid shrink-0 place-items-center"
|
|
3386
|
-
[class]="size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5'"
|
|
3387
|
-
aria-hidden="true"
|
|
6181
|
+
<div class="mt-3 flex items-center gap-2">
|
|
6182
|
+
<button
|
|
6183
|
+
type="button"
|
|
6184
|
+
class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
|
|
6185
|
+
[attr.aria-label]="zoomOutLabel()"
|
|
6186
|
+
[disabled]="!loaded() || zoom() <= 1"
|
|
6187
|
+
(click)="zoomBy(-zoomStep())"
|
|
3388
6188
|
>
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
6189
|
+
<sds-icon name="zoom-out" [size]="18" />
|
|
6190
|
+
</button>
|
|
6191
|
+
<input
|
|
6192
|
+
type="range"
|
|
6193
|
+
class="h-2 min-w-0 flex-1 cursor-pointer accent-brand-600"
|
|
6194
|
+
min="1"
|
|
6195
|
+
[max]="maxZoom()"
|
|
6196
|
+
step="0.01"
|
|
6197
|
+
[value]="zoom()"
|
|
6198
|
+
[disabled]="!loaded()"
|
|
6199
|
+
[attr.aria-label]="zoomLabel()"
|
|
6200
|
+
[attr.aria-valuetext]="zoomText()"
|
|
6201
|
+
(input)="onSlider($event)"
|
|
6202
|
+
/>
|
|
6203
|
+
<button
|
|
6204
|
+
type="button"
|
|
6205
|
+
class="grid h-9 w-9 shrink-0 place-items-center rounded-control text-ink-soft hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500 disabled:opacity-40"
|
|
6206
|
+
[attr.aria-label]="zoomInLabel()"
|
|
6207
|
+
[disabled]="!loaded() || zoom() >= maxZoom()"
|
|
6208
|
+
(click)="zoomBy(zoomStep())"
|
|
6209
|
+
>
|
|
6210
|
+
<sds-icon name="zoom-in" [size]="18" />
|
|
6211
|
+
</button>
|
|
6212
|
+
</div>
|
|
3404
6213
|
`,
|
|
3405
6214
|
}]
|
|
3406
|
-
}], propDecorators: {
|
|
6215
|
+
}], ctorParameters: () => [], propDecorators: { viewport: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], maxZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxZoom", required: false }] }], zoomStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomStep", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], crossOrigin: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossOrigin", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], roleDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "roleDescription", required: false }] }], instructions: [{ type: i0.Input, args: [{ isSignal: true, alias: "instructions", required: false }] }], zoomLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomLabel", required: false }] }], zoomInLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomInLabel", required: false }] }], zoomOutLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomOutLabel", required: false }] }], cropChange: [{ type: i0.Output, args: ["cropChange"] }], imageLoad: [{ type: i0.Output, args: ["imageLoad"] }], imageError: [{ type: i0.Output, args: ["imageError"] }] } });
|
|
3407
6216
|
|
|
3408
6217
|
/** Small, dependency-free date helpers (day-granularity, local time). */
|
|
3409
6218
|
function startOfDay(d) {
|
|
@@ -8242,6 +11051,16 @@ let nextId$1 = 0;
|
|
|
8242
11051
|
* every filtered row regardless of pagination (this is an intentional
|
|
8243
11052
|
* deviation from PrimeNG's per-page select-all).
|
|
8244
11053
|
*
|
|
11054
|
+
* **Editable grid (opt-in).** Set `editable` and give columns an `editor` (`text` · `number` ·
|
|
11055
|
+
* `currency` · `select`). The table becomes an ARIA grid with one tab stop: arrow keys move between
|
|
11056
|
+
* cells (Home/End, Ctrl+Home/End), Enter or F2 edits (typing a character starts editing with it),
|
|
11057
|
+
* Enter commits and moves down (Shift+Enter up), Tab / Shift+Tab commit and move to the next/previous
|
|
11058
|
+
* editable cell, Escape cancels (and is stopped, so an enclosing drawer stays open), clicking elsewhere
|
|
11059
|
+
* commits. Each changed commit emits `(cellCommit)` — the host applies it to its data (the table never
|
|
11060
|
+
* mutates rows) — and marks the cell dirty until {@link clearDirty}. Errors from `[cellErrors]` show
|
|
11061
|
+
* under the cell with `aria-invalid`. Number/currency cells use sds-input-number (live, locale-aware
|
|
11062
|
+
* formatting: "11,9" → "€11.90"). Everything else — sort, paging, selection — is unchanged.
|
|
11063
|
+
*
|
|
8245
11064
|
* @example
|
|
8246
11065
|
* <sds-table [value]="staff" [columns]="cols" dataKey="id"
|
|
8247
11066
|
* sortable paginator [rows]="10" globalFilter
|
|
@@ -8250,6 +11069,11 @@ let nextId$1 = 0;
|
|
|
8250
11069
|
* <sds-badge [variant]="row.actif ? 'success' : 'neutral'">{{ value }}</sds-badge>
|
|
8251
11070
|
* </ng-template>
|
|
8252
11071
|
* </sds-table>
|
|
11072
|
+
*
|
|
11073
|
+
* @example
|
|
11074
|
+
* <sds-table [value]="drafts()" [columns]="[{ field: 'name', header: 'Name', editor: 'text' },
|
|
11075
|
+
* { field: 'price', header: 'Price', editor: 'currency', align: 'right' }]"
|
|
11076
|
+
* dataKey="id" editable [cellErrors]="errors()" (cellCommit)="apply($event)" />
|
|
8253
11077
|
*/
|
|
8254
11078
|
class SdsTable {
|
|
8255
11079
|
tableId = `sds-table-${nextId$1++}`;
|
|
@@ -8308,6 +11132,16 @@ class SdsTable {
|
|
|
8308
11132
|
resizableColumns = input(false, ...(ngDevMode ? [{ debugName: "resizableColumns", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
8309
11133
|
/** Drag a column header to reorder columns. */
|
|
8310
11134
|
reorderableColumns = input(false, ...(ngDevMode ? [{ debugName: "reorderableColumns", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
11135
|
+
/** Turn on the editable grid (columns opt in with `editor`). Off: the table is unchanged. */
|
|
11136
|
+
editable = input(false, ...(ngDevMode ? [{ debugName: "editable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
11137
|
+
/** Whether a single click (default) or a double click starts editing an editable cell. */
|
|
11138
|
+
editOn = input('click', ...(ngDevMode ? [{ debugName: "editOn" }] : []));
|
|
11139
|
+
/** Per-cell error messages, keyed by row key then field (see {@link SdsTableCellErrors}). */
|
|
11140
|
+
cellErrors = input({}, ...(ngDevMode ? [{ debugName: "cellErrors" }] : []));
|
|
11141
|
+
/** Default locale for number/currency cells (a column's `locale` wins). Empty = the browser's. */
|
|
11142
|
+
locale = input('', ...(ngDevMode ? [{ debugName: "locale" }] : []));
|
|
11143
|
+
/** Spoken marker of an edited (dirty) cell. */
|
|
11144
|
+
dirtyLabel = input('edited', ...(ngDevMode ? [{ debugName: "dirtyLabel" }] : []));
|
|
8311
11145
|
// --- outputs --------------------------------------------------------------
|
|
8312
11146
|
sortChange = output();
|
|
8313
11147
|
pageChange = output();
|
|
@@ -8321,6 +11155,8 @@ class SdsTable {
|
|
|
8321
11155
|
rowCollapse = output();
|
|
8322
11156
|
/** Emitted after a drag-reorder with the moved field and its old/new index. */
|
|
8323
11157
|
colReorder = output();
|
|
11158
|
+
/** Emitted when an edited cell is committed with a CHANGED value. Apply it to your data. */
|
|
11159
|
+
cellCommit = output();
|
|
8324
11160
|
// --- custom cell templates ------------------------------------------------
|
|
8325
11161
|
cellTemplates = contentChildren(SdsTableCell, ...(ngDevMode ? [{ debugName: "cellTemplates" }] : []));
|
|
8326
11162
|
templateMap = computed(() => {
|
|
@@ -8573,7 +11409,7 @@ class SdsTable {
|
|
|
8573
11409
|
}
|
|
8574
11410
|
/** Enter/Space on a (single-mode) focusable row toggles its selection. */
|
|
8575
11411
|
onRowKeydown(event, row) {
|
|
8576
|
-
if (this.selectionMode() !== 'single' || this.loading())
|
|
11412
|
+
if (this.selectionMode() !== 'single' || this.loading() || this.editable())
|
|
8577
11413
|
return;
|
|
8578
11414
|
event.preventDefault();
|
|
8579
11415
|
this.onRowClick(row);
|
|
@@ -8609,6 +11445,7 @@ class SdsTable {
|
|
|
8609
11445
|
}
|
|
8610
11446
|
// --- paginator ------------------------------------------------------------
|
|
8611
11447
|
onPageChange(state) {
|
|
11448
|
+
this.editing.set(null);
|
|
8612
11449
|
this.pageChange.emit(state);
|
|
8613
11450
|
if (this.lazy())
|
|
8614
11451
|
this.emitLazy();
|
|
@@ -8721,6 +11558,408 @@ class SdsTable {
|
|
|
8721
11558
|
return row[k];
|
|
8722
11559
|
return index;
|
|
8723
11560
|
}
|
|
11561
|
+
// --- editable grid ----------------------------------------------------------
|
|
11562
|
+
host = inject(ElementRef);
|
|
11563
|
+
injector = inject(Injector);
|
|
11564
|
+
numberEditor = viewChild(SdsInputNumber, ...(ngDevMode ? [{ debugName: "numberEditor" }] : []));
|
|
11565
|
+
/** The roving tab stop (row index in the displayed page, column index). */
|
|
11566
|
+
activeCell = signal({ r: 0, c: 0 }, ...(ngDevMode ? [{ debugName: "activeCell" }] : []));
|
|
11567
|
+
/** The cell being edited, if any. */
|
|
11568
|
+
editing = signal(null, ...(ngDevMode ? [{ debugName: "editing" }] : []));
|
|
11569
|
+
/** The editor's working value. */
|
|
11570
|
+
draft = signal(null, ...(ngDevMode ? [{ debugName: "draft" }] : []));
|
|
11571
|
+
/** Edited cells: row key → fields. */
|
|
11572
|
+
dirty = signal(new Map(), ...(ngDevMode ? [{ debugName: "dirty" }] : []));
|
|
11573
|
+
/** Set while the table moves focus itself, so the editor's blur doesn't commit twice. */
|
|
11574
|
+
suppressBlur = false;
|
|
11575
|
+
/** Active cell clamped to the current page and columns. */
|
|
11576
|
+
tabStop = computed(() => {
|
|
11577
|
+
const rows = this.displayRows().length;
|
|
11578
|
+
const cols = this.effectiveColumns().length;
|
|
11579
|
+
const { r, c } = this.activeCell();
|
|
11580
|
+
return { r: Math.max(0, Math.min(r, rows - 1)), c: Math.max(0, Math.min(c, cols - 1)) };
|
|
11581
|
+
}, ...(ngDevMode ? [{ debugName: "tabStop" }] : []));
|
|
11582
|
+
draftText = computed(() => {
|
|
11583
|
+
const d = this.draft();
|
|
11584
|
+
return d == null ? '' : String(d);
|
|
11585
|
+
}, ...(ngDevMode ? [{ debugName: "draftText" }] : []));
|
|
11586
|
+
isTabStop(r, c) {
|
|
11587
|
+
const t = this.tabStop();
|
|
11588
|
+
return t.r === r && t.c === c;
|
|
11589
|
+
}
|
|
11590
|
+
isEditing(r, c) {
|
|
11591
|
+
const e = this.editing();
|
|
11592
|
+
return !!e && e.r === r && e.c === c;
|
|
11593
|
+
}
|
|
11594
|
+
isCellEditable(row, col) {
|
|
11595
|
+
if (!this.editable() || !col.editor || this.loading())
|
|
11596
|
+
return false;
|
|
11597
|
+
const rule = col.editable;
|
|
11598
|
+
if (rule === undefined)
|
|
11599
|
+
return true;
|
|
11600
|
+
return typeof rule === 'function' ? !!rule(row) : rule;
|
|
11601
|
+
}
|
|
11602
|
+
/** Row key used by dirty tracking, errors and commit events. */
|
|
11603
|
+
rowKeyOf(row) {
|
|
11604
|
+
if (this.dataKey() && row && typeof row === 'object')
|
|
11605
|
+
return row[this.dataKey()];
|
|
11606
|
+
const idx = this.value().indexOf(row);
|
|
11607
|
+
return idx >= 0 ? idx : row;
|
|
11608
|
+
}
|
|
11609
|
+
cellErrorFor(row, field) {
|
|
11610
|
+
const byRow = this.cellErrors()?.[String(this.rowKeyOf(row))];
|
|
11611
|
+
return (byRow && byRow[field]) || null;
|
|
11612
|
+
}
|
|
11613
|
+
cellErrorId(r, c) {
|
|
11614
|
+
return `${this.tableId}-err-${r}-${c}`;
|
|
11615
|
+
}
|
|
11616
|
+
isDirty(row, field) {
|
|
11617
|
+
return this.dirty().get(this.rowKeyOf(row))?.has(field) ?? false;
|
|
11618
|
+
}
|
|
11619
|
+
/** Forget the dirty marks — all of them, or one row's (by row key). */
|
|
11620
|
+
clearDirty(rowKey) {
|
|
11621
|
+
if (rowKey === undefined) {
|
|
11622
|
+
this.dirty.set(new Map());
|
|
11623
|
+
return;
|
|
11624
|
+
}
|
|
11625
|
+
const next = new Map(this.dirty());
|
|
11626
|
+
next.delete(rowKey);
|
|
11627
|
+
this.dirty.set(next);
|
|
11628
|
+
}
|
|
11629
|
+
/** Display text of a cell in view mode: `format`, else the editor's formatting, else the raw value. */
|
|
11630
|
+
displayCell(row, col) {
|
|
11631
|
+
const v = this.getValue(row, col.field);
|
|
11632
|
+
if (col.format)
|
|
11633
|
+
return col.format(v, row);
|
|
11634
|
+
if (v == null || v === '')
|
|
11635
|
+
return '';
|
|
11636
|
+
if ((col.editor === 'currency' || col.editor === 'number') && typeof v === 'number') {
|
|
11637
|
+
try {
|
|
11638
|
+
const opts = col.editor === 'currency'
|
|
11639
|
+
? { style: 'currency', currency: col.currency ?? 'EUR' }
|
|
11640
|
+
: { style: 'decimal' };
|
|
11641
|
+
if (col.minFractionDigits != null)
|
|
11642
|
+
opts.minimumFractionDigits = col.minFractionDigits;
|
|
11643
|
+
if (col.maxFractionDigits != null)
|
|
11644
|
+
opts.maximumFractionDigits = col.maxFractionDigits;
|
|
11645
|
+
return new Intl.NumberFormat(col.locale ?? (this.locale() || undefined), opts).format(v);
|
|
11646
|
+
}
|
|
11647
|
+
catch {
|
|
11648
|
+
return String(v);
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11651
|
+
if (col.editor === 'select') {
|
|
11652
|
+
return col.options?.find((o) => o.value === v)?.label ?? String(v);
|
|
11653
|
+
}
|
|
11654
|
+
return String(v);
|
|
11655
|
+
}
|
|
11656
|
+
gridCellClass(row, col, r, c) {
|
|
11657
|
+
const align = ALIGN[col.align ?? 'left'];
|
|
11658
|
+
const editing = this.isEditing(r, c);
|
|
11659
|
+
// While editing, the cell trades its padding for the editor's own, so the row height barely moves.
|
|
11660
|
+
const pad = editing ? 'px-1 py-1 text-sm' : SIZES[this.size()].cell;
|
|
11661
|
+
const parts = [
|
|
11662
|
+
pad,
|
|
11663
|
+
align,
|
|
11664
|
+
'relative text-ink align-middle focus:outline-none focus:ring-2 focus:ring-inset focus:ring-brand-500',
|
|
11665
|
+
];
|
|
11666
|
+
if (!editing && this.isCellEditable(row, col))
|
|
11667
|
+
parts.push('cursor-text hover:bg-muted/60');
|
|
11668
|
+
if (this.cellErrorFor(row, col.field))
|
|
11669
|
+
parts.push('bg-accent-red-soft/50 shadow-[inset_0_0_0_1px_var(--color-danger)]');
|
|
11670
|
+
return parts.join(' ');
|
|
11671
|
+
}
|
|
11672
|
+
onCellFocus(r, c) {
|
|
11673
|
+
const a = this.activeCell();
|
|
11674
|
+
if (a.r !== r || a.c !== c)
|
|
11675
|
+
this.activeCell.set({ r, c });
|
|
11676
|
+
}
|
|
11677
|
+
onCellClick(event, r, c) {
|
|
11678
|
+
if (this.isEditing(r, c)) {
|
|
11679
|
+
event.stopPropagation();
|
|
11680
|
+
return;
|
|
11681
|
+
}
|
|
11682
|
+
this.activeCell.set({ r, c });
|
|
11683
|
+
const row = this.displayRows()[r];
|
|
11684
|
+
const col = this.effectiveColumns()[c];
|
|
11685
|
+
if (this.editOn() === 'click' && row && col && this.isCellEditable(row, col)) {
|
|
11686
|
+
// Keep a click that edits a cell from also toggling single-row selection.
|
|
11687
|
+
event.stopPropagation();
|
|
11688
|
+
this.startEdit(r, c);
|
|
11689
|
+
}
|
|
11690
|
+
}
|
|
11691
|
+
onCellDblClick(event, r, c) {
|
|
11692
|
+
if (this.editOn() !== 'dblclick' || this.isEditing(r, c))
|
|
11693
|
+
return;
|
|
11694
|
+
const row = this.displayRows()[r];
|
|
11695
|
+
const col = this.effectiveColumns()[c];
|
|
11696
|
+
if (row && col && this.isCellEditable(row, col)) {
|
|
11697
|
+
event.stopPropagation();
|
|
11698
|
+
this.startEdit(r, c);
|
|
11699
|
+
}
|
|
11700
|
+
}
|
|
11701
|
+
/**
|
|
11702
|
+
* Start editing a cell of the displayed page (e.g. the first cell of a row you just added).
|
|
11703
|
+
* `field` names the column. Returns false when the cell isn't editable.
|
|
11704
|
+
*/
|
|
11705
|
+
editCell(rowIndex, field) {
|
|
11706
|
+
const c = this.effectiveColumns().findIndex((col) => col.field === field);
|
|
11707
|
+
const row = this.displayRows()[rowIndex];
|
|
11708
|
+
const col = this.effectiveColumns()[c];
|
|
11709
|
+
if (c < 0 || !row || !this.isCellEditable(row, col))
|
|
11710
|
+
return false;
|
|
11711
|
+
this.activeCell.set({ r: rowIndex, c });
|
|
11712
|
+
this.startEdit(rowIndex, c);
|
|
11713
|
+
return true;
|
|
11714
|
+
}
|
|
11715
|
+
startEdit(r, c, initialChar) {
|
|
11716
|
+
const row = this.displayRows()[r];
|
|
11717
|
+
const col = this.effectiveColumns()[c];
|
|
11718
|
+
if (!row || !col || !this.isCellEditable(row, col))
|
|
11719
|
+
return;
|
|
11720
|
+
if (this.editing())
|
|
11721
|
+
this.commitEdit('none', false);
|
|
11722
|
+
const current = this.getValue(row, col.field);
|
|
11723
|
+
const numeric = col.editor === 'number' || col.editor === 'currency';
|
|
11724
|
+
this.draft.set(initialChar !== undefined && !numeric ? initialChar : initialChar !== undefined ? null : current);
|
|
11725
|
+
this.activeCell.set({ r, c });
|
|
11726
|
+
this.editing.set({ r, c });
|
|
11727
|
+
afterNextRender(() => this.focusEditor(col, initialChar), { injector: this.injector });
|
|
11728
|
+
}
|
|
11729
|
+
focusEditor(col, initialChar) {
|
|
11730
|
+
const e = this.editing();
|
|
11731
|
+
if (!e)
|
|
11732
|
+
return;
|
|
11733
|
+
const td = this.cellEl(e.r, e.c);
|
|
11734
|
+
if (!td)
|
|
11735
|
+
return;
|
|
11736
|
+
if (col.editor === 'number' || col.editor === 'currency') {
|
|
11737
|
+
const cmp = this.numberEditor();
|
|
11738
|
+
if (!cmp)
|
|
11739
|
+
return;
|
|
11740
|
+
const d = this.draft();
|
|
11741
|
+
cmp.writeValue(typeof d === 'number' ? d : null);
|
|
11742
|
+
cmp.registerOnChange((v) => this.draft.set(v));
|
|
11743
|
+
cmp.focus({ select: initialChar === undefined });
|
|
11744
|
+
if (initialChar !== undefined) {
|
|
11745
|
+
const field = td.querySelector('input');
|
|
11746
|
+
if (field) {
|
|
11747
|
+
field.value = initialChar;
|
|
11748
|
+
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
11749
|
+
}
|
|
11750
|
+
}
|
|
11751
|
+
return;
|
|
11752
|
+
}
|
|
11753
|
+
const el = td.querySelector('[data-sds-cell-editor]');
|
|
11754
|
+
el?.focus();
|
|
11755
|
+
if (el instanceof HTMLInputElement) {
|
|
11756
|
+
if (initialChar === undefined)
|
|
11757
|
+
el.select();
|
|
11758
|
+
else
|
|
11759
|
+
el.setSelectionRange(el.value.length, el.value.length);
|
|
11760
|
+
}
|
|
11761
|
+
}
|
|
11762
|
+
onTextEditorInput(event) {
|
|
11763
|
+
this.draft.set(event.target.value);
|
|
11764
|
+
}
|
|
11765
|
+
onSelectEditorChange(event, col) {
|
|
11766
|
+
const idx = Number(event.target.value);
|
|
11767
|
+
const opt = col.options?.[idx];
|
|
11768
|
+
this.draft.set(opt ? opt.value : null);
|
|
11769
|
+
}
|
|
11770
|
+
/** Focus left the editor (a click elsewhere): commit, but don't pull focus back. */
|
|
11771
|
+
onEditorBlur() {
|
|
11772
|
+
if (this.suppressBlur || !this.editing())
|
|
11773
|
+
return;
|
|
11774
|
+
// A focus move INSIDE the editor (sds-input-number's own buttons) is not a blur of the cell.
|
|
11775
|
+
setTimeout(() => {
|
|
11776
|
+
const e = this.editing();
|
|
11777
|
+
if (!e || this.suppressBlur)
|
|
11778
|
+
return;
|
|
11779
|
+
const td = this.cellEl(e.r, e.c);
|
|
11780
|
+
if (td && td.contains(document.activeElement) && document.activeElement !== td)
|
|
11781
|
+
return;
|
|
11782
|
+
this.commitEdit('none', false);
|
|
11783
|
+
});
|
|
11784
|
+
}
|
|
11785
|
+
/**
|
|
11786
|
+
* Commit the edit. `move` picks the next active cell; `refocus` puts focus back on the grid (not when
|
|
11787
|
+
* the user clicked away).
|
|
11788
|
+
*/
|
|
11789
|
+
commitEdit(move, refocus = true) {
|
|
11790
|
+
const e = this.editing();
|
|
11791
|
+
if (!e)
|
|
11792
|
+
return;
|
|
11793
|
+
// Move focus to the cell first: the number editor clamps/tidies its value on blur.
|
|
11794
|
+
this.suppressBlur = true;
|
|
11795
|
+
if (refocus)
|
|
11796
|
+
this.cellEl(e.r, e.c)?.focus();
|
|
11797
|
+
this.suppressBlur = false;
|
|
11798
|
+
const row = this.displayRows()[e.r];
|
|
11799
|
+
const col = this.effectiveColumns()[e.c];
|
|
11800
|
+
this.editing.set(null);
|
|
11801
|
+
if (row && col) {
|
|
11802
|
+
let value = this.draft();
|
|
11803
|
+
if (col.editor === 'text' || col.editor === undefined)
|
|
11804
|
+
value = value == null ? '' : String(value);
|
|
11805
|
+
const previousValue = this.getValue(row, col.field);
|
|
11806
|
+
const same = value === previousValue ||
|
|
11807
|
+
((value === '' || value == null) && (previousValue === '' || previousValue == null));
|
|
11808
|
+
if (!same) {
|
|
11809
|
+
const key = this.rowKeyOf(row);
|
|
11810
|
+
const next = new Map(this.dirty());
|
|
11811
|
+
next.set(key, new Set([...(next.get(key) ?? []), col.field]));
|
|
11812
|
+
this.dirty.set(next);
|
|
11813
|
+
this.cellCommit.emit({
|
|
11814
|
+
row,
|
|
11815
|
+
field: col.field,
|
|
11816
|
+
value,
|
|
11817
|
+
previousValue,
|
|
11818
|
+
rowKey: key,
|
|
11819
|
+
rowIndex: this.value().indexOf(row),
|
|
11820
|
+
});
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
let target = { r: e.r, c: e.c };
|
|
11824
|
+
if (move === 'down')
|
|
11825
|
+
target = { r: Math.min(e.r + 1, this.displayRows().length - 1), c: e.c };
|
|
11826
|
+
else if (move === 'up')
|
|
11827
|
+
target = { r: Math.max(0, e.r - 1), c: e.c };
|
|
11828
|
+
else if (move === 'next' || move === 'prev')
|
|
11829
|
+
target = this.nextEditable(e.r, e.c, move === 'next' ? 1 : -1) ?? target;
|
|
11830
|
+
this.activeCell.set(target);
|
|
11831
|
+
if (refocus) {
|
|
11832
|
+
if (move === 'next' || move === 'prev') {
|
|
11833
|
+
// Tab keeps the spreadsheet rhythm: land in the next editable cell already editing.
|
|
11834
|
+
if (target.r !== e.r || target.c !== e.c)
|
|
11835
|
+
this.startEdit(target.r, target.c);
|
|
11836
|
+
else
|
|
11837
|
+
this.focusCellSoon(target.r, target.c);
|
|
11838
|
+
}
|
|
11839
|
+
else {
|
|
11840
|
+
this.focusCellSoon(target.r, target.c);
|
|
11841
|
+
}
|
|
11842
|
+
}
|
|
11843
|
+
}
|
|
11844
|
+
cancelEdit() {
|
|
11845
|
+
const e = this.editing();
|
|
11846
|
+
if (!e)
|
|
11847
|
+
return;
|
|
11848
|
+
this.suppressBlur = true;
|
|
11849
|
+
this.cellEl(e.r, e.c)?.focus();
|
|
11850
|
+
this.suppressBlur = false;
|
|
11851
|
+
this.editing.set(null);
|
|
11852
|
+
this.focusCellSoon(e.r, e.c);
|
|
11853
|
+
}
|
|
11854
|
+
/** The next editable cell in reading order (wrapping rows), or null at the grid's edge. */
|
|
11855
|
+
nextEditable(r, c, dir) {
|
|
11856
|
+
const rows = this.displayRows();
|
|
11857
|
+
const cols = this.effectiveColumns();
|
|
11858
|
+
let rr = r;
|
|
11859
|
+
let cc = c;
|
|
11860
|
+
for (let n = 0; n < rows.length * cols.length; n++) {
|
|
11861
|
+
cc += dir;
|
|
11862
|
+
if (cc >= cols.length) {
|
|
11863
|
+
cc = 0;
|
|
11864
|
+
rr++;
|
|
11865
|
+
}
|
|
11866
|
+
else if (cc < 0) {
|
|
11867
|
+
cc = cols.length - 1;
|
|
11868
|
+
rr--;
|
|
11869
|
+
}
|
|
11870
|
+
if (rr < 0 || rr >= rows.length)
|
|
11871
|
+
return null;
|
|
11872
|
+
if (this.isCellEditable(rows[rr], cols[cc]))
|
|
11873
|
+
return { r: rr, c: cc };
|
|
11874
|
+
}
|
|
11875
|
+
return null;
|
|
11876
|
+
}
|
|
11877
|
+
cellEl(r, c) {
|
|
11878
|
+
return this.host.nativeElement.querySelector(`td[data-cell="${r}:${c}"]`);
|
|
11879
|
+
}
|
|
11880
|
+
focusCellSoon(r, c) {
|
|
11881
|
+
afterNextRender(() => this.cellEl(r, c)?.focus(), { injector: this.injector });
|
|
11882
|
+
}
|
|
11883
|
+
onGridKeydown(event) {
|
|
11884
|
+
if (!this.editable() || this.loading())
|
|
11885
|
+
return;
|
|
11886
|
+
const target = event.target;
|
|
11887
|
+
const e = this.editing();
|
|
11888
|
+
// --- editing: Enter / Tab / Escape drive the edit; everything else belongs to the editor.
|
|
11889
|
+
if (e && this.cellEl(e.r, e.c)?.contains(target) && target.tagName !== 'TD') {
|
|
11890
|
+
switch (event.key) {
|
|
11891
|
+
case 'Enter':
|
|
11892
|
+
event.preventDefault();
|
|
11893
|
+
this.commitEdit(event.shiftKey ? 'up' : 'down');
|
|
11894
|
+
return;
|
|
11895
|
+
case 'Tab': {
|
|
11896
|
+
const next = this.nextEditable(e.r, e.c, event.shiftKey ? -1 : 1);
|
|
11897
|
+
if (!next) {
|
|
11898
|
+
// Last editable cell: commit and let Tab leave the grid naturally.
|
|
11899
|
+
this.commitEdit('none', false);
|
|
11900
|
+
return;
|
|
11901
|
+
}
|
|
11902
|
+
event.preventDefault();
|
|
11903
|
+
this.commitEdit(event.shiftKey ? 'prev' : 'next');
|
|
11904
|
+
return;
|
|
11905
|
+
}
|
|
11906
|
+
case 'Escape':
|
|
11907
|
+
event.preventDefault();
|
|
11908
|
+
event.stopPropagation();
|
|
11909
|
+
this.cancelEdit();
|
|
11910
|
+
return;
|
|
11911
|
+
}
|
|
11912
|
+
return;
|
|
11913
|
+
}
|
|
11914
|
+
// --- navigation: only when a grid cell itself has focus.
|
|
11915
|
+
const td = target.closest('td[data-cell]');
|
|
11916
|
+
if (!td || td !== target)
|
|
11917
|
+
return;
|
|
11918
|
+
const [r, c] = (td.dataset['cell'] ?? '0:0').split(':').map(Number);
|
|
11919
|
+
const rows = this.displayRows().length;
|
|
11920
|
+
const cols = this.effectiveColumns().length;
|
|
11921
|
+
let next = null;
|
|
11922
|
+
switch (event.key) {
|
|
11923
|
+
case 'ArrowUp':
|
|
11924
|
+
next = { r: Math.max(0, r - 1), c };
|
|
11925
|
+
break;
|
|
11926
|
+
case 'ArrowDown':
|
|
11927
|
+
next = { r: Math.min(rows - 1, r + 1), c };
|
|
11928
|
+
break;
|
|
11929
|
+
case 'ArrowLeft':
|
|
11930
|
+
next = { r, c: Math.max(0, c - 1) };
|
|
11931
|
+
break;
|
|
11932
|
+
case 'ArrowRight':
|
|
11933
|
+
next = { r, c: Math.min(cols - 1, c + 1) };
|
|
11934
|
+
break;
|
|
11935
|
+
case 'Home':
|
|
11936
|
+
next = event.ctrlKey || event.metaKey ? { r: 0, c: 0 } : { r, c: 0 };
|
|
11937
|
+
break;
|
|
11938
|
+
case 'End':
|
|
11939
|
+
next = event.ctrlKey || event.metaKey ? { r: rows - 1, c: cols - 1 } : { r, c: cols - 1 };
|
|
11940
|
+
break;
|
|
11941
|
+
case 'Enter':
|
|
11942
|
+
case 'F2':
|
|
11943
|
+
event.preventDefault();
|
|
11944
|
+
this.startEdit(r, c);
|
|
11945
|
+
return;
|
|
11946
|
+
default: {
|
|
11947
|
+
const row = this.displayRows()[r];
|
|
11948
|
+
const col = this.effectiveColumns()[c];
|
|
11949
|
+
const printable = event.key.length === 1 && event.key !== ' ' && !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
11950
|
+
if (printable && row && col && col.editor !== 'select' && this.isCellEditable(row, col)) {
|
|
11951
|
+
event.preventDefault();
|
|
11952
|
+
this.startEdit(r, c, event.key);
|
|
11953
|
+
}
|
|
11954
|
+
return;
|
|
11955
|
+
}
|
|
11956
|
+
}
|
|
11957
|
+
event.preventDefault();
|
|
11958
|
+
if (next) {
|
|
11959
|
+
this.activeCell.set(next);
|
|
11960
|
+
this.cellEl(next.r, next.c)?.focus();
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
8724
11963
|
// --- styling --------------------------------------------------------------
|
|
8725
11964
|
headCellClass(col) {
|
|
8726
11965
|
const align = ALIGN[col?.align ?? 'left'];
|
|
@@ -8753,7 +11992,7 @@ class SdsTable {
|
|
|
8753
11992
|
: 'border-border bg-surface';
|
|
8754
11993
|
}
|
|
8755
11994
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8756
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTable, isStandalone: true, selector: "sds-table", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, gridlines: { classPropertyName: "gridlines", publicName: "gridlines", isSignal: true, isRequired: false, transformFunction: null }, hoverable: { classPropertyName: "hoverable", publicName: "hoverable", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, paginator: { classPropertyName: "paginator", publicName: "paginator", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageOptions: { classPropertyName: "rowsPerPageOptions", publicName: "rowsPerPageOptions", isSignal: true, isRequired: false, transformFunction: null }, showCurrentPageReport: { classPropertyName: "showCurrentPageReport", publicName: "showCurrentPageReport", isSignal: true, isRequired: false, transformFunction: null }, currentPageReportTemplate: { classPropertyName: "currentPageReportTemplate", publicName: "currentPageReportTemplate", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageLabel: { classPropertyName: "rowsPerPageLabel", publicName: "rowsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, paginationLabel: { classPropertyName: "paginationLabel", publicName: "paginationLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLinkLabel: { classPropertyName: "pageLinkLabel", publicName: "pageLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilter: { classPropertyName: "globalFilter", publicName: "globalFilter", isSignal: true, isRequired: false, transformFunction: null }, globalFilterPlaceholder: { classPropertyName: "globalFilterPlaceholder", publicName: "globalFilterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, globalFilterAriaLabel: { classPropertyName: "globalFilterAriaLabel", publicName: "globalFilterAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilterFields: { classPropertyName: "globalFilterFields", publicName: "globalFilterFields", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, scrollHeight: { classPropertyName: "scrollHeight", publicName: "scrollHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, resizableColumns: { classPropertyName: "resizableColumns", publicName: "resizableColumns", isSignal: true, isRequired: false, transformFunction: null }, reorderableColumns: { classPropertyName: "reorderableColumns", publicName: "reorderableColumns", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", sortChange: "sortChange", pageChange: "pageChange", rowSelect: "rowSelect", rowUnselect: "rowUnselect", lazyLoad: "lazyLoad", rowClick: "rowClick", rowExpand: "rowExpand", rowCollapse: "rowCollapse", colReorder: "colReorder" }, host: { classAttribute: "block" }, queries: [{ propertyName: "cellTemplates", predicate: SdsTableCell, isSignal: true }, { propertyName: "rowExpansion", first: true, predicate: SdsTableRowExpansion, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
11995
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTable, isStandalone: true, selector: "sds-table", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, gridlines: { classPropertyName: "gridlines", publicName: "gridlines", isSignal: true, isRequired: false, transformFunction: null }, hoverable: { classPropertyName: "hoverable", publicName: "hoverable", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, paginator: { classPropertyName: "paginator", publicName: "paginator", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageOptions: { classPropertyName: "rowsPerPageOptions", publicName: "rowsPerPageOptions", isSignal: true, isRequired: false, transformFunction: null }, showCurrentPageReport: { classPropertyName: "showCurrentPageReport", publicName: "showCurrentPageReport", isSignal: true, isRequired: false, transformFunction: null }, currentPageReportTemplate: { classPropertyName: "currentPageReportTemplate", publicName: "currentPageReportTemplate", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageLabel: { classPropertyName: "rowsPerPageLabel", publicName: "rowsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, paginationLabel: { classPropertyName: "paginationLabel", publicName: "paginationLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLinkLabel: { classPropertyName: "pageLinkLabel", publicName: "pageLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilter: { classPropertyName: "globalFilter", publicName: "globalFilter", isSignal: true, isRequired: false, transformFunction: null }, globalFilterPlaceholder: { classPropertyName: "globalFilterPlaceholder", publicName: "globalFilterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, globalFilterAriaLabel: { classPropertyName: "globalFilterAriaLabel", publicName: "globalFilterAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilterFields: { classPropertyName: "globalFilterFields", publicName: "globalFilterFields", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, scrollHeight: { classPropertyName: "scrollHeight", publicName: "scrollHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, resizableColumns: { classPropertyName: "resizableColumns", publicName: "resizableColumns", isSignal: true, isRequired: false, transformFunction: null }, reorderableColumns: { classPropertyName: "reorderableColumns", publicName: "reorderableColumns", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, editOn: { classPropertyName: "editOn", publicName: "editOn", isSignal: true, isRequired: false, transformFunction: null }, cellErrors: { classPropertyName: "cellErrors", publicName: "cellErrors", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, dirtyLabel: { classPropertyName: "dirtyLabel", publicName: "dirtyLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", sortChange: "sortChange", pageChange: "pageChange", rowSelect: "rowSelect", rowUnselect: "rowUnselect", lazyLoad: "lazyLoad", rowClick: "rowClick", rowExpand: "rowExpand", rowCollapse: "rowCollapse", colReorder: "colReorder", cellCommit: "cellCommit" }, host: { classAttribute: "block" }, queries: [{ propertyName: "cellTemplates", predicate: SdsTableCell, isSignal: true }, { propertyName: "rowExpansion", first: true, predicate: SdsTableRowExpansion, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "numberEditor", first: true, predicate: SdsInputNumber, descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
8757
11996
|
<div class="rounded-card border border-border bg-surface overflow-hidden">
|
|
8758
11997
|
<!-- Toolbar: caption + global filter -->
|
|
8759
11998
|
@if (caption() || globalFilter()) {
|
|
@@ -8798,6 +12037,8 @@ class SdsTable {
|
|
|
8798
12037
|
>
|
|
8799
12038
|
<table
|
|
8800
12039
|
class="w-full border-collapse text-left"
|
|
12040
|
+
[attr.role]="editable() ? 'grid' : null"
|
|
12041
|
+
(keydown)="onGridKeydown($event)"
|
|
8801
12042
|
[attr.aria-busy]="loading() ? 'true' : null"
|
|
8802
12043
|
[attr.aria-labelledby]="caption() ? tableId + '-caption' : null"
|
|
8803
12044
|
[attr.aria-label]="caption() ? null : ariaLabel()"
|
|
@@ -8901,7 +12142,7 @@ class SdsTable {
|
|
|
8901
12142
|
@for (row of displayRows(); track trackRow($index, row); let i = $index) {
|
|
8902
12143
|
<tr
|
|
8903
12144
|
[class]="rowClass(row, i)"
|
|
8904
|
-
[attr.tabindex]="selectionMode() === 'single' && !loading() ? 0 : null"
|
|
12145
|
+
[attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
|
|
8905
12146
|
[attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
|
|
8906
12147
|
(click)="onRowClick(row)"
|
|
8907
12148
|
(keydown.enter)="onRowKeydown($event, row)"
|
|
@@ -8967,20 +12208,128 @@ class SdsTable {
|
|
|
8967
12208
|
</td>
|
|
8968
12209
|
}
|
|
8969
12210
|
|
|
8970
|
-
@for (col of effectiveColumns(); track col.field) {
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
12211
|
+
@for (col of effectiveColumns(); track col.field; let ci = $index) {
|
|
12212
|
+
@if (!editable()) {
|
|
12213
|
+
<td
|
|
12214
|
+
[class]="cellClass(col)"
|
|
12215
|
+
[style.width]="columnStyleWidth(col)"
|
|
12216
|
+
>
|
|
12217
|
+
@if (templateFor(col.field); as tpl) {
|
|
12218
|
+
<ng-container
|
|
12219
|
+
[ngTemplateOutlet]="tpl"
|
|
12220
|
+
[ngTemplateOutletContext]="cellContext(row, col, i)"
|
|
12221
|
+
/>
|
|
12222
|
+
} @else {
|
|
12223
|
+
{{ displayCell(row, col) }}
|
|
12224
|
+
}
|
|
12225
|
+
</td>
|
|
12226
|
+
} @else {
|
|
12227
|
+
<!-- Editable grid cell: one roving tab stop, arrow-key navigation, in-place editor. -->
|
|
12228
|
+
<td
|
|
12229
|
+
role="gridcell"
|
|
12230
|
+
[class]="gridCellClass(row, col, i, ci)"
|
|
12231
|
+
[style.width]="columnStyleWidth(col)"
|
|
12232
|
+
[attr.data-cell]="i + ':' + ci"
|
|
12233
|
+
[attr.tabindex]="isTabStop(i, ci) ? 0 : -1"
|
|
12234
|
+
[attr.aria-readonly]="isCellEditable(row, col) ? null : 'true'"
|
|
12235
|
+
[attr.aria-invalid]="cellErrorFor(row, col.field) ? 'true' : null"
|
|
12236
|
+
[attr.aria-describedby]="cellErrorFor(row, col.field) ? cellErrorId(i, ci) : null"
|
|
12237
|
+
(click)="onCellClick($event, i, ci)"
|
|
12238
|
+
(dblclick)="onCellDblClick($event, i, ci)"
|
|
12239
|
+
(focus)="onCellFocus(i, ci)"
|
|
12240
|
+
>
|
|
12241
|
+
@if (isEditing(i, ci)) {
|
|
12242
|
+
@switch (col.editor) {
|
|
12243
|
+
@case ('select') {
|
|
12244
|
+
<select
|
|
12245
|
+
data-sds-cell-editor
|
|
12246
|
+
class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2 text-sm text-ink outline-none ring-2 ring-brand-500/25"
|
|
12247
|
+
[attr.aria-label]="col.header"
|
|
12248
|
+
(change)="onSelectEditorChange($event, col)"
|
|
12249
|
+
(blur)="onEditorBlur()"
|
|
12250
|
+
>
|
|
12251
|
+
@if (draft() === null || draft() === undefined) {
|
|
12252
|
+
<option value="" disabled selected>{{ col.placeholder ?? '' }}</option>
|
|
12253
|
+
}
|
|
12254
|
+
@for (opt of col.options ?? []; track $index; let oi = $index) {
|
|
12255
|
+
<option [value]="oi" [selected]="opt.value === draft()">{{ opt.label }}</option>
|
|
12256
|
+
}
|
|
12257
|
+
</select>
|
|
12258
|
+
}
|
|
12259
|
+
@case ('number') {
|
|
12260
|
+
<sds-input-number
|
|
12261
|
+
data-sds-cell-editor
|
|
12262
|
+
size="sm"
|
|
12263
|
+
mode="decimal"
|
|
12264
|
+
[locale]="col.locale ?? locale()"
|
|
12265
|
+
[min]="col.min ?? null"
|
|
12266
|
+
[max]="col.max ?? null"
|
|
12267
|
+
[step]="col.step ?? 1"
|
|
12268
|
+
[minFractionDigits]="col.minFractionDigits ?? null"
|
|
12269
|
+
[maxFractionDigits]="col.maxFractionDigits ?? null"
|
|
12270
|
+
[placeholder]="col.placeholder ?? ''"
|
|
12271
|
+
[ariaLabel]="col.header"
|
|
12272
|
+
(focusout)="onEditorBlur()"
|
|
12273
|
+
/>
|
|
12274
|
+
}
|
|
12275
|
+
@case ('currency') {
|
|
12276
|
+
<sds-input-number
|
|
12277
|
+
data-sds-cell-editor
|
|
12278
|
+
size="sm"
|
|
12279
|
+
mode="currency"
|
|
12280
|
+
[currency]="col.currency ?? 'EUR'"
|
|
12281
|
+
[locale]="col.locale ?? locale()"
|
|
12282
|
+
[min]="col.min ?? null"
|
|
12283
|
+
[max]="col.max ?? null"
|
|
12284
|
+
[step]="col.step ?? 1"
|
|
12285
|
+
[minFractionDigits]="col.minFractionDigits ?? null"
|
|
12286
|
+
[maxFractionDigits]="col.maxFractionDigits ?? null"
|
|
12287
|
+
[placeholder]="col.placeholder ?? ''"
|
|
12288
|
+
[ariaLabel]="col.header"
|
|
12289
|
+
(focusout)="onEditorBlur()"
|
|
12290
|
+
/>
|
|
12291
|
+
}
|
|
12292
|
+
@default {
|
|
12293
|
+
<input
|
|
12294
|
+
data-sds-cell-editor
|
|
12295
|
+
type="text"
|
|
12296
|
+
class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2.5 text-sm text-ink outline-none ring-2 ring-brand-500/25 placeholder:text-ink-faint"
|
|
12297
|
+
[value]="draftText()"
|
|
12298
|
+
[placeholder]="col.placeholder ?? ''"
|
|
12299
|
+
[attr.aria-label]="col.header"
|
|
12300
|
+
(input)="onTextEditorInput($event)"
|
|
12301
|
+
(blur)="onEditorBlur()"
|
|
12302
|
+
/>
|
|
12303
|
+
}
|
|
12304
|
+
}
|
|
12305
|
+
} @else {
|
|
12306
|
+
@if (templateFor(col.field); as tpl) {
|
|
12307
|
+
<ng-container
|
|
12308
|
+
[ngTemplateOutlet]="tpl"
|
|
12309
|
+
[ngTemplateOutletContext]="cellContext(row, col, i)"
|
|
12310
|
+
/>
|
|
12311
|
+
} @else {
|
|
12312
|
+
{{ displayCell(row, col) }}
|
|
12313
|
+
}
|
|
12314
|
+
@if (isDirty(row, col.field)) {
|
|
12315
|
+
<span
|
|
12316
|
+
class="pointer-events-none absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-brand-500"
|
|
12317
|
+
aria-hidden="true"
|
|
12318
|
+
></span>
|
|
12319
|
+
<span class="sr-only">({{ dirtyLabel() }})</span>
|
|
12320
|
+
}
|
|
12321
|
+
}
|
|
12322
|
+
@if (cellErrorFor(row, col.field); as message) {
|
|
12323
|
+
<span
|
|
12324
|
+
[id]="cellErrorId(i, ci)"
|
|
12325
|
+
class="mt-1 flex items-start gap-1 text-xs font-medium normal-case text-danger"
|
|
12326
|
+
>
|
|
12327
|
+
<sds-icon name="alert-circle" [size]="13" class="mt-px shrink-0" />
|
|
12328
|
+
<span>{{ message }}</span>
|
|
12329
|
+
</span>
|
|
12330
|
+
}
|
|
12331
|
+
</td>
|
|
12332
|
+
}
|
|
8984
12333
|
}
|
|
8985
12334
|
</tr>
|
|
8986
12335
|
|
|
@@ -9047,14 +12396,14 @@ class SdsTable {
|
|
|
9047
12396
|
</div>
|
|
9048
12397
|
}
|
|
9049
12398
|
</div>
|
|
9050
|
-
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }, { kind: "component", type: SdsPaginator, selector: "sds-paginator", inputs: ["totalRecords", "first", "rows", "rowsPerPageOptions", "pageLinkSize", "showFirstLastIcon", "showPageLinks", "showCurrentPageReport", "currentPageReportTemplate", "showJumpToPageInput", "rowsPerPageLabel", "jumpToPageLabel", "paginationLabel", "firstPageLabel", "previousPageLabel", "nextPageLabel", "lastPageLabel", "pageLinkLabel", "jumpToPageAriaLabel", "size", "disabled"], outputs: ["firstChange", "rowsChange", "pageChange"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12399
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }, { kind: "component", type: SdsPaginator, selector: "sds-paginator", inputs: ["totalRecords", "first", "rows", "rowsPerPageOptions", "pageLinkSize", "showFirstLastIcon", "showPageLinks", "showCurrentPageReport", "currentPageReportTemplate", "showJumpToPageInput", "rowsPerPageLabel", "jumpToPageLabel", "paginationLabel", "firstPageLabel", "previousPageLabel", "nextPageLabel", "lastPageLabel", "pageLinkLabel", "jumpToPageAriaLabel", "size", "disabled"], outputs: ["firstChange", "rowsChange", "pageChange"] }, { kind: "component", type: SdsInputNumber, selector: "sds-input-number", inputs: ["label", "ariaLabel", "placeholder", "hint", "error", "size", "appearance", "disabled", "readonly", "mode", "locale", "currency", "currencyDisplay", "useGrouping", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "min", "max", "step", "showButtons", "buttonLayout", "incrementIcon", "decrementIcon", "allowEmpty", "clearable", "clearLabel", "incrementLabel", "decrementLabel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9051
12400
|
}
|
|
9052
12401
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, decorators: [{
|
|
9053
12402
|
type: Component,
|
|
9054
12403
|
args: [{
|
|
9055
12404
|
selector: 'sds-table',
|
|
9056
12405
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9057
|
-
imports: [SdsIcon, SdsPaginator, NgTemplateOutlet],
|
|
12406
|
+
imports: [SdsIcon, SdsPaginator, SdsInputNumber, NgTemplateOutlet],
|
|
9058
12407
|
host: { class: 'block' },
|
|
9059
12408
|
template: `
|
|
9060
12409
|
<div class="rounded-card border border-border bg-surface overflow-hidden">
|
|
@@ -9101,6 +12450,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9101
12450
|
>
|
|
9102
12451
|
<table
|
|
9103
12452
|
class="w-full border-collapse text-left"
|
|
12453
|
+
[attr.role]="editable() ? 'grid' : null"
|
|
12454
|
+
(keydown)="onGridKeydown($event)"
|
|
9104
12455
|
[attr.aria-busy]="loading() ? 'true' : null"
|
|
9105
12456
|
[attr.aria-labelledby]="caption() ? tableId + '-caption' : null"
|
|
9106
12457
|
[attr.aria-label]="caption() ? null : ariaLabel()"
|
|
@@ -9204,7 +12555,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9204
12555
|
@for (row of displayRows(); track trackRow($index, row); let i = $index) {
|
|
9205
12556
|
<tr
|
|
9206
12557
|
[class]="rowClass(row, i)"
|
|
9207
|
-
[attr.tabindex]="selectionMode() === 'single' && !loading() ? 0 : null"
|
|
12558
|
+
[attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
|
|
9208
12559
|
[attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
|
|
9209
12560
|
(click)="onRowClick(row)"
|
|
9210
12561
|
(keydown.enter)="onRowKeydown($event, row)"
|
|
@@ -9270,20 +12621,128 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9270
12621
|
</td>
|
|
9271
12622
|
}
|
|
9272
12623
|
|
|
9273
|
-
@for (col of effectiveColumns(); track col.field) {
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
12624
|
+
@for (col of effectiveColumns(); track col.field; let ci = $index) {
|
|
12625
|
+
@if (!editable()) {
|
|
12626
|
+
<td
|
|
12627
|
+
[class]="cellClass(col)"
|
|
12628
|
+
[style.width]="columnStyleWidth(col)"
|
|
12629
|
+
>
|
|
12630
|
+
@if (templateFor(col.field); as tpl) {
|
|
12631
|
+
<ng-container
|
|
12632
|
+
[ngTemplateOutlet]="tpl"
|
|
12633
|
+
[ngTemplateOutletContext]="cellContext(row, col, i)"
|
|
12634
|
+
/>
|
|
12635
|
+
} @else {
|
|
12636
|
+
{{ displayCell(row, col) }}
|
|
12637
|
+
}
|
|
12638
|
+
</td>
|
|
12639
|
+
} @else {
|
|
12640
|
+
<!-- Editable grid cell: one roving tab stop, arrow-key navigation, in-place editor. -->
|
|
12641
|
+
<td
|
|
12642
|
+
role="gridcell"
|
|
12643
|
+
[class]="gridCellClass(row, col, i, ci)"
|
|
12644
|
+
[style.width]="columnStyleWidth(col)"
|
|
12645
|
+
[attr.data-cell]="i + ':' + ci"
|
|
12646
|
+
[attr.tabindex]="isTabStop(i, ci) ? 0 : -1"
|
|
12647
|
+
[attr.aria-readonly]="isCellEditable(row, col) ? null : 'true'"
|
|
12648
|
+
[attr.aria-invalid]="cellErrorFor(row, col.field) ? 'true' : null"
|
|
12649
|
+
[attr.aria-describedby]="cellErrorFor(row, col.field) ? cellErrorId(i, ci) : null"
|
|
12650
|
+
(click)="onCellClick($event, i, ci)"
|
|
12651
|
+
(dblclick)="onCellDblClick($event, i, ci)"
|
|
12652
|
+
(focus)="onCellFocus(i, ci)"
|
|
12653
|
+
>
|
|
12654
|
+
@if (isEditing(i, ci)) {
|
|
12655
|
+
@switch (col.editor) {
|
|
12656
|
+
@case ('select') {
|
|
12657
|
+
<select
|
|
12658
|
+
data-sds-cell-editor
|
|
12659
|
+
class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2 text-sm text-ink outline-none ring-2 ring-brand-500/25"
|
|
12660
|
+
[attr.aria-label]="col.header"
|
|
12661
|
+
(change)="onSelectEditorChange($event, col)"
|
|
12662
|
+
(blur)="onEditorBlur()"
|
|
12663
|
+
>
|
|
12664
|
+
@if (draft() === null || draft() === undefined) {
|
|
12665
|
+
<option value="" disabled selected>{{ col.placeholder ?? '' }}</option>
|
|
12666
|
+
}
|
|
12667
|
+
@for (opt of col.options ?? []; track $index; let oi = $index) {
|
|
12668
|
+
<option [value]="oi" [selected]="opt.value === draft()">{{ opt.label }}</option>
|
|
12669
|
+
}
|
|
12670
|
+
</select>
|
|
12671
|
+
}
|
|
12672
|
+
@case ('number') {
|
|
12673
|
+
<sds-input-number
|
|
12674
|
+
data-sds-cell-editor
|
|
12675
|
+
size="sm"
|
|
12676
|
+
mode="decimal"
|
|
12677
|
+
[locale]="col.locale ?? locale()"
|
|
12678
|
+
[min]="col.min ?? null"
|
|
12679
|
+
[max]="col.max ?? null"
|
|
12680
|
+
[step]="col.step ?? 1"
|
|
12681
|
+
[minFractionDigits]="col.minFractionDigits ?? null"
|
|
12682
|
+
[maxFractionDigits]="col.maxFractionDigits ?? null"
|
|
12683
|
+
[placeholder]="col.placeholder ?? ''"
|
|
12684
|
+
[ariaLabel]="col.header"
|
|
12685
|
+
(focusout)="onEditorBlur()"
|
|
12686
|
+
/>
|
|
12687
|
+
}
|
|
12688
|
+
@case ('currency') {
|
|
12689
|
+
<sds-input-number
|
|
12690
|
+
data-sds-cell-editor
|
|
12691
|
+
size="sm"
|
|
12692
|
+
mode="currency"
|
|
12693
|
+
[currency]="col.currency ?? 'EUR'"
|
|
12694
|
+
[locale]="col.locale ?? locale()"
|
|
12695
|
+
[min]="col.min ?? null"
|
|
12696
|
+
[max]="col.max ?? null"
|
|
12697
|
+
[step]="col.step ?? 1"
|
|
12698
|
+
[minFractionDigits]="col.minFractionDigits ?? null"
|
|
12699
|
+
[maxFractionDigits]="col.maxFractionDigits ?? null"
|
|
12700
|
+
[placeholder]="col.placeholder ?? ''"
|
|
12701
|
+
[ariaLabel]="col.header"
|
|
12702
|
+
(focusout)="onEditorBlur()"
|
|
12703
|
+
/>
|
|
12704
|
+
}
|
|
12705
|
+
@default {
|
|
12706
|
+
<input
|
|
12707
|
+
data-sds-cell-editor
|
|
12708
|
+
type="text"
|
|
12709
|
+
class="h-9 w-full rounded-control border border-brand-500 bg-surface px-2.5 text-sm text-ink outline-none ring-2 ring-brand-500/25 placeholder:text-ink-faint"
|
|
12710
|
+
[value]="draftText()"
|
|
12711
|
+
[placeholder]="col.placeholder ?? ''"
|
|
12712
|
+
[attr.aria-label]="col.header"
|
|
12713
|
+
(input)="onTextEditorInput($event)"
|
|
12714
|
+
(blur)="onEditorBlur()"
|
|
12715
|
+
/>
|
|
12716
|
+
}
|
|
12717
|
+
}
|
|
12718
|
+
} @else {
|
|
12719
|
+
@if (templateFor(col.field); as tpl) {
|
|
12720
|
+
<ng-container
|
|
12721
|
+
[ngTemplateOutlet]="tpl"
|
|
12722
|
+
[ngTemplateOutletContext]="cellContext(row, col, i)"
|
|
12723
|
+
/>
|
|
12724
|
+
} @else {
|
|
12725
|
+
{{ displayCell(row, col) }}
|
|
12726
|
+
}
|
|
12727
|
+
@if (isDirty(row, col.field)) {
|
|
12728
|
+
<span
|
|
12729
|
+
class="pointer-events-none absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-brand-500"
|
|
12730
|
+
aria-hidden="true"
|
|
12731
|
+
></span>
|
|
12732
|
+
<span class="sr-only">({{ dirtyLabel() }})</span>
|
|
12733
|
+
}
|
|
12734
|
+
}
|
|
12735
|
+
@if (cellErrorFor(row, col.field); as message) {
|
|
12736
|
+
<span
|
|
12737
|
+
[id]="cellErrorId(i, ci)"
|
|
12738
|
+
class="mt-1 flex items-start gap-1 text-xs font-medium normal-case text-danger"
|
|
12739
|
+
>
|
|
12740
|
+
<sds-icon name="alert-circle" [size]="13" class="mt-px shrink-0" />
|
|
12741
|
+
<span>{{ message }}</span>
|
|
12742
|
+
</span>
|
|
12743
|
+
}
|
|
12744
|
+
</td>
|
|
12745
|
+
}
|
|
9287
12746
|
}
|
|
9288
12747
|
</tr>
|
|
9289
12748
|
|
|
@@ -9352,7 +12811,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9352
12811
|
</div>
|
|
9353
12812
|
`,
|
|
9354
12813
|
}]
|
|
9355
|
-
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], gridlines: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridlines", required: false }] }], hoverable: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverable", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], paginator: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginator", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], showCurrentPageReport: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCurrentPageReport", required: false }] }], currentPageReportTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPageReportTemplate", required: false }] }], rowsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageLabel", required: false }] }], paginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], pageLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLinkLabel", required: false }] }], globalFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilter", required: false }] }], globalFilterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterPlaceholder", required: false }] }], globalFilterAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterAriaLabel", required: false }] }], globalFilterFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterFields", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], scrollHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], resizableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableColumns", required: false }] }], reorderableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderableColumns", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowSelect: [{ type: i0.Output, args: ["rowSelect"] }], rowUnselect: [{ type: i0.Output, args: ["rowUnselect"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowExpand: [{ type: i0.Output, args: ["rowExpand"] }], rowCollapse: [{ type: i0.Output, args: ["rowCollapse"] }], colReorder: [{ type: i0.Output, args: ["colReorder"] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SdsTableCell), { isSignal: true }] }], rowExpansion: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableRowExpansion), { isSignal: true }] }] } });
|
|
12814
|
+
}], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], striped: [{ type: i0.Input, args: [{ isSignal: true, alias: "striped", required: false }] }], gridlines: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridlines", required: false }] }], hoverable: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoverable", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], paginator: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginator", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], showCurrentPageReport: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCurrentPageReport", required: false }] }], currentPageReportTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPageReportTemplate", required: false }] }], rowsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageLabel", required: false }] }], paginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], pageLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLinkLabel", required: false }] }], globalFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilter", required: false }] }], globalFilterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterPlaceholder", required: false }] }], globalFilterAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterAriaLabel", required: false }] }], globalFilterFields: [{ type: i0.Input, args: [{ isSignal: true, alias: "globalFilterFields", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }, { type: i0.Output, args: ["selectionChange"] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], caption: [{ type: i0.Input, args: [{ isSignal: true, alias: "caption", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], scrollHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "scrollHeight", required: false }] }], lazy: [{ type: i0.Input, args: [{ isSignal: true, alias: "lazy", required: false }] }], totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], resizableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "resizableColumns", required: false }] }], reorderableColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderableColumns", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], editOn: [{ type: i0.Input, args: [{ isSignal: true, alias: "editOn", required: false }] }], cellErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellErrors", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], dirtyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dirtyLabel", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], rowSelect: [{ type: i0.Output, args: ["rowSelect"] }], rowUnselect: [{ type: i0.Output, args: ["rowUnselect"] }], lazyLoad: [{ type: i0.Output, args: ["lazyLoad"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowExpand: [{ type: i0.Output, args: ["rowExpand"] }], rowCollapse: [{ type: i0.Output, args: ["rowCollapse"] }], colReorder: [{ type: i0.Output, args: ["colReorder"] }], cellCommit: [{ type: i0.Output, args: ["cellCommit"] }], cellTemplates: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SdsTableCell), { isSignal: true }] }], rowExpansion: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsTableRowExpansion), { isSignal: true }] }], numberEditor: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SdsInputNumber), { isSignal: true }] }] } });
|
|
9356
12815
|
|
|
9357
12816
|
/**
|
|
9358
12817
|
* A tiny transient-notification bus. Application code calls {@link success} / {@link error} / {@link info}
|
|
@@ -10579,6 +14038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10579
14038
|
*
|
|
10580
14039
|
* @example
|
|
10581
14040
|
* <sds-nav-item icon="home" label="Accueil" link="/" [exact]="true" />
|
|
14041
|
+
* <sds-nav-item icon="book-open" label="Menu" link="/menu" [badge]="3" badgeLabel="{count} issues" badgeTone="danger" />
|
|
10582
14042
|
*/
|
|
10583
14043
|
class SdsNavItem {
|
|
10584
14044
|
store = inject(SidebarStore);
|
|
@@ -10589,8 +14049,52 @@ class SdsNavItem {
|
|
|
10589
14049
|
exact = input(false, ...(ngDevMode ? [{ debugName: "exact", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
10590
14050
|
/** Manual active state (combined with routerLinkActive when a link is set). */
|
|
10591
14051
|
active = input(false, ...(ngDevMode ? [{ debugName: "active", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
14052
|
+
/**
|
|
14053
|
+
* Optional count shown as a pill after the label (e.g. menu issues to fix). Hidden when null or 0.
|
|
14054
|
+
* On the collapsed rail it sits on the icon's corner.
|
|
14055
|
+
*/
|
|
14056
|
+
badge = input(null, ...(ngDevMode ? [{ debugName: "badge" }] : []));
|
|
14057
|
+
/** Spoken text for the badge; `{count}` is replaced. Pass a translated string. */
|
|
14058
|
+
badgeLabel = input('{count} pending', ...(ngDevMode ? [{ debugName: "badgeLabel" }] : []));
|
|
14059
|
+
/** Counts above this read as "max+" (e.g. 99+). */
|
|
14060
|
+
badgeMax = input(99, ...(ngDevMode ? [{ debugName: "badgeMax" }] : []));
|
|
14061
|
+
/** Badge colour. `danger` for things that block, `brand` (default) for a neutral count. */
|
|
14062
|
+
badgeTone = input('brand', ...(ngDevMode ? [{ debugName: "badgeTone" }] : []));
|
|
10592
14063
|
select = output();
|
|
10593
14064
|
collapsed = this.store.collapsed;
|
|
14065
|
+
/** The badge's spoken text, or null when there is nothing to show. */
|
|
14066
|
+
badgeText = computed(() => {
|
|
14067
|
+
const n = this.badge();
|
|
14068
|
+
if (n === null || n === undefined || !Number.isFinite(n) || n <= 0)
|
|
14069
|
+
return null;
|
|
14070
|
+
return this.badgeLabel().replace('{count}', String(n));
|
|
14071
|
+
}, ...(ngDevMode ? [{ debugName: "badgeText" }] : []));
|
|
14072
|
+
badgeDisplay = computed(() => {
|
|
14073
|
+
const n = this.badge() ?? 0;
|
|
14074
|
+
return n > this.badgeMax() ? `${this.badgeMax()}+` : String(n);
|
|
14075
|
+
}, ...(ngDevMode ? [{ debugName: "badgeDisplay" }] : []));
|
|
14076
|
+
/** Accessible name on the collapsed rail, where the visible label is hidden. */
|
|
14077
|
+
collapsedName = computed(() => {
|
|
14078
|
+
const text = this.badgeText();
|
|
14079
|
+
return text ? `${this.label()}, ${text}` : this.label();
|
|
14080
|
+
}, ...(ngDevMode ? [{ debugName: "collapsedName" }] : []));
|
|
14081
|
+
badgeClass = computed(() => {
|
|
14082
|
+
const tone = this.badgeTone() === 'danger'
|
|
14083
|
+
? 'bg-danger text-white'
|
|
14084
|
+
: this.badgeTone() === 'neutral'
|
|
14085
|
+
? 'bg-muted text-ink-soft border border-border'
|
|
14086
|
+
: this.store.appearance() === 'surface'
|
|
14087
|
+
? 'bg-brand-600 text-white dark:bg-ink dark:text-surface'
|
|
14088
|
+
: 'bg-white text-brand-800';
|
|
14089
|
+
const base = 'ml-auto inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-full px-1.5 ' +
|
|
14090
|
+
'text-[11px] font-bold tabular-nums leading-none';
|
|
14091
|
+
// Collapsed desktop rail: a small pill pinned to the top-right of the centred icon.
|
|
14092
|
+
const collapsed = this.collapsed()
|
|
14093
|
+
? ' lg:absolute lg:right-1.5 lg:top-0.5 lg:ml-0 lg:h-4 lg:min-w-4 lg:px-1 lg:text-[10px]' +
|
|
14094
|
+
(this.store.appearance() === 'surface' ? ' lg:ring-2 lg:ring-surface' : '')
|
|
14095
|
+
: '';
|
|
14096
|
+
return `${base} ${tone}${collapsed}`;
|
|
14097
|
+
}, ...(ngDevMode ? [{ debugName: "badgeClass" }] : []));
|
|
10594
14098
|
itemClass(isActive) {
|
|
10595
14099
|
// A `surface` rail is light: an active item is a brand-tinted pill with a left accent bar, brand
|
|
10596
14100
|
// text and a soft shadow (matching the certicheck reference); inactive is muted ink that lifts on
|
|
@@ -10618,7 +14122,7 @@ class SdsNavItem {
|
|
|
10618
14122
|
accentBarClass = computed(() => 'pointer-events-none absolute left-1 top-1/2 h-5 w-1 -translate-y-1/2 rounded-full ' +
|
|
10619
14123
|
(this.store.appearance() === 'surface' ? 'bg-brand-500' : 'bg-white'), ...(ngDevMode ? [{ debugName: "accentBarClass" }] : []));
|
|
10620
14124
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10621
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsNavItem, isStandalone: true, selector: "sds-nav-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, exact: { classPropertyName: "exact", publicName: "exact", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
14125
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsNavItem, isStandalone: true, selector: "sds-nav-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, exact: { classPropertyName: "exact", publicName: "exact", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeLabel: { classPropertyName: "badgeLabel", publicName: "badgeLabel", isSignal: true, isRequired: false, transformFunction: null }, badgeMax: { classPropertyName: "badgeMax", publicName: "badgeMax", isSignal: true, isRequired: false, transformFunction: null }, badgeTone: { classPropertyName: "badgeTone", publicName: "badgeTone", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, host: { classAttribute: "block" }, ngImport: i0, template: `
|
|
10622
14126
|
@if (link() !== null) {
|
|
10623
14127
|
<a
|
|
10624
14128
|
[routerLink]="link()"
|
|
@@ -10626,8 +14130,8 @@ class SdsNavItem {
|
|
|
10626
14130
|
#rla="routerLinkActive"
|
|
10627
14131
|
[routerLinkActiveOptions]="{ exact: exact() }"
|
|
10628
14132
|
[class]="itemClass(rla.isActive || active())"
|
|
10629
|
-
[attr.title]="collapsed() ?
|
|
10630
|
-
[attr.aria-label]="collapsed() ?
|
|
14133
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14134
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10631
14135
|
[attr.aria-current]="rla.isActive || active() ? 'page' : null"
|
|
10632
14136
|
(click)="store.closeMobile()"
|
|
10633
14137
|
>
|
|
@@ -10640,8 +14144,8 @@ class SdsNavItem {
|
|
|
10640
14144
|
<button
|
|
10641
14145
|
type="button"
|
|
10642
14146
|
[class]="itemClass(active())"
|
|
10643
|
-
[attr.title]="collapsed() ?
|
|
10644
|
-
[attr.aria-label]="collapsed() ?
|
|
14147
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14148
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10645
14149
|
[attr.aria-current]="active() ? 'page' : null"
|
|
10646
14150
|
(click)="select.emit(); store.closeMobile()"
|
|
10647
14151
|
>
|
|
@@ -10658,6 +14162,12 @@ class SdsNavItem {
|
|
|
10658
14162
|
}
|
|
10659
14163
|
<!-- Label is hidden only on the collapsed desktop rail; always shown in the mobile drawer. -->
|
|
10660
14164
|
<span class="truncate" [class]="collapsed() ? 'lg:hidden' : ''">{{ label() }}</span>
|
|
14165
|
+
@if (badgeText(); as text) {
|
|
14166
|
+
<!-- The number is drawn; the words are what is spoken ("Menu, 3 pending"). On the collapsed
|
|
14167
|
+
desktop rail it becomes a small corner pill on the icon. -->
|
|
14168
|
+
<span [class]="badgeClass()" aria-hidden="true">{{ badgeDisplay() }}</span>
|
|
14169
|
+
<span class="sr-only">, {{ text }}</span>
|
|
14170
|
+
}
|
|
10661
14171
|
</ng-template>
|
|
10662
14172
|
`, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10663
14173
|
}
|
|
@@ -10676,8 +14186,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10676
14186
|
#rla="routerLinkActive"
|
|
10677
14187
|
[routerLinkActiveOptions]="{ exact: exact() }"
|
|
10678
14188
|
[class]="itemClass(rla.isActive || active())"
|
|
10679
|
-
[attr.title]="collapsed() ?
|
|
10680
|
-
[attr.aria-label]="collapsed() ?
|
|
14189
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14190
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10681
14191
|
[attr.aria-current]="rla.isActive || active() ? 'page' : null"
|
|
10682
14192
|
(click)="store.closeMobile()"
|
|
10683
14193
|
>
|
|
@@ -10690,8 +14200,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10690
14200
|
<button
|
|
10691
14201
|
type="button"
|
|
10692
14202
|
[class]="itemClass(active())"
|
|
10693
|
-
[attr.title]="collapsed() ?
|
|
10694
|
-
[attr.aria-label]="collapsed() ?
|
|
14203
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14204
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10695
14205
|
[attr.aria-current]="active() ? 'page' : null"
|
|
10696
14206
|
(click)="select.emit(); store.closeMobile()"
|
|
10697
14207
|
>
|
|
@@ -10708,10 +14218,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10708
14218
|
}
|
|
10709
14219
|
<!-- Label is hidden only on the collapsed desktop rail; always shown in the mobile drawer. -->
|
|
10710
14220
|
<span class="truncate" [class]="collapsed() ? 'lg:hidden' : ''">{{ label() }}</span>
|
|
14221
|
+
@if (badgeText(); as text) {
|
|
14222
|
+
<!-- The number is drawn; the words are what is spoken ("Menu, 3 pending"). On the collapsed
|
|
14223
|
+
desktop rail it becomes a small corner pill on the icon. -->
|
|
14224
|
+
<span [class]="badgeClass()" aria-hidden="true">{{ badgeDisplay() }}</span>
|
|
14225
|
+
<span class="sr-only">, {{ text }}</span>
|
|
14226
|
+
}
|
|
10711
14227
|
</ng-template>
|
|
10712
14228
|
`,
|
|
10713
14229
|
}]
|
|
10714
|
-
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], exact: [{ type: i0.Input, args: [{ isSignal: true, alias: "exact", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
|
|
14230
|
+
}], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], exact: [{ type: i0.Input, args: [{ isSignal: true, alias: "exact", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeLabel", required: false }] }], badgeMax: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeMax", required: false }] }], badgeTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeTone", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
|
|
10715
14231
|
|
|
10716
14232
|
/**
|
|
10717
14233
|
* The signed-in user's trigger and dropdown for the top bar: an avatar (with the name and email beside
|
|
@@ -11934,5 +15450,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
11934
15450
|
* Generated bundle index. Do not edit.
|
|
11935
15451
|
*/
|
|
11936
15452
|
|
|
11937
|
-
export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsDatePicker, SdsIcon, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenuItem, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsSearchCommand, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
|
|
15453
|
+
export { SDS_ALLERGENS, SDS_ALLERGEN_ICONS, SDS_ALLERGEN_LABELS_EN, SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAllergenIcon, SdsAvatar, SdsBadge, SdsBulkBar, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsChecklistRow, SdsDatePicker, SdsFileDrop, SdsIcon, SdsImageCropper, SdsInput, SdsInputNumber, SdsLanguageSwitcher, SdsLocalizedInput, SdsMenu, SdsMenuItem, SdsMenuTrigger, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsRadio, SdsScore, SdsSearchCommand, SdsSegmented, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsSortableHandle, SdsSortableItem, SdsSortableList, SdsStatusTile, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsTriStateChip, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFileMatchesAccept, sdsFormatBytes, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
|
|
11938
15454
|
//# sourceMappingURL=besaitech-ng-design-system.mjs.map
|