@besaitech/ng-design-system 0.0.51 → 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 +4016 -113
- package/fesm2022/besaitech-ng-design-system.mjs.map +1 -1
- package/index.d.ts +1011 -19
- 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, ElementRef,
|
|
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,43 @@ 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"/>',
|
|
127
|
+
// A question mark in a circle: the "may contain" mark on sds-tri-state-chip, and a generic help glyph.
|
|
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"/>',
|
|
129
|
+
// Stock / inventory (the kitchen stock board, the sold-out toolkit). Original glyph: three stacked boxes.
|
|
130
|
+
stock: '<rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/><rect x="8" y="3" width="8" height="8" rx="1"/><path d="M6 13v2.5h2V13"/><path d="M16 13v2.5h2V13"/><path d="M11 3v2.5h2V3"/>',
|
|
131
|
+
// The 14 EU allergens of Regulation (EU) 1169/2011 Annex II, in Annex order. Original Belvigo drawings
|
|
132
|
+
// (Lucide has none), same grammar as the rest of the set: 24x24, stroke only, 2px, round caps/joins.
|
|
133
|
+
// Names mirror the API enum (CEREALS_GLUTEN -> allergen-cereals-gluten); sds-allergen-icon maps them.
|
|
134
|
+
'allergen-cereals-gluten': '<path d="M12 22V8"/><path d="M12 8c-1.6-1.6-1.6-4.2 0-6 1.6 1.8 1.6 4.4 0 6z"/><path d="M12 13c-2.6 0-4.6-1.6-5.2-4.2 2.6 0 4.6 1.6 5.2 4.2z"/><path d="M12 13c2.6 0 4.6-1.6 5.2-4.2-2.6 0-4.6 1.6-5.2 4.2z"/><path d="M12 18c-2.6 0-4.6-1.6-5.2-4.2 2.6 0 4.6 1.6 5.2 4.2z"/><path d="M12 18c2.6 0 4.6-1.6 5.2-4.2-2.6 0-4.6 1.6-5.2 4.2z"/>',
|
|
135
|
+
'allergen-crustaceans': '<ellipse cx="12" cy="15" rx="6" ry="4"/><path d="M3.5 4a2.5 2.5 0 1 0 5 0"/><path d="M15.5 4a2.5 2.5 0 1 0 5 0"/><path d="M6 6.5 8.5 11.8"/><path d="M18 6.5 15.5 11.8"/><path d="M6 15H3"/><path d="M18 15h3"/><path d="M7.3 17.5 4.5 20"/><path d="M16.7 17.5 19.5 20"/><path d="M10.5 11.2V9.5"/><path d="M13.5 11.2V9.5"/>',
|
|
136
|
+
'allergen-eggs': '<path d="M12 2.5c-3.6 0-6.5 5.5-6.5 10.5a6.5 6.5 0 0 0 13 0c0-5-2.9-10.5-6.5-10.5z"/><path d="M9 13.5a3 3 0 0 0 2.5 3"/>',
|
|
137
|
+
'allergen-fish': '<path d="M2.5 12c2-3.5 5-5.5 8.5-5.5 3 0 5.5 2 7 5.5-1.5 3.5-4 5.5-7 5.5-3.5 0-6.5-2-8.5-5.5z"/><path d="M18 12l3.5-3.5v7z"/><path d="M10 9a4.5 4.5 0 0 1 0 6"/><path d="M6.5 11h.01"/>',
|
|
138
|
+
'allergen-peanuts': '<path d="M8.8 12a5 5 0 1 1 6.4 0 5 5 0 1 1-6.4 0z"/><path d="M10.5 6.5h.01"/><path d="M13.5 8.5h.01"/><path d="M10.5 15.5h.01"/><path d="M13.5 17.5h.01"/>',
|
|
139
|
+
'allergen-soybeans': '<path d="M15.88 3.88 3.88 15.88a3 3 0 0 0 4.24 4.24L20.12 8.12a3 3 0 0 0-4.24-4.24z"/><circle cx="8.5" cy="15.5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="15.5" cy="8.5" r="1.5"/><path d="M20.5 3.5 22 2"/>',
|
|
140
|
+
'allergen-milk': '<path d="M6 10v10.5A1.5 1.5 0 0 0 7.5 22h9a1.5 1.5 0 0 0 1.5-1.5V10"/><path d="M6 10l3-5h6l3 5z"/><path d="M9 5V2.5h6V5"/><path d="M12 13.5c-1 1.3-1.5 2.1-1.5 2.8a1.5 1.5 0 0 0 3 0c0-.7-.5-1.5-1.5-2.8z"/>',
|
|
141
|
+
'allergen-nuts': '<path d="M5 9.5C5 6.5 8 4 12 4s7 2.5 7 5.5z"/><path d="M6.5 9.5v1.5c0 4.5 2.5 8.5 5.5 10 3-1.5 5.5-5.5 5.5-10V9.5"/><path d="M12 4c0-1 .5-1.7 1.5-2"/>',
|
|
142
|
+
'allergen-celery': '<path d="M7 22c-.8-4-.5-8.5 1-12.5h8c1.5 4 1.8 8.5 1 12.5z"/><path d="M10.5 22c-.3-4-.2-8.5.5-12.5"/><path d="M13.5 22c.3-4 .2-8.5-.5-12.5"/><path d="M9.5 9.5 7.5 6.5"/><path d="M12 9.5V5"/><path d="M14.5 9.5l2-3"/><circle cx="6.5" cy="5" r="1.5"/><circle cx="12" cy="3.5" r="1.5"/><circle cx="17.5" cy="5" r="1.5"/>',
|
|
143
|
+
'allergen-mustard': '<rect x="5" y="7" width="14" height="3" rx="1"/><path d="M6 10v8.5A3.5 3.5 0 0 0 9.5 22h5a3.5 3.5 0 0 0 3.5-3.5V10"/><path d="M13.5 7l3-4.5"/><rect x="9" y="13" width="6" height="5" rx="1"/>',
|
|
144
|
+
'allergen-sesame': '<path d="M7 3c1.2 1.2 1.8 2.5 1.8 3.5a1.8 1.8 0 0 1-3.6 0C5.2 5.5 5.8 4.2 7 3z" transform="rotate(-25 7 6)"/><path d="M16.5 3.5c1.2 1.2 1.8 2.5 1.8 3.5a1.8 1.8 0 0 1-3.6 0c0-1 .6-2.3 1.8-3.5z" transform="rotate(30 16.5 6.5)"/><path d="M8.5 13c1.2 1.2 1.8 2.5 1.8 3.5a1.8 1.8 0 0 1-3.6 0c0-1 .6-2.3 1.8-3.5z" transform="rotate(15 8.5 16)"/><path d="M17 12c1.2 1.2 1.8 2.5 1.8 3.5a1.8 1.8 0 0 1-3.6 0c0-1 .6-2.3 1.8-3.5z" transform="rotate(-20 17 15)"/>',
|
|
145
|
+
'allergen-sulphites': '<path d="M7.5 3h9c.3 1.5.5 3 .5 4.5a5 5 0 0 1-10 0c0-1.5.2-3 .5-4.5z"/><path d="M12 12.5V21"/><path d="M8.5 21h7"/><path d="M7.2 7h9.6"/>',
|
|
146
|
+
'allergen-lupin': '<path d="M12 2.5a1.7 1.7 0 0 1 1.2 3 1.8 1.8 0 0 1 .8 3 1.9 1.9 0 0 1 .8 3 2 2 0 0 1-.3 2.8H9.5a2 2 0 0 1-.3-2.8 1.9 1.9 0 0 1 .8-3 1.8 1.8 0 0 1 .8-3 1.7 1.7 0 0 1 1.2-3z"/><path d="M10.8 8.5h2.4"/><path d="M10.2 11.5h3.6"/><path d="M12 14.3V22"/><path d="M12 19.5 7 17.5"/><path d="M12 19.5 8.5 16"/><path d="M12 19.5l5-2"/><path d="M12 19.5l3.5-3.5"/>',
|
|
147
|
+
'allergen-molluscs': '<path d="M12 19 4.5 11a7.5 7.5 0 0 1 15 0z"/><path d="M12 19V3.5"/><path d="M12 19 7.2 5.2"/><path d="M12 19l4.8-13.8"/><path d="M9.5 22 12 19l2.5 3z"/>',
|
|
111
148
|
};
|
|
112
149
|
|
|
113
150
|
const SIZE_PX = { sm: 16, md: 20, lg: 24, xl: 32 };
|
|
@@ -293,7 +330,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
293
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"] }]
|
|
294
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 }] }] } });
|
|
295
332
|
|
|
296
|
-
let nextId$
|
|
333
|
+
let nextId$l = 0;
|
|
297
334
|
const SIZES$a = {
|
|
298
335
|
sm: 'h-9 text-sm',
|
|
299
336
|
md: 'h-10 text-sm',
|
|
@@ -306,7 +343,7 @@ const SIZES$a = {
|
|
|
306
343
|
* sidebar search field).
|
|
307
344
|
*/
|
|
308
345
|
class SdsInput {
|
|
309
|
-
inputId = `sds-input-${nextId$
|
|
346
|
+
inputId = `sds-input-${nextId$l++}`;
|
|
310
347
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
311
348
|
/** Accessible name used when no visible [label] is provided. */
|
|
312
349
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -639,7 +676,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
639
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"] }]
|
|
640
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"] }] } });
|
|
641
678
|
|
|
642
|
-
let nextId$
|
|
679
|
+
let nextId$k = 0;
|
|
643
680
|
const HEIGHTS = {
|
|
644
681
|
sm: 'h-9',
|
|
645
682
|
md: 'h-10',
|
|
@@ -662,7 +699,7 @@ const TEXT = {
|
|
|
662
699
|
* {@link min}/{@link max} on blur and on every spinner step; ArrowUp / ArrowDown also step it.
|
|
663
700
|
*/
|
|
664
701
|
class SdsInputNumber {
|
|
665
|
-
inputId = `sds-input-number-${nextId$
|
|
702
|
+
inputId = `sds-input-number-${nextId$k++}`;
|
|
666
703
|
fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
|
|
667
704
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
668
705
|
/** Accessible name used when no visible [label] is provided. */
|
|
@@ -835,6 +872,20 @@ class SdsInputNumber {
|
|
|
835
872
|
el.value = v === null ? '' : this.format(v);
|
|
836
873
|
});
|
|
837
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
|
+
}
|
|
838
889
|
isActive(el) {
|
|
839
890
|
return typeof document !== 'undefined' && document.activeElement === el;
|
|
840
891
|
}
|
|
@@ -1369,7 +1420,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1369
1420
|
}]
|
|
1370
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 }] }] } });
|
|
1371
1422
|
|
|
1372
|
-
let nextId$
|
|
1423
|
+
let nextId$j = 0;
|
|
1373
1424
|
const SIZES$9 = {
|
|
1374
1425
|
sm: 'min-h-9 text-sm',
|
|
1375
1426
|
md: 'min-h-10 text-sm',
|
|
@@ -1389,7 +1440,7 @@ const SIZES$9 = {
|
|
|
1389
1440
|
* `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
|
|
1390
1441
|
*/
|
|
1391
1442
|
class SdsLocalizedInput {
|
|
1392
|
-
inputId = `sds-localized-input-${nextId$
|
|
1443
|
+
inputId = `sds-localized-input-${nextId$j++}`;
|
|
1393
1444
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
1394
1445
|
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
1395
1446
|
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
@@ -1645,7 +1696,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1645
1696
|
}]
|
|
1646
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 }] }] } });
|
|
1647
1698
|
|
|
1648
|
-
let nextId$
|
|
1699
|
+
let nextId$i = 0;
|
|
1649
1700
|
const SIZES$8 = {
|
|
1650
1701
|
md: 'h-12 w-11 text-lg',
|
|
1651
1702
|
lg: 'h-14 w-12 text-xl',
|
|
@@ -1662,7 +1713,7 @@ const SIZES$8 = {
|
|
|
1662
1713
|
* first box and have it land correctly.
|
|
1663
1714
|
*/
|
|
1664
1715
|
class SdsOtpInput {
|
|
1665
|
-
groupId = `sds-otp-${nextId$
|
|
1716
|
+
groupId = `sds-otp-${nextId$i++}`;
|
|
1666
1717
|
/** How many boxes / characters the code has. */
|
|
1667
1718
|
length = input(6, ...(ngDevMode ? [{ debugName: "length", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
1668
1719
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
@@ -1962,7 +2013,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1962
2013
|
}]
|
|
1963
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 }] }] } });
|
|
1964
2015
|
|
|
1965
|
-
let nextId$
|
|
2016
|
+
let nextId$h = 0;
|
|
1966
2017
|
const SIZES$7 = {
|
|
1967
2018
|
sm: 'text-sm px-3 py-2',
|
|
1968
2019
|
md: 'text-sm px-3.5 py-2.5',
|
|
@@ -1974,7 +2025,7 @@ const SIZES$7 = {
|
|
|
1974
2025
|
* [(ngModel)].
|
|
1975
2026
|
*/
|
|
1976
2027
|
class SdsTextarea {
|
|
1977
|
-
fieldId = `sds-textarea-${nextId$
|
|
2028
|
+
fieldId = `sds-textarea-${nextId$h++}`;
|
|
1978
2029
|
field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
1979
2030
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
1980
2031
|
/** Accessible name used when no visible [label] is provided. */
|
|
@@ -2227,7 +2278,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2227
2278
|
}]
|
|
2228
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 }] }] } });
|
|
2229
2280
|
|
|
2230
|
-
let nextId$
|
|
2281
|
+
let nextId$g = 0;
|
|
2231
2282
|
const SIZES$6 = {
|
|
2232
2283
|
sm: { track: 'h-5 w-9', knob: 'h-4 w-4', on: 'translate-x-4', icon: 11 },
|
|
2233
2284
|
md: { track: 'h-6 w-11', knob: 'h-5 w-5', on: 'translate-x-5', icon: 13 },
|
|
@@ -2240,7 +2291,7 @@ const SIZES$6 = {
|
|
|
2240
2291
|
* states.
|
|
2241
2292
|
*/
|
|
2242
2293
|
class SdsToggleSwitch {
|
|
2243
|
-
fieldId = `sds-toggle-switch-${nextId$
|
|
2294
|
+
fieldId = `sds-toggle-switch-${nextId$g++}`;
|
|
2244
2295
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2245
2296
|
/** Accessible name used when no visible [label] is provided. */
|
|
2246
2297
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -2484,7 +2535,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2484
2535
|
}]
|
|
2485
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 }] }] } });
|
|
2486
2537
|
|
|
2487
|
-
let nextId$
|
|
2538
|
+
let nextId$f = 0;
|
|
2488
2539
|
/**
|
|
2489
2540
|
* Boolean checkbox. Implements ControlValueAccessor — use with reactive forms
|
|
2490
2541
|
* or `[(ngModel)]`. Renders a styled box (brand-filled with a check when on)
|
|
@@ -2496,7 +2547,7 @@ let nextId$9 = 0;
|
|
|
2496
2547
|
* <sds-checkbox label="Fusion automatique" [(ngModel)]="autoMerge" />
|
|
2497
2548
|
*/
|
|
2498
2549
|
class SdsCheckbox {
|
|
2499
|
-
id = `sds-checkbox-${nextId$
|
|
2550
|
+
id = `sds-checkbox-${nextId$f++}`;
|
|
2500
2551
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2501
2552
|
/** Accessible name used when no visible [label] is provided. */
|
|
2502
2553
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -2630,7 +2681,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2630
2681
|
}]
|
|
2631
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 }] }] } });
|
|
2632
2683
|
|
|
2633
|
-
let nextId$
|
|
2684
|
+
let nextId$e = 0;
|
|
2634
2685
|
/**
|
|
2635
2686
|
* Mounted radios by group name. A pick updates the rest of its group, because a native radio says
|
|
2636
2687
|
* nothing when the browser unchecks it — without this a reactive form would leave the old choice drawn.
|
|
@@ -2656,7 +2707,7 @@ const groups = new Map();
|
|
|
2656
2707
|
* <sds-radio name="size" value="large" label="Grand" [(ngModel)]="size" />
|
|
2657
2708
|
*/
|
|
2658
2709
|
class SdsRadio {
|
|
2659
|
-
id = `sds-radio-${nextId$
|
|
2710
|
+
id = `sds-radio-${nextId$e++}`;
|
|
2660
2711
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2661
2712
|
/** Accessible name used when no visible [label] is provided. */
|
|
2662
2713
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -2957,66 +3008,3211 @@ class SdsBadge {
|
|
|
2957
3008
|
class="inline-flex items-center gap-1.5 rounded-full border font-semibold uppercase tracking-wide"
|
|
2958
3009
|
[class]="classes()"
|
|
2959
3010
|
>
|
|
2960
|
-
@if (dot()) {
|
|
2961
|
-
<span class="h-1.5 w-1.5 rounded-full" [class]="dotClass()"></span>
|
|
3011
|
+
@if (dot()) {
|
|
3012
|
+
<span class="h-1.5 w-1.5 rounded-full" [class]="dotClass()"></span>
|
|
3013
|
+
}
|
|
3014
|
+
<ng-content />
|
|
3015
|
+
</span>
|
|
3016
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3017
|
+
}
|
|
3018
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsBadge, decorators: [{
|
|
3019
|
+
type: Component,
|
|
3020
|
+
args: [{
|
|
3021
|
+
selector: 'sds-badge',
|
|
3022
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3023
|
+
host: { class: 'inline-flex' },
|
|
3024
|
+
template: `
|
|
3025
|
+
<span
|
|
3026
|
+
class="inline-flex items-center gap-1.5 rounded-full border font-semibold uppercase tracking-wide"
|
|
3027
|
+
[class]="classes()"
|
|
3028
|
+
>
|
|
3029
|
+
@if (dot()) {
|
|
3030
|
+
<span class="h-1.5 w-1.5 rounded-full" [class]="dotClass()"></span>
|
|
3031
|
+
}
|
|
3032
|
+
<ng-content />
|
|
3033
|
+
</span>
|
|
3034
|
+
`,
|
|
3035
|
+
}]
|
|
3036
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], dot: [{ type: i0.Input, args: [{ isSignal: true, alias: "dot", required: false }] }] } });
|
|
3037
|
+
|
|
3038
|
+
const VARIANTS = {
|
|
3039
|
+
elevated: 'bg-surface border border-border shadow-sm',
|
|
3040
|
+
outlined: 'bg-surface border border-border',
|
|
3041
|
+
subtle: 'bg-muted border border-border',
|
|
3042
|
+
};
|
|
3043
|
+
/**
|
|
3044
|
+
* Generic surface container. Set [interactive]="true" for hover affordance
|
|
3045
|
+
* (used by the dashboard action cards).
|
|
3046
|
+
*/
|
|
3047
|
+
class SdsCard {
|
|
3048
|
+
variant = input('elevated', ...(ngDevMode ? [{ debugName: "variant" }] : []));
|
|
3049
|
+
/** Inner padding utility (override with e.g. "p-4"). */
|
|
3050
|
+
padding = input('p-5', ...(ngDevMode ? [{ debugName: "padding" }] : []));
|
|
3051
|
+
interactive = input(false, ...(ngDevMode ? [{ debugName: "interactive", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3052
|
+
hostClass = computed(() => {
|
|
3053
|
+
const parts = ['block rounded-card', VARIANTS[this.variant()], this.padding()];
|
|
3054
|
+
if (this.interactive()) {
|
|
3055
|
+
parts.push('cursor-pointer transition-all duration-150 hover:border-brand-300 hover:shadow-md');
|
|
3056
|
+
}
|
|
3057
|
+
return parts.join(' ');
|
|
3058
|
+
}, ...(ngDevMode ? [{ debugName: "hostClass" }] : []));
|
|
3059
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3060
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.25", type: SdsCard, isStandalone: true, selector: "sds-card", inputs: { variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "hostClass()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3061
|
+
}
|
|
3062
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsCard, decorators: [{
|
|
3063
|
+
type: Component,
|
|
3064
|
+
args: [{
|
|
3065
|
+
selector: 'sds-card',
|
|
3066
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3067
|
+
host: { '[class]': 'hostClass()' },
|
|
3068
|
+
template: `<ng-content />`,
|
|
3069
|
+
}]
|
|
3070
|
+
}], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }] } });
|
|
3071
|
+
|
|
3072
|
+
/** Every allergen in Annex II order — iterate it to render a declaration grid of 14. */
|
|
3073
|
+
const SDS_ALLERGENS = [
|
|
3074
|
+
'CEREALS_GLUTEN',
|
|
3075
|
+
'CRUSTACEANS',
|
|
3076
|
+
'EGGS',
|
|
3077
|
+
'FISH',
|
|
3078
|
+
'PEANUTS',
|
|
3079
|
+
'SOYBEANS',
|
|
3080
|
+
'MILK',
|
|
3081
|
+
'NUTS',
|
|
3082
|
+
'CELERY',
|
|
3083
|
+
'MUSTARD',
|
|
3084
|
+
'SESAME',
|
|
3085
|
+
'SULPHITES',
|
|
3086
|
+
'LUPIN',
|
|
3087
|
+
'MOLLUSCS',
|
|
3088
|
+
];
|
|
3089
|
+
/** Registry icon for each allergen. */
|
|
3090
|
+
const SDS_ALLERGEN_ICONS = {
|
|
3091
|
+
CEREALS_GLUTEN: 'allergen-cereals-gluten',
|
|
3092
|
+
CRUSTACEANS: 'allergen-crustaceans',
|
|
3093
|
+
EGGS: 'allergen-eggs',
|
|
3094
|
+
FISH: 'allergen-fish',
|
|
3095
|
+
PEANUTS: 'allergen-peanuts',
|
|
3096
|
+
SOYBEANS: 'allergen-soybeans',
|
|
3097
|
+
MILK: 'allergen-milk',
|
|
3098
|
+
NUTS: 'allergen-nuts',
|
|
3099
|
+
CELERY: 'allergen-celery',
|
|
3100
|
+
MUSTARD: 'allergen-mustard',
|
|
3101
|
+
SESAME: 'allergen-sesame',
|
|
3102
|
+
SULPHITES: 'allergen-sulphites',
|
|
3103
|
+
LUPIN: 'allergen-lupin',
|
|
3104
|
+
MOLLUSCS: 'allergen-molluscs',
|
|
3105
|
+
};
|
|
3106
|
+
/**
|
|
3107
|
+
* English names, as a fallback and for the showcase. An app serving more than one language passes its
|
|
3108
|
+
* own translated name through [label] — the library never picks a language for you.
|
|
3109
|
+
*/
|
|
3110
|
+
const SDS_ALLERGEN_LABELS_EN = {
|
|
3111
|
+
CEREALS_GLUTEN: 'Cereals containing gluten',
|
|
3112
|
+
CRUSTACEANS: 'Crustaceans',
|
|
3113
|
+
EGGS: 'Eggs',
|
|
3114
|
+
FISH: 'Fish',
|
|
3115
|
+
PEANUTS: 'Peanuts',
|
|
3116
|
+
SOYBEANS: 'Soybeans',
|
|
3117
|
+
MILK: 'Milk',
|
|
3118
|
+
NUTS: 'Nuts',
|
|
3119
|
+
CELERY: 'Celery',
|
|
3120
|
+
MUSTARD: 'Mustard',
|
|
3121
|
+
SESAME: 'Sesame',
|
|
3122
|
+
SULPHITES: 'Sulphites',
|
|
3123
|
+
LUPIN: 'Lupin',
|
|
3124
|
+
MOLLUSCS: 'Molluscs',
|
|
3125
|
+
};
|
|
3126
|
+
/** Accepts the enum spelling, and tolerates `milk` / `cereals-gluten` so a hand-typed value still resolves. */
|
|
3127
|
+
function normalize(value) {
|
|
3128
|
+
if (!value)
|
|
3129
|
+
return null;
|
|
3130
|
+
const key = String(value).trim().toUpperCase().replace(/-/g, '_');
|
|
3131
|
+
return key in SDS_ALLERGEN_ICONS ? key : null;
|
|
3132
|
+
}
|
|
3133
|
+
/**
|
|
3134
|
+
* The pictogram for one EU allergen, taking the API enum value directly. With a [label] it is an image
|
|
3135
|
+
* with that accessible name (role="img"); without one it is decorative (aria-hidden), for when visible
|
|
3136
|
+
* text beside it already names the allergen.
|
|
3137
|
+
*
|
|
3138
|
+
* @example
|
|
3139
|
+
* <sds-allergen-icon allergen="MILK" label="Milk" />
|
|
3140
|
+
* <sds-allergen-icon [allergen]="a" [size]="16" /> <!-- decorative: text next to it names it -->
|
|
3141
|
+
*/
|
|
3142
|
+
class SdsAllergenIcon {
|
|
3143
|
+
/** The allergen, as the API enum value (`MILK`, `CEREALS_GLUTEN`, …). */
|
|
3144
|
+
allergen = input.required(...(ngDevMode ? [{ debugName: "allergen" }] : []));
|
|
3145
|
+
/** Accessible name. Empty (default) = decorative, hidden from assistive tech. */
|
|
3146
|
+
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
3147
|
+
/** Also show the [label] as a native tooltip on hover. */
|
|
3148
|
+
showTitle = input(false, ...(ngDevMode ? [{ debugName: "showTitle", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3149
|
+
/** Square size: a pixel number, a numeric string, or a keyword (sm·md·lg·xl). */
|
|
3150
|
+
size = input(20, ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
3151
|
+
/** Stroke width of the line icon. */
|
|
3152
|
+
strokeWidth = input(2, ...(ngDevMode ? [{ debugName: "strokeWidth" }] : []));
|
|
3153
|
+
iconName = computed(() => {
|
|
3154
|
+
const key = normalize(this.allergen());
|
|
3155
|
+
return key ? SDS_ALLERGEN_ICONS[key] : null;
|
|
3156
|
+
}, ...(ngDevMode ? [{ debugName: "iconName" }] : []));
|
|
3157
|
+
constructor() {
|
|
3158
|
+
if (isDevMode()) {
|
|
3159
|
+
effect(() => {
|
|
3160
|
+
if (!this.iconName()) {
|
|
3161
|
+
console.warn(`sds-allergen-icon: unknown allergen "${this.allergen()}".`);
|
|
3162
|
+
}
|
|
3163
|
+
});
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAllergenIcon, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3167
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsAllergenIcon, isStandalone: true, selector: "sds-allergen-icon", inputs: { allergen: { classPropertyName: "allergen", publicName: "allergen", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, showTitle: { classPropertyName: "showTitle", publicName: "showTitle", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, strokeWidth: { classPropertyName: "strokeWidth", publicName: "strokeWidth", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.role": "label() ? \"img\" : null", "attr.aria-label": "label() || null", "attr.aria-hidden": "label() ? null : \"true\"", "attr.title": "showTitle() && label() ? label() : null" }, classAttribute: "inline-flex shrink-0" }, ngImport: i0, template: `
|
|
3168
|
+
@if (iconName(); as name) {
|
|
3169
|
+
<sds-icon [name]="name" [size]="size()" [strokeWidth]="strokeWidth()" />
|
|
3170
|
+
}
|
|
3171
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3172
|
+
}
|
|
3173
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsAllergenIcon, decorators: [{
|
|
3174
|
+
type: Component,
|
|
3175
|
+
args: [{
|
|
3176
|
+
selector: 'sds-allergen-icon',
|
|
3177
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
3178
|
+
imports: [SdsIcon],
|
|
3179
|
+
host: {
|
|
3180
|
+
class: 'inline-flex shrink-0',
|
|
3181
|
+
'[attr.role]': 'label() ? "img" : null',
|
|
3182
|
+
'[attr.aria-label]': 'label() || null',
|
|
3183
|
+
'[attr.aria-hidden]': 'label() ? null : "true"',
|
|
3184
|
+
'[attr.title]': 'showTitle() && label() ? label() : null',
|
|
3185
|
+
},
|
|
3186
|
+
template: `
|
|
3187
|
+
@if (iconName(); as name) {
|
|
3188
|
+
<sds-icon [name]="name" [size]="size()" [strokeWidth]="strokeWidth()" />
|
|
3189
|
+
}
|
|
3190
|
+
`,
|
|
3191
|
+
}]
|
|
3192
|
+
}], ctorParameters: () => [], propDecorators: { allergen: [{ type: i0.Input, args: [{ isSignal: true, alias: "allergen", required: true }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], showTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTitle", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], strokeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "strokeWidth", required: false }] }] } });
|
|
3193
|
+
|
|
3194
|
+
/** Activation order: none → contains → may-contain → none. */
|
|
3195
|
+
const ORDER = ['none', 'contains', 'may-contain'];
|
|
3196
|
+
let nextId$d = 0;
|
|
3197
|
+
/**
|
|
3198
|
+
* A chip that cycles through three states — `none`, `contains`, `may-contain` — built for the 14-tile
|
|
3199
|
+
* allergen declaration grid, but generic (any "no / yes / maybe" declaration).
|
|
3200
|
+
*
|
|
3201
|
+
* **The state never relies on colour alone.** Each state has its own border (thin solid · thick solid
|
|
3202
|
+
* and filled · thick dashed), its own mark (none · a check · a question mark), a visible state caption
|
|
3203
|
+
* (turn off with `showState`), and the state text is part of the button's accessible name.
|
|
3204
|
+
*
|
|
3205
|
+
* Interaction: click, Enter or Space moves to the next state; Shift+Enter / Shift+Space moves back;
|
|
3206
|
+
* Delete or Backspace resets to `none`. A polite live region announces the new state after a change.
|
|
3207
|
+
* The value is a two-way `model` — bind `[(value)]`.
|
|
3208
|
+
*
|
|
3209
|
+
* All visible and spoken text comes from inputs with English defaults; pass translated labels.
|
|
3210
|
+
*
|
|
3211
|
+
* @example
|
|
3212
|
+
* <sds-tri-state-chip label="Milk" icon="allergen-milk" [(value)]="state.MILK"
|
|
3213
|
+
* containsLabel="Contains" mayContainLabel="May contain" noneLabel="Not present" />
|
|
3214
|
+
*/
|
|
3215
|
+
class SdsTriStateChip {
|
|
3216
|
+
chipId = `sds-tri-state-chip-${nextId$d++}`;
|
|
3217
|
+
/** Current state. Two-way: `[(value)]`. */
|
|
3218
|
+
value = model('none', ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
3219
|
+
/** Visible name (e.g. the allergen). Required — the chip has no other accessible name. */
|
|
3220
|
+
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
3221
|
+
/** Optional leading registry icon, e.g. an `allergen-*` pictogram. */
|
|
3222
|
+
icon = input(null, ...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
3223
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
3224
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3225
|
+
/** Show the state as a small visible caption under the label. */
|
|
3226
|
+
showState = input(true, ...(ngDevMode ? [{ debugName: "showState", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
3227
|
+
/** State text for `none` (visible caption and spoken). */
|
|
3228
|
+
noneLabel = input('Not present', ...(ngDevMode ? [{ debugName: "noneLabel" }] : []));
|
|
3229
|
+
/** State text for `contains`. */
|
|
3230
|
+
containsLabel = input('Contains', ...(ngDevMode ? [{ debugName: "containsLabel" }] : []));
|
|
3231
|
+
/** State text for `may-contain`. */
|
|
3232
|
+
mayContainLabel = input('May contain', ...(ngDevMode ? [{ debugName: "mayContainLabel" }] : []));
|
|
3233
|
+
/**
|
|
3234
|
+
* Optional usage hint read after the name, e.g. "Press to change". Off by default: a button that
|
|
3235
|
+
* announces its state and changes on activation is self-explanatory to most users.
|
|
3236
|
+
*/
|
|
3237
|
+
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
3238
|
+
/** Last user-driven change, spoken by the live region. Programmatic writes stay silent. */
|
|
3239
|
+
announcement = signal('', ...(ngDevMode ? [{ debugName: "announcement" }] : []));
|
|
3240
|
+
stateText = computed(() => this.textFor(this.value()), ...(ngDevMode ? [{ debugName: "stateText" }] : []));
|
|
3241
|
+
chipClass = computed(() => {
|
|
3242
|
+
const sm = this.size() === 'sm';
|
|
3243
|
+
const base = 'relative flex w-full min-w-0 cursor-pointer select-none items-center rounded-control border-2 ' +
|
|
3244
|
+
'text-left transition-colors focus-visible:outline-none focus-visible:ring-2 ' +
|
|
3245
|
+
'focus-visible:ring-brand-500 focus-visible:ring-offset-2 focus-visible:ring-offset-surface ' +
|
|
3246
|
+
'disabled:cursor-not-allowed disabled:opacity-60 ' +
|
|
3247
|
+
(sm ? 'min-h-9 gap-2 px-2 py-1' : 'min-h-12 gap-2.5 px-3 py-2');
|
|
3248
|
+
switch (this.value()) {
|
|
3249
|
+
case 'contains':
|
|
3250
|
+
// Filled + solid thick border + check. In dark mode a monochrome brand-600 is a dim grey, so the
|
|
3251
|
+
// fill inverts to ink/surface (the same rule as sds-toggle-switch and sds-button primary).
|
|
3252
|
+
return (`${base} border-brand-600 bg-brand-600 text-white ` +
|
|
3253
|
+
'dark:border-ink dark:bg-ink dark:text-surface hover:bg-brand-700 dark:hover:bg-ink-soft');
|
|
3254
|
+
case 'may-contain':
|
|
3255
|
+
// Unfilled + DASHED thick border + question mark.
|
|
3256
|
+
return (`${base} border-dashed border-brand-600 bg-brand-50 text-ink ` +
|
|
3257
|
+
'dark:border-ink-soft dark:bg-muted hover:bg-brand-100 dark:hover:bg-surface');
|
|
3258
|
+
default:
|
|
3259
|
+
return `${base} border-border bg-surface text-ink-soft hover:border-ink-faint hover:text-ink`;
|
|
3260
|
+
}
|
|
3261
|
+
}, ...(ngDevMode ? [{ debugName: "chipClass" }] : []));
|
|
3262
|
+
captionClass = computed(() => {
|
|
3263
|
+
switch (this.value()) {
|
|
3264
|
+
case 'contains':
|
|
3265
|
+
return 'text-white/85 dark:text-surface/85';
|
|
3266
|
+
case 'may-contain':
|
|
3267
|
+
return 'text-ink-soft';
|
|
3268
|
+
default:
|
|
3269
|
+
return 'text-ink-faint';
|
|
3270
|
+
}
|
|
3271
|
+
}, ...(ngDevMode ? [{ debugName: "captionClass" }] : []));
|
|
3272
|
+
onClick(event) {
|
|
3273
|
+
if (this.disabled())
|
|
3274
|
+
return;
|
|
3275
|
+
// A real Shift+click steps back too; keyboard activation is handled in onKeydown.
|
|
3276
|
+
this.step(event.shiftKey ? -1 : 1);
|
|
3277
|
+
}
|
|
3278
|
+
onKeydown(event) {
|
|
3279
|
+
if (this.disabled())
|
|
3280
|
+
return;
|
|
3281
|
+
if ((event.key === 'Enter' || event.key === ' ') && event.shiftKey) {
|
|
3282
|
+
// Handle it here so the native button click (always "forward") does not also fire.
|
|
3283
|
+
event.preventDefault();
|
|
3284
|
+
this.step(-1);
|
|
3285
|
+
}
|
|
3286
|
+
else if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
3287
|
+
event.preventDefault();
|
|
3288
|
+
this.set('none');
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
step(delta) {
|
|
3292
|
+
const i = ORDER.indexOf(this.value());
|
|
3293
|
+
const next = ORDER[(Math.max(0, i) + delta + ORDER.length) % ORDER.length];
|
|
3294
|
+
this.set(next);
|
|
3295
|
+
}
|
|
3296
|
+
set(next) {
|
|
3297
|
+
if (next === this.value())
|
|
3298
|
+
return;
|
|
3299
|
+
this.value.set(next);
|
|
3300
|
+
this.announcement.set(`${this.label()}: ${this.textFor(next)}`);
|
|
3301
|
+
}
|
|
3302
|
+
textFor(state) {
|
|
3303
|
+
switch (state) {
|
|
3304
|
+
case 'contains':
|
|
3305
|
+
return this.containsLabel();
|
|
3306
|
+
case 'may-contain':
|
|
3307
|
+
return this.mayContainLabel();
|
|
3308
|
+
default:
|
|
3309
|
+
return this.noneLabel();
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTriStateChip, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3313
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTriStateChip, isStandalone: true, selector: "sds-tri-state-chip", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, showState: { classPropertyName: "showState", publicName: "showState", isSignal: true, isRequired: false, transformFunction: null }, noneLabel: { classPropertyName: "noneLabel", publicName: "noneLabel", isSignal: true, isRequired: false, transformFunction: null }, containsLabel: { classPropertyName: "containsLabel", publicName: "containsLabel", isSignal: true, isRequired: false, transformFunction: null }, mayContainLabel: { classPropertyName: "mayContainLabel", publicName: "mayContainLabel", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, host: { classAttribute: "inline-flex min-w-0" }, ngImport: i0, template: `
|
|
3314
|
+
<button
|
|
3315
|
+
type="button"
|
|
3316
|
+
[id]="chipId"
|
|
3317
|
+
[class]="chipClass()"
|
|
3318
|
+
[disabled]="disabled()"
|
|
3319
|
+
[attr.data-state]="value()"
|
|
3320
|
+
[attr.aria-describedby]="hint() ? chipId + '-hint' : null"
|
|
3321
|
+
(click)="onClick($event)"
|
|
3322
|
+
(keydown)="onKeydown($event)"
|
|
3323
|
+
>
|
|
3324
|
+
@if (icon()) {
|
|
3325
|
+
<sds-icon [name]="icon()!" [size]="size() === 'sm' ? 18 : 22" class="shrink-0" />
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
<span class="flex min-w-0 flex-1 flex-col text-left leading-tight">
|
|
3329
|
+
<!-- Up to two lines, so "Cereals containing gluten" stays readable in a narrow grid cell. -->
|
|
3330
|
+
<span
|
|
3331
|
+
class="line-clamp-2 break-words font-semibold"
|
|
3332
|
+
[class]="size() === 'sm' ? 'text-xs' : 'text-sm'"
|
|
3333
|
+
>{{ label() }}</span
|
|
3334
|
+
>
|
|
3335
|
+
@if (showState()) {
|
|
3336
|
+
<span
|
|
3337
|
+
class="truncate text-[11px] font-medium"
|
|
3338
|
+
[class]="captionClass()"
|
|
3339
|
+
aria-hidden="true"
|
|
3340
|
+
>{{ stateText() }}</span
|
|
3341
|
+
>
|
|
3342
|
+
}
|
|
3343
|
+
</span>
|
|
3344
|
+
<!-- The state is part of the accessible name ("Milk, may contain"), visible caption or not. -->
|
|
3345
|
+
<span class="sr-only">, {{ stateText() }}</span>
|
|
3346
|
+
|
|
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
|
+
/>
|
|
2962
6063
|
}
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
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())"
|
|
6092
|
+
>
|
|
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>
|
|
6117
|
+
`, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2966
6118
|
}
|
|
2967
|
-
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: [{
|
|
2968
6120
|
type: Component,
|
|
2969
6121
|
args: [{
|
|
2970
|
-
selector: 'sds-
|
|
6122
|
+
selector: 'sds-image-cropper',
|
|
2971
6123
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2972
|
-
|
|
6124
|
+
imports: [SdsIcon],
|
|
6125
|
+
host: { class: 'block' },
|
|
2973
6126
|
template: `
|
|
2974
|
-
<
|
|
2975
|
-
|
|
2976
|
-
|
|
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)"
|
|
6143
|
+
(keydown)="onKeydown($event)"
|
|
2977
6144
|
>
|
|
2978
|
-
@if (
|
|
2979
|
-
<
|
|
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
|
+
/>
|
|
2980
6159
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
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>
|
|
2986
6180
|
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
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())"
|
|
6188
|
+
>
|
|
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>
|
|
6213
|
+
`,
|
|
3018
6214
|
}]
|
|
3019
|
-
}], 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"] }] } });
|
|
3020
6216
|
|
|
3021
6217
|
/** Small, dependency-free date helpers (day-granularity, local time). */
|
|
3022
6218
|
function startOfDay(d) {
|
|
@@ -7855,6 +11051,16 @@ let nextId$1 = 0;
|
|
|
7855
11051
|
* every filtered row regardless of pagination (this is an intentional
|
|
7856
11052
|
* deviation from PrimeNG's per-page select-all).
|
|
7857
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
|
+
*
|
|
7858
11064
|
* @example
|
|
7859
11065
|
* <sds-table [value]="staff" [columns]="cols" dataKey="id"
|
|
7860
11066
|
* sortable paginator [rows]="10" globalFilter
|
|
@@ -7863,6 +11069,11 @@ let nextId$1 = 0;
|
|
|
7863
11069
|
* <sds-badge [variant]="row.actif ? 'success' : 'neutral'">{{ value }}</sds-badge>
|
|
7864
11070
|
* </ng-template>
|
|
7865
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)" />
|
|
7866
11077
|
*/
|
|
7867
11078
|
class SdsTable {
|
|
7868
11079
|
tableId = `sds-table-${nextId$1++}`;
|
|
@@ -7921,6 +11132,16 @@ class SdsTable {
|
|
|
7921
11132
|
resizableColumns = input(false, ...(ngDevMode ? [{ debugName: "resizableColumns", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
7922
11133
|
/** Drag a column header to reorder columns. */
|
|
7923
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" }] : []));
|
|
7924
11145
|
// --- outputs --------------------------------------------------------------
|
|
7925
11146
|
sortChange = output();
|
|
7926
11147
|
pageChange = output();
|
|
@@ -7934,6 +11155,8 @@ class SdsTable {
|
|
|
7934
11155
|
rowCollapse = output();
|
|
7935
11156
|
/** Emitted after a drag-reorder with the moved field and its old/new index. */
|
|
7936
11157
|
colReorder = output();
|
|
11158
|
+
/** Emitted when an edited cell is committed with a CHANGED value. Apply it to your data. */
|
|
11159
|
+
cellCommit = output();
|
|
7937
11160
|
// --- custom cell templates ------------------------------------------------
|
|
7938
11161
|
cellTemplates = contentChildren(SdsTableCell, ...(ngDevMode ? [{ debugName: "cellTemplates" }] : []));
|
|
7939
11162
|
templateMap = computed(() => {
|
|
@@ -8186,7 +11409,7 @@ class SdsTable {
|
|
|
8186
11409
|
}
|
|
8187
11410
|
/** Enter/Space on a (single-mode) focusable row toggles its selection. */
|
|
8188
11411
|
onRowKeydown(event, row) {
|
|
8189
|
-
if (this.selectionMode() !== 'single' || this.loading())
|
|
11412
|
+
if (this.selectionMode() !== 'single' || this.loading() || this.editable())
|
|
8190
11413
|
return;
|
|
8191
11414
|
event.preventDefault();
|
|
8192
11415
|
this.onRowClick(row);
|
|
@@ -8222,6 +11445,7 @@ class SdsTable {
|
|
|
8222
11445
|
}
|
|
8223
11446
|
// --- paginator ------------------------------------------------------------
|
|
8224
11447
|
onPageChange(state) {
|
|
11448
|
+
this.editing.set(null);
|
|
8225
11449
|
this.pageChange.emit(state);
|
|
8226
11450
|
if (this.lazy())
|
|
8227
11451
|
this.emitLazy();
|
|
@@ -8334,6 +11558,408 @@ class SdsTable {
|
|
|
8334
11558
|
return row[k];
|
|
8335
11559
|
return index;
|
|
8336
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
|
+
}
|
|
8337
11963
|
// --- styling --------------------------------------------------------------
|
|
8338
11964
|
headCellClass(col) {
|
|
8339
11965
|
const align = ALIGN[col?.align ?? 'left'];
|
|
@@ -8366,7 +11992,7 @@ class SdsTable {
|
|
|
8366
11992
|
: 'border-border bg-surface';
|
|
8367
11993
|
}
|
|
8368
11994
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8369
|
-
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: `
|
|
8370
11996
|
<div class="rounded-card border border-border bg-surface overflow-hidden">
|
|
8371
11997
|
<!-- Toolbar: caption + global filter -->
|
|
8372
11998
|
@if (caption() || globalFilter()) {
|
|
@@ -8411,6 +12037,8 @@ class SdsTable {
|
|
|
8411
12037
|
>
|
|
8412
12038
|
<table
|
|
8413
12039
|
class="w-full border-collapse text-left"
|
|
12040
|
+
[attr.role]="editable() ? 'grid' : null"
|
|
12041
|
+
(keydown)="onGridKeydown($event)"
|
|
8414
12042
|
[attr.aria-busy]="loading() ? 'true' : null"
|
|
8415
12043
|
[attr.aria-labelledby]="caption() ? tableId + '-caption' : null"
|
|
8416
12044
|
[attr.aria-label]="caption() ? null : ariaLabel()"
|
|
@@ -8514,7 +12142,7 @@ class SdsTable {
|
|
|
8514
12142
|
@for (row of displayRows(); track trackRow($index, row); let i = $index) {
|
|
8515
12143
|
<tr
|
|
8516
12144
|
[class]="rowClass(row, i)"
|
|
8517
|
-
[attr.tabindex]="selectionMode() === 'single' && !loading() ? 0 : null"
|
|
12145
|
+
[attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
|
|
8518
12146
|
[attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
|
|
8519
12147
|
(click)="onRowClick(row)"
|
|
8520
12148
|
(keydown.enter)="onRowKeydown($event, row)"
|
|
@@ -8580,20 +12208,128 @@ class SdsTable {
|
|
|
8580
12208
|
</td>
|
|
8581
12209
|
}
|
|
8582
12210
|
|
|
8583
|
-
@for (col of effectiveColumns(); track col.field) {
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
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
|
+
}
|
|
8597
12333
|
}
|
|
8598
12334
|
</tr>
|
|
8599
12335
|
|
|
@@ -8660,14 +12396,14 @@ class SdsTable {
|
|
|
8660
12396
|
</div>
|
|
8661
12397
|
}
|
|
8662
12398
|
</div>
|
|
8663
|
-
`, 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 });
|
|
8664
12400
|
}
|
|
8665
12401
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, decorators: [{
|
|
8666
12402
|
type: Component,
|
|
8667
12403
|
args: [{
|
|
8668
12404
|
selector: 'sds-table',
|
|
8669
12405
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
8670
|
-
imports: [SdsIcon, SdsPaginator, NgTemplateOutlet],
|
|
12406
|
+
imports: [SdsIcon, SdsPaginator, SdsInputNumber, NgTemplateOutlet],
|
|
8671
12407
|
host: { class: 'block' },
|
|
8672
12408
|
template: `
|
|
8673
12409
|
<div class="rounded-card border border-border bg-surface overflow-hidden">
|
|
@@ -8714,6 +12450,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8714
12450
|
>
|
|
8715
12451
|
<table
|
|
8716
12452
|
class="w-full border-collapse text-left"
|
|
12453
|
+
[attr.role]="editable() ? 'grid' : null"
|
|
12454
|
+
(keydown)="onGridKeydown($event)"
|
|
8717
12455
|
[attr.aria-busy]="loading() ? 'true' : null"
|
|
8718
12456
|
[attr.aria-labelledby]="caption() ? tableId + '-caption' : null"
|
|
8719
12457
|
[attr.aria-label]="caption() ? null : ariaLabel()"
|
|
@@ -8817,7 +12555,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8817
12555
|
@for (row of displayRows(); track trackRow($index, row); let i = $index) {
|
|
8818
12556
|
<tr
|
|
8819
12557
|
[class]="rowClass(row, i)"
|
|
8820
|
-
[attr.tabindex]="selectionMode() === 'single' && !loading() ? 0 : null"
|
|
12558
|
+
[attr.tabindex]="selectionMode() === 'single' && !loading() && !editable() ? 0 : null"
|
|
8821
12559
|
[attr.aria-selected]="selectionMode() === 'single' ? (isRowSelected(row) ? 'true' : 'false') : null"
|
|
8822
12560
|
(click)="onRowClick(row)"
|
|
8823
12561
|
(keydown.enter)="onRowKeydown($event, row)"
|
|
@@ -8883,20 +12621,128 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8883
12621
|
</td>
|
|
8884
12622
|
}
|
|
8885
12623
|
|
|
8886
|
-
@for (col of effectiveColumns(); track col.field) {
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
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
|
+
}
|
|
8900
12746
|
}
|
|
8901
12747
|
</tr>
|
|
8902
12748
|
|
|
@@ -8965,7 +12811,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8965
12811
|
</div>
|
|
8966
12812
|
`,
|
|
8967
12813
|
}]
|
|
8968
|
-
}], 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 }] }] } });
|
|
8969
12815
|
|
|
8970
12816
|
/**
|
|
8971
12817
|
* A tiny transient-notification bus. Application code calls {@link success} / {@link error} / {@link info}
|
|
@@ -10192,6 +14038,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10192
14038
|
*
|
|
10193
14039
|
* @example
|
|
10194
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" />
|
|
10195
14042
|
*/
|
|
10196
14043
|
class SdsNavItem {
|
|
10197
14044
|
store = inject(SidebarStore);
|
|
@@ -10202,8 +14049,52 @@ class SdsNavItem {
|
|
|
10202
14049
|
exact = input(false, ...(ngDevMode ? [{ debugName: "exact", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
|
|
10203
14050
|
/** Manual active state (combined with routerLinkActive when a link is set). */
|
|
10204
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" }] : []));
|
|
10205
14063
|
select = output();
|
|
10206
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" }] : []));
|
|
10207
14098
|
itemClass(isActive) {
|
|
10208
14099
|
// A `surface` rail is light: an active item is a brand-tinted pill with a left accent bar, brand
|
|
10209
14100
|
// text and a soft shadow (matching the certicheck reference); inactive is muted ink that lifts on
|
|
@@ -10231,7 +14122,7 @@ class SdsNavItem {
|
|
|
10231
14122
|
accentBarClass = computed(() => 'pointer-events-none absolute left-1 top-1/2 h-5 w-1 -translate-y-1/2 rounded-full ' +
|
|
10232
14123
|
(this.store.appearance() === 'surface' ? 'bg-brand-500' : 'bg-white'), ...(ngDevMode ? [{ debugName: "accentBarClass" }] : []));
|
|
10233
14124
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10234
|
-
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: `
|
|
10235
14126
|
@if (link() !== null) {
|
|
10236
14127
|
<a
|
|
10237
14128
|
[routerLink]="link()"
|
|
@@ -10239,8 +14130,8 @@ class SdsNavItem {
|
|
|
10239
14130
|
#rla="routerLinkActive"
|
|
10240
14131
|
[routerLinkActiveOptions]="{ exact: exact() }"
|
|
10241
14132
|
[class]="itemClass(rla.isActive || active())"
|
|
10242
|
-
[attr.title]="collapsed() ?
|
|
10243
|
-
[attr.aria-label]="collapsed() ?
|
|
14133
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14134
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10244
14135
|
[attr.aria-current]="rla.isActive || active() ? 'page' : null"
|
|
10245
14136
|
(click)="store.closeMobile()"
|
|
10246
14137
|
>
|
|
@@ -10253,8 +14144,8 @@ class SdsNavItem {
|
|
|
10253
14144
|
<button
|
|
10254
14145
|
type="button"
|
|
10255
14146
|
[class]="itemClass(active())"
|
|
10256
|
-
[attr.title]="collapsed() ?
|
|
10257
|
-
[attr.aria-label]="collapsed() ?
|
|
14147
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14148
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10258
14149
|
[attr.aria-current]="active() ? 'page' : null"
|
|
10259
14150
|
(click)="select.emit(); store.closeMobile()"
|
|
10260
14151
|
>
|
|
@@ -10271,6 +14162,12 @@ class SdsNavItem {
|
|
|
10271
14162
|
}
|
|
10272
14163
|
<!-- Label is hidden only on the collapsed desktop rail; always shown in the mobile drawer. -->
|
|
10273
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
|
+
}
|
|
10274
14171
|
</ng-template>
|
|
10275
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 });
|
|
10276
14173
|
}
|
|
@@ -10289,8 +14186,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10289
14186
|
#rla="routerLinkActive"
|
|
10290
14187
|
[routerLinkActiveOptions]="{ exact: exact() }"
|
|
10291
14188
|
[class]="itemClass(rla.isActive || active())"
|
|
10292
|
-
[attr.title]="collapsed() ?
|
|
10293
|
-
[attr.aria-label]="collapsed() ?
|
|
14189
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14190
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10294
14191
|
[attr.aria-current]="rla.isActive || active() ? 'page' : null"
|
|
10295
14192
|
(click)="store.closeMobile()"
|
|
10296
14193
|
>
|
|
@@ -10303,8 +14200,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10303
14200
|
<button
|
|
10304
14201
|
type="button"
|
|
10305
14202
|
[class]="itemClass(active())"
|
|
10306
|
-
[attr.title]="collapsed() ?
|
|
10307
|
-
[attr.aria-label]="collapsed() ?
|
|
14203
|
+
[attr.title]="collapsed() ? collapsedName() : null"
|
|
14204
|
+
[attr.aria-label]="collapsed() ? collapsedName() : null"
|
|
10308
14205
|
[attr.aria-current]="active() ? 'page' : null"
|
|
10309
14206
|
(click)="select.emit(); store.closeMobile()"
|
|
10310
14207
|
>
|
|
@@ -10321,10 +14218,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
10321
14218
|
}
|
|
10322
14219
|
<!-- Label is hidden only on the collapsed desktop rail; always shown in the mobile drawer. -->
|
|
10323
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
|
+
}
|
|
10324
14227
|
</ng-template>
|
|
10325
14228
|
`,
|
|
10326
14229
|
}]
|
|
10327
|
-
}], 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"] }] } });
|
|
10328
14231
|
|
|
10329
14232
|
/**
|
|
10330
14233
|
* The signed-in user's trigger and dropdown for the top bar: an avatar (with the name and email beside
|
|
@@ -11547,5 +15450,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
11547
15450
|
* Generated bundle index. Do not edit.
|
|
11548
15451
|
*/
|
|
11549
15452
|
|
|
11550
|
-
export { SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, 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, 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 };
|
|
11551
15454
|
//# sourceMappingURL=besaitech-ng-design-system.mjs.map
|