@mozaic-ds/angular 2.0.41 → 2.0.43

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.
@@ -4,7 +4,7 @@ import { RouterLink, RouterLinkActive, RouterLinkWithHref } from '@angular/route
4
4
  import { NgTemplateOutlet, NgClass, NgComponentOutlet, JsonPipe, DOCUMENT } from '@angular/common';
5
5
  import * as i1 from '@angular/forms';
6
6
  import { FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
7
- import { WarningCircle32, Uploading32, CheckCircle32, CrossCircleFilled20, Refresh32, Refresh20, Eye20, Upload24, Cross24, ChevronLeft24, ChevronRight24, ChevronLeft20, ChevronRight20, CrossCircleFilled24, More24, Less24, InfoCircle32, CrossCircle32, Cross20, CrossCircle24, ImageAlt32, ChevronDown24, CheckCircleFilled32, WarningCircleFilled32, CrossCircleFilled32, InfoCircleFilled32, SidebarExpand24, ChevronDown20, InfoCircleFilled24, WarningCircleFilled24, CheckCircleFilled24, ArrowBottomRight24, ArrowTopRight24, StarHalf32, StarFilled32, Star32, StarHalf24, StarFilled24, Star24, StarHalf20, StarFilled20, Star20, Check20, Check24, ArrowBack24, ArrowNext24, HelpCircle24, Menu24, Notification24, Search24, PauseCircle24, PlayCircle24, ChevronUp20, Settings20, ErrorFilled24, Drag20, ListAdd20, ViewGridX420, Filter20, FullscreenEnter20, FullscreenExit20, Download20, CheckCircle24 } from '@mozaic-ds/icons-angular';
7
+ import { WarningCircle32, Uploading32, CheckCircle32, CrossCircleFilled20, Refresh32, Refresh20, Eye20, Upload24, Cross24, ChevronLeft24, ChevronRight24, ChevronLeft20, ChevronRight20, CrossCircleFilled24, More24, Less24, InfoCircle32, CrossCircle32, Cross20, CrossCircle24, ImageAlt32, ChevronDown24, CheckCircleFilled32, WarningCircleFilled32, CrossCircleFilled32, InfoCircleFilled32, SidebarExpand24, ChevronDown20, InfoCircleFilled24, WarningCircleFilled24, CheckCircleFilled24, ArrowBottomRight24, ArrowTopRight24, StarHalf32, StarFilled32, Star32, StarHalf24, StarFilled24, Star24, StarHalf20, StarFilled20, Star20, Check20, Check24, ArrowBack24, ArrowNext24, HelpCircle24, Menu24, Notification24, Search24, PauseCircle24, PlayCircle24, ChevronUp20, Settings20, ErrorFilled24, Drag20, ListAdd20, ViewGridX420, Filter20, FullscreenEnter20, FullscreenExit20, Download20, Keyboard20, CheckCircle24 } from '@mozaic-ds/icons-angular';
8
8
  import { Overlay, OverlayConfig, OverlayPositionBuilder, CdkConnectedOverlay, CdkOverlayOrigin } from '@angular/cdk/overlay';
9
9
  import { CdkPortalOutlet, ComponentPortal } from '@angular/cdk/portal';
10
10
  import { Subject, take, tap, of, firstValueFrom } from 'rxjs';
@@ -4897,6 +4897,7 @@ class GridStateManager {
4897
4897
  scrollLeft = signal(0, ...(ngDevMode ? [{ debugName: "scrollLeft" }] : /* istanbul ignore next */ []));
4898
4898
  scrollTop = signal(0, ...(ngDevMode ? [{ debugName: "scrollTop" }] : /* istanbul ignore next */ []));
4899
4899
  scrollViewportWidth = signal(0, ...(ngDevMode ? [{ debugName: "scrollViewportWidth" }] : /* istanbul ignore next */ []));
4900
+ scrollViewportHeight = signal(0, ...(ngDevMode ? [{ debugName: "scrollViewportHeight" }] : /* istanbul ignore next */ []));
4900
4901
  scrollContentTotalWidth = signal(0, ...(ngDevMode ? [{ debugName: "scrollContentTotalWidth" }] : /* istanbul ignore next */ []));
4901
4902
  // --- Horizontal virtual scroll ---
4902
4903
  horizontalVirtualScrollEnabled = signal(false, ...(ngDevMode ? [{ debugName: "horizontalVirtualScrollEnabled" }] : /* istanbul ignore next */ []));
@@ -4919,6 +4920,8 @@ class GridStateManager {
4919
4920
  isFilling = signal(false, ...(ngDevMode ? [{ debugName: "isFilling" }] : /* istanbul ignore next */ []));
4920
4921
  fillAnchor = signal(null, ...(ngDevMode ? [{ debugName: "fillAnchor" }] : /* istanbul ignore next */ []));
4921
4922
  fillTarget = signal(null, ...(ngDevMode ? [{ debugName: "fillTarget" }] : /* istanbul ignore next */ []));
4923
+ // --- Cut (Ctrl+X) source — drives the marching-ants outline in view ---
4924
+ cutSource = signal(null, ...(ngDevMode ? [{ debugName: "cutSource" }] : /* istanbul ignore next */ []));
4922
4925
  // --- Expandable Rows ---
4923
4926
  expandedRowIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedRowIds" }] : /* istanbul ignore next */ []));
4924
4927
  rowIdField = signal('id', ...(ngDevMode ? [{ debugName: "rowIdField" }] : /* istanbul ignore next */ []));
@@ -5524,8 +5527,485 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
5524
5527
  type: Injectable
5525
5528
  }] });
5526
5529
 
5530
+ const PASTE_SKIP$1 = Symbol('PASTE_SKIP');
5531
+ /**
5532
+ * Applies a set of cell-level mutations to sourceData and returns the list of
5533
+ * actual changes that occurred, so the caller (usually the history engine) can
5534
+ * record them. `PASTE_SKIP` return values are filtered out transparently.
5535
+ */
5536
+ class ClipboardEngine {
5537
+ state = inject(GridStateManager);
5538
+ /** Derived by components (marching-ants outline). */
5539
+ cutRange = computed(() => this.state.cutSource(), ...(ngDevMode ? [{ debugName: "cutRange" }] : /* istanbul ignore next */ []));
5540
+ markCut(range) {
5541
+ this.state.cutSource.set(range);
5542
+ }
5543
+ clearCut() {
5544
+ this.state.cutSource.set(null);
5545
+ }
5546
+ /** Vertical fill: row 0 of the range is the source, subsequent rows are targets. */
5547
+ fillDown(range) {
5548
+ if (range.start.row === range.end.row)
5549
+ return [];
5550
+ const cols = this.state.visibleColumns();
5551
+ const defMap = this.state.columnDefMap();
5552
+ const changes = [];
5553
+ this.state.sourceData.update((data) => {
5554
+ const updated = [...data];
5555
+ const sourceRow = updated[range.start.row];
5556
+ if (!sourceRow)
5557
+ return updated;
5558
+ for (let r = range.start.row + 1; r <= range.end.row; r++) {
5559
+ if (!updated[r])
5560
+ continue;
5561
+ const rowCopy = { ...updated[r] };
5562
+ let changed = false;
5563
+ for (let c = range.start.col; c <= range.end.col; c++) {
5564
+ const field = cols[c]?.field;
5565
+ if (!field)
5566
+ continue;
5567
+ const def = defMap.get(field);
5568
+ if (!def?.editable)
5569
+ continue;
5570
+ const sourceValue = def.valueGetter
5571
+ ? def.valueGetter(sourceRow)
5572
+ : sourceRow[field];
5573
+ const coerced = this.coerceAndValidate(field, sourceValue, updated[r]);
5574
+ if (coerced === PASTE_SKIP$1)
5575
+ continue;
5576
+ const before = updated[r][field];
5577
+ if (before === coerced)
5578
+ continue;
5579
+ rowCopy[field] = coerced;
5580
+ changes.push({ rowIndex: r, field, before, after: coerced });
5581
+ changed = true;
5582
+ }
5583
+ if (changed)
5584
+ updated[r] = rowCopy;
5585
+ }
5586
+ return updated;
5587
+ });
5588
+ return changes;
5589
+ }
5590
+ /** Horizontal fill: col 0 of the range is the source, subsequent cols are targets. */
5591
+ fillRight(range) {
5592
+ if (range.start.col === range.end.col)
5593
+ return [];
5594
+ const cols = this.state.visibleColumns();
5595
+ const defMap = this.state.columnDefMap();
5596
+ const sourceField = cols[range.start.col]?.field;
5597
+ if (!sourceField)
5598
+ return [];
5599
+ const sourceDef = defMap.get(sourceField);
5600
+ if (!sourceDef)
5601
+ return [];
5602
+ const changes = [];
5603
+ this.state.sourceData.update((data) => {
5604
+ const updated = [...data];
5605
+ for (let r = range.start.row; r <= range.end.row; r++) {
5606
+ const row = updated[r];
5607
+ if (!row)
5608
+ continue;
5609
+ const sourceValue = sourceDef.valueGetter
5610
+ ? sourceDef.valueGetter(row)
5611
+ : row[sourceField];
5612
+ const rowCopy = { ...row };
5613
+ let changed = false;
5614
+ for (let c = range.start.col + 1; c <= range.end.col; c++) {
5615
+ const field = cols[c]?.field;
5616
+ if (!field)
5617
+ continue;
5618
+ const def = defMap.get(field);
5619
+ if (!def?.editable)
5620
+ continue;
5621
+ const coerced = this.coerceAndValidate(field, sourceValue, row);
5622
+ if (coerced === PASTE_SKIP$1)
5623
+ continue;
5624
+ const before = row[field];
5625
+ if (before === coerced)
5626
+ continue;
5627
+ rowCopy[field] = coerced;
5628
+ changes.push({ rowIndex: r, field, before, after: coerced });
5629
+ changed = true;
5630
+ }
5631
+ if (changed)
5632
+ updated[r] = rowCopy;
5633
+ }
5634
+ return updated;
5635
+ });
5636
+ return changes;
5637
+ }
5638
+ /** Ctrl+Enter: write `value` into every editable cell of `range`. */
5639
+ fillSelection(range, value) {
5640
+ const cols = this.state.visibleColumns();
5641
+ const defMap = this.state.columnDefMap();
5642
+ const changes = [];
5643
+ this.state.sourceData.update((data) => {
5644
+ const updated = [...data];
5645
+ for (let r = range.start.row; r <= range.end.row; r++) {
5646
+ const row = updated[r];
5647
+ if (!row)
5648
+ continue;
5649
+ const rowCopy = { ...row };
5650
+ let changed = false;
5651
+ for (let c = range.start.col; c <= range.end.col; c++) {
5652
+ const field = cols[c]?.field;
5653
+ if (!field)
5654
+ continue;
5655
+ const def = defMap.get(field);
5656
+ if (!def?.editable)
5657
+ continue;
5658
+ const coerced = this.coerceAndValidate(field, value, row);
5659
+ if (coerced === PASTE_SKIP$1)
5660
+ continue;
5661
+ const before = row[field];
5662
+ if (before === coerced)
5663
+ continue;
5664
+ rowCopy[field] = coerced;
5665
+ changes.push({ rowIndex: r, field, before, after: coerced });
5666
+ changed = true;
5667
+ }
5668
+ if (changed)
5669
+ updated[r] = rowCopy;
5670
+ }
5671
+ return updated;
5672
+ });
5673
+ return changes;
5674
+ }
5675
+ /** Clears every editable cell in `range` and returns the undo payload. */
5676
+ clearRange(range) {
5677
+ const cols = this.state.visibleColumns();
5678
+ const defMap = this.state.columnDefMap();
5679
+ const changes = [];
5680
+ this.state.sourceData.update((data) => {
5681
+ const updated = [...data];
5682
+ for (let r = range.start.row; r <= range.end.row; r++) {
5683
+ const row = updated[r];
5684
+ if (!row)
5685
+ continue;
5686
+ const rowCopy = { ...row };
5687
+ let changed = false;
5688
+ for (let c = range.start.col; c <= range.end.col; c++) {
5689
+ const field = cols[c]?.field;
5690
+ if (!field)
5691
+ continue;
5692
+ const def = defMap.get(field);
5693
+ if (!def?.editable)
5694
+ continue;
5695
+ const coerced = this.coerceAndValidate(field, null, row);
5696
+ if (coerced === PASTE_SKIP$1)
5697
+ continue;
5698
+ const before = row[field];
5699
+ if (before === coerced)
5700
+ continue;
5701
+ rowCopy[field] = coerced;
5702
+ changes.push({ rowIndex: r, field, before, after: coerced });
5703
+ changed = true;
5704
+ }
5705
+ if (changed)
5706
+ updated[r] = rowCopy;
5707
+ }
5708
+ return updated;
5709
+ });
5710
+ return changes;
5711
+ }
5712
+ /** Applies TSV `pasteRows` starting at `range.start`, returning actual changes. */
5713
+ applyPaste(range, pasteRows) {
5714
+ const cols = this.state.visibleColumns();
5715
+ const changes = [];
5716
+ this.state.sourceData.update((data) => {
5717
+ const updated = [...data];
5718
+ for (let ri = 0; ri < pasteRows.length; ri++) {
5719
+ const targetRow = range.start.row + ri;
5720
+ if (targetRow >= updated.length)
5721
+ break;
5722
+ const row = updated[targetRow];
5723
+ if (!row)
5724
+ continue;
5725
+ const rowCopy = { ...row };
5726
+ let changed = false;
5727
+ for (let ci = 0; ci < pasteRows[ri].length; ci++) {
5728
+ const targetCol = range.start.col + ci;
5729
+ if (targetCol >= cols.length)
5730
+ break;
5731
+ const field = cols[targetCol]?.field;
5732
+ if (!field)
5733
+ continue;
5734
+ const coerced = this.coerceAndValidate(field, pasteRows[ri][ci], row);
5735
+ if (coerced === PASTE_SKIP$1)
5736
+ continue;
5737
+ const before = row[field];
5738
+ if (before === coerced)
5739
+ continue;
5740
+ rowCopy[field] = coerced;
5741
+ changes.push({ rowIndex: targetRow, field, before, after: coerced });
5742
+ changed = true;
5743
+ }
5744
+ if (changed)
5745
+ updated[targetRow] = rowCopy;
5746
+ }
5747
+ return updated;
5748
+ });
5749
+ return changes;
5750
+ }
5751
+ /**
5752
+ * Reverses a previously-recorded list of changes by writing `before` back into
5753
+ * the cells. Used by the history engine for both undo and redo.
5754
+ */
5755
+ applyChanges(changes, direction) {
5756
+ if (changes.length === 0)
5757
+ return;
5758
+ this.state.sourceData.update((data) => {
5759
+ const updated = [...data];
5760
+ // Group changes by rowIndex so each row is cloned once.
5761
+ const byRow = new Map();
5762
+ for (const change of changes) {
5763
+ const list = byRow.get(change.rowIndex) ?? [];
5764
+ list.push(change);
5765
+ byRow.set(change.rowIndex, list);
5766
+ }
5767
+ for (const [rowIndex, rowChanges] of byRow) {
5768
+ const row = updated[rowIndex];
5769
+ if (!row)
5770
+ continue;
5771
+ const rowCopy = { ...row };
5772
+ for (const change of rowChanges) {
5773
+ rowCopy[change.field] = direction === 'before' ? change.before : change.after;
5774
+ }
5775
+ updated[rowIndex] = rowCopy;
5776
+ }
5777
+ return updated;
5778
+ });
5779
+ }
5780
+ /**
5781
+ * Coerces a raw value (string from TSV, unknown from fill, null for clears)
5782
+ * into the editor's expected type, running the field's validator when present.
5783
+ * Returns PASTE_SKIP when the column isn't editable or the value is rejected.
5784
+ */
5785
+ coerceAndValidate(field, rawValue, row) {
5786
+ const def = this.state.columnDefMap().get(field);
5787
+ if (!def?.editable)
5788
+ return PASTE_SKIP$1;
5789
+ const editorType = def.cellEditor;
5790
+ if (rawValue === null) {
5791
+ let clearValue;
5792
+ switch (editorType) {
5793
+ case 'number':
5794
+ clearValue = null;
5795
+ break;
5796
+ case 'checkbox':
5797
+ clearValue = false;
5798
+ break;
5799
+ default:
5800
+ clearValue = '';
5801
+ }
5802
+ if (def.cellEditorValidator) {
5803
+ const result = def.cellEditorValidator(clearValue, row);
5804
+ if (result === false || typeof result === 'string')
5805
+ return PASTE_SKIP$1;
5806
+ }
5807
+ return clearValue;
5808
+ }
5809
+ let value = rawValue;
5810
+ if (editorType === 'number') {
5811
+ const num = Number(rawValue);
5812
+ if (isNaN(num))
5813
+ return PASTE_SKIP$1;
5814
+ value = num;
5815
+ }
5816
+ else if (editorType === 'checkbox') {
5817
+ if (rawValue === 'true' || rawValue === true) {
5818
+ value = true;
5819
+ }
5820
+ else if (rawValue === 'false' || rawValue === false) {
5821
+ value = false;
5822
+ }
5823
+ else {
5824
+ return PASTE_SKIP$1;
5825
+ }
5826
+ }
5827
+ else if (editorType === 'select' && def.cellEditorOptions?.length) {
5828
+ const allowed = def.cellEditorOptions.map((o) => String(o.value));
5829
+ if (!allowed.includes(String(rawValue)))
5830
+ return PASTE_SKIP$1;
5831
+ value = rawValue;
5832
+ }
5833
+ if (def.cellEditorValidator) {
5834
+ const result = def.cellEditorValidator(value, row);
5835
+ if (result === false || typeof result === 'string')
5836
+ return PASTE_SKIP$1;
5837
+ }
5838
+ return value;
5839
+ }
5840
+ /** TSV string for a range — used by copy / cut. */
5841
+ extractTsv(range) {
5842
+ const cols = this.state.visibleColumns();
5843
+ const data = this.state.sourceData();
5844
+ const defMap = this.state.columnDefMap();
5845
+ const values = [];
5846
+ for (let r = range.start.row; r <= range.end.row; r++) {
5847
+ const row = data[r];
5848
+ if (!row)
5849
+ continue;
5850
+ const rowValues = [];
5851
+ for (let c = range.start.col; c <= range.end.col; c++) {
5852
+ const field = cols[c]?.field;
5853
+ if (!field) {
5854
+ rowValues.push('');
5855
+ continue;
5856
+ }
5857
+ const def = defMap.get(field);
5858
+ const val = def?.valueGetter
5859
+ ? def.valueGetter(row)
5860
+ : row[field];
5861
+ rowValues.push(val != null ? String(val) : '');
5862
+ }
5863
+ values.push(rowValues);
5864
+ }
5865
+ return values;
5866
+ }
5867
+ /**
5868
+ * Checks whether a cell sits on any edge of the current cut outline. Four
5869
+ * booleans rather than a single "isCut" so the view can paint only the edges
5870
+ * that face outward — which is what produces the Excel-like marching ants.
5871
+ */
5872
+ cutEdges(row, col) {
5873
+ const cut = this.state.cutSource();
5874
+ if (!cut)
5875
+ return { top: false, right: false, bottom: false, left: false, any: false };
5876
+ const minRow = Math.min(cut.start.row, cut.end.row);
5877
+ const maxRow = Math.max(cut.start.row, cut.end.row);
5878
+ const minCol = Math.min(cut.start.col, cut.end.col);
5879
+ const maxCol = Math.max(cut.start.col, cut.end.col);
5880
+ if (row < minRow || row > maxRow || col < minCol || col > maxCol) {
5881
+ return { top: false, right: false, bottom: false, left: false, any: false };
5882
+ }
5883
+ return {
5884
+ top: row === minRow,
5885
+ right: col === maxCol,
5886
+ bottom: row === maxRow,
5887
+ left: col === minCol,
5888
+ any: true,
5889
+ };
5890
+ }
5891
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardEngine, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
5892
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardEngine });
5893
+ }
5894
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: ClipboardEngine, decorators: [{
5895
+ type: Injectable
5896
+ }] });
5897
+
5898
+ const MAX_HISTORY = 50;
5899
+ const STORAGE_PREFIX = 'moz-grid-history:';
5900
+ class HistoryEngine {
5901
+ clipboard = inject(ClipboardEngine);
5902
+ past = signal([], ...(ngDevMode ? [{ debugName: "past" }] : /* istanbul ignore next */ []));
5903
+ future = signal([], ...(ngDevMode ? [{ debugName: "future" }] : /* istanbul ignore next */ []));
5904
+ storageKey = null;
5905
+ canUndo = computed(() => this.past().length > 0, ...(ngDevMode ? [{ debugName: "canUndo" }] : /* istanbul ignore next */ []));
5906
+ canRedo = computed(() => this.future().length > 0, ...(ngDevMode ? [{ debugName: "canRedo" }] : /* istanbul ignore next */ []));
5907
+ /**
5908
+ * Binds a persistence key: all record/undo/redo calls will mirror the stacks
5909
+ * to localStorage, and past state is restored on bind. Pass null to detach.
5910
+ */
5911
+ attach(gridId) {
5912
+ this.storageKey = gridId ? `${STORAGE_PREFIX}${gridId}` : null;
5913
+ if (!this.storageKey) {
5914
+ this.past.set([]);
5915
+ this.future.set([]);
5916
+ return;
5917
+ }
5918
+ this.restore();
5919
+ }
5920
+ /** Records a new mutation. Clears the redo stack (standard undo semantics). */
5921
+ record(type, changes) {
5922
+ if (changes.length === 0)
5923
+ return;
5924
+ const op = { type, changes, timestamp: Date.now() };
5925
+ this.past.update((stack) => {
5926
+ const next = [...stack, op];
5927
+ return next.length > MAX_HISTORY ? next.slice(next.length - MAX_HISTORY) : next;
5928
+ });
5929
+ this.future.set([]);
5930
+ this.persist();
5931
+ }
5932
+ undo() {
5933
+ const stack = this.past();
5934
+ if (stack.length === 0)
5935
+ return null;
5936
+ const op = stack[stack.length - 1];
5937
+ this.clipboard.applyChanges(op.changes, 'before');
5938
+ this.past.set(stack.slice(0, -1));
5939
+ this.future.update((f) => [...f, op]);
5940
+ this.persist();
5941
+ return op;
5942
+ }
5943
+ redo() {
5944
+ const stack = this.future();
5945
+ if (stack.length === 0)
5946
+ return null;
5947
+ const op = stack[stack.length - 1];
5948
+ this.clipboard.applyChanges(op.changes, 'after');
5949
+ this.future.set(stack.slice(0, -1));
5950
+ this.past.update((p) => [...p, op]);
5951
+ this.persist();
5952
+ return op;
5953
+ }
5954
+ clear() {
5955
+ this.past.set([]);
5956
+ this.future.set([]);
5957
+ if (this.storageKey) {
5958
+ try {
5959
+ localStorage.removeItem(this.storageKey);
5960
+ }
5961
+ catch {
5962
+ // Storage unavailable (private mode, quota) — non-fatal.
5963
+ }
5964
+ }
5965
+ }
5966
+ persist() {
5967
+ if (!this.storageKey)
5968
+ return;
5969
+ try {
5970
+ const payload = JSON.stringify({
5971
+ past: this.past(),
5972
+ future: this.future(),
5973
+ });
5974
+ localStorage.setItem(this.storageKey, payload);
5975
+ }
5976
+ catch {
5977
+ // Quota exceeded or storage disabled — we silently drop persistence.
5978
+ }
5979
+ }
5980
+ restore() {
5981
+ if (!this.storageKey)
5982
+ return;
5983
+ try {
5984
+ const raw = localStorage.getItem(this.storageKey);
5985
+ if (!raw) {
5986
+ this.past.set([]);
5987
+ this.future.set([]);
5988
+ return;
5989
+ }
5990
+ const parsed = JSON.parse(raw);
5991
+ this.past.set(Array.isArray(parsed.past) ? parsed.past : []);
5992
+ this.future.set(Array.isArray(parsed.future) ? parsed.future : []);
5993
+ }
5994
+ catch {
5995
+ this.past.set([]);
5996
+ this.future.set([]);
5997
+ }
5998
+ }
5999
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HistoryEngine, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6000
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HistoryEngine });
6001
+ }
6002
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: HistoryEngine, decorators: [{
6003
+ type: Injectable
6004
+ }] });
6005
+
5527
6006
  class InlineEditEngine {
5528
6007
  state = inject(GridStateManager);
6008
+ history = inject(HistoryEngine);
5529
6009
  startEdit(rowIndex, field) {
5530
6010
  const defMap = this.state.columnDefMap();
5531
6011
  const def = defMap.get(field);
@@ -5545,6 +6025,54 @@ class InlineEditEngine {
5545
6025
  validationError: null,
5546
6026
  });
5547
6027
  }
6028
+ /**
6029
+ * Excel-style "typing-to-edit": starts the editor with the cell value replaced
6030
+ * by the character the user just typed. For non-text editors (select / date /
6031
+ * checkbox / number) we coerce: the character is kept if it's compatible with
6032
+ * the editor, otherwise the editor opens on a cleared value.
6033
+ */
6034
+ startEditWithChar(rowIndex, field, char) {
6035
+ const defMap = this.state.columnDefMap();
6036
+ const def = defMap.get(field);
6037
+ if (!def?.editable)
6038
+ return;
6039
+ const colIndex = this.state.visibleColumns().findIndex((c) => c.field === field);
6040
+ if (colIndex < 0)
6041
+ return;
6042
+ const row = this.state.sourceData()[rowIndex];
6043
+ if (!row)
6044
+ return;
6045
+ const currentValue = def.valueGetter
6046
+ ? def.valueGetter(row)
6047
+ : row[field];
6048
+ const editorType = def.cellEditor ?? this.resolveEditorType(field, currentValue);
6049
+ let draftValue = char;
6050
+ switch (editorType) {
6051
+ case 'number': {
6052
+ const n = Number(char);
6053
+ draftValue = Number.isNaN(n) ? '' : n;
6054
+ break;
6055
+ }
6056
+ case 'checkbox':
6057
+ // A character press toggles the checkbox to true — closest Excel-equivalent
6058
+ // (Excel doesn't have checkbox cells but booleans flip on typing).
6059
+ draftValue = true;
6060
+ break;
6061
+ case 'select':
6062
+ case 'date':
6063
+ // These editors have picker UIs; typing just opens them with an empty draft.
6064
+ draftValue = '';
6065
+ break;
6066
+ default:
6067
+ draftValue = char;
6068
+ }
6069
+ this.state.cellEditState.set({
6070
+ editingCell: { row: rowIndex, col: colIndex },
6071
+ originalValue: currentValue,
6072
+ draftValue,
6073
+ validationError: null,
6074
+ });
6075
+ }
5548
6076
  updateDraft(value) {
5549
6077
  this.state.cellEditState.update((s) => ({ ...s, draftValue: value }));
5550
6078
  }
@@ -5596,6 +6124,11 @@ class InlineEditEngine {
5596
6124
  oldValue: editState.originalValue,
5597
6125
  newValue: editState.draftValue,
5598
6126
  };
6127
+ if (this.state.mode() === 'client' && event.oldValue !== event.newValue) {
6128
+ this.history.record('edit', [
6129
+ { rowIndex, field, before: event.oldValue, after: event.newValue },
6130
+ ]);
6131
+ }
5599
6132
  this.state.cellEditState.set({
5600
6133
  editingCell: null,
5601
6134
  originalValue: undefined,
@@ -5899,6 +6432,272 @@ class CellSelectionEngine {
5899
6432
  moveRight() {
5900
6433
  this.moveBy(0, 1);
5901
6434
  }
6435
+ // --- Home / End / Grid bounds -------------------------------------------------
6436
+ moveToRowStart() {
6437
+ const focused = this.state.focusedCell();
6438
+ if (!focused)
6439
+ return;
6440
+ this.focusCell(focused.row, 0, 'keyboard');
6441
+ }
6442
+ moveToRowEnd() {
6443
+ const focused = this.state.focusedCell();
6444
+ if (!focused)
6445
+ return;
6446
+ const maxCol = this.state.visibleColumns().length - 1;
6447
+ if (maxCol < 0)
6448
+ return;
6449
+ this.focusCell(focused.row, this.findLastNonEmptyCol(focused.row, maxCol), 'keyboard');
6450
+ }
6451
+ moveToGridStart() {
6452
+ const pageStart = this.state.pageIndex() * this.state.pageSize();
6453
+ this.focusCell(pageStart, 0, 'keyboard');
6454
+ }
6455
+ moveToGridEnd() {
6456
+ const pageStart = this.state.pageIndex() * this.state.pageSize();
6457
+ const pageEnd = pageStart + Math.max(0, this.state.visibleRowCount() - 1);
6458
+ const maxCol = this.state.visibleColumns().length - 1;
6459
+ if (maxCol < 0)
6460
+ return;
6461
+ this.focusCell(pageEnd, this.findLastNonEmptyCol(pageEnd, maxCol), 'keyboard');
6462
+ }
6463
+ /**
6464
+ * Excel-style Ctrl+Arrow: jump to the edge of the current data block.
6465
+ * If on an empty cell, jumps to the next non-empty cell. If on a filled
6466
+ * cell, jumps to the last filled cell before the next empty transition
6467
+ * (or to the grid edge if no empty cell is encountered).
6468
+ */
6469
+ jumpToEdge(direction) {
6470
+ const focused = this.state.focusedCell();
6471
+ if (!focused)
6472
+ return;
6473
+ const { dRow, dCol } = this.directionVector(direction);
6474
+ const bounds = this.pageBounds();
6475
+ const maxCol = this.state.visibleColumns().length - 1;
6476
+ if (maxCol < 0)
6477
+ return;
6478
+ const startFilled = this.isCellFilled(focused.row, focused.col);
6479
+ let row = focused.row;
6480
+ let col = focused.col;
6481
+ // Step once to start looking at the neighbour
6482
+ let nextRow = row + dRow;
6483
+ let nextCol = col + dCol;
6484
+ while (this.inBounds(nextRow, nextCol, bounds, maxCol)) {
6485
+ const filled = this.isCellFilled(nextRow, nextCol);
6486
+ if (startFilled) {
6487
+ // Moving through filled cells — stop right before the next empty gap.
6488
+ if (!filled)
6489
+ break;
6490
+ row = nextRow;
6491
+ col = nextCol;
6492
+ }
6493
+ else {
6494
+ // Moving through empty cells — stop on the first filled cell we meet.
6495
+ if (filled) {
6496
+ row = nextRow;
6497
+ col = nextCol;
6498
+ break;
6499
+ }
6500
+ row = nextRow;
6501
+ col = nextCol;
6502
+ }
6503
+ nextRow += dRow;
6504
+ nextCol += dCol;
6505
+ }
6506
+ this.focusCell(row, col, 'keyboard');
6507
+ }
6508
+ movePage(direction) {
6509
+ const focused = this.state.focusedCell();
6510
+ if (!focused)
6511
+ return;
6512
+ const step = this.pageRowStep() * (direction === 'down' ? 1 : -1);
6513
+ this.moveBy(step, 0);
6514
+ }
6515
+ // --- Shift + navigation : extend current range --------------------------------
6516
+ extendRangeBy(dRow, dCol) {
6517
+ const focused = this.state.focusedCell();
6518
+ if (!focused)
6519
+ return;
6520
+ const range = this.state.cellRange();
6521
+ const bounds = this.pageBounds();
6522
+ const maxCol = this.state.visibleColumns().length - 1;
6523
+ if (maxCol < 0)
6524
+ return;
6525
+ const currentEnd = range ? range.end : focused;
6526
+ const newEnd = {
6527
+ row: Math.max(bounds.start, Math.min(bounds.end, currentEnd.row + dRow)),
6528
+ col: Math.max(0, Math.min(maxCol, currentEnd.col + dCol)),
6529
+ };
6530
+ const start = range ? range.start : focused;
6531
+ this.state.cellRange.set({ start, end: newEnd });
6532
+ }
6533
+ extendRangeToRowStart() {
6534
+ const focused = this.state.focusedCell();
6535
+ if (!focused)
6536
+ return;
6537
+ const range = this.state.cellRange();
6538
+ const start = range?.start ?? focused;
6539
+ this.state.cellRange.set({ start, end: { row: (range?.end ?? focused).row, col: 0 } });
6540
+ }
6541
+ extendRangeToRowEnd() {
6542
+ const focused = this.state.focusedCell();
6543
+ if (!focused)
6544
+ return;
6545
+ const maxCol = this.state.visibleColumns().length - 1;
6546
+ if (maxCol < 0)
6547
+ return;
6548
+ const range = this.state.cellRange();
6549
+ const start = range?.start ?? focused;
6550
+ this.state.cellRange.set({ start, end: { row: (range?.end ?? focused).row, col: maxCol } });
6551
+ }
6552
+ extendRangeToGridStart() {
6553
+ const focused = this.state.focusedCell();
6554
+ if (!focused)
6555
+ return;
6556
+ const bounds = this.pageBounds();
6557
+ const range = this.state.cellRange();
6558
+ const start = range?.start ?? focused;
6559
+ this.state.cellRange.set({ start, end: { row: bounds.start, col: 0 } });
6560
+ }
6561
+ extendRangeToGridEnd() {
6562
+ const focused = this.state.focusedCell();
6563
+ if (!focused)
6564
+ return;
6565
+ const bounds = this.pageBounds();
6566
+ const maxCol = this.state.visibleColumns().length - 1;
6567
+ if (maxCol < 0)
6568
+ return;
6569
+ const range = this.state.cellRange();
6570
+ const start = range?.start ?? focused;
6571
+ this.state.cellRange.set({ start, end: { row: bounds.end, col: maxCol } });
6572
+ }
6573
+ extendRangeJumpToEdge(direction) {
6574
+ const focused = this.state.focusedCell();
6575
+ if (!focused)
6576
+ return;
6577
+ const range = this.state.cellRange();
6578
+ const anchor = range?.start ?? focused;
6579
+ const end = range?.end ?? focused;
6580
+ const target = this.edgeFromCell(end, direction);
6581
+ this.state.cellRange.set({ start: anchor, end: target });
6582
+ }
6583
+ extendRangeByPage(direction) {
6584
+ const step = this.pageRowStep() * (direction === 'down' ? 1 : -1);
6585
+ this.extendRangeBy(step, 0);
6586
+ }
6587
+ // --- Whole row / column / grid selection --------------------------------------
6588
+ selectRow(row) {
6589
+ const maxCol = this.state.visibleColumns().length - 1;
6590
+ if (maxCol < 0)
6591
+ return;
6592
+ this.state.focusedCell.set({ row, col: 0 });
6593
+ this.state.focusSource.set('keyboard');
6594
+ this.state.cellRange.set({
6595
+ start: { row, col: 0 },
6596
+ end: { row, col: maxCol },
6597
+ });
6598
+ }
6599
+ selectColumn(col) {
6600
+ const bounds = this.pageBounds();
6601
+ this.state.focusedCell.set({ row: bounds.start, col });
6602
+ this.state.focusSource.set('keyboard');
6603
+ this.state.cellRange.set({
6604
+ start: { row: bounds.start, col },
6605
+ end: { row: bounds.end, col },
6606
+ });
6607
+ }
6608
+ selectAll() {
6609
+ const bounds = this.pageBounds();
6610
+ const maxCol = this.state.visibleColumns().length - 1;
6611
+ if (maxCol < 0)
6612
+ return;
6613
+ this.state.focusedCell.set({ row: bounds.start, col: 0 });
6614
+ this.state.focusSource.set('keyboard');
6615
+ this.state.cellRange.set({
6616
+ start: { row: bounds.start, col: 0 },
6617
+ end: { row: bounds.end, col: maxCol },
6618
+ });
6619
+ }
6620
+ // --- Private helpers ----------------------------------------------------------
6621
+ pageBounds() {
6622
+ const start = this.state.pageIndex() * this.state.pageSize();
6623
+ const end = start + Math.max(0, this.state.visibleRowCount() - 1);
6624
+ return { start, end };
6625
+ }
6626
+ pageRowStep() {
6627
+ const rowHeight = this.state.rowHeight() || 48;
6628
+ const viewportHeight = this.state.scrollViewportHeight();
6629
+ if (viewportHeight > 0) {
6630
+ return Math.max(1, Math.floor(viewportHeight / rowHeight));
6631
+ }
6632
+ // Fallback: a sensible default when the viewport hasn't been measured yet.
6633
+ return Math.max(1, Math.floor(this.state.visibleRowCount() / 2) || 10);
6634
+ }
6635
+ directionVector(dir) {
6636
+ switch (dir) {
6637
+ case 'up':
6638
+ return { dRow: -1, dCol: 0 };
6639
+ case 'down':
6640
+ return { dRow: 1, dCol: 0 };
6641
+ case 'left':
6642
+ return { dRow: 0, dCol: -1 };
6643
+ case 'right':
6644
+ return { dRow: 0, dCol: 1 };
6645
+ }
6646
+ }
6647
+ inBounds(row, col, bounds, maxCol) {
6648
+ return row >= bounds.start && row <= bounds.end && col >= 0 && col <= maxCol;
6649
+ }
6650
+ isCellFilled(row, col) {
6651
+ const cols = this.state.visibleColumns();
6652
+ const field = cols[col]?.field;
6653
+ if (!field)
6654
+ return false;
6655
+ const data = this.state.sourceData();
6656
+ const rowData = data[row];
6657
+ if (!rowData)
6658
+ return false;
6659
+ const def = this.state.columnDefMap().get(field);
6660
+ const value = def?.valueGetter
6661
+ ? def.valueGetter(rowData)
6662
+ : rowData[field];
6663
+ return value !== null && value !== undefined && value !== '';
6664
+ }
6665
+ findLastNonEmptyCol(row, maxCol) {
6666
+ for (let c = maxCol; c >= 0; c--) {
6667
+ if (this.isCellFilled(row, c))
6668
+ return c;
6669
+ }
6670
+ return maxCol;
6671
+ }
6672
+ edgeFromCell(from, direction) {
6673
+ const { dRow, dCol } = this.directionVector(direction);
6674
+ const bounds = this.pageBounds();
6675
+ const maxCol = this.state.visibleColumns().length - 1;
6676
+ if (maxCol < 0)
6677
+ return from;
6678
+ const startFilled = this.isCellFilled(from.row, from.col);
6679
+ let row = from.row;
6680
+ let col = from.col;
6681
+ let nextRow = row + dRow;
6682
+ let nextCol = col + dCol;
6683
+ while (this.inBounds(nextRow, nextCol, bounds, maxCol)) {
6684
+ const filled = this.isCellFilled(nextRow, nextCol);
6685
+ if (startFilled) {
6686
+ if (!filled)
6687
+ break;
6688
+ }
6689
+ else if (filled) {
6690
+ row = nextRow;
6691
+ col = nextCol;
6692
+ break;
6693
+ }
6694
+ row = nextRow;
6695
+ col = nextCol;
6696
+ nextRow += dRow;
6697
+ nextCol += dCol;
6698
+ }
6699
+ return { row, col };
6700
+ }
5902
6701
  moveToNextEditableCell() {
5903
6702
  const focused = this.state.focusedCell();
5904
6703
  if (!focused)
@@ -6162,113 +6961,203 @@ class KeyboardEngine {
6162
6961
  cellSelection = inject(CellSelectionEngine);
6163
6962
  inlineEdit = inject(InlineEditEngine);
6164
6963
  state = inject(GridStateManager);
6964
+ actions = null;
6965
+ registerActions(actions) {
6966
+ this.actions = actions;
6967
+ }
6165
6968
  handleKeydown(event) {
6166
- // Edit mode keys are handled by MozGridComponent.handleEditModeKeydown
6167
- const editState = this.state.cellEditState();
6168
- if (editState.editingCell !== null)
6969
+ // Edit-mode keys are handled by MozGridComponent.handleEditModeKeydown;
6970
+ // this engine only owns navigation + shortcuts in non-editing state.
6971
+ if (this.state.cellEditState().editingCell !== null)
6169
6972
  return;
6170
- this.handleNavigationKey(event);
6171
- }
6172
- handleNavigationKey(event) {
6173
6973
  const focused = this.state.focusedCell();
6174
6974
  if (!focused)
6175
6975
  return;
6176
- switch (event.key) {
6177
- case 'ArrowUp':
6178
- event.preventDefault();
6179
- if (event.shiftKey) {
6180
- this.extendRangeBy(-1, 0);
6181
- }
6182
- else {
6183
- this.cellSelection.moveUp();
6184
- }
6185
- break;
6186
- case 'ArrowDown':
6187
- event.preventDefault();
6188
- if (event.shiftKey) {
6189
- this.extendRangeBy(1, 0);
6190
- }
6191
- else {
6192
- this.cellSelection.moveDown();
6193
- }
6194
- break;
6195
- case 'ArrowLeft':
6196
- event.preventDefault();
6197
- if (event.shiftKey) {
6198
- this.extendRangeBy(0, -1);
6199
- }
6200
- else {
6201
- this.cellSelection.moveLeft();
6202
- }
6203
- break;
6204
- case 'ArrowRight':
6205
- event.preventDefault();
6206
- if (event.shiftKey) {
6207
- this.extendRangeBy(0, 1);
6208
- }
6209
- else {
6210
- this.cellSelection.moveRight();
6211
- }
6212
- break;
6213
- case 'Tab':
6214
- event.preventDefault();
6215
- if (event.shiftKey) {
6216
- this.cellSelection.moveLeft();
6217
- }
6218
- else {
6219
- this.cellSelection.moveRight();
6220
- }
6221
- break;
6222
- case 'Enter': {
6223
- event.preventDefault();
6224
- const col = this.state.visibleColumns()[focused.col];
6225
- if (col) {
6226
- const def = this.state.columnDefMap().get(col.field);
6227
- if (def?.editable) {
6228
- this.inlineEdit.startEdit(focused.row, col.field);
6229
- }
6230
- }
6231
- break;
6976
+ if (this.dispatch(event, focused)) {
6977
+ event.preventDefault();
6978
+ }
6979
+ }
6980
+ /** Returns true when the event was handled (so the caller should preventDefault). */
6981
+ dispatch(event, focused) {
6982
+ const mod = event.ctrlKey || event.metaKey;
6983
+ const shift = event.shiftKey;
6984
+ const alt = event.altKey;
6985
+ const key = event.key;
6986
+ // ---- Clipboard / history shortcuts --------------------------------------
6987
+ if (mod && !shift && !alt) {
6988
+ switch (key.toLowerCase()) {
6989
+ case 'c':
6990
+ this.actions?.copy();
6991
+ return true;
6992
+ case 'v':
6993
+ this.actions?.paste();
6994
+ return true;
6995
+ case 'x':
6996
+ this.actions?.cut();
6997
+ return true;
6998
+ case 'z':
6999
+ this.actions?.undo();
7000
+ return true;
7001
+ case 'y':
7002
+ this.actions?.redo();
7003
+ return true;
7004
+ case 'a':
7005
+ this.cellSelection.selectAll();
7006
+ return true;
7007
+ case 'd':
7008
+ this.actions?.fillDown();
7009
+ return true;
7010
+ case 'r':
7011
+ this.actions?.fillRight();
7012
+ return true;
7013
+ }
7014
+ }
7015
+ if (mod && shift && !alt && key.toLowerCase() === 'z') {
7016
+ this.actions?.redo();
7017
+ return true;
7018
+ }
7019
+ // ---- Whole row / column selection ---------------------------------------
7020
+ if (key === ' ') {
7021
+ if (mod && shift) {
7022
+ this.cellSelection.selectAll();
7023
+ return true;
6232
7024
  }
6233
- case 'F2': {
6234
- event.preventDefault();
6235
- const col2 = this.state.visibleColumns()[focused.col];
6236
- if (col2) {
6237
- const def2 = this.state.columnDefMap().get(col2.field);
6238
- if (def2?.editable) {
6239
- this.inlineEdit.startEdit(focused.row, col2.field);
6240
- }
6241
- }
7025
+ if (mod) {
7026
+ this.cellSelection.selectColumn(focused.col);
7027
+ return true;
7028
+ }
7029
+ if (shift) {
7030
+ this.cellSelection.selectRow(focused.row);
7031
+ return true;
7032
+ }
7033
+ }
7034
+ // ---- Delete / Backspace: clear cells ------------------------------------
7035
+ if (key === 'Delete' || key === 'Backspace') {
7036
+ this.actions?.deleteRange();
7037
+ return true;
7038
+ }
7039
+ // ---- Arrow keys ---------------------------------------------------------
7040
+ if (key === 'ArrowUp' || key === 'ArrowDown' || key === 'ArrowLeft' || key === 'ArrowRight') {
7041
+ return this.handleArrow(key, { mod, shift });
7042
+ }
7043
+ // ---- Home / End / PageUp / PageDown -------------------------------------
7044
+ if (key === 'Home') {
7045
+ if (mod && shift)
7046
+ this.cellSelection.extendRangeToGridStart();
7047
+ else if (mod)
7048
+ this.cellSelection.moveToGridStart();
7049
+ else if (shift)
7050
+ this.cellSelection.extendRangeToRowStart();
7051
+ else
7052
+ this.cellSelection.moveToRowStart();
7053
+ return true;
7054
+ }
7055
+ if (key === 'End') {
7056
+ if (mod && shift)
7057
+ this.cellSelection.extendRangeToGridEnd();
7058
+ else if (mod)
7059
+ this.cellSelection.moveToGridEnd();
7060
+ else if (shift)
7061
+ this.cellSelection.extendRangeToRowEnd();
7062
+ else
7063
+ this.cellSelection.moveToRowEnd();
7064
+ return true;
7065
+ }
7066
+ if (key === 'PageUp') {
7067
+ if (shift)
7068
+ this.cellSelection.extendRangeByPage('up');
7069
+ else
7070
+ this.cellSelection.movePage('up');
7071
+ return true;
7072
+ }
7073
+ if (key === 'PageDown') {
7074
+ if (shift)
7075
+ this.cellSelection.extendRangeByPage('down');
7076
+ else
7077
+ this.cellSelection.movePage('down');
7078
+ return true;
7079
+ }
7080
+ // ---- Tab / Enter / F2 / Escape ------------------------------------------
7081
+ if (key === 'Tab') {
7082
+ if (shift)
7083
+ this.cellSelection.moveLeft();
7084
+ else
7085
+ this.cellSelection.moveRight();
7086
+ return true;
7087
+ }
7088
+ if (key === 'Enter') {
7089
+ const col = this.state.visibleColumns()[focused.col];
7090
+ const def = col ? this.state.columnDefMap().get(col.field) : undefined;
7091
+ if (def?.editable) {
7092
+ this.actions?.startEdit(focused.row, focused.col);
7093
+ return true;
7094
+ }
7095
+ if (shift)
7096
+ this.cellSelection.moveUp();
7097
+ else
7098
+ this.cellSelection.moveDown();
7099
+ return true;
7100
+ }
7101
+ if (key === 'F2') {
7102
+ this.actions?.startEdit(focused.row, focused.col);
7103
+ return true;
7104
+ }
7105
+ if (key === 'Escape') {
7106
+ this.cellSelection.clearFocus();
7107
+ return true;
7108
+ }
7109
+ // ---- Typing-to-edit -----------------------------------------------------
7110
+ if (!mod && !alt && this.isPrintableKey(event)) {
7111
+ this.actions?.startEdit(focused.row, focused.col, key);
7112
+ return true;
7113
+ }
7114
+ return false;
7115
+ }
7116
+ handleArrow(key, mods) {
7117
+ const dir = key === 'ArrowUp' ? 'up' : key === 'ArrowDown' ? 'down' : key === 'ArrowLeft' ? 'left' : 'right';
7118
+ if (mods.mod && mods.shift) {
7119
+ this.cellSelection.extendRangeJumpToEdge(dir);
7120
+ return true;
7121
+ }
7122
+ if (mods.mod) {
7123
+ this.cellSelection.jumpToEdge(dir);
7124
+ return true;
7125
+ }
7126
+ if (mods.shift) {
7127
+ const dRow = dir === 'up' ? -1 : dir === 'down' ? 1 : 0;
7128
+ const dCol = dir === 'left' ? -1 : dir === 'right' ? 1 : 0;
7129
+ this.cellSelection.extendRangeBy(dRow, dCol);
7130
+ return true;
7131
+ }
7132
+ switch (dir) {
7133
+ case 'up':
7134
+ this.cellSelection.moveUp();
6242
7135
  break;
6243
- }
6244
- case 'Escape':
6245
- event.preventDefault();
6246
- this.cellSelection.clearFocus();
7136
+ case 'down':
7137
+ this.cellSelection.moveDown();
7138
+ break;
7139
+ case 'left':
7140
+ this.cellSelection.moveLeft();
7141
+ break;
7142
+ case 'right':
7143
+ this.cellSelection.moveRight();
6247
7144
  break;
6248
7145
  }
7146
+ return true;
6249
7147
  }
6250
- extendRangeBy(dRow, dCol) {
6251
- const range = this.state.cellRange();
6252
- const focused = this.state.focusedCell();
6253
- if (!focused)
6254
- return;
6255
- const pageStart = this.state.pageIndex() * this.state.pageSize();
6256
- const pageEnd = pageStart + Math.max(0, this.state.visibleRowCount() - 1);
6257
- const maxCol = this.state.visibleColumns().length - 1;
6258
- if (range) {
6259
- const newEnd = {
6260
- row: Math.max(pageStart, Math.min(pageEnd, range.end.row + dRow)),
6261
- col: Math.max(0, Math.min(maxCol, range.end.col + dCol)),
6262
- };
6263
- this.state.cellRange.set({ start: range.start, end: newEnd });
6264
- }
6265
- else {
6266
- const end = {
6267
- row: Math.max(pageStart, Math.min(pageEnd, focused.row + dRow)),
6268
- col: Math.max(0, Math.min(maxCol, focused.col + dCol)),
6269
- };
6270
- this.state.cellRange.set({ start: focused, end });
6271
- }
7148
+ /**
7149
+ * A key press is "printable" when it represents exactly one character that
7150
+ * the user intends as input — excluding named keys like F1, Home, Arrow*,
7151
+ * etc. Also excludes IME composition events (event.isComposing).
7152
+ */
7153
+ isPrintableKey(event) {
7154
+ if (event.isComposing)
7155
+ return false;
7156
+ if (event.key.length !== 1)
7157
+ return false;
7158
+ // Guard against NUL / control characters slipping through on some layouts.
7159
+ const code = event.key.charCodeAt(0);
7160
+ return code >= 32 && code !== 127;
6272
7161
  }
6273
7162
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: KeyboardEngine, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6274
7163
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: KeyboardEngine });
@@ -7363,11 +8252,11 @@ class MozGridHeaderComponent {
7363
8252
  this.dragEngine.startDrag(event, colIndex, center.nativeElement);
7364
8253
  }
7365
8254
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MozGridHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7366
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: MozGridHeaderComponent, isStandalone: true, selector: "moz-grid-header", inputs: { showCheckbox: { classPropertyName: "showCheckbox", publicName: "showCheckbox", isSignal: true, isRequired: false, transformFunction: null }, showExpand: { classPropertyName: "showExpand", publicName: "showExpand", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortClick: "sortClick", menuAction: "menuAction", resizeStart: "resizeStart", selectAllToggle: "selectAllToggle", columnReorder: "columnReorder" }, viewQueries: [{ propertyName: "headerCenter", first: true, predicate: ["headerCenter"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"grid-header\" [class.grid-header--no-bottom-border]=\"hasFilterRow()\" role=\"row\">\n <!-- Left pinned -->\n @if (hasLeftSection()) {\n <div class=\"grid-header__left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div class=\"grid-header__expand-spacer\"></div>\n } @if (showCheckbox()) {\n <div class=\"grid-header__checkbox\">\n <moz-checkbox\n id=\"grid-select-all\"\n label=\"Select all rows\"\n [indeterminate]=\"rowSelection.isIndeterminate()\"\n [ngModel]=\"rowSelection.isAllSelected()\"\n (change)=\"onSelectAllClick($event)\"\n />\n </div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"false\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n\n <!-- Center (scrolls natively inside viewport) -->\n <div class=\"grid-header__center\" #headerCenter [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__cell-wrapper\" [class.grid-header__cell-wrapper--last]=\"$last\">\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"$last\"\n [reorderable]=\"reorderable()\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n (mousedown)=\"onDragStart($event, $index)\"\n />\n </div>\n }\n </div>\n\n <!-- Right pinned -->\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [pinnedEnd]=\"true\"\n [isLast]=\"$last\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n</div>\n\n@if (hasFilterRow()) {\n<div class=\"grid-header__filter-row\">\n @if (hasLeftSection()) {\n <div class=\"grid-header__filter-left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 36px; min-width: 36px;\"\n ></div>\n } @if (showCheckbox()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 48px; min-width: 48px;\"\n ></div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n <div class=\"grid-header__filter-center\" [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell\" [class.grid-header__filter-cell--last]=\"$last\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n <div class=\"grid-header__filler\"></div>\n </div>\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__filter-right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell grid-header__filter-cell--pinned-end\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [":host{display:block;flex-shrink:0;z-index:3;overflow:hidden}.grid-header{display:flex;height:48px;width:100%;min-width:fit-content;border-bottom:1px solid var(--color-border-primary);background:var(--color-background-primary, #fff)}.grid-header--no-bottom-border{border-bottom:none}.grid-header__left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__center{display:flex;flex:1 0 auto;height:100%}.grid-header__right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__checkbox{display:flex;align-items:center;justify-content:center;width:48px;min-width:48px;box-sizing:border-box;cursor:pointer;background:inherit}.grid-header__checkbox ::ng-deep .checkbox{margin:0}.grid-header__checkbox ::ng-deep .checkbox__label{display:none}.grid-header__cell-wrapper{display:flex;position:relative;flex:0 0 auto}.grid-header__cell-wrapper--last{flex:1 0 auto}.grid-header__expand-spacer{width:36px;min-width:36px;box-sizing:border-box;background:inherit}.grid-header__filter-row{display:flex;min-width:fit-content;height:40px;background:var(--color-background-primary);border-bottom:1px solid var(--color-border-primary)}.grid-header__filter-left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-center{display:flex;flex:1 0 auto;height:100%}.grid-header__filter-right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-cell{display:flex;align-items:center;padding:0 var(--spacing-xs, 4px);box-sizing:border-box;overflow:hidden;height:100%;border-right:1px solid var(--color-border-primary)}.grid-header__filter-cell--spacer{flex-shrink:0;border-right:none}.grid-header__left moz-grid-header-cell:last-child ::ng-deep .grid-header-cell{border-right:none}.grid-header__right moz-grid-header-cell:first-child ::ng-deep .grid-header-cell{border-left:none}.grid-header__filter-cell--last{border-right:none}.grid-header__filter-left .grid-header__filter-cell:last-child{border-right:none}.grid-header__filter-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}.grid-header__filter-right .grid-header__filter-cell--pinned-end:first-child{border-left:none}\n"], dependencies: [{ kind: "component", type: MozGridHeaderCellComponent, selector: "moz-grid-header-cell", inputs: ["columnState", "def", "isLast", "pinnedEnd", "reorderable"], outputs: ["sortClick", "menuAction", "resizeStart"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8255
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: MozGridHeaderComponent, isStandalone: true, selector: "moz-grid-header", inputs: { showCheckbox: { classPropertyName: "showCheckbox", publicName: "showCheckbox", isSignal: true, isRequired: false, transformFunction: null }, showExpand: { classPropertyName: "showExpand", publicName: "showExpand", isSignal: true, isRequired: false, transformFunction: null }, reorderable: { classPropertyName: "reorderable", publicName: "reorderable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortClick: "sortClick", menuAction: "menuAction", resizeStart: "resizeStart", selectAllToggle: "selectAllToggle", columnReorder: "columnReorder" }, viewQueries: [{ propertyName: "headerCenter", first: true, predicate: ["headerCenter"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"grid-header\" [class.grid-header--no-bottom-border]=\"hasFilterRow()\" role=\"row\">\n <!-- Left pinned -->\n @if (hasLeftSection()) {\n <div class=\"grid-header__left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div class=\"grid-header__expand-spacer\"></div>\n } @if (showCheckbox()) {\n <div class=\"grid-header__checkbox\">\n <moz-checkbox\n id=\"grid-select-all\"\n label=\"Select all rows\"\n [indeterminate]=\"rowSelection.isIndeterminate()\"\n [ngModel]=\"rowSelection.isAllSelected()\"\n (change)=\"onSelectAllClick($event)\"\n />\n </div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"false\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n\n <!-- Center (scrolls natively inside viewport) -->\n <div class=\"grid-header__center\" #headerCenter [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__cell-wrapper\" [class.grid-header__cell-wrapper--last]=\"$last\">\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"$last\"\n [reorderable]=\"reorderable()\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n (mousedown)=\"onDragStart($event, $index)\"\n />\n </div>\n }\n </div>\n\n <!-- Right pinned -->\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [pinnedEnd]=\"true\"\n [isLast]=\"$last\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n</div>\n\n@if (hasFilterRow()) {\n<div class=\"grid-header__filter-row\">\n @if (hasLeftSection()) {\n <div class=\"grid-header__filter-left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 36px; min-width: 36px\"\n ></div>\n } @if (showCheckbox()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 48px; min-width: 48px\"\n ></div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n <div class=\"grid-header__filter-center\" [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div\n class=\"grid-header__filter-cell\"\n [class.grid-header__filter-cell--last]=\"$last\"\n [style.width.px]=\"col.currentWidth\"\n >\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n <div class=\"grid-header__filler\"></div>\n </div>\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__filter-right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--pinned-end\"\n [style.width.px]=\"col.currentWidth\"\n >\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [":host{display:block;flex-shrink:0;z-index:3;overflow:hidden}.grid-header{display:flex;height:48px;width:100%;min-width:fit-content;border-bottom:1px solid var(--color-border-primary);background:var(--color-background-primary, #fff)}.grid-header--no-bottom-border{border-bottom:none}.grid-header__left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__center{display:flex;flex:1 0 auto;height:100%}.grid-header__right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__checkbox{display:flex;align-items:center;justify-content:center;width:48px;min-width:48px;box-sizing:border-box;cursor:pointer;background:inherit}.grid-header__checkbox ::ng-deep .checkbox{margin:0}.grid-header__checkbox ::ng-deep .checkbox__label{display:none}.grid-header__cell-wrapper{display:flex;position:relative;flex:0 0 auto}.grid-header__cell-wrapper--last{flex:1 0 auto}.grid-header__filler{flex:1 1 auto;min-width:0;height:100%;background:inherit}.grid-header__expand-spacer{width:36px;min-width:36px;box-sizing:border-box;background:inherit}.grid-header__filter-row{display:flex;min-width:fit-content;height:40px;background:var(--color-background-primary);border-bottom:1px solid var(--color-border-primary)}.grid-header__filter-left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-center{display:flex;flex:1 0 auto;height:100%}.grid-header__filter-right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-cell{display:flex;align-items:center;padding:0 var(--spacing-xs, 4px);box-sizing:border-box;overflow:hidden;height:100%;border-right:1px solid var(--color-border-primary)}.grid-header__filter-cell--spacer{flex-shrink:0;border-right:none}.grid-header__left moz-grid-header-cell:last-child ::ng-deep .grid-header-cell{border-right:none}.grid-header__right moz-grid-header-cell:first-child ::ng-deep .grid-header-cell{border-left:none}.grid-header__filter-cell--last{border-right:none}.grid-header__filter-left .grid-header__filter-cell:last-child{border-right:none}.grid-header__filter-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}.grid-header__filter-right .grid-header__filter-cell--pinned-end:first-child{border-left:none}\n"], dependencies: [{ kind: "component", type: MozGridHeaderCellComponent, selector: "moz-grid-header-cell", inputs: ["columnState", "def", "isLast", "pinnedEnd", "reorderable"], outputs: ["sortClick", "menuAction", "resizeStart"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7367
8256
  }
7368
8257
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MozGridHeaderComponent, decorators: [{
7369
8258
  type: Component,
7370
- args: [{ selector: 'moz-grid-header', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MozGridHeaderCellComponent, MozCheckboxComponent, FormsModule, NgTemplateOutlet], template: "<div class=\"grid-header\" [class.grid-header--no-bottom-border]=\"hasFilterRow()\" role=\"row\">\n <!-- Left pinned -->\n @if (hasLeftSection()) {\n <div class=\"grid-header__left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div class=\"grid-header__expand-spacer\"></div>\n } @if (showCheckbox()) {\n <div class=\"grid-header__checkbox\">\n <moz-checkbox\n id=\"grid-select-all\"\n label=\"Select all rows\"\n [indeterminate]=\"rowSelection.isIndeterminate()\"\n [ngModel]=\"rowSelection.isAllSelected()\"\n (change)=\"onSelectAllClick($event)\"\n />\n </div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"false\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n\n <!-- Center (scrolls natively inside viewport) -->\n <div class=\"grid-header__center\" #headerCenter [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__cell-wrapper\" [class.grid-header__cell-wrapper--last]=\"$last\">\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"$last\"\n [reorderable]=\"reorderable()\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n (mousedown)=\"onDragStart($event, $index)\"\n />\n </div>\n }\n </div>\n\n <!-- Right pinned -->\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [pinnedEnd]=\"true\"\n [isLast]=\"$last\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n</div>\n\n@if (hasFilterRow()) {\n<div class=\"grid-header__filter-row\">\n @if (hasLeftSection()) {\n <div class=\"grid-header__filter-left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 36px; min-width: 36px;\"\n ></div>\n } @if (showCheckbox()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 48px; min-width: 48px;\"\n ></div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n <div class=\"grid-header__filter-center\" [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell\" [class.grid-header__filter-cell--last]=\"$last\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n <div class=\"grid-header__filler\"></div>\n </div>\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__filter-right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell grid-header__filter-cell--pinned-end\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [":host{display:block;flex-shrink:0;z-index:3;overflow:hidden}.grid-header{display:flex;height:48px;width:100%;min-width:fit-content;border-bottom:1px solid var(--color-border-primary);background:var(--color-background-primary, #fff)}.grid-header--no-bottom-border{border-bottom:none}.grid-header__left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__center{display:flex;flex:1 0 auto;height:100%}.grid-header__right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__checkbox{display:flex;align-items:center;justify-content:center;width:48px;min-width:48px;box-sizing:border-box;cursor:pointer;background:inherit}.grid-header__checkbox ::ng-deep .checkbox{margin:0}.grid-header__checkbox ::ng-deep .checkbox__label{display:none}.grid-header__cell-wrapper{display:flex;position:relative;flex:0 0 auto}.grid-header__cell-wrapper--last{flex:1 0 auto}.grid-header__expand-spacer{width:36px;min-width:36px;box-sizing:border-box;background:inherit}.grid-header__filter-row{display:flex;min-width:fit-content;height:40px;background:var(--color-background-primary);border-bottom:1px solid var(--color-border-primary)}.grid-header__filter-left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-center{display:flex;flex:1 0 auto;height:100%}.grid-header__filter-right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-cell{display:flex;align-items:center;padding:0 var(--spacing-xs, 4px);box-sizing:border-box;overflow:hidden;height:100%;border-right:1px solid var(--color-border-primary)}.grid-header__filter-cell--spacer{flex-shrink:0;border-right:none}.grid-header__left moz-grid-header-cell:last-child ::ng-deep .grid-header-cell{border-right:none}.grid-header__right moz-grid-header-cell:first-child ::ng-deep .grid-header-cell{border-left:none}.grid-header__filter-cell--last{border-right:none}.grid-header__filter-left .grid-header__filter-cell:last-child{border-right:none}.grid-header__filter-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}.grid-header__filter-right .grid-header__filter-cell--pinned-end:first-child{border-left:none}\n"] }]
8259
+ args: [{ selector: 'moz-grid-header', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MozGridHeaderCellComponent, MozCheckboxComponent, FormsModule, NgTemplateOutlet], template: "<div class=\"grid-header\" [class.grid-header--no-bottom-border]=\"hasFilterRow()\" role=\"row\">\n <!-- Left pinned -->\n @if (hasLeftSection()) {\n <div class=\"grid-header__left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div class=\"grid-header__expand-spacer\"></div>\n } @if (showCheckbox()) {\n <div class=\"grid-header__checkbox\">\n <moz-checkbox\n id=\"grid-select-all\"\n label=\"Select all rows\"\n [indeterminate]=\"rowSelection.isIndeterminate()\"\n [ngModel]=\"rowSelection.isAllSelected()\"\n (change)=\"onSelectAllClick($event)\"\n />\n </div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"false\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n\n <!-- Center (scrolls natively inside viewport) -->\n <div class=\"grid-header__center\" #headerCenter [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__cell-wrapper\" [class.grid-header__cell-wrapper--last]=\"$last\">\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [isLast]=\"$last\"\n [reorderable]=\"reorderable()\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n (mousedown)=\"onDragStart($event, $index)\"\n />\n </div>\n }\n </div>\n\n <!-- Right pinned -->\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <moz-grid-header-cell\n [columnState]=\"col\"\n [def]=\"state.columnDefMap().get(col.field)!\"\n [pinnedEnd]=\"true\"\n [isLast]=\"$last\"\n (sortClick)=\"sortClick.emit($event)\"\n (menuAction)=\"menuAction.emit($event)\"\n (resizeStart)=\"resizeStart.emit({ field: col.field, event: $event })\"\n />\n }\n </div>\n }\n</div>\n\n@if (hasFilterRow()) {\n<div class=\"grid-header__filter-row\">\n @if (hasLeftSection()) {\n <div class=\"grid-header__filter-left\" [style.width.px]=\"leftSectionWidth()\">\n @if (showExpand()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 36px; min-width: 36px\"\n ></div>\n } @if (showCheckbox()) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--spacer\"\n style=\"width: 48px; min-width: 48px\"\n ></div>\n } @for (col of state.pinnedLeftColumns(); track trackByField($index, col)) {\n <div class=\"grid-header__filter-cell\" [style.width.px]=\"col.currentWidth\">\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n <div class=\"grid-header__filter-center\" [style.min-width.px]=\"state.unpinnedWidth()\">\n @for (col of state.unpinnedColumns(); track trackByField($index, col)) {\n <div\n class=\"grid-header__filter-cell\"\n [class.grid-header__filter-cell--last]=\"$last\"\n [style.width.px]=\"col.currentWidth\"\n >\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n <div class=\"grid-header__filler\"></div>\n </div>\n @if (state.pinnedRightColumns().length > 0) {\n <div class=\"grid-header__filter-right\" [style.width.px]=\"state.pinnedRightWidth()\">\n @for (col of state.pinnedRightColumns(); track trackByField($index, col)) {\n <div\n class=\"grid-header__filter-cell grid-header__filter-cell--pinned-end\"\n [style.width.px]=\"col.currentWidth\"\n >\n @if (getFilterTemplate(col.field); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: col.field }\"\n />\n }\n </div>\n }\n </div>\n }\n</div>\n}\n", styles: [":host{display:block;flex-shrink:0;z-index:3;overflow:hidden}.grid-header{display:flex;height:48px;width:100%;min-width:fit-content;border-bottom:1px solid var(--color-border-primary);background:var(--color-background-primary, #fff)}.grid-header--no-bottom-border{border-bottom:none}.grid-header__left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__center{display:flex;flex:1 0 auto;height:100%}.grid-header__right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary, #fff);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__checkbox{display:flex;align-items:center;justify-content:center;width:48px;min-width:48px;box-sizing:border-box;cursor:pointer;background:inherit}.grid-header__checkbox ::ng-deep .checkbox{margin:0}.grid-header__checkbox ::ng-deep .checkbox__label{display:none}.grid-header__cell-wrapper{display:flex;position:relative;flex:0 0 auto}.grid-header__cell-wrapper--last{flex:1 0 auto}.grid-header__filler{flex:1 1 auto;min-width:0;height:100%;background:inherit}.grid-header__expand-spacer{width:36px;min-width:36px;box-sizing:border-box;background:inherit}.grid-header__filter-row{display:flex;min-width:fit-content;height:40px;background:var(--color-background-primary);border-bottom:1px solid var(--color-border-primary)}.grid-header__filter-left{display:flex;flex:0 0 auto;position:sticky;left:0;z-index:3;background:var(--color-background-primary);box-shadow:4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-center{display:flex;flex:1 0 auto;height:100%}.grid-header__filter-right{display:flex;flex:0 0 auto;position:sticky;right:0;z-index:3;background:var(--color-background-primary);box-shadow:-4px 0 8px -2px #0000001a;box-sizing:border-box}.grid-header__filter-cell{display:flex;align-items:center;padding:0 var(--spacing-xs, 4px);box-sizing:border-box;overflow:hidden;height:100%;border-right:1px solid var(--color-border-primary)}.grid-header__filter-cell--spacer{flex-shrink:0;border-right:none}.grid-header__left moz-grid-header-cell:last-child ::ng-deep .grid-header-cell{border-right:none}.grid-header__right moz-grid-header-cell:first-child ::ng-deep .grid-header-cell{border-left:none}.grid-header__filter-cell--last{border-right:none}.grid-header__filter-left .grid-header__filter-cell:last-child{border-right:none}.grid-header__filter-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}.grid-header__filter-right .grid-header__filter-cell--pinned-end:first-child{border-left:none}\n"] }]
7371
8260
  }], ctorParameters: () => [], propDecorators: { headerCenter: [{ type: i0.ViewChild, args: ['headerCenter', { isSignal: true }] }], showCheckbox: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCheckbox", required: false }] }], showExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExpand", required: false }] }], reorderable: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorderable", required: false }] }], sortClick: [{ type: i0.Output, args: ["sortClick"] }], menuAction: [{ type: i0.Output, args: ["menuAction"] }], resizeStart: [{ type: i0.Output, args: ["resizeStart"] }], selectAllToggle: [{ type: i0.Output, args: ["selectAllToggle"] }], columnReorder: [{ type: i0.Output, args: ["columnReorder"] }] } });
7372
8261
 
7373
8262
  /**
@@ -7779,6 +8668,7 @@ class MozGridCellComponent {
7779
8668
  inlineEdit = inject(InlineEditEngine);
7780
8669
  cellSelectionEngine = inject(CellSelectionEngine);
7781
8670
  validationEngine = inject(CellValidationEngine);
8671
+ clipboard = inject(ClipboardEngine);
7782
8672
  elRef = inject((ElementRef));
7783
8673
  preEditWidth = null;
7784
8674
  constructor() {
@@ -7893,6 +8783,7 @@ class MozGridCellComponent {
7893
8783
  isInFillRejectRange = computed(() => {
7894
8784
  return this.cellSelectionEngine.isCellInFillRejectRange(this.rowIndex(), this.colIndex());
7895
8785
  }, ...(ngDevMode ? [{ debugName: "isInFillRejectRange" }] : /* istanbul ignore next */ []));
8786
+ cutEdges = computed(() => this.clipboard.cutEdges(this.rowIndex(), this.colIndex()), ...(ngDevMode ? [{ debugName: "cutEdges" }] : /* istanbul ignore next */ []));
7896
8787
  cellError = computed(() => this.validationEngine.getCellError(this.rowIndex(), this.def().field), ...(ngDevMode ? [{ debugName: "cellError" }] : /* istanbul ignore next */ []));
7897
8788
  onCellClick(event) {
7898
8789
  // Shift+click: extend range from current focused cell to this cell
@@ -7997,7 +8888,7 @@ class MozGridCellComponent {
7997
8888
  });
7998
8889
  }
7999
8890
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MozGridCellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
8000
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: MozGridCellComponent, isStandalone: true, selector: "moz-grid-cell", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, rowIndex: { classPropertyName: "rowIndex", publicName: "rowIndex", isSignal: true, isRequired: true, transformFunction: null }, colIndex: { classPropertyName: "colIndex", publicName: "colIndex", isSignal: true, isRequired: true, transformFunction: null }, colState: { classPropertyName: "colState", publicName: "colState", isSignal: true, isRequired: true, transformFunction: null }, def: { classPropertyName: "def", publicName: "def", isSignal: true, isRequired: true, transformFunction: null }, isLast: { classPropertyName: "isLast", publicName: "isLast", isSignal: true, isRequired: false, transformFunction: null }, pinnedEnd: { classPropertyName: "pinnedEnd", publicName: "pinnedEnd", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { commitEdit: "commitEdit", cancelEdit: "cancelEdit" }, host: { properties: { "style.flex": "isLast() ? \"1 0 auto\" : \"0 0 auto\"", "style.width.px": "isLast() ? undefined : colState().currentWidth", "style.min-width.px": "isLast() ? colState().currentWidth : resolvedMinWidth()", "style.overflow": "isFocused() && !isEditing() ? \"visible\" : \"hidden\"" } }, ngImport: i0, template: "<div\n class=\"grid-cell\"\n [class.grid-cell--focused]=\"isFocused()\"\n [class.grid-cell--in-range]=\"isInRange()\"\n [class.grid-cell--in-fill-range]=\"isInFillRange()\"\n [class.grid-cell--in-fill-reject-range]=\"isInFillRejectRange()\"\n [class.grid-cell--last]=\"isLast()\"\n [class.grid-cell--pinned-end]=\"pinnedEnd()\"\n [class.grid-cell--readonly]=\"!def().editable\"\n [class.grid-cell--error]=\"cellError()\"\n [attr.aria-invalid]=\"cellError() ? 'true' : null\"\n (click)=\"onCellClick($event)\"\n (dblclick)=\"onDoubleClick()\"\n (mousedown)=\"onMouseDown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n>\n @if (isEditing()) {\n <div class=\"grid-cell__editor\" (focusout)=\"onEditorBlur($event)\">\n @if (editTemplate()) {\n <div class=\"grid-cell__editor-custom\">\n <ng-container\n [ngTemplateOutlet]=\"editTemplate()!\"\n [ngTemplateOutletContext]=\"{\n $implicit: value(),\n row: row(),\n field: def().field,\n draft: editState().draftValue,\n updateDraft: updateDraftFn,\n commitEdit: commitEditFn\n }\"\n />\n </div>\n } @else { @switch (editorType()) { @case ('text') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('number') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"number\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('select') {\n <moz-select\n name=\"cell-editor\"\n [options]=\"def().cellEditorOptions ?? []\"\n [ngModel]=\"editState().draftValue\"\n (change)=\"onSelectChange($event)\"\n [size]=\"'s'\"\n />\n } @case ('checkbox') {\n <moz-checkbox\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n [ngModel]=\"!!editState().draftValue\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @case ('date') {\n <moz-datepicker\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n size=\"s\"\n [ngModel]=\"editState().draftValue\"\n (ngModelChange)=\"onDateChange($event)\"\n />\n } @default {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } } }\n </div>\n } @else { @if (cellTemplate()) {\n <div class=\"grid-cell__custom\">\n <ng-container\n [ngTemplateOutlet]=\"cellTemplate()!\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), row: row(), field: def().field }\"\n />\n </div>\n } @else {\n <span class=\"grid-cell__value\">{{ displayValue() }}</span>\n } } @if (isFocused() && !isEditing() && def().editable) {\n <div class=\"grid-cell__fill-handle\" (mousedown)=\"onFillHandleMouseDown($event)\"></div>\n } @if (cellError(); as error) {\n <div\n class=\"grid-cell__error-icon\"\n [mozTooltip]=\"error.message\"\n tooltipPosition=\"top\"\n aria-label=\"Erreur de validation\"\n >\n <ErrorFilled24 />\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-width:0}.grid-cell{display:flex;align-items:center;position:relative;padding:0 var(--spacing-s, 8px);height:100%;border-right:1px solid var(--color-border-primary);overflow:hidden;box-sizing:border-box;min-width:0;background:inherit;cursor:pointer}.grid-cell--last{border-right:none}.grid-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}:host(:first-child) .grid-cell--pinned-end{border-left:none}.grid-cell__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);position:relative;z-index:1}.grid-cell__editor{width:100%;min-width:0;max-width:100%;height:100%;display:flex;align-items:center;overflow:hidden;box-sizing:border-box;position:relative;z-index:1}.grid-cell__editor-custom{width:100%;min-width:0;max-width:100%;overflow:hidden;box-sizing:border-box;display:flex;align-items:center;flex-wrap:wrap;gap:4px}.grid-cell__editor input,.grid-cell__editor moz-select{width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain{border:none;outline:none;background:transparent;font-family:inherit;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);padding:0;height:100%;width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain:focus{outline:none}.grid-cell__input--plain[type=number]::-webkit-inner-spin-button,.grid-cell__input--plain[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.grid-cell__input--plain[type=number]{-moz-appearance:textfield}.grid-cell__editor ::ng-deep .text-input{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .select,.grid-cell__editor ::ng-deep .select__trigger{min-width:0;width:100%}.grid-cell__editor ::ng-deep *{max-width:100%}.grid-cell__editor moz-datepicker{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .mc-datepicker,.grid-cell__editor ::ng-deep .mc-text-input{width:100%;min-width:0;box-sizing:border-box;height:28px;font-size:var(--font-size-xs, 12px)}.grid-cell__editor ::ng-deep moz-datepicker label{display:none}.grid-cell>*:not(.grid-cell__fill-handle){position:relative;z-index:1}.grid-cell:before{content:\"\";position:absolute;inset:3px;border-radius:4px;pointer-events:none;z-index:0}.grid-cell:hover:before{background:#f1f3f4}.grid-cell--focused:hover:before,.grid-cell--in-range:hover:before,.grid-cell--in-fill-range:hover:before,.grid-cell--in-fill-reject-range:hover:before{background:transparent}.grid-cell--focused{z-index:2;overflow:visible}.grid-cell--focused:after{content:\"\";position:absolute;inset:0;border:2px solid var(--color-background-accent-inverse);border-radius:4px;pointer-events:none}.grid-cell--in-range{background:var(--color-background-accent)}.grid-cell--readonly .grid-cell__value{color:var(--color-text-secondary, #666)}.grid-cell--in-fill-range{background:#34a85314}.grid-cell--in-fill-range:after{content:\"\";position:absolute;inset:0;border:1px dashed var(--color-success, #34a853);border-radius:4px;pointer-events:none}.grid-cell--in-fill-reject-range{background:#ea302d1f;cursor:not-allowed;z-index:2}.grid-cell--in-fill-reject-range:after{content:\"\";position:absolute;inset:0;border:2px dashed var(--Status-Standalone-element-Error, #ea302d);border-radius:4px;pointer-events:none;z-index:3}.grid-cell__fill-handle{position:absolute;right:-4px;bottom:-4px;width:8px;height:8px;background:var(--color-background-primary, #fff);border:1px solid var(--color-background-accent-inverse);border-radius:2px;cursor:crosshair;z-index:4}.grid-cell--error{background:var(--Background-Primary, #FFF);outline:2px solid var(--Status-Border-Error, #EF5F5C);outline-offset:-2px;z-index:1}.grid-cell--error:hover:before{background:transparent}.grid-cell__error-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:auto;cursor:help;position:relative;z-index:2}.grid-cell__error-icon ::ng-deep svg{fill:var(--Status-Standalone-element-Error, #EA302D)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MozSelectComponent, selector: "moz-select", inputs: ["id", "name", "options", "placeholder", "isInvalid", "disabled", "readonly", "size"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "component", type: MozDatepickerComponent, selector: "moz-datepicker", inputs: ["id", "disabled", "readonly", "invalid", "error", "clearable", "size", "label"] }, { kind: "directive", type: MozTooltipDirective, selector: "[mozTooltip]", inputs: ["mozTooltip", "tooltipPosition", "tooltipNoPointer"] }, { kind: "component", type: ErrorFilled24, selector: "ErrorFilled24", inputs: ["hostClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8891
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: MozGridCellComponent, isStandalone: true, selector: "moz-grid-cell", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null }, rowIndex: { classPropertyName: "rowIndex", publicName: "rowIndex", isSignal: true, isRequired: true, transformFunction: null }, colIndex: { classPropertyName: "colIndex", publicName: "colIndex", isSignal: true, isRequired: true, transformFunction: null }, colState: { classPropertyName: "colState", publicName: "colState", isSignal: true, isRequired: true, transformFunction: null }, def: { classPropertyName: "def", publicName: "def", isSignal: true, isRequired: true, transformFunction: null }, isLast: { classPropertyName: "isLast", publicName: "isLast", isSignal: true, isRequired: false, transformFunction: null }, pinnedEnd: { classPropertyName: "pinnedEnd", publicName: "pinnedEnd", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { commitEdit: "commitEdit", cancelEdit: "cancelEdit" }, host: { properties: { "style.flex": "isLast() ? \"1 0 auto\" : \"0 0 auto\"", "style.width.px": "isLast() ? undefined : colState().currentWidth", "style.min-width.px": "isLast() ? colState().currentWidth : resolvedMinWidth()" } }, ngImport: i0, template: "<div\n class=\"grid-cell\"\n [class.grid-cell--focused]=\"isFocused()\"\n [class.grid-cell--in-range]=\"isInRange()\"\n [class.grid-cell--in-fill-range]=\"isInFillRange()\"\n [class.grid-cell--in-fill-reject-range]=\"isInFillRejectRange()\"\n [class.grid-cell--cut]=\"cutEdges().any\"\n [class.grid-cell--last]=\"isLast()\"\n [class.grid-cell--pinned-end]=\"pinnedEnd()\"\n [class.grid-cell--readonly]=\"!def().editable\"\n [class.grid-cell--error]=\"cellError()\"\n [attr.aria-invalid]=\"cellError() ? 'true' : null\"\n (click)=\"onCellClick($event)\"\n (dblclick)=\"onDoubleClick()\"\n (mousedown)=\"onMouseDown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n>\n @if (isEditing()) {\n <div class=\"grid-cell__editor\" (focusout)=\"onEditorBlur($event)\">\n @if (editTemplate()) {\n <div class=\"grid-cell__editor-custom\">\n <ng-container\n [ngTemplateOutlet]=\"editTemplate()!\"\n [ngTemplateOutletContext]=\"{\n $implicit: value(),\n row: row(),\n field: def().field,\n draft: editState().draftValue,\n updateDraft: updateDraftFn,\n commitEdit: commitEditFn\n }\"\n />\n </div>\n } @else { @switch (editorType()) { @case ('text') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('number') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"number\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('select') {\n <moz-select\n name=\"cell-editor\"\n [options]=\"def().cellEditorOptions ?? []\"\n [ngModel]=\"editState().draftValue\"\n (change)=\"onSelectChange($event)\"\n [size]=\"'s'\"\n />\n } @case ('checkbox') {\n <moz-checkbox\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n [ngModel]=\"!!editState().draftValue\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @case ('date') {\n <moz-datepicker\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n size=\"s\"\n [ngModel]=\"editState().draftValue\"\n (ngModelChange)=\"onDateChange($event)\"\n />\n } @default {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } } }\n </div>\n } @else { @if (cellTemplate()) {\n <div class=\"grid-cell__custom\">\n <ng-container\n [ngTemplateOutlet]=\"cellTemplate()!\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), row: row(), field: def().field }\"\n />\n </div>\n } @else {\n <span class=\"grid-cell__value\">{{ displayValue() }}</span>\n } } @if (cutEdges(); as edges) { @if (edges.top) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--top\"></div>\n } @if (edges.bottom) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--bottom\"></div>\n } @if (edges.left) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--left\"></div>\n } @if (edges.right) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--right\"></div>\n } } @if (isFocused() && !isEditing() && def().editable) {\n <div class=\"grid-cell__fill-handle\" (mousedown)=\"onFillHandleMouseDown($event)\"></div>\n } @if (cellError(); as error) {\n <div\n class=\"grid-cell__error-icon\"\n [mozTooltip]=\"error.message\"\n tooltipPosition=\"top\"\n aria-label=\"Erreur de validation\"\n >\n <ErrorFilled24 />\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;height:100%;min-width:0}.grid-cell{display:flex;align-items:center;position:relative;padding:0 var(--spacing-s, 8px);height:100%;border-right:1px solid var(--color-border-primary);overflow:hidden;box-sizing:border-box;min-width:0;background:inherit;cursor:pointer}.grid-cell--last{border-right:none}.grid-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}:host(:first-child) .grid-cell--pinned-end{border-left:none}.grid-cell__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);position:relative;z-index:1}.grid-cell__editor{width:100%;min-width:0;max-width:100%;height:100%;display:flex;align-items:center;overflow:hidden;box-sizing:border-box;position:relative;z-index:1}.grid-cell__editor-custom{width:100%;min-width:0;max-width:100%;overflow:hidden;box-sizing:border-box;display:flex;align-items:center;flex-wrap:wrap;gap:4px}.grid-cell__editor input,.grid-cell__editor moz-select{width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain{border:none;outline:none;background:transparent;font-family:inherit;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);padding:0;height:100%;width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain:focus{outline:none}.grid-cell__input--plain[type=number]::-webkit-inner-spin-button,.grid-cell__input--plain[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.grid-cell__input--plain[type=number]{-moz-appearance:textfield}.grid-cell__editor ::ng-deep .text-input{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .select,.grid-cell__editor ::ng-deep .select__trigger{min-width:0;width:100%}.grid-cell__editor ::ng-deep *{max-width:100%}.grid-cell__editor moz-datepicker{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .mc-datepicker,.grid-cell__editor ::ng-deep .mc-text-input{width:100%;min-width:0;box-sizing:border-box;height:28px;font-size:var(--font-size-xs, 12px)}.grid-cell__editor ::ng-deep moz-datepicker label{display:none}.grid-cell>*:not(.grid-cell__fill-handle){position:relative;z-index:1}.grid-cell:before{content:\"\";position:absolute;inset:3px;border-radius:4px;pointer-events:none;z-index:0}.grid-cell:hover:before{background:#f1f3f4}.grid-cell--focused:hover:before,.grid-cell--in-range:hover:before,.grid-cell--in-fill-range:hover:before,.grid-cell--in-fill-reject-range:hover:before{background:transparent}.grid-cell--focused{z-index:2}.grid-cell--focused:after{content:\"\";position:absolute;inset:0;border:2px solid var(--color-background-accent-inverse);border-radius:4px;pointer-events:none}.grid-cell--in-range{background:var(--color-background-accent)}.grid-cell--readonly .grid-cell__value{color:var(--color-text-secondary, #666)}.grid-cell--in-fill-range{background:#34a85314}.grid-cell--in-fill-range:after{content:\"\";position:absolute;inset:0;border:1px dashed var(--color-success, #34a853);border-radius:4px;pointer-events:none}.grid-cell--in-fill-reject-range{background:#ea302d1f;cursor:not-allowed;z-index:2}.grid-cell--in-fill-reject-range:after{content:\"\";position:absolute;inset:0;border:2px dashed var(--Status-Standalone-element-Error, #ea302d);border-radius:4px;pointer-events:none;z-index:3}.grid-cell--cut{background:#1a73e80f}.grid-cell__cut-mark{position:absolute;pointer-events:none;z-index:5;--cut-color: var(--color-background-accent-inverse, #1a73e8)}.grid-cell__cut-mark--top,.grid-cell__cut-mark--bottom{left:0;right:0;height:2px;background-image:linear-gradient(90deg,var(--cut-color) 50%,transparent 50%);background-size:8px 2px;background-repeat:repeat-x;animation:moz-grid-marching-ants-x .5s linear infinite}.grid-cell__cut-mark--top{top:0}.grid-cell__cut-mark--bottom{bottom:0}.grid-cell__cut-mark--left,.grid-cell__cut-mark--right{top:0;bottom:0;width:2px;background-image:linear-gradient(180deg,var(--cut-color) 50%,transparent 50%);background-size:2px 8px;background-repeat:repeat-y;animation:moz-grid-marching-ants-y .5s linear infinite}.grid-cell__cut-mark--left{left:0}.grid-cell__cut-mark--right{right:0}@keyframes moz-grid-marching-ants-x{0%{background-position-x:0}to{background-position-x:8px}}@keyframes moz-grid-marching-ants-y{0%{background-position-y:0}to{background-position-y:8px}}.grid-cell__fill-handle{position:absolute;right:0;bottom:0;width:8px;height:8px;background:var(--color-background-accent-inverse);cursor:crosshair;z-index:4}.grid-cell--error{background:var(--Background-Primary, #fff);outline:2px solid var(--Status-Border-Error, #ef5f5c);outline-offset:-2px;z-index:1}.grid-cell--error:hover:before{background:transparent}.grid-cell__error-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:auto;cursor:help;position:relative;z-index:2}.grid-cell__error-icon ::ng-deep svg{fill:var(--Status-Standalone-element-Error, #ea302d)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MozSelectComponent, selector: "moz-select", inputs: ["id", "name", "options", "placeholder", "isInvalid", "disabled", "readonly", "size"] }, { kind: "component", type: MozCheckboxComponent, selector: "moz-checkbox", inputs: ["id", "name", "label", "indeterminate", "isInvalid", "disabled", "indented"] }, { kind: "component", type: MozDatepickerComponent, selector: "moz-datepicker", inputs: ["id", "disabled", "readonly", "invalid", "error", "clearable", "size", "label"] }, { kind: "directive", type: MozTooltipDirective, selector: "[mozTooltip]", inputs: ["mozTooltip", "tooltipPosition", "tooltipNoPointer"] }, { kind: "component", type: ErrorFilled24, selector: "ErrorFilled24", inputs: ["hostClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8001
8892
  }
8002
8893
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MozGridCellComponent, decorators: [{
8003
8894
  type: Component,
@@ -8013,8 +8904,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
8013
8904
  '[style.flex]': 'isLast() ? "1 0 auto" : "0 0 auto"',
8014
8905
  '[style.width.px]': 'isLast() ? undefined : colState().currentWidth',
8015
8906
  '[style.min-width.px]': 'isLast() ? colState().currentWidth : resolvedMinWidth()',
8016
- '[style.overflow]': 'isFocused() && !isEditing() ? "visible" : "hidden"',
8017
- }, template: "<div\n class=\"grid-cell\"\n [class.grid-cell--focused]=\"isFocused()\"\n [class.grid-cell--in-range]=\"isInRange()\"\n [class.grid-cell--in-fill-range]=\"isInFillRange()\"\n [class.grid-cell--in-fill-reject-range]=\"isInFillRejectRange()\"\n [class.grid-cell--last]=\"isLast()\"\n [class.grid-cell--pinned-end]=\"pinnedEnd()\"\n [class.grid-cell--readonly]=\"!def().editable\"\n [class.grid-cell--error]=\"cellError()\"\n [attr.aria-invalid]=\"cellError() ? 'true' : null\"\n (click)=\"onCellClick($event)\"\n (dblclick)=\"onDoubleClick()\"\n (mousedown)=\"onMouseDown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n>\n @if (isEditing()) {\n <div class=\"grid-cell__editor\" (focusout)=\"onEditorBlur($event)\">\n @if (editTemplate()) {\n <div class=\"grid-cell__editor-custom\">\n <ng-container\n [ngTemplateOutlet]=\"editTemplate()!\"\n [ngTemplateOutletContext]=\"{\n $implicit: value(),\n row: row(),\n field: def().field,\n draft: editState().draftValue,\n updateDraft: updateDraftFn,\n commitEdit: commitEditFn\n }\"\n />\n </div>\n } @else { @switch (editorType()) { @case ('text') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('number') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"number\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('select') {\n <moz-select\n name=\"cell-editor\"\n [options]=\"def().cellEditorOptions ?? []\"\n [ngModel]=\"editState().draftValue\"\n (change)=\"onSelectChange($event)\"\n [size]=\"'s'\"\n />\n } @case ('checkbox') {\n <moz-checkbox\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n [ngModel]=\"!!editState().draftValue\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @case ('date') {\n <moz-datepicker\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n size=\"s\"\n [ngModel]=\"editState().draftValue\"\n (ngModelChange)=\"onDateChange($event)\"\n />\n } @default {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } } }\n </div>\n } @else { @if (cellTemplate()) {\n <div class=\"grid-cell__custom\">\n <ng-container\n [ngTemplateOutlet]=\"cellTemplate()!\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), row: row(), field: def().field }\"\n />\n </div>\n } @else {\n <span class=\"grid-cell__value\">{{ displayValue() }}</span>\n } } @if (isFocused() && !isEditing() && def().editable) {\n <div class=\"grid-cell__fill-handle\" (mousedown)=\"onFillHandleMouseDown($event)\"></div>\n } @if (cellError(); as error) {\n <div\n class=\"grid-cell__error-icon\"\n [mozTooltip]=\"error.message\"\n tooltipPosition=\"top\"\n aria-label=\"Erreur de validation\"\n >\n <ErrorFilled24 />\n </div>\n }\n</div>\n", styles: [":host{display:block;height:100%;min-width:0}.grid-cell{display:flex;align-items:center;position:relative;padding:0 var(--spacing-s, 8px);height:100%;border-right:1px solid var(--color-border-primary);overflow:hidden;box-sizing:border-box;min-width:0;background:inherit;cursor:pointer}.grid-cell--last{border-right:none}.grid-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}:host(:first-child) .grid-cell--pinned-end{border-left:none}.grid-cell__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);position:relative;z-index:1}.grid-cell__editor{width:100%;min-width:0;max-width:100%;height:100%;display:flex;align-items:center;overflow:hidden;box-sizing:border-box;position:relative;z-index:1}.grid-cell__editor-custom{width:100%;min-width:0;max-width:100%;overflow:hidden;box-sizing:border-box;display:flex;align-items:center;flex-wrap:wrap;gap:4px}.grid-cell__editor input,.grid-cell__editor moz-select{width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain{border:none;outline:none;background:transparent;font-family:inherit;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);padding:0;height:100%;width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain:focus{outline:none}.grid-cell__input--plain[type=number]::-webkit-inner-spin-button,.grid-cell__input--plain[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.grid-cell__input--plain[type=number]{-moz-appearance:textfield}.grid-cell__editor ::ng-deep .text-input{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .select,.grid-cell__editor ::ng-deep .select__trigger{min-width:0;width:100%}.grid-cell__editor ::ng-deep *{max-width:100%}.grid-cell__editor moz-datepicker{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .mc-datepicker,.grid-cell__editor ::ng-deep .mc-text-input{width:100%;min-width:0;box-sizing:border-box;height:28px;font-size:var(--font-size-xs, 12px)}.grid-cell__editor ::ng-deep moz-datepicker label{display:none}.grid-cell>*:not(.grid-cell__fill-handle){position:relative;z-index:1}.grid-cell:before{content:\"\";position:absolute;inset:3px;border-radius:4px;pointer-events:none;z-index:0}.grid-cell:hover:before{background:#f1f3f4}.grid-cell--focused:hover:before,.grid-cell--in-range:hover:before,.grid-cell--in-fill-range:hover:before,.grid-cell--in-fill-reject-range:hover:before{background:transparent}.grid-cell--focused{z-index:2;overflow:visible}.grid-cell--focused:after{content:\"\";position:absolute;inset:0;border:2px solid var(--color-background-accent-inverse);border-radius:4px;pointer-events:none}.grid-cell--in-range{background:var(--color-background-accent)}.grid-cell--readonly .grid-cell__value{color:var(--color-text-secondary, #666)}.grid-cell--in-fill-range{background:#34a85314}.grid-cell--in-fill-range:after{content:\"\";position:absolute;inset:0;border:1px dashed var(--color-success, #34a853);border-radius:4px;pointer-events:none}.grid-cell--in-fill-reject-range{background:#ea302d1f;cursor:not-allowed;z-index:2}.grid-cell--in-fill-reject-range:after{content:\"\";position:absolute;inset:0;border:2px dashed var(--Status-Standalone-element-Error, #ea302d);border-radius:4px;pointer-events:none;z-index:3}.grid-cell__fill-handle{position:absolute;right:-4px;bottom:-4px;width:8px;height:8px;background:var(--color-background-primary, #fff);border:1px solid var(--color-background-accent-inverse);border-radius:2px;cursor:crosshair;z-index:4}.grid-cell--error{background:var(--Background-Primary, #FFF);outline:2px solid var(--Status-Border-Error, #EF5F5C);outline-offset:-2px;z-index:1}.grid-cell--error:hover:before{background:transparent}.grid-cell__error-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:auto;cursor:help;position:relative;z-index:2}.grid-cell__error-icon ::ng-deep svg{fill:var(--Status-Standalone-element-Error, #EA302D)}\n"] }]
8907
+ }, template: "<div\n class=\"grid-cell\"\n [class.grid-cell--focused]=\"isFocused()\"\n [class.grid-cell--in-range]=\"isInRange()\"\n [class.grid-cell--in-fill-range]=\"isInFillRange()\"\n [class.grid-cell--in-fill-reject-range]=\"isInFillRejectRange()\"\n [class.grid-cell--cut]=\"cutEdges().any\"\n [class.grid-cell--last]=\"isLast()\"\n [class.grid-cell--pinned-end]=\"pinnedEnd()\"\n [class.grid-cell--readonly]=\"!def().editable\"\n [class.grid-cell--error]=\"cellError()\"\n [attr.aria-invalid]=\"cellError() ? 'true' : null\"\n (click)=\"onCellClick($event)\"\n (dblclick)=\"onDoubleClick()\"\n (mousedown)=\"onMouseDown($event)\"\n (mouseenter)=\"onMouseEnter()\"\n>\n @if (isEditing()) {\n <div class=\"grid-cell__editor\" (focusout)=\"onEditorBlur($event)\">\n @if (editTemplate()) {\n <div class=\"grid-cell__editor-custom\">\n <ng-container\n [ngTemplateOutlet]=\"editTemplate()!\"\n [ngTemplateOutletContext]=\"{\n $implicit: value(),\n row: row(),\n field: def().field,\n draft: editState().draftValue,\n updateDraft: updateDraftFn,\n commitEdit: commitEditFn\n }\"\n />\n </div>\n } @else { @switch (editorType()) { @case ('text') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('number') {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"number\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } @case ('select') {\n <moz-select\n name=\"cell-editor\"\n [options]=\"def().cellEditorOptions ?? []\"\n [ngModel]=\"editState().draftValue\"\n (change)=\"onSelectChange($event)\"\n [size]=\"'s'\"\n />\n } @case ('checkbox') {\n <moz-checkbox\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n [ngModel]=\"!!editState().draftValue\"\n (change)=\"onCheckboxChange($event)\"\n />\n } @case ('date') {\n <moz-datepicker\n [id]=\"'grid-cell-editor-' + rowIndex() + '-' + colIndex()\"\n size=\"s\"\n [ngModel]=\"editState().draftValue\"\n (ngModelChange)=\"onDateChange($event)\"\n />\n } @default {\n <input\n class=\"grid-cell__input grid-cell__input--plain\"\n type=\"text\"\n [value]=\"editState().draftValue\"\n (input)=\"onEditorInput($event)\"\n />\n } } }\n </div>\n } @else { @if (cellTemplate()) {\n <div class=\"grid-cell__custom\">\n <ng-container\n [ngTemplateOutlet]=\"cellTemplate()!\"\n [ngTemplateOutletContext]=\"{ $implicit: value(), row: row(), field: def().field }\"\n />\n </div>\n } @else {\n <span class=\"grid-cell__value\">{{ displayValue() }}</span>\n } } @if (cutEdges(); as edges) { @if (edges.top) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--top\"></div>\n } @if (edges.bottom) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--bottom\"></div>\n } @if (edges.left) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--left\"></div>\n } @if (edges.right) {\n <div class=\"grid-cell__cut-mark grid-cell__cut-mark--right\"></div>\n } } @if (isFocused() && !isEditing() && def().editable) {\n <div class=\"grid-cell__fill-handle\" (mousedown)=\"onFillHandleMouseDown($event)\"></div>\n } @if (cellError(); as error) {\n <div\n class=\"grid-cell__error-icon\"\n [mozTooltip]=\"error.message\"\n tooltipPosition=\"top\"\n aria-label=\"Erreur de validation\"\n >\n <ErrorFilled24 />\n </div>\n }\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block;height:100%;min-width:0}.grid-cell{display:flex;align-items:center;position:relative;padding:0 var(--spacing-s, 8px);height:100%;border-right:1px solid var(--color-border-primary);overflow:hidden;box-sizing:border-box;min-width:0;background:inherit;cursor:pointer}.grid-cell--last{border-right:none}.grid-cell--pinned-end{border-right:none;border-left:1px solid var(--color-border-primary)}:host(:first-child) .grid-cell--pinned-end{border-left:none}.grid-cell__value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);position:relative;z-index:1}.grid-cell__editor{width:100%;min-width:0;max-width:100%;height:100%;display:flex;align-items:center;overflow:hidden;box-sizing:border-box;position:relative;z-index:1}.grid-cell__editor-custom{width:100%;min-width:0;max-width:100%;overflow:hidden;box-sizing:border-box;display:flex;align-items:center;flex-wrap:wrap;gap:4px}.grid-cell__editor input,.grid-cell__editor moz-select{width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain{border:none;outline:none;background:transparent;font-family:inherit;font-size:var(--font-size-s, 14px);color:var(--color-text-primary);padding:0;height:100%;width:100%;min-width:0;box-sizing:border-box}.grid-cell__input--plain:focus{outline:none}.grid-cell__input--plain[type=number]::-webkit-inner-spin-button,.grid-cell__input--plain[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.grid-cell__input--plain[type=number]{-moz-appearance:textfield}.grid-cell__editor ::ng-deep .text-input{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .select,.grid-cell__editor ::ng-deep .select__trigger{min-width:0;width:100%}.grid-cell__editor ::ng-deep *{max-width:100%}.grid-cell__editor moz-datepicker{width:100%;min-width:0;box-sizing:border-box}.grid-cell__editor ::ng-deep .mc-datepicker,.grid-cell__editor ::ng-deep .mc-text-input{width:100%;min-width:0;box-sizing:border-box;height:28px;font-size:var(--font-size-xs, 12px)}.grid-cell__editor ::ng-deep moz-datepicker label{display:none}.grid-cell>*:not(.grid-cell__fill-handle){position:relative;z-index:1}.grid-cell:before{content:\"\";position:absolute;inset:3px;border-radius:4px;pointer-events:none;z-index:0}.grid-cell:hover:before{background:#f1f3f4}.grid-cell--focused:hover:before,.grid-cell--in-range:hover:before,.grid-cell--in-fill-range:hover:before,.grid-cell--in-fill-reject-range:hover:before{background:transparent}.grid-cell--focused{z-index:2}.grid-cell--focused:after{content:\"\";position:absolute;inset:0;border:2px solid var(--color-background-accent-inverse);border-radius:4px;pointer-events:none}.grid-cell--in-range{background:var(--color-background-accent)}.grid-cell--readonly .grid-cell__value{color:var(--color-text-secondary, #666)}.grid-cell--in-fill-range{background:#34a85314}.grid-cell--in-fill-range:after{content:\"\";position:absolute;inset:0;border:1px dashed var(--color-success, #34a853);border-radius:4px;pointer-events:none}.grid-cell--in-fill-reject-range{background:#ea302d1f;cursor:not-allowed;z-index:2}.grid-cell--in-fill-reject-range:after{content:\"\";position:absolute;inset:0;border:2px dashed var(--Status-Standalone-element-Error, #ea302d);border-radius:4px;pointer-events:none;z-index:3}.grid-cell--cut{background:#1a73e80f}.grid-cell__cut-mark{position:absolute;pointer-events:none;z-index:5;--cut-color: var(--color-background-accent-inverse, #1a73e8)}.grid-cell__cut-mark--top,.grid-cell__cut-mark--bottom{left:0;right:0;height:2px;background-image:linear-gradient(90deg,var(--cut-color) 50%,transparent 50%);background-size:8px 2px;background-repeat:repeat-x;animation:moz-grid-marching-ants-x .5s linear infinite}.grid-cell__cut-mark--top{top:0}.grid-cell__cut-mark--bottom{bottom:0}.grid-cell__cut-mark--left,.grid-cell__cut-mark--right{top:0;bottom:0;width:2px;background-image:linear-gradient(180deg,var(--cut-color) 50%,transparent 50%);background-size:2px 8px;background-repeat:repeat-y;animation:moz-grid-marching-ants-y .5s linear infinite}.grid-cell__cut-mark--left{left:0}.grid-cell__cut-mark--right{right:0}@keyframes moz-grid-marching-ants-x{0%{background-position-x:0}to{background-position-x:8px}}@keyframes moz-grid-marching-ants-y{0%{background-position-y:0}to{background-position-y:8px}}.grid-cell__fill-handle{position:absolute;right:0;bottom:0;width:8px;height:8px;background:var(--color-background-accent-inverse);cursor:crosshair;z-index:4}.grid-cell--error{background:var(--Background-Primary, #fff);outline:2px solid var(--Status-Border-Error, #ef5f5c);outline-offset:-2px;z-index:1}.grid-cell--error:hover:before{background:transparent}.grid-cell__error-icon{display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:auto;cursor:help;position:relative;z-index:2}.grid-cell__error-icon ::ng-deep svg{fill:var(--Status-Standalone-element-Error, #ea302d)}\n"] }]
8018
8908
  }], ctorParameters: () => [], propDecorators: { row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: true }] }], rowIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowIndex", required: true }] }], colIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "colIndex", required: true }] }], colState: [{ type: i0.Input, args: [{ isSignal: true, alias: "colState", required: true }] }], def: [{ type: i0.Input, args: [{ isSignal: true, alias: "def", required: true }] }], isLast: [{ type: i0.Input, args: [{ isSignal: true, alias: "isLast", required: false }] }], pinnedEnd: [{ type: i0.Input, args: [{ isSignal: true, alias: "pinnedEnd", required: false }] }], commitEdit: [{ type: i0.Output, args: ["commitEdit"] }], cancelEdit: [{ type: i0.Output, args: ["cancelEdit"] }] } });
8019
8909
 
8020
8910
  class MozGridRowComponent {
@@ -8510,6 +9400,85 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
8510
9400
  ], template: "<div class=\"group-drawer__list\" cdkDropList (cdkDropListDropped)=\"onDrop($event)\">\n @for (item of draftGrouped(); track item.field; let idx = $index) {\n <div class=\"group-drawer__item\" cdkDrag>\n <span class=\"group-drawer__drag-handle\" cdkDragHandle>\n <Drag20 />\n </span>\n <span class=\"group-drawer__item-label\">{{ item.headerName }}</span>\n <select\n class=\"group-drawer__sort-select\"\n [value]=\"item.sortDirection\"\n [attr.aria-label]=\"'Sort direction for ' + item.headerName\"\n (change)=\"onSortDirectionChange(idx, $event)\"\n >\n <option value=\"asc\">A \u2192 Z</option>\n <option value=\"desc\">Z \u2192 A</option>\n </select>\n <button\n type=\"button\"\n moz-button\n [attr.aria-label]=\"'Remove ' + item.headerName\"\n (click)=\"removeGroup(item.field)\"\n [iconPosition]=\"'only'\"\n [size]=\"'s'\"\n [ghost]=\"true\"\n >\n <Cross20 icon />\n </button>\n </div>\n }\n</div>\n\n@if (available().length > 0) {\n<div class=\"group-drawer__available\">\n <h4 class=\"group-drawer__section-title\">Available columns</h4>\n @for (col of available(); track col.field) {\n <div class=\"group-drawer__available-item\">\n <span class=\"group-drawer__available-label\">{{ col.headerName }}</span>\n <button\n type=\"button\"\n class=\"group-drawer__add-btn\"\n [attr.aria-label]=\"'Add ' + col.headerName + ' as group'\"\n (click)=\"addGroup(col.field)\"\n >\n <ListAdd20 />\n </button>\n </div>\n }\n</div>\n} @if (draftGrouped().length === 0 && available().length === 0) {\n<p class=\"group-drawer__empty\">No groupable columns available.</p>\n}\n\n<ng-template mozDrawerFooter>\n <button moz-button (click)=\"apply()\">Apply</button>\n <button moz-button [outlined]=\"true\" (click)=\"reset()\">Reset</button>\n</ng-template>\n", styles: [".group-drawer__list{display:flex;flex-direction:column}.group-drawer__item{display:flex;align-items:center;gap:8px;padding:12px 0;border-bottom:1px solid var(--color-border-primary);background:var(--color-background-primary, #fff)}.group-drawer__item:last-child{border-bottom:none}.group-drawer__drag-handle{display:flex;align-items:center;cursor:grab;color:var(--color-text-secondary);flex-shrink:0}.group-drawer__drag-handle:active{cursor:grabbing}.group-drawer__item-label{flex:1;font-size:var(--font-size-m, 16px);color:var(--color-text-primary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-drawer__sort-select{flex-shrink:0;padding:4px 8px;border:1px solid var(--color-border-primary);border-radius:var(--border-radius-s, 4px);background:var(--color-background-primary, #fff);font-size:var(--font-size-s, 14px);color:var(--color-text-primary);cursor:pointer}.group-drawer__sort-select:focus{outline:2px solid var(--color-background-accent-inverse);outline-offset:-1px}.group-drawer__available{margin-top:16px;margin-bottom:32px}.group-drawer__section-title{font-size:var(--font-size-s, 14px);font-weight:600;color:var(--color-text-secondary);margin:0 0 8px;text-transform:uppercase;letter-spacing:.5px}.group-drawer__available-item{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--color-border-primary)}.group-drawer__available-item:last-child{border-bottom:none}.group-drawer__available-label{font-size:var(--font-size-m, 16px);color:var(--color-text-primary)}.group-drawer__add-btn{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:28px;height:28px;padding:0;border:none;border-radius:var(--border-radius-s, 4px);background:transparent;cursor:pointer;color:var(--color-background-accent-inverse)}.group-drawer__add-btn:hover{background:#1a73e814;color:var(--color-primary-dark, #1557b0)}.group-drawer__empty{font-size:var(--font-size-s, 14px);color:var(--color-text-secondary);text-align:center;padding:24px 0}.cdk-drag-preview{display:flex;align-items:center;gap:8px;padding:12px 8px;background:var(--color-background-primary, #fff);border:1px solid var(--color-border-primary);border-radius:4px;box-shadow:0 4px 12px #00000026;font-size:var(--font-size-m, 16px)}.cdk-drag-placeholder{opacity:.3}.cdk-drag-animating{transition:transform .2s ease}\n"] }]
8511
9401
  }] });
8512
9402
 
9403
+ const EXCEL_SHORTCUTS = [
9404
+ {
9405
+ title: 'Navigation',
9406
+ items: [
9407
+ { keys: '← ↑ → ↓', label: 'Déplacer la cellule active' },
9408
+ { keys: 'Ctrl + Flèche', label: 'Sauter au bord du bloc de données' },
9409
+ { keys: 'Home / End', label: 'Début / fin de la ligne' },
9410
+ { keys: 'Ctrl + Home / End', label: 'Première / dernière cellule' },
9411
+ { keys: 'PageUp / PageDown', label: 'Page précédente / suivante' },
9412
+ { keys: 'Tab / Shift+Tab', label: 'Cellule suivante / précédente' },
9413
+ { keys: 'Enter / Shift+Enter', label: 'Descendre / remonter' },
9414
+ ],
9415
+ },
9416
+ {
9417
+ title: 'Sélection',
9418
+ items: [
9419
+ { keys: 'Shift + Flèche', label: 'Étendre la plage' },
9420
+ { keys: 'Shift + Ctrl + Flèche', label: "Étendre jusqu'au bord du bloc" },
9421
+ { keys: 'Shift + Home / End', label: 'Étendre au début / fin de la ligne' },
9422
+ { keys: 'Shift + Ctrl + Home / End', label: 'Étendre au début / fin du tableau' },
9423
+ { keys: 'Shift + PageUp / Down', label: "Étendre d'une page" },
9424
+ { keys: 'Ctrl + A', label: 'Sélectionner tout' },
9425
+ { keys: 'Shift + Espace', label: 'Sélectionner la ligne' },
9426
+ { keys: 'Ctrl + Espace', label: 'Sélectionner la colonne' },
9427
+ ],
9428
+ },
9429
+ {
9430
+ title: 'Édition',
9431
+ items: [
9432
+ { keys: 'Enter / F2', label: 'Entrer en édition' },
9433
+ { keys: 'Touche imprimable', label: 'Typing-to-edit (remplace la valeur)' },
9434
+ { keys: 'Escape', label: "Annuler l'édition" },
9435
+ { keys: 'Enter', label: 'Valider + descendre' },
9436
+ { keys: 'Tab / Shift+Tab', label: 'Valider + droite / gauche' },
9437
+ { keys: 'Alt + Enter', label: 'Retour à la ligne (texte)' },
9438
+ { keys: 'Ctrl + Enter', label: 'Valider + remplir la sélection' },
9439
+ { keys: 'Backspace / Delete', label: 'Effacer les cellules sélectionnées' },
9440
+ ],
9441
+ },
9442
+ {
9443
+ title: 'Presse-papier',
9444
+ items: [
9445
+ { keys: 'Ctrl + C', label: 'Copier (TSV)' },
9446
+ { keys: 'Ctrl + X', label: 'Couper (marching ants)' },
9447
+ { keys: 'Ctrl + V', label: 'Coller (déplace après Ctrl+X)' },
9448
+ { keys: 'Ctrl + D', label: 'Remplir vers le bas (fill down)' },
9449
+ { keys: 'Ctrl + R', label: 'Remplir vers la droite (fill right)' },
9450
+ ],
9451
+ },
9452
+ {
9453
+ title: 'Historique',
9454
+ items: [
9455
+ { keys: 'Ctrl + Z', label: 'Annuler (undo)' },
9456
+ { keys: 'Ctrl + Y', label: 'Rétablir (redo)' },
9457
+ { keys: 'Ctrl + Shift + Z', label: 'Rétablir (redo, alt.)' },
9458
+ ],
9459
+ },
9460
+ ];
9461
+ class GridKeyboardShortcutsDrawerComponent {
9462
+ drawerRef = inject(MozDrawerRef);
9463
+ groups = EXCEL_SHORTCUTS.map((group) => ({
9464
+ title: group.title,
9465
+ items: group.items.map((item) => ({
9466
+ keys: item.keys,
9467
+ label: item.label,
9468
+ parts: item.keys.split(' '),
9469
+ })),
9470
+ }));
9471
+ close() {
9472
+ this.drawerRef.close();
9473
+ }
9474
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GridKeyboardShortcutsDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9475
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: GridKeyboardShortcutsDrawerComponent, isStandalone: true, selector: "moz-grid-keyboard-shortcuts-drawer", ngImport: i0, template: "<div class=\"shortcuts\">\n @for (group of groups; track group.title) {\n <section class=\"shortcuts__group\">\n <h4 class=\"shortcuts__group-title\">{{ group.title }}</h4>\n <dl class=\"shortcuts__list\">\n @for (item of group.items; track item.keys) {\n <div class=\"shortcuts__item\">\n <dt class=\"shortcuts__keys\">\n @for (part of item.parts; track $index) { @if (part === '+' || part === '/') {\n <span class=\"shortcuts__separator\">{{ part }}</span>\n } @else {\n <kbd class=\"shortcuts__key\">{{ part }}</kbd>\n } }\n </dt>\n <dd class=\"shortcuts__label\">{{ item.label }}</dd>\n </div>\n }\n </dl>\n </section>\n }\n</div>\n\n<ng-template mozDrawerFooter>\n <button moz-button (click)=\"close()\">Close</button>\n</ng-template>\n", styles: [".shortcuts{padding-bottom:24px}.shortcuts__group{margin-bottom:24px}.shortcuts__group:last-child{margin-bottom:0}.shortcuts__group-title{margin:0 0 8px;font-size:var(--font-size-s, 14px);font-weight:700;color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.5px}.shortcuts__list{display:flex;flex-direction:column;margin:0;padding:0}.shortcuts__item{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--color-border-primary)}.shortcuts__item:last-child{border-bottom:none}.shortcuts__label{margin:0;flex:1;font-size:var(--font-size-s, 14px);color:var(--color-text-primary)}.shortcuts__keys{display:flex;align-items:center;flex-wrap:wrap;gap:4px;flex-shrink:0;margin:0}.shortcuts__key{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;padding:0 6px;background:var(--color-background-secondary, #f5f5f5);border:1px solid var(--color-border-primary);border-radius:var(--border-radius-s, 4px);font-family:var(--font-family-monospace, \"SFMono-Regular\", Consolas, monospace);font-size:var(--font-size-50, 12px);font-weight:600;color:var(--color-text-primary);box-shadow:inset 0 -1px #00000014}.shortcuts__separator{font-size:var(--font-size-s, 14px);color:var(--color-text-secondary);padding:0 2px}\n"], dependencies: [{ kind: "component", type: MozButtonComponent, selector: "button[moz-button]", inputs: ["appearance", "size", "disabled", "ghost", "outlined", "iconPosition", "type", "isLoading"] }, { kind: "directive", type: MozDrawerFooterDirective, selector: "[mozDrawerFooter]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9476
+ }
9477
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: GridKeyboardShortcutsDrawerComponent, decorators: [{
9478
+ type: Component,
9479
+ args: [{ selector: 'moz-grid-keyboard-shortcuts-drawer', changeDetection: ChangeDetectionStrategy.OnPush, imports: [MozButtonComponent, MozDrawerFooterDirective], template: "<div class=\"shortcuts\">\n @for (group of groups; track group.title) {\n <section class=\"shortcuts__group\">\n <h4 class=\"shortcuts__group-title\">{{ group.title }}</h4>\n <dl class=\"shortcuts__list\">\n @for (item of group.items; track item.keys) {\n <div class=\"shortcuts__item\">\n <dt class=\"shortcuts__keys\">\n @for (part of item.parts; track $index) { @if (part === '+' || part === '/') {\n <span class=\"shortcuts__separator\">{{ part }}</span>\n } @else {\n <kbd class=\"shortcuts__key\">{{ part }}</kbd>\n } }\n </dt>\n <dd class=\"shortcuts__label\">{{ item.label }}</dd>\n </div>\n }\n </dl>\n </section>\n }\n</div>\n\n<ng-template mozDrawerFooter>\n <button moz-button (click)=\"close()\">Close</button>\n</ng-template>\n", styles: [".shortcuts{padding-bottom:24px}.shortcuts__group{margin-bottom:24px}.shortcuts__group:last-child{margin-bottom:0}.shortcuts__group-title{margin:0 0 8px;font-size:var(--font-size-s, 14px);font-weight:700;color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.5px}.shortcuts__list{display:flex;flex-direction:column;margin:0;padding:0}.shortcuts__item{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:10px 0;border-bottom:1px solid var(--color-border-primary)}.shortcuts__item:last-child{border-bottom:none}.shortcuts__label{margin:0;flex:1;font-size:var(--font-size-s, 14px);color:var(--color-text-primary)}.shortcuts__keys{display:flex;align-items:center;flex-wrap:wrap;gap:4px;flex-shrink:0;margin:0}.shortcuts__key{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;padding:0 6px;background:var(--color-background-secondary, #f5f5f5);border:1px solid var(--color-border-primary);border-radius:var(--border-radius-s, 4px);font-family:var(--font-family-monospace, \"SFMono-Regular\", Consolas, monospace);font-size:var(--font-size-50, 12px);font-weight:600;color:var(--color-text-primary);box-shadow:inset 0 -1px #00000014}.shortcuts__separator{font-size:var(--font-size-s, 14px);color:var(--color-text-secondary);padding:0 2px}\n"] }]
9480
+ }] });
9481
+
8513
9482
  class MozGridComponent {
8514
9483
  state = inject(GridStateManager);
8515
9484
  gridEngine = inject(GridEngine);
@@ -8519,6 +9488,8 @@ class MozGridComponent {
8519
9488
  rowSelectionEngine = inject(RowSelectionEngine);
8520
9489
  cellSelectionEngine = inject(CellSelectionEngine);
8521
9490
  keyboardEngine = inject(KeyboardEngine);
9491
+ clipboardEngine = inject(ClipboardEngine);
9492
+ historyEngine = inject(HistoryEngine);
8522
9493
  groupEngine = inject(GroupEngine);
8523
9494
  filterEngine = inject(FilterEngine);
8524
9495
  persistenceEngine = inject(StatePersistenceEngine);
@@ -8596,6 +9567,24 @@ class MozGridComponent {
8596
9567
  stateRestored = false;
8597
9568
  documentMouseUpHandler = null;
8598
9569
  constructor() {
9570
+ this.keyboardEngine.registerActions({
9571
+ copy: () => this.onBulkCopy(),
9572
+ paste: () => {
9573
+ void this.onBulkPaste();
9574
+ },
9575
+ cut: () => this.onCutShortcut(),
9576
+ deleteRange: () => this.onBulkDelete(),
9577
+ undo: () => this.onUndo(),
9578
+ redo: () => this.onRedo(),
9579
+ fillDown: () => this.onFillDownShortcut(),
9580
+ fillRight: () => this.onFillRightShortcut(),
9581
+ startEdit: (row, col, char) => this.onStartEditShortcut(row, col, char),
9582
+ });
9583
+ // Bind history persistence to the grid's stateKey (same key we use for
9584
+ // column/sort persistence — one localStorage namespace per grid).
9585
+ effect(() => {
9586
+ this.historyEngine.attach(this.stateKey());
9587
+ });
8599
9588
  // Global mouseup listener for fill handle — if the user drags the fill
8600
9589
  // handle outside the grid and releases, we still need to end the fill.
8601
9590
  afterNextRender(() => {
@@ -8631,6 +9620,7 @@ class MozGridComponent {
8631
9620
  const pushScroll = () => {
8632
9621
  this.ngZone.run(() => {
8633
9622
  this.horizontalVirtualScrollEngine.onScroll(scrollEl.scrollLeft, scrollEl.clientWidth);
9623
+ this.state.scrollViewportHeight.set(scrollEl.clientHeight);
8634
9624
  });
8635
9625
  };
8636
9626
  // Prime the engine with the initial viewport width so the range is
@@ -8907,64 +9897,154 @@ class MozGridComponent {
8907
9897
  if (tag === 'input' || tag === 'select' || tag === 'textarea') {
8908
9898
  return;
8909
9899
  }
8910
- // Bulk action shortcuts work for both cell and row selection modes
9900
+ // Row-selection mode forwards a small set of shortcuts without touching
9901
+ // cell focus. Cell-mode (and anywhere else) goes through the keyboard engine.
8911
9902
  const selMode = this.state.activeSelectionMode();
8912
- if (selMode === 'rows' || selMode === 'cells') {
8913
- if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
9903
+ if (selMode === 'rows') {
9904
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c') {
8914
9905
  event.preventDefault();
8915
9906
  this.onBulkCopy();
8916
9907
  return;
8917
9908
  }
8918
- if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
9909
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v') {
8919
9910
  event.preventDefault();
8920
- this.onBulkPaste();
9911
+ void this.onBulkPaste();
8921
9912
  return;
8922
9913
  }
8923
9914
  if (event.key === 'Delete' || event.key === 'Backspace') {
8924
- if (selMode === 'rows' || this.state.cellRange() || this.state.focusedCell()) {
8925
- event.preventDefault();
8926
- this.onBulkDelete();
8927
- return;
8928
- }
9915
+ event.preventDefault();
9916
+ this.onBulkDelete();
9917
+ return;
8929
9918
  }
8930
9919
  }
8931
9920
  this.keyboardEngine.handleKeydown(event);
8932
9921
  }
8933
9922
  handleEditModeKeydown(event) {
8934
- switch (event.key) {
8935
- case 'Escape': {
8936
- event.preventDefault();
8937
- const cancelEvt = this.inlineEditEngine.cancelEdit();
8938
- if (cancelEvt) {
8939
- this.cellEditCancel.emit(cancelEvt);
8940
- }
8941
- break;
9923
+ if (event.key === 'Escape') {
9924
+ event.preventDefault();
9925
+ const cancelEvt = this.inlineEditEngine.cancelEdit();
9926
+ if (cancelEvt) {
9927
+ this.cellEditCancel.emit(cancelEvt);
8942
9928
  }
8943
- case 'Enter': {
8944
- event.preventDefault();
8945
- const commitEvt = this.inlineEditEngine.commitEdit();
8946
- if (commitEvt) {
8947
- this.cellEdit.emit(commitEvt);
9929
+ return;
9930
+ }
9931
+ if (event.key === 'Enter') {
9932
+ // Alt+Enter: insert a newline in the draft for text editors (Excel-style).
9933
+ if (event.altKey) {
9934
+ const editState = this.state.cellEditState();
9935
+ const def = this.state
9936
+ .columnDefMap()
9937
+ .get(this.state.visibleColumns()[editState.editingCell?.col ?? -1]?.field ?? '');
9938
+ const editorType = def?.cellEditor ?? 'text';
9939
+ if (editorType === 'text') {
9940
+ event.preventDefault();
9941
+ const draft = editState.draftValue;
9942
+ const next = (typeof draft === 'string' ? draft : String(draft ?? '')) + '\n';
9943
+ this.inlineEditEngine.updateDraft(next);
8948
9944
  }
8949
- this.cellSelectionEngine.moveRight();
8950
- this.refocusGrid();
8951
- break;
9945
+ return;
8952
9946
  }
8953
- case 'Tab': {
9947
+ // Ctrl+Enter: commit current draft and broadcast it to the active range.
9948
+ if (event.ctrlKey || event.metaKey) {
8954
9949
  event.preventDefault();
8955
- const tabEvt = this.inlineEditEngine.commitEdit();
8956
- if (tabEvt) {
8957
- this.cellEdit.emit(tabEvt);
8958
- }
8959
- if (event.shiftKey) {
8960
- this.cellSelectionEngine.moveLeft();
8961
- }
8962
- else {
8963
- this.cellSelectionEngine.moveRight();
9950
+ const editState = this.state.cellEditState();
9951
+ const value = editState.draftValue;
9952
+ const cancel = this.inlineEditEngine.cancelEdit();
9953
+ if (cancel)
9954
+ this.cellEditCancel.emit(cancel);
9955
+ const range = this.cellSelectionEngine.getNormalizedRange();
9956
+ if (range) {
9957
+ const changes = this.clipboardEngine.fillSelection(range, value);
9958
+ this.historyEngine.record('fill-selection', changes);
8964
9959
  }
8965
9960
  this.refocusGrid();
8966
- break;
9961
+ return;
8967
9962
  }
9963
+ event.preventDefault();
9964
+ this.commitFromEditMode();
9965
+ if (event.shiftKey)
9966
+ this.cellSelectionEngine.moveUp();
9967
+ else
9968
+ this.cellSelectionEngine.moveDown();
9969
+ this.refocusGrid();
9970
+ return;
9971
+ }
9972
+ if (event.key === 'Tab') {
9973
+ event.preventDefault();
9974
+ this.commitFromEditMode();
9975
+ if (event.shiftKey)
9976
+ this.cellSelectionEngine.moveLeft();
9977
+ else
9978
+ this.cellSelectionEngine.moveRight();
9979
+ this.refocusGrid();
9980
+ }
9981
+ }
9982
+ commitFromEditMode() {
9983
+ const evt = this.inlineEditEngine.commitEdit();
9984
+ if (!evt)
9985
+ return;
9986
+ this.cellEdit.emit(evt);
9987
+ this.clipboardEngine.clearCut();
9988
+ }
9989
+ onCutShortcut() {
9990
+ const range = this.cellSelectionEngine.getNormalizedRange();
9991
+ if (!range)
9992
+ return;
9993
+ const values = this.clipboardEngine.extractTsv(range);
9994
+ const tsv = values.map((row) => row.join('\t')).join('\n');
9995
+ navigator.clipboard.writeText(tsv);
9996
+ this.clipboardEngine.markCut(range);
9997
+ this.bulkCopy.emit({
9998
+ range,
9999
+ data: values,
10000
+ rowIds: this.getRangeRowIds(range),
10001
+ fields: this.getRangeFields(range),
10002
+ });
10003
+ }
10004
+ onUndo() {
10005
+ this.historyEngine.undo();
10006
+ this.clipboardEngine.clearCut();
10007
+ }
10008
+ onRedo() {
10009
+ this.historyEngine.redo();
10010
+ this.clipboardEngine.clearCut();
10011
+ }
10012
+ onFillDownShortcut() {
10013
+ const range = this.cellSelectionEngine.getNormalizedRange();
10014
+ if (!range)
10015
+ return;
10016
+ if (this.state.mode() !== 'client')
10017
+ return;
10018
+ const changes = this.clipboardEngine.fillDown(range);
10019
+ if (changes.length === 0)
10020
+ return;
10021
+ this.historyEngine.record('fill-down', changes);
10022
+ this.clipboardEngine.clearCut();
10023
+ }
10024
+ onFillRightShortcut() {
10025
+ const range = this.cellSelectionEngine.getNormalizedRange();
10026
+ if (!range)
10027
+ return;
10028
+ if (this.state.mode() !== 'client')
10029
+ return;
10030
+ const changes = this.clipboardEngine.fillRight(range);
10031
+ if (changes.length === 0)
10032
+ return;
10033
+ this.historyEngine.record('fill-right', changes);
10034
+ this.clipboardEngine.clearCut();
10035
+ }
10036
+ onStartEditShortcut(row, col, initialChar) {
10037
+ const colDef = this.state.visibleColumns()[col];
10038
+ if (!colDef)
10039
+ return;
10040
+ const def = this.state.columnDefMap().get(colDef.field);
10041
+ if (!def?.editable)
10042
+ return;
10043
+ if (initialChar !== undefined) {
10044
+ this.inlineEditEngine.startEditWithChar(row, colDef.field, initialChar);
10045
+ }
10046
+ else {
10047
+ this.inlineEditEngine.startEdit(row, colDef.field);
8968
10048
  }
8969
10049
  }
8970
10050
  resetInfiniteScrollIfActive() {
@@ -9051,17 +10131,26 @@ class MozGridComponent {
9051
10131
  if (srcIdx >= 0)
9052
10132
  indexMap.set(r, srcIdx);
9053
10133
  }
10134
+ const changes = [];
9054
10135
  this.state.sourceData.update((d) => {
9055
10136
  const updated = [...d];
9056
10137
  for (const [, srcIdx] of indexMap) {
9057
10138
  if (!updated[srcIdx])
9058
10139
  continue;
10140
+ const before = updated[srcIdx][field];
10141
+ if (before === sourceValue)
10142
+ continue;
9059
10143
  const rowCopy = { ...updated[srcIdx] };
9060
10144
  rowCopy[field] = sourceValue;
9061
10145
  updated[srcIdx] = rowCopy;
10146
+ changes.push({ rowIndex: srcIdx, field, before, after: sourceValue });
9062
10147
  }
9063
10148
  return updated;
9064
10149
  });
10150
+ if (changes.length > 0) {
10151
+ this.historyEngine.record('fill', changes);
10152
+ }
10153
+ this.clipboardEngine.clearCut();
9065
10154
  }
9066
10155
  this.fillDown.emit({
9067
10156
  sourceCell: anchor,
@@ -9102,6 +10191,7 @@ class MozGridComponent {
9102
10191
  if (targetFields.length === 0)
9103
10192
  return;
9104
10193
  if (this.state.mode() === 'client') {
10194
+ const changes = [];
9105
10195
  this.state.sourceData.update((d) => {
9106
10196
  const updated = [...d];
9107
10197
  const src = updated[anchorSourceIdx];
@@ -9109,11 +10199,19 @@ class MozGridComponent {
9109
10199
  return updated;
9110
10200
  const rowCopy = { ...src };
9111
10201
  for (const f of targetFields) {
10202
+ const before = src[f];
10203
+ if (before === sourceValue)
10204
+ continue;
9112
10205
  rowCopy[f] = sourceValue;
10206
+ changes.push({ rowIndex: anchorSourceIdx, field: f, before, after: sourceValue });
9113
10207
  }
9114
10208
  updated[anchorSourceIdx] = rowCopy;
9115
10209
  return updated;
9116
10210
  });
10211
+ if (changes.length > 0) {
10212
+ this.historyEngine.record('fill', changes);
10213
+ }
10214
+ this.clipboardEngine.clearCut();
9117
10215
  }
9118
10216
  this.fillDown.emit({
9119
10217
  sourceCell: anchor,
@@ -9216,6 +10314,10 @@ class MozGridComponent {
9216
10314
  groups: [...result.groups],
9217
10315
  });
9218
10316
  }
10317
+ // --- Keyboard shortcuts drawer ---
10318
+ onKeyboardShortcutsClick() {
10319
+ this.drawerService.open(GridKeyboardShortcutsDrawerComponent, { title: 'Keyboard shortcuts' });
10320
+ }
9219
10321
  // --- Settings drawer ---
9220
10322
  static DENSITY_ROW_HEIGHT = {
9221
10323
  small: 32,
@@ -9320,6 +10422,8 @@ class MozGridComponent {
9320
10422
  });
9321
10423
  }
9322
10424
  onBulkCopy() {
10425
+ // A fresh copy always cancels any pending cut (Excel parity).
10426
+ this.clipboardEngine.clearCut();
9323
10427
  if (this.state.activeSelectionMode() === 'rows') {
9324
10428
  const event = this.rowSelectionEngine.getSelectionEvent();
9325
10429
  const rowData = this.extractRowSelectionData(event.selectedRows);
@@ -9387,7 +10491,16 @@ class MozGridComponent {
9387
10491
  },
9388
10492
  };
9389
10493
  if (this.state.mode() === 'client') {
9390
- this.applyPasteData(pasteRange, rows);
10494
+ // If a cut is pending, first wipe the source cells so cut+paste == move,
10495
+ // and fold both halves into a single undoable history op.
10496
+ const cutSource = this.state.cutSource();
10497
+ const clearChanges = cutSource ? this.clipboardEngine.clearRange(cutSource) : [];
10498
+ const pasteChanges = this.clipboardEngine.applyPaste(pasteRange, rows);
10499
+ const allChanges = [...clearChanges, ...pasteChanges];
10500
+ if (allChanges.length > 0) {
10501
+ this.historyEngine.record(cutSource ? 'cut' : 'paste', allChanges);
10502
+ }
10503
+ this.clipboardEngine.clearCut();
9391
10504
  }
9392
10505
  this.bulkPaste.emit({
9393
10506
  range: pasteRange,
@@ -9449,33 +10562,14 @@ class MozGridComponent {
9449
10562
  const pageEnd = pageStart + this.gridEngine.paginatedData().length - 1;
9450
10563
  if (range.start.row < pageStart || range.end.row > pageEnd)
9451
10564
  return;
9452
- const cols = this.state.visibleColumns();
9453
10565
  const rows = range.end.row - range.start.row + 1;
9454
10566
  const colCount = range.end.col - range.start.col + 1;
9455
10567
  if (this.state.mode() === 'client') {
9456
- this.state.sourceData.update((data) => {
9457
- const updated = [...data];
9458
- for (let r = range.start.row; r <= range.end.row; r++) {
9459
- if (!updated[r])
9460
- continue;
9461
- const rowCopy = { ...updated[r] };
9462
- let changed = false;
9463
- for (let c = range.start.col; c <= range.end.col; c++) {
9464
- const field = cols[c]?.field;
9465
- if (!field)
9466
- continue;
9467
- const coerced = this.coerceAndValidate(field, null, updated[r]);
9468
- if (coerced !== PASTE_SKIP) {
9469
- rowCopy[field] = coerced;
9470
- changed = true;
9471
- }
9472
- }
9473
- if (changed) {
9474
- updated[r] = rowCopy;
9475
- }
9476
- }
9477
- return updated;
9478
- });
10568
+ const changes = this.clipboardEngine.clearRange(range);
10569
+ if (changes.length > 0) {
10570
+ this.historyEngine.record('delete', changes);
10571
+ }
10572
+ this.clipboardEngine.clearCut();
9479
10573
  }
9480
10574
  this.bulkDelete.emit({
9481
10575
  range,
@@ -9513,36 +10607,6 @@ class MozGridComponent {
9513
10607
  }
9514
10608
  return { range, values };
9515
10609
  }
9516
- applyPasteData(range, pasteRows) {
9517
- const cols = this.state.visibleColumns();
9518
- this.state.sourceData.update((data) => {
9519
- const updated = [...data];
9520
- for (let ri = 0; ri < pasteRows.length; ri++) {
9521
- const targetRow = range.start.row + ri;
9522
- if (targetRow >= updated.length)
9523
- break;
9524
- const rowCopy = { ...updated[targetRow] };
9525
- let changed = false;
9526
- for (let ci = 0; ci < pasteRows[ri].length; ci++) {
9527
- const targetCol = range.start.col + ci;
9528
- if (targetCol >= cols.length)
9529
- break;
9530
- const field = cols[targetCol]?.field;
9531
- if (!field)
9532
- continue;
9533
- const coerced = this.coerceAndValidate(field, pasteRows[ri][ci], updated[targetRow]);
9534
- if (coerced !== PASTE_SKIP) {
9535
- rowCopy[field] = coerced;
9536
- changed = true;
9537
- }
9538
- }
9539
- if (changed) {
9540
- updated[targetRow] = rowCopy;
9541
- }
9542
- }
9543
- return updated;
9544
- });
9545
- }
9546
10610
  extractRowSelectionData(selectedRows) {
9547
10611
  const cols = this.state.visibleColumns();
9548
10612
  const defMap = this.state.columnDefMap();
@@ -9679,6 +10743,8 @@ class MozGridComponent {
9679
10743
  RowSelectionEngine,
9680
10744
  CellSelectionEngine,
9681
10745
  KeyboardEngine,
10746
+ ClipboardEngine,
10747
+ HistoryEngine,
9682
10748
  GroupEngine,
9683
10749
  FilterEngine,
9684
10750
  ColumnReorderEngine,
@@ -9698,30 +10764,29 @@ class MozGridComponent {
9698
10764
  <div class="moz-grid__toolbar">
9699
10765
  <div class="moz-grid__toolbar-left">
9700
10766
  @if (fullscreen()) {
9701
- <moz-icon-button
9702
- id="grid-fullscreen"
9703
- [ghost]="true"
9704
- size="s"
9705
- [ariaLabel]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
9706
- (activated)="toggleFullscreen()"
9707
- >
9708
- @if (isFullscreen()) {
9709
- <FullscreenExit20 icon />
9710
- } @else {
9711
- <FullscreenEnter20 icon />
9712
- }
9713
- </moz-icon-button>
9714
- }
9715
- @if (exportable()) {
9716
- <moz-icon-button
9717
- id="grid-export"
9718
- [ghost]="true"
9719
- size="s"
9720
- ariaLabel="Export CSV"
9721
- (activated)="onExportCsv()"
9722
- >
9723
- <Download20 icon />
9724
- </moz-icon-button>
10767
+ <moz-icon-button
10768
+ id="grid-fullscreen"
10769
+ [ghost]="true"
10770
+ size="s"
10771
+ [ariaLabel]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
10772
+ (activated)="toggleFullscreen()"
10773
+ >
10774
+ @if (isFullscreen()) {
10775
+ <FullscreenExit20 icon />
10776
+ } @else {
10777
+ <FullscreenEnter20 icon />
10778
+ }
10779
+ </moz-icon-button>
10780
+ } @if (exportable()) {
10781
+ <moz-icon-button
10782
+ id="grid-export"
10783
+ [ghost]="true"
10784
+ size="s"
10785
+ ariaLabel="Export CSV"
10786
+ (activated)="onExportCsv()"
10787
+ >
10788
+ <Download20 icon />
10789
+ </moz-icon-button>
9725
10790
  }
9726
10791
  <moz-icon-button
9727
10792
  id="grid-filter"
@@ -9750,113 +10815,113 @@ class MozGridComponent {
9750
10815
  >
9751
10816
  <ViewGridX420 icon />
9752
10817
  </moz-icon-button>
10818
+ <moz-icon-button
10819
+ id="grid-keyboard-shortcuts"
10820
+ size="s"
10821
+ [ghost]="true"
10822
+ ariaLabel="Keyboard shortcuts"
10823
+ (activated)="onKeyboardShortcutsClick()"
10824
+ >
10825
+ <Keyboard20 icon />
10826
+ </moz-icon-button>
9753
10827
  @for (def of toolbarStartDefs(); track def) {
9754
- <ng-container [ngTemplateOutlet]="def.template" />
10828
+ <ng-container [ngTemplateOutlet]="def.template" />
9755
10829
  }
9756
10830
  </div>
9757
10831
 
9758
10832
  <!-- Selection banner -->
9759
10833
  @if (selectionBannerVisible()) {
9760
- <div class="moz-grid__selection-banner">
9761
- <span class="moz-grid__selection-text">
9762
- {{ rowSelectionEngine.count() }} row(s) selected
9763
- </span>
9764
- @if (rowSelectionEngine.count() < selectionTotalCount()) {
9765
- <button
9766
- moz-button
9767
- type="button"
9768
- [size]="'s'"
9769
- [ghost]="true"
9770
- [appearance]="'accent'"
9771
- (click)="onSelectAllRows()"
9772
- >
9773
- Select all {{ selectionTotalCount() }} rows
9774
- </button>
9775
- }
9776
- <button
9777
- moz-button
9778
- type="button"
9779
- [size]="'s'"
9780
- [ghost]="true"
9781
- (click)="onClearSelection()"
9782
- >
9783
- Clear
9784
- </button>
9785
- </div>
10834
+ <div class="moz-grid__selection-banner">
10835
+ <span class="moz-grid__selection-text">
10836
+ {{ rowSelectionEngine.count() }} row(s) selected
10837
+ </span>
10838
+ @if (rowSelectionEngine.count() < selectionTotalCount()) {
10839
+ <button
10840
+ moz-button
10841
+ type="button"
10842
+ [size]="'s'"
10843
+ [ghost]="true"
10844
+ [appearance]="'accent'"
10845
+ (click)="onSelectAllRows()"
10846
+ >
10847
+ Select all {{ selectionTotalCount() }} rows
10848
+ </button>
10849
+ }
10850
+ <button moz-button type="button" [size]="'s'" [ghost]="true" (click)="onClearSelection()">
10851
+ Clear
10852
+ </button>
10853
+ </div>
9786
10854
  }
9787
10855
 
9788
10856
  <div class="moz-grid__toolbar-right">
9789
10857
  @for (def of toolbarEndDefs(); track def) {
9790
- <ng-container [ngTemplateOutlet]="def.template" />
10858
+ <ng-container [ngTemplateOutlet]="def.template" />
9791
10859
  }
9792
10860
  </div>
9793
10861
  </div>
9794
10862
 
9795
10863
  <!-- Tag bars (outside .moz-grid) -->
9796
10864
  @if (state.groupColumns().length > 0) {
9797
- <div class="moz-grid__tag-bar">
9798
- <span class="moz-grid__tag-bar-label">GROUPED BY</span>
9799
- @for (entry of state.groupColumns(); track entry.field) {
9800
- <div>
9801
- <moz-tag
9802
- type="removable"
9803
- size="s"
9804
- [id]="'group-' + entry.field"
9805
- (removeTag)="onRemoveGroup(entry.field)"
9806
- >
9807
- <button
9808
- type="button"
9809
- class="moz-grid__group-tag-btn"
9810
- (click)="onToggleGroupSort(entry.field)"
9811
- >
9812
- {{ getColumnLabel(entry.field) }}
9813
- @if (entry.sortDirection === 'asc') {
9814
- <ChevronUp20 />
9815
- } @else {
9816
- <ChevronDown20 />
9817
- }
9818
- </button>
9819
- </moz-tag>
9820
- </div>
9821
- }
9822
- </div>
9823
- }
9824
- @if (hasHiddenColumns()) {
9825
- <div class="moz-grid__tag-bar">
9826
- <span class="moz-grid__tag-bar-label">HIDDEN COLUMNS</span>
9827
- @for (col of hiddenColumnsList(); track col.field) {
9828
- <moz-tag
9829
- type="removable"
9830
- size="s"
9831
- [id]="'hidden-' + col.field"
9832
- removableLabel="Restore"
9833
- (removeTag)="onRestoreColumn(col.field)"
9834
- >{{ col.label }}</moz-tag
10865
+ <div class="moz-grid__tag-bar">
10866
+ <span class="moz-grid__tag-bar-label">GROUPED BY</span>
10867
+ @for (entry of state.groupColumns(); track entry.field) {
10868
+ <div>
10869
+ <moz-tag
10870
+ type="removable"
10871
+ size="s"
10872
+ [id]="'group-' + entry.field"
10873
+ (removeTag)="onRemoveGroup(entry.field)"
10874
+ >
10875
+ <button
10876
+ type="button"
10877
+ class="moz-grid__group-tag-btn"
10878
+ (click)="onToggleGroupSort(entry.field)"
9835
10879
  >
9836
- }
9837
- @if (hiddenColumnsList().length > 1) {
9838
- <button type="button" class="moz-grid__tag-action-btn" (click)="onRestoreAllColumns()">
9839
- Restore all
10880
+ {{ getColumnLabel(entry.field) }}
10881
+ @if (entry.sortDirection === 'asc') {
10882
+ <ChevronUp20 />
10883
+ } @else {
10884
+ <ChevronDown20 />
10885
+ }
9840
10886
  </button>
9841
- }
9842
- </div>
9843
- }
9844
- @if (state.activeFilters().length > 0) {
9845
- <div class="moz-grid__tag-bar">
9846
- <span class="moz-grid__tag-bar-label">FILTERED BY</span>
9847
- @for (filter of state.activeFilters(); track filter.field) {
9848
- <moz-tag
9849
- [type]="filter.removable ? 'removable' : 'informative'"
9850
- size="s"
9851
- [id]="'filter-' + filter.field"
9852
- (removeTag)="onRemoveFilter(filter.field)"
9853
- >{{ filter.label }}</moz-tag
9854
- >
9855
- }
9856
- <button type="button" class="moz-grid__tag-action-btn" (click)="onRemoveAllFilters()">
9857
- Remove all
9858
- </button>
10887
+ </moz-tag>
9859
10888
  </div>
10889
+ }
10890
+ </div>
10891
+ } @if (hasHiddenColumns()) {
10892
+ <div class="moz-grid__tag-bar">
10893
+ <span class="moz-grid__tag-bar-label">HIDDEN COLUMNS</span>
10894
+ @for (col of hiddenColumnsList(); track col.field) {
10895
+ <moz-tag
10896
+ type="removable"
10897
+ size="s"
10898
+ [id]="'hidden-' + col.field"
10899
+ removableLabel="Restore"
10900
+ (removeTag)="onRestoreColumn(col.field)"
10901
+ >{{ col.label }}</moz-tag
10902
+ >
10903
+ } @if (hiddenColumnsList().length > 1) {
10904
+ <button type="button" class="moz-grid__tag-action-btn" (click)="onRestoreAllColumns()">
10905
+ Restore all
10906
+ </button>
10907
+ }
10908
+ </div>
10909
+ } @if (state.activeFilters().length > 0) {
10910
+ <div class="moz-grid__tag-bar">
10911
+ <span class="moz-grid__tag-bar-label">FILTERED BY</span>
10912
+ @for (filter of state.activeFilters(); track filter.field) {
10913
+ <moz-tag
10914
+ [type]="filter.removable ? 'removable' : 'informative'"
10915
+ size="s"
10916
+ [id]="'filter-' + filter.field"
10917
+ (removeTag)="onRemoveFilter(filter.field)"
10918
+ >{{ filter.label }}</moz-tag
10919
+ >
10920
+ }
10921
+ <button type="button" class="moz-grid__tag-action-btn" (click)="onRemoveAllFilters()">
10922
+ Remove all
10923
+ </button>
10924
+ </div>
9860
10925
  }
9861
10926
  <ng-content select="[mozGridFilterTags]" />
9862
10927
 
@@ -9893,13 +10958,12 @@ class MozGridComponent {
9893
10958
 
9894
10959
  <!-- Footer: pagination or infinite scroll loading indicator -->
9895
10960
  @if (showPagination()) {
9896
- <moz-grid-footer
9897
- [pageSizeOptions]="pageSizeOptions()"
9898
- (pageChange)="pageChange.emit($event)"
9899
- />
9900
- }
9901
- @if (showInfiniteScrollLoader()) {
9902
- <moz-grid-loading-indicator />
10961
+ <moz-grid-footer
10962
+ [pageSizeOptions]="pageSizeOptions()"
10963
+ (pageChange)="pageChange.emit($event)"
10964
+ />
10965
+ } @if (showInfiniteScrollLoader()) {
10966
+ <moz-grid-loading-indicator />
9903
10967
  }
9904
10968
 
9905
10969
  <!-- Bulk selection action bar -->
@@ -9912,7 +10976,7 @@ class MozGridComponent {
9912
10976
  />
9913
10977
  </div>
9914
10978
  </div>
9915
- `, isInline: true, styles: [":host{display:block;height:100%}.moz-grid-wrapper{display:flex;flex-direction:column;font-family:var(--font-family-primary);height:100%;min-height:0;gap:16px}.moz-grid-wrapper--fullscreen{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;background:var(--color-background-primary)}.moz-grid{display:flex;flex-direction:column;border-radius:var(--border-radius-l);overflow:hidden;background:var(--color-background-primary);flex:1;min-height:0;position:relative;box-shadow:0 0 6px #cdd4d8}.moz-grid:focus{outline:none}.moz-grid--loading{opacity:.6;pointer-events:none}.moz-grid__toolbar{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;min-height:48px;gap:var(--spacing-s, 8px)}.moz-grid__toolbar-left,.moz-grid__toolbar-right{display:flex;align-items:center;gap:var(--spacing-xs, 4px)}.moz-grid__selection-banner{display:flex;align-items:center;gap:var(--spacing-s, 8px);flex:1;justify-content:center;border-radius:var(--border-radius-s);border:1px solid var(--Border-Primary, #cdd4d8);background:var(--Neutral-Grey-000, #fff)}.moz-grid__selection-text{font-size:var(--font-size-s, 14px);color:var(--color-text-primary);white-space:nowrap}.moz-grid__selection-link{padding:0;border:none;background:transparent;color:var(--color-background-accent-inverse);font-size:var(--font-size-s, 14px);font-weight:600;cursor:pointer;white-space:nowrap;text-decoration:underline}.moz-grid__selection-link:hover{color:var(--color-primary-dark, #1557b0)}.moz-grid__tag-bar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--spacing-xs, 4px);padding:var(--spacing-xxs, 2px) var(--spacing-s, 8px);flex-shrink:0}.moz-grid__tag-bar-label{width:100%;font-size:var(--font-size-xs, 12px);text-transform:uppercase;white-space:nowrap;color:var(--text-icon-tertiary);font-size:var(--Typography-Font-size-Body-XS, 12px);font-weight:400}.moz-grid__tag-action-btn{padding:2px 8px;border:none;background:transparent;color:var(--color-background-accent-inverse);font-size:var(--font-size-xs, 12px);font-weight:600;cursor:pointer}.moz-grid__tag-action-btn:hover{text-decoration:underline}.moz-grid__group-tag-btn{display:inline-flex;align-items:center;gap:2px;padding:0;border:none;background:transparent;cursor:pointer;font:inherit;color:inherit;line-height:1}.moz-grid__group-tag-btn ::ng-deep svg{fill:#fff!important;width:16px;height:16px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MozGridHeaderComponent, selector: "moz-grid-header", inputs: ["showCheckbox", "showExpand", "reorderable"], outputs: ["sortClick", "menuAction", "resizeStart", "selectAllToggle", "columnReorder"] }, { kind: "component", type: MozGridBodyComponent, selector: "moz-grid-body", inputs: ["showCheckbox", "showExpand", "detailTemplate"], outputs: ["cellEdit", "cellEditCancel", "rowSelectionToggle", "groupToggle"] }, { kind: "component", type: MozGridFooterComponent, selector: "moz-grid-footer", inputs: ["pageSizeOptions"], outputs: ["pageChange"] }, { kind: "component", type: MozGridLoadingIndicatorComponent, selector: "moz-grid-loading-indicator" }, { kind: "component", type: MozGridSelectionBarComponent, selector: "moz-grid-selection-bar", outputs: ["editClick", "copyClick", "pasteClick", "deleteClick", "exportClick"] }, { kind: "component", type: MozTagComponent, selector: "moz-tag", inputs: ["type", "size", "id", "name", "disabled", "contextualisedNumber", "removableLabel"], outputs: ["removeTag"] }, { kind: "component", type: MozIconButtonComponent, selector: "moz-icon-button", inputs: ["id", "appearance", "size", "disabled", "ghost", "outlined", "type", "ariaLabel"], outputs: ["activated"] }, { kind: "component", type: ViewGridX420, selector: "ViewGridX420", inputs: ["hostClass"] }, { kind: "component", type: Filter20, selector: "Filter20", inputs: ["hostClass"] }, { kind: "component", type: Settings20, selector: "Settings20", inputs: ["hostClass"] }, { kind: "component", type: FullscreenEnter20, selector: "FullscreenEnter20", inputs: ["hostClass"] }, { kind: "component", type: FullscreenExit20, selector: "FullscreenExit20", inputs: ["hostClass"] }, { kind: "component", type: Download20, selector: "Download20", inputs: ["hostClass"] }, { kind: "component", type: ChevronUp20, selector: "ChevronUp20", inputs: ["hostClass"] }, { kind: "component", type: ChevronDown20, selector: "ChevronDown20", inputs: ["hostClass"] }, { kind: "component", type: MozButtonComponent, selector: "button[moz-button]", inputs: ["appearance", "size", "disabled", "ghost", "outlined", "iconPosition", "type", "isLoading"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10979
+ `, isInline: true, styles: [":host{display:block;height:100%}.moz-grid-wrapper{display:flex;flex-direction:column;font-family:var(--font-family-primary);height:100%;min-height:0;gap:16px}.moz-grid-wrapper--fullscreen{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:9999;background:var(--color-background-primary)}.moz-grid{display:flex;flex-direction:column;border-radius:var(--border-radius-l);overflow:hidden;background:var(--color-background-primary);flex:1;min-height:0;position:relative;box-shadow:0 0 6px #cdd4d8}.moz-grid:focus{outline:none}.moz-grid--loading{opacity:.6;pointer-events:none}.moz-grid__toolbar{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;min-height:48px;gap:var(--spacing-s, 8px)}.moz-grid__toolbar-left,.moz-grid__toolbar-right{display:flex;align-items:center;gap:var(--spacing-xs, 4px)}.moz-grid__selection-banner{display:flex;align-items:center;gap:var(--spacing-s, 8px);flex:1;justify-content:center;border-radius:var(--border-radius-s);border:1px solid var(--Border-Primary, #cdd4d8);background:var(--Neutral-Grey-000, #fff)}.moz-grid__selection-text{font-size:var(--font-size-s, 14px);color:var(--color-text-primary);white-space:nowrap}.moz-grid__selection-link{padding:0;border:none;background:transparent;color:var(--color-background-accent-inverse);font-size:var(--font-size-s, 14px);font-weight:600;cursor:pointer;white-space:nowrap;text-decoration:underline}.moz-grid__selection-link:hover{color:var(--color-primary-dark, #1557b0)}.moz-grid__tag-bar{display:flex;align-items:center;flex-wrap:wrap;gap:var(--spacing-xs, 4px);padding:var(--spacing-xxs, 2px) var(--spacing-s, 8px);flex-shrink:0}.moz-grid__tag-bar-label{width:100%;font-size:var(--font-size-xs, 12px);text-transform:uppercase;white-space:nowrap;color:var(--text-icon-tertiary);font-size:var(--Typography-Font-size-Body-XS, 12px);font-weight:400}.moz-grid__tag-action-btn{padding:2px 8px;border:none;background:transparent;color:var(--color-background-accent-inverse);font-size:var(--font-size-xs, 12px);font-weight:600;cursor:pointer}.moz-grid__tag-action-btn:hover{text-decoration:underline}.moz-grid__group-tag-btn{display:inline-flex;align-items:center;gap:2px;padding:0;border:none;background:transparent;cursor:pointer;font:inherit;color:inherit;line-height:1}.moz-grid__group-tag-btn ::ng-deep svg{fill:#fff!important;width:16px;height:16px}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MozGridHeaderComponent, selector: "moz-grid-header", inputs: ["showCheckbox", "showExpand", "reorderable"], outputs: ["sortClick", "menuAction", "resizeStart", "selectAllToggle", "columnReorder"] }, { kind: "component", type: MozGridBodyComponent, selector: "moz-grid-body", inputs: ["showCheckbox", "showExpand", "detailTemplate"], outputs: ["cellEdit", "cellEditCancel", "rowSelectionToggle", "groupToggle"] }, { kind: "component", type: MozGridFooterComponent, selector: "moz-grid-footer", inputs: ["pageSizeOptions"], outputs: ["pageChange"] }, { kind: "component", type: MozGridLoadingIndicatorComponent, selector: "moz-grid-loading-indicator" }, { kind: "component", type: MozGridSelectionBarComponent, selector: "moz-grid-selection-bar", outputs: ["editClick", "copyClick", "pasteClick", "deleteClick", "exportClick"] }, { kind: "component", type: MozTagComponent, selector: "moz-tag", inputs: ["type", "size", "id", "name", "disabled", "contextualisedNumber", "removableLabel"], outputs: ["removeTag"] }, { kind: "component", type: MozIconButtonComponent, selector: "moz-icon-button", inputs: ["id", "appearance", "size", "disabled", "ghost", "outlined", "type", "ariaLabel"], outputs: ["activated"] }, { kind: "component", type: ViewGridX420, selector: "ViewGridX420", inputs: ["hostClass"] }, { kind: "component", type: Filter20, selector: "Filter20", inputs: ["hostClass"] }, { kind: "component", type: Settings20, selector: "Settings20", inputs: ["hostClass"] }, { kind: "component", type: FullscreenEnter20, selector: "FullscreenEnter20", inputs: ["hostClass"] }, { kind: "component", type: FullscreenExit20, selector: "FullscreenExit20", inputs: ["hostClass"] }, { kind: "component", type: Download20, selector: "Download20", inputs: ["hostClass"] }, { kind: "component", type: ChevronUp20, selector: "ChevronUp20", inputs: ["hostClass"] }, { kind: "component", type: ChevronDown20, selector: "ChevronDown20", inputs: ["hostClass"] }, { kind: "component", type: Keyboard20, selector: "Keyboard20", inputs: ["hostClass"] }, { kind: "component", type: MozButtonComponent, selector: "button[moz-button]", inputs: ["appearance", "size", "disabled", "ghost", "outlined", "iconPosition", "type", "isLoading"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9916
10980
  }
9917
10981
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MozGridComponent, decorators: [{
9918
10982
  type: Component,
@@ -9925,6 +10989,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
9925
10989
  RowSelectionEngine,
9926
10990
  CellSelectionEngine,
9927
10991
  KeyboardEngine,
10992
+ ClipboardEngine,
10993
+ HistoryEngine,
9928
10994
  GroupEngine,
9929
10995
  FilterEngine,
9930
10996
  ColumnReorderEngine,
@@ -9955,6 +11021,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
9955
11021
  Download20,
9956
11022
  ChevronUp20,
9957
11023
  ChevronDown20,
11024
+ Keyboard20,
9958
11025
  MozButtonComponent,
9959
11026
  ], template: `
9960
11027
  <div class="moz-grid-wrapper" [class.moz-grid-wrapper--fullscreen]="isFullscreen()">
@@ -9962,30 +11029,29 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
9962
11029
  <div class="moz-grid__toolbar">
9963
11030
  <div class="moz-grid__toolbar-left">
9964
11031
  @if (fullscreen()) {
9965
- <moz-icon-button
9966
- id="grid-fullscreen"
9967
- [ghost]="true"
9968
- size="s"
9969
- [ariaLabel]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
9970
- (activated)="toggleFullscreen()"
9971
- >
9972
- @if (isFullscreen()) {
9973
- <FullscreenExit20 icon />
9974
- } @else {
9975
- <FullscreenEnter20 icon />
9976
- }
9977
- </moz-icon-button>
9978
- }
9979
- @if (exportable()) {
9980
- <moz-icon-button
9981
- id="grid-export"
9982
- [ghost]="true"
9983
- size="s"
9984
- ariaLabel="Export CSV"
9985
- (activated)="onExportCsv()"
9986
- >
9987
- <Download20 icon />
9988
- </moz-icon-button>
11032
+ <moz-icon-button
11033
+ id="grid-fullscreen"
11034
+ [ghost]="true"
11035
+ size="s"
11036
+ [ariaLabel]="isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'"
11037
+ (activated)="toggleFullscreen()"
11038
+ >
11039
+ @if (isFullscreen()) {
11040
+ <FullscreenExit20 icon />
11041
+ } @else {
11042
+ <FullscreenEnter20 icon />
11043
+ }
11044
+ </moz-icon-button>
11045
+ } @if (exportable()) {
11046
+ <moz-icon-button
11047
+ id="grid-export"
11048
+ [ghost]="true"
11049
+ size="s"
11050
+ ariaLabel="Export CSV"
11051
+ (activated)="onExportCsv()"
11052
+ >
11053
+ <Download20 icon />
11054
+ </moz-icon-button>
9989
11055
  }
9990
11056
  <moz-icon-button
9991
11057
  id="grid-filter"
@@ -10014,113 +11080,113 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
10014
11080
  >
10015
11081
  <ViewGridX420 icon />
10016
11082
  </moz-icon-button>
11083
+ <moz-icon-button
11084
+ id="grid-keyboard-shortcuts"
11085
+ size="s"
11086
+ [ghost]="true"
11087
+ ariaLabel="Keyboard shortcuts"
11088
+ (activated)="onKeyboardShortcutsClick()"
11089
+ >
11090
+ <Keyboard20 icon />
11091
+ </moz-icon-button>
10017
11092
  @for (def of toolbarStartDefs(); track def) {
10018
- <ng-container [ngTemplateOutlet]="def.template" />
11093
+ <ng-container [ngTemplateOutlet]="def.template" />
10019
11094
  }
10020
11095
  </div>
10021
11096
 
10022
11097
  <!-- Selection banner -->
10023
11098
  @if (selectionBannerVisible()) {
10024
- <div class="moz-grid__selection-banner">
10025
- <span class="moz-grid__selection-text">
10026
- {{ rowSelectionEngine.count() }} row(s) selected
10027
- </span>
10028
- @if (rowSelectionEngine.count() < selectionTotalCount()) {
10029
- <button
10030
- moz-button
10031
- type="button"
10032
- [size]="'s'"
10033
- [ghost]="true"
10034
- [appearance]="'accent'"
10035
- (click)="onSelectAllRows()"
10036
- >
10037
- Select all {{ selectionTotalCount() }} rows
10038
- </button>
10039
- }
10040
- <button
10041
- moz-button
10042
- type="button"
10043
- [size]="'s'"
10044
- [ghost]="true"
10045
- (click)="onClearSelection()"
10046
- >
10047
- Clear
10048
- </button>
10049
- </div>
11099
+ <div class="moz-grid__selection-banner">
11100
+ <span class="moz-grid__selection-text">
11101
+ {{ rowSelectionEngine.count() }} row(s) selected
11102
+ </span>
11103
+ @if (rowSelectionEngine.count() < selectionTotalCount()) {
11104
+ <button
11105
+ moz-button
11106
+ type="button"
11107
+ [size]="'s'"
11108
+ [ghost]="true"
11109
+ [appearance]="'accent'"
11110
+ (click)="onSelectAllRows()"
11111
+ >
11112
+ Select all {{ selectionTotalCount() }} rows
11113
+ </button>
11114
+ }
11115
+ <button moz-button type="button" [size]="'s'" [ghost]="true" (click)="onClearSelection()">
11116
+ Clear
11117
+ </button>
11118
+ </div>
10050
11119
  }
10051
11120
 
10052
11121
  <div class="moz-grid__toolbar-right">
10053
11122
  @for (def of toolbarEndDefs(); track def) {
10054
- <ng-container [ngTemplateOutlet]="def.template" />
11123
+ <ng-container [ngTemplateOutlet]="def.template" />
10055
11124
  }
10056
11125
  </div>
10057
11126
  </div>
10058
11127
 
10059
11128
  <!-- Tag bars (outside .moz-grid) -->
10060
11129
  @if (state.groupColumns().length > 0) {
10061
- <div class="moz-grid__tag-bar">
10062
- <span class="moz-grid__tag-bar-label">GROUPED BY</span>
10063
- @for (entry of state.groupColumns(); track entry.field) {
10064
- <div>
10065
- <moz-tag
10066
- type="removable"
10067
- size="s"
10068
- [id]="'group-' + entry.field"
10069
- (removeTag)="onRemoveGroup(entry.field)"
10070
- >
10071
- <button
10072
- type="button"
10073
- class="moz-grid__group-tag-btn"
10074
- (click)="onToggleGroupSort(entry.field)"
10075
- >
10076
- {{ getColumnLabel(entry.field) }}
10077
- @if (entry.sortDirection === 'asc') {
10078
- <ChevronUp20 />
10079
- } @else {
10080
- <ChevronDown20 />
10081
- }
10082
- </button>
10083
- </moz-tag>
10084
- </div>
10085
- }
10086
- </div>
10087
- }
10088
- @if (hasHiddenColumns()) {
10089
- <div class="moz-grid__tag-bar">
10090
- <span class="moz-grid__tag-bar-label">HIDDEN COLUMNS</span>
10091
- @for (col of hiddenColumnsList(); track col.field) {
10092
- <moz-tag
10093
- type="removable"
10094
- size="s"
10095
- [id]="'hidden-' + col.field"
10096
- removableLabel="Restore"
10097
- (removeTag)="onRestoreColumn(col.field)"
10098
- >{{ col.label }}</moz-tag
11130
+ <div class="moz-grid__tag-bar">
11131
+ <span class="moz-grid__tag-bar-label">GROUPED BY</span>
11132
+ @for (entry of state.groupColumns(); track entry.field) {
11133
+ <div>
11134
+ <moz-tag
11135
+ type="removable"
11136
+ size="s"
11137
+ [id]="'group-' + entry.field"
11138
+ (removeTag)="onRemoveGroup(entry.field)"
11139
+ >
11140
+ <button
11141
+ type="button"
11142
+ class="moz-grid__group-tag-btn"
11143
+ (click)="onToggleGroupSort(entry.field)"
10099
11144
  >
10100
- }
10101
- @if (hiddenColumnsList().length > 1) {
10102
- <button type="button" class="moz-grid__tag-action-btn" (click)="onRestoreAllColumns()">
10103
- Restore all
11145
+ {{ getColumnLabel(entry.field) }}
11146
+ @if (entry.sortDirection === 'asc') {
11147
+ <ChevronUp20 />
11148
+ } @else {
11149
+ <ChevronDown20 />
11150
+ }
10104
11151
  </button>
10105
- }
10106
- </div>
10107
- }
10108
- @if (state.activeFilters().length > 0) {
10109
- <div class="moz-grid__tag-bar">
10110
- <span class="moz-grid__tag-bar-label">FILTERED BY</span>
10111
- @for (filter of state.activeFilters(); track filter.field) {
10112
- <moz-tag
10113
- [type]="filter.removable ? 'removable' : 'informative'"
10114
- size="s"
10115
- [id]="'filter-' + filter.field"
10116
- (removeTag)="onRemoveFilter(filter.field)"
10117
- >{{ filter.label }}</moz-tag
10118
- >
10119
- }
10120
- <button type="button" class="moz-grid__tag-action-btn" (click)="onRemoveAllFilters()">
10121
- Remove all
10122
- </button>
11152
+ </moz-tag>
10123
11153
  </div>
11154
+ }
11155
+ </div>
11156
+ } @if (hasHiddenColumns()) {
11157
+ <div class="moz-grid__tag-bar">
11158
+ <span class="moz-grid__tag-bar-label">HIDDEN COLUMNS</span>
11159
+ @for (col of hiddenColumnsList(); track col.field) {
11160
+ <moz-tag
11161
+ type="removable"
11162
+ size="s"
11163
+ [id]="'hidden-' + col.field"
11164
+ removableLabel="Restore"
11165
+ (removeTag)="onRestoreColumn(col.field)"
11166
+ >{{ col.label }}</moz-tag
11167
+ >
11168
+ } @if (hiddenColumnsList().length > 1) {
11169
+ <button type="button" class="moz-grid__tag-action-btn" (click)="onRestoreAllColumns()">
11170
+ Restore all
11171
+ </button>
11172
+ }
11173
+ </div>
11174
+ } @if (state.activeFilters().length > 0) {
11175
+ <div class="moz-grid__tag-bar">
11176
+ <span class="moz-grid__tag-bar-label">FILTERED BY</span>
11177
+ @for (filter of state.activeFilters(); track filter.field) {
11178
+ <moz-tag
11179
+ [type]="filter.removable ? 'removable' : 'informative'"
11180
+ size="s"
11181
+ [id]="'filter-' + filter.field"
11182
+ (removeTag)="onRemoveFilter(filter.field)"
11183
+ >{{ filter.label }}</moz-tag
11184
+ >
11185
+ }
11186
+ <button type="button" class="moz-grid__tag-action-btn" (click)="onRemoveAllFilters()">
11187
+ Remove all
11188
+ </button>
11189
+ </div>
10124
11190
  }
10125
11191
  <ng-content select="[mozGridFilterTags]" />
10126
11192
 
@@ -10157,13 +11223,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
10157
11223
 
10158
11224
  <!-- Footer: pagination or infinite scroll loading indicator -->
10159
11225
  @if (showPagination()) {
10160
- <moz-grid-footer
10161
- [pageSizeOptions]="pageSizeOptions()"
10162
- (pageChange)="pageChange.emit($event)"
10163
- />
10164
- }
10165
- @if (showInfiniteScrollLoader()) {
10166
- <moz-grid-loading-indicator />
11226
+ <moz-grid-footer
11227
+ [pageSizeOptions]="pageSizeOptions()"
11228
+ (pageChange)="pageChange.emit($event)"
11229
+ />
11230
+ } @if (showInfiniteScrollLoader()) {
11231
+ <moz-grid-loading-indicator />
10167
11232
  }
10168
11233
 
10169
11234
  <!-- Bulk selection action bar -->