@mozaic-ds/angular 2.0.41 → 2.0.42

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.
@@ -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,
@@ -5880,24 +6413,290 @@ class CellSelectionEngine {
5880
6413
  if (!this.state.isDragging())
5881
6414
  return;
5882
6415
  const range = this.state.cellRange();
5883
- if (!range)
6416
+ if (!range)
6417
+ return;
6418
+ this.state.cellRange.set({ start: range.start, end: { row, col } });
6419
+ }
6420
+ endRangeSelection() {
6421
+ this.state.isDragging.set(false);
6422
+ }
6423
+ moveUp() {
6424
+ this.moveBy(-1, 0);
6425
+ }
6426
+ moveDown() {
6427
+ this.moveBy(1, 0);
6428
+ }
6429
+ moveLeft() {
6430
+ this.moveBy(0, -1);
6431
+ }
6432
+ moveRight() {
6433
+ this.moveBy(0, 1);
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)
5884
6591
  return;
5885
- this.state.cellRange.set({ start: range.start, end: { row, col } });
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
+ });
5886
6598
  }
5887
- endRangeSelection() {
5888
- this.state.isDragging.set(false);
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
+ });
5889
6607
  }
5890
- moveUp() {
5891
- this.moveBy(-1, 0);
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
+ });
5892
6619
  }
5893
- moveDown() {
5894
- this.moveBy(1, 0);
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 !== '';
5895
6664
  }
5896
- moveLeft() {
5897
- this.moveBy(0, -1);
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;
5898
6671
  }
5899
- moveRight() {
5900
- this.moveBy(0, 1);
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 };
5901
6700
  }
5902
6701
  moveToNextEditableCell() {
5903
6702
  const focused = this.state.focusedCell();
@@ -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
- }
6242
- break;
7025
+ if (mod) {
7026
+ this.cellSelection.selectColumn(focused.col);
7027
+ return true;
6243
7028
  }
6244
- case 'Escape':
6245
- event.preventDefault();
6246
- this.cellSelection.clearFocus();
6247
- break;
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;
6248
7113
  }
7114
+ return false;
6249
7115
  }
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 });
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;
6264
7121
  }
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 });
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();
7135
+ break;
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();
7144
+ break;
6271
7145
  }
7146
+ return true;
7147
+ }
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 });
@@ -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()", "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--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;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--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:-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 });
8001
8892
  }
8002
8893
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: MozGridCellComponent, decorators: [{
8003
8894
  type: Component,
@@ -8014,7 +8905,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
8014
8905
  '[style.width.px]': 'isLast() ? undefined : colState().currentWidth',
8015
8906
  '[style.min-width.px]': 'isLast() ? colState().currentWidth : resolvedMinWidth()',
8016
8907
  '[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"] }]
8908
+ }, 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;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--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:-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"] }]
8018
8909
  }], 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
8910
 
8020
8911
  class MozGridRowComponent {
@@ -8519,6 +9410,8 @@ class MozGridComponent {
8519
9410
  rowSelectionEngine = inject(RowSelectionEngine);
8520
9411
  cellSelectionEngine = inject(CellSelectionEngine);
8521
9412
  keyboardEngine = inject(KeyboardEngine);
9413
+ clipboardEngine = inject(ClipboardEngine);
9414
+ historyEngine = inject(HistoryEngine);
8522
9415
  groupEngine = inject(GroupEngine);
8523
9416
  filterEngine = inject(FilterEngine);
8524
9417
  persistenceEngine = inject(StatePersistenceEngine);
@@ -8596,6 +9489,22 @@ class MozGridComponent {
8596
9489
  stateRestored = false;
8597
9490
  documentMouseUpHandler = null;
8598
9491
  constructor() {
9492
+ this.keyboardEngine.registerActions({
9493
+ copy: () => this.onBulkCopy(),
9494
+ paste: () => { void this.onBulkPaste(); },
9495
+ cut: () => this.onCutShortcut(),
9496
+ deleteRange: () => this.onBulkDelete(),
9497
+ undo: () => this.onUndo(),
9498
+ redo: () => this.onRedo(),
9499
+ fillDown: () => this.onFillDownShortcut(),
9500
+ fillRight: () => this.onFillRightShortcut(),
9501
+ startEdit: (row, col, char) => this.onStartEditShortcut(row, col, char),
9502
+ });
9503
+ // Bind history persistence to the grid's stateKey (same key we use for
9504
+ // column/sort persistence — one localStorage namespace per grid).
9505
+ effect(() => {
9506
+ this.historyEngine.attach(this.stateKey());
9507
+ });
8599
9508
  // Global mouseup listener for fill handle — if the user drags the fill
8600
9509
  // handle outside the grid and releases, we still need to end the fill.
8601
9510
  afterNextRender(() => {
@@ -8631,6 +9540,7 @@ class MozGridComponent {
8631
9540
  const pushScroll = () => {
8632
9541
  this.ngZone.run(() => {
8633
9542
  this.horizontalVirtualScrollEngine.onScroll(scrollEl.scrollLeft, scrollEl.clientWidth);
9543
+ this.state.scrollViewportHeight.set(scrollEl.clientHeight);
8634
9544
  });
8635
9545
  };
8636
9546
  // Prime the engine with the initial viewport width so the range is
@@ -8907,64 +9817,152 @@ class MozGridComponent {
8907
9817
  if (tag === 'input' || tag === 'select' || tag === 'textarea') {
8908
9818
  return;
8909
9819
  }
8910
- // Bulk action shortcuts work for both cell and row selection modes
9820
+ // Row-selection mode forwards a small set of shortcuts without touching
9821
+ // cell focus. Cell-mode (and anywhere else) goes through the keyboard engine.
8911
9822
  const selMode = this.state.activeSelectionMode();
8912
- if (selMode === 'rows' || selMode === 'cells') {
8913
- if ((event.ctrlKey || event.metaKey) && event.key === 'c') {
9823
+ if (selMode === 'rows') {
9824
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c') {
8914
9825
  event.preventDefault();
8915
9826
  this.onBulkCopy();
8916
9827
  return;
8917
9828
  }
8918
- if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
9829
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v') {
8919
9830
  event.preventDefault();
8920
- this.onBulkPaste();
9831
+ void this.onBulkPaste();
8921
9832
  return;
8922
9833
  }
8923
9834
  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
- }
9835
+ event.preventDefault();
9836
+ this.onBulkDelete();
9837
+ return;
8929
9838
  }
8930
9839
  }
8931
9840
  this.keyboardEngine.handleKeydown(event);
8932
9841
  }
8933
9842
  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;
9843
+ if (event.key === 'Escape') {
9844
+ event.preventDefault();
9845
+ const cancelEvt = this.inlineEditEngine.cancelEdit();
9846
+ if (cancelEvt) {
9847
+ this.cellEditCancel.emit(cancelEvt);
8942
9848
  }
8943
- case 'Enter': {
8944
- event.preventDefault();
8945
- const commitEvt = this.inlineEditEngine.commitEdit();
8946
- if (commitEvt) {
8947
- this.cellEdit.emit(commitEvt);
9849
+ return;
9850
+ }
9851
+ if (event.key === 'Enter') {
9852
+ // Alt+Enter: insert a newline in the draft for text editors (Excel-style).
9853
+ if (event.altKey) {
9854
+ const editState = this.state.cellEditState();
9855
+ const def = this.state.columnDefMap().get(this.state.visibleColumns()[editState.editingCell?.col ?? -1]?.field ?? '');
9856
+ const editorType = def?.cellEditor ?? 'text';
9857
+ if (editorType === 'text') {
9858
+ event.preventDefault();
9859
+ const draft = editState.draftValue;
9860
+ const next = (typeof draft === 'string' ? draft : String(draft ?? '')) + '\n';
9861
+ this.inlineEditEngine.updateDraft(next);
8948
9862
  }
8949
- this.cellSelectionEngine.moveRight();
8950
- this.refocusGrid();
8951
- break;
9863
+ return;
8952
9864
  }
8953
- case 'Tab': {
9865
+ // Ctrl+Enter: commit current draft and broadcast it to the active range.
9866
+ if (event.ctrlKey || event.metaKey) {
8954
9867
  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();
9868
+ const editState = this.state.cellEditState();
9869
+ const value = editState.draftValue;
9870
+ const cancel = this.inlineEditEngine.cancelEdit();
9871
+ if (cancel)
9872
+ this.cellEditCancel.emit(cancel);
9873
+ const range = this.cellSelectionEngine.getNormalizedRange();
9874
+ if (range) {
9875
+ const changes = this.clipboardEngine.fillSelection(range, value);
9876
+ this.historyEngine.record('fill-selection', changes);
8964
9877
  }
8965
9878
  this.refocusGrid();
8966
- break;
9879
+ return;
8967
9880
  }
9881
+ event.preventDefault();
9882
+ this.commitFromEditMode();
9883
+ if (event.shiftKey)
9884
+ this.cellSelectionEngine.moveUp();
9885
+ else
9886
+ this.cellSelectionEngine.moveDown();
9887
+ this.refocusGrid();
9888
+ return;
9889
+ }
9890
+ if (event.key === 'Tab') {
9891
+ event.preventDefault();
9892
+ this.commitFromEditMode();
9893
+ if (event.shiftKey)
9894
+ this.cellSelectionEngine.moveLeft();
9895
+ else
9896
+ this.cellSelectionEngine.moveRight();
9897
+ this.refocusGrid();
9898
+ }
9899
+ }
9900
+ commitFromEditMode() {
9901
+ const evt = this.inlineEditEngine.commitEdit();
9902
+ if (!evt)
9903
+ return;
9904
+ this.cellEdit.emit(evt);
9905
+ this.clipboardEngine.clearCut();
9906
+ }
9907
+ onCutShortcut() {
9908
+ const range = this.cellSelectionEngine.getNormalizedRange();
9909
+ if (!range)
9910
+ return;
9911
+ const values = this.clipboardEngine.extractTsv(range);
9912
+ const tsv = values.map((row) => row.join('\t')).join('\n');
9913
+ navigator.clipboard.writeText(tsv);
9914
+ this.clipboardEngine.markCut(range);
9915
+ this.bulkCopy.emit({
9916
+ range,
9917
+ data: values,
9918
+ rowIds: this.getRangeRowIds(range),
9919
+ fields: this.getRangeFields(range),
9920
+ });
9921
+ }
9922
+ onUndo() {
9923
+ this.historyEngine.undo();
9924
+ this.clipboardEngine.clearCut();
9925
+ }
9926
+ onRedo() {
9927
+ this.historyEngine.redo();
9928
+ this.clipboardEngine.clearCut();
9929
+ }
9930
+ onFillDownShortcut() {
9931
+ const range = this.cellSelectionEngine.getNormalizedRange();
9932
+ if (!range)
9933
+ return;
9934
+ if (this.state.mode() !== 'client')
9935
+ return;
9936
+ const changes = this.clipboardEngine.fillDown(range);
9937
+ if (changes.length === 0)
9938
+ return;
9939
+ this.historyEngine.record('fill-down', changes);
9940
+ this.clipboardEngine.clearCut();
9941
+ }
9942
+ onFillRightShortcut() {
9943
+ const range = this.cellSelectionEngine.getNormalizedRange();
9944
+ if (!range)
9945
+ return;
9946
+ if (this.state.mode() !== 'client')
9947
+ return;
9948
+ const changes = this.clipboardEngine.fillRight(range);
9949
+ if (changes.length === 0)
9950
+ return;
9951
+ this.historyEngine.record('fill-right', changes);
9952
+ this.clipboardEngine.clearCut();
9953
+ }
9954
+ onStartEditShortcut(row, col, initialChar) {
9955
+ const colDef = this.state.visibleColumns()[col];
9956
+ if (!colDef)
9957
+ return;
9958
+ const def = this.state.columnDefMap().get(colDef.field);
9959
+ if (!def?.editable)
9960
+ return;
9961
+ if (initialChar !== undefined) {
9962
+ this.inlineEditEngine.startEditWithChar(row, colDef.field, initialChar);
9963
+ }
9964
+ else {
9965
+ this.inlineEditEngine.startEdit(row, colDef.field);
8968
9966
  }
8969
9967
  }
8970
9968
  resetInfiniteScrollIfActive() {
@@ -9051,17 +10049,26 @@ class MozGridComponent {
9051
10049
  if (srcIdx >= 0)
9052
10050
  indexMap.set(r, srcIdx);
9053
10051
  }
10052
+ const changes = [];
9054
10053
  this.state.sourceData.update((d) => {
9055
10054
  const updated = [...d];
9056
10055
  for (const [, srcIdx] of indexMap) {
9057
10056
  if (!updated[srcIdx])
9058
10057
  continue;
10058
+ const before = updated[srcIdx][field];
10059
+ if (before === sourceValue)
10060
+ continue;
9059
10061
  const rowCopy = { ...updated[srcIdx] };
9060
10062
  rowCopy[field] = sourceValue;
9061
10063
  updated[srcIdx] = rowCopy;
10064
+ changes.push({ rowIndex: srcIdx, field, before, after: sourceValue });
9062
10065
  }
9063
10066
  return updated;
9064
10067
  });
10068
+ if (changes.length > 0) {
10069
+ this.historyEngine.record('fill', changes);
10070
+ }
10071
+ this.clipboardEngine.clearCut();
9065
10072
  }
9066
10073
  this.fillDown.emit({
9067
10074
  sourceCell: anchor,
@@ -9102,6 +10109,7 @@ class MozGridComponent {
9102
10109
  if (targetFields.length === 0)
9103
10110
  return;
9104
10111
  if (this.state.mode() === 'client') {
10112
+ const changes = [];
9105
10113
  this.state.sourceData.update((d) => {
9106
10114
  const updated = [...d];
9107
10115
  const src = updated[anchorSourceIdx];
@@ -9109,11 +10117,19 @@ class MozGridComponent {
9109
10117
  return updated;
9110
10118
  const rowCopy = { ...src };
9111
10119
  for (const f of targetFields) {
10120
+ const before = src[f];
10121
+ if (before === sourceValue)
10122
+ continue;
9112
10123
  rowCopy[f] = sourceValue;
10124
+ changes.push({ rowIndex: anchorSourceIdx, field: f, before, after: sourceValue });
9113
10125
  }
9114
10126
  updated[anchorSourceIdx] = rowCopy;
9115
10127
  return updated;
9116
10128
  });
10129
+ if (changes.length > 0) {
10130
+ this.historyEngine.record('fill', changes);
10131
+ }
10132
+ this.clipboardEngine.clearCut();
9117
10133
  }
9118
10134
  this.fillDown.emit({
9119
10135
  sourceCell: anchor,
@@ -9320,6 +10336,8 @@ class MozGridComponent {
9320
10336
  });
9321
10337
  }
9322
10338
  onBulkCopy() {
10339
+ // A fresh copy always cancels any pending cut (Excel parity).
10340
+ this.clipboardEngine.clearCut();
9323
10341
  if (this.state.activeSelectionMode() === 'rows') {
9324
10342
  const event = this.rowSelectionEngine.getSelectionEvent();
9325
10343
  const rowData = this.extractRowSelectionData(event.selectedRows);
@@ -9387,7 +10405,16 @@ class MozGridComponent {
9387
10405
  },
9388
10406
  };
9389
10407
  if (this.state.mode() === 'client') {
9390
- this.applyPasteData(pasteRange, rows);
10408
+ // If a cut is pending, first wipe the source cells so cut+paste == move,
10409
+ // and fold both halves into a single undoable history op.
10410
+ const cutSource = this.state.cutSource();
10411
+ const clearChanges = cutSource ? this.clipboardEngine.clearRange(cutSource) : [];
10412
+ const pasteChanges = this.clipboardEngine.applyPaste(pasteRange, rows);
10413
+ const allChanges = [...clearChanges, ...pasteChanges];
10414
+ if (allChanges.length > 0) {
10415
+ this.historyEngine.record(cutSource ? 'cut' : 'paste', allChanges);
10416
+ }
10417
+ this.clipboardEngine.clearCut();
9391
10418
  }
9392
10419
  this.bulkPaste.emit({
9393
10420
  range: pasteRange,
@@ -9449,33 +10476,14 @@ class MozGridComponent {
9449
10476
  const pageEnd = pageStart + this.gridEngine.paginatedData().length - 1;
9450
10477
  if (range.start.row < pageStart || range.end.row > pageEnd)
9451
10478
  return;
9452
- const cols = this.state.visibleColumns();
9453
10479
  const rows = range.end.row - range.start.row + 1;
9454
10480
  const colCount = range.end.col - range.start.col + 1;
9455
10481
  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
- });
10482
+ const changes = this.clipboardEngine.clearRange(range);
10483
+ if (changes.length > 0) {
10484
+ this.historyEngine.record('delete', changes);
10485
+ }
10486
+ this.clipboardEngine.clearCut();
9479
10487
  }
9480
10488
  this.bulkDelete.emit({
9481
10489
  range,
@@ -9513,36 +10521,6 @@ class MozGridComponent {
9513
10521
  }
9514
10522
  return { range, values };
9515
10523
  }
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
10524
  extractRowSelectionData(selectedRows) {
9547
10525
  const cols = this.state.visibleColumns();
9548
10526
  const defMap = this.state.columnDefMap();
@@ -9679,6 +10657,8 @@ class MozGridComponent {
9679
10657
  RowSelectionEngine,
9680
10658
  CellSelectionEngine,
9681
10659
  KeyboardEngine,
10660
+ ClipboardEngine,
10661
+ HistoryEngine,
9682
10662
  GroupEngine,
9683
10663
  FilterEngine,
9684
10664
  ColumnReorderEngine,
@@ -9925,6 +10905,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
9925
10905
  RowSelectionEngine,
9926
10906
  CellSelectionEngine,
9927
10907
  KeyboardEngine,
10908
+ ClipboardEngine,
10909
+ HistoryEngine,
9928
10910
  GroupEngine,
9929
10911
  FilterEngine,
9930
10912
  ColumnReorderEngine,