• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

iddan / react-spreadsheet / 6062637297

03 Sep 2023 05:18AM UTC coverage: 80.269% (-0.09%) from 80.362%
6062637297

push

github

iddan
Fix cut deletion

408 of 556 branches covered (0.0%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 1 file covered. (100.0%)

967 of 1157 relevant lines covered (83.58%)

28.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.56
/src/reducer.ts
1
import { PointRange } from "./point-range";
9✔
2
import * as Matrix from "./matrix";
9✔
3
import * as Types from "./types";
4
import * as Point from "./point";
9✔
5
import {
9✔
6
  Selection,
7
  EmptySelection,
8
  RangeSelection,
9
  EntireColumnsSelection,
10
  EntireRowsSelection,
11
  EntireWorksheetSelection,
12
} from "./selection";
13
import { isActive } from "./util";
9✔
14
import * as Actions from "./actions";
9✔
15
import { Model, updateCellValue } from "./engine";
9✔
16

17
export const INITIAL_STATE: Types.StoreState = {
9✔
18
  active: null,
19
  mode: "view",
20
  rowDimensions: {},
21
  columnDimensions: {},
22
  lastChanged: null,
23
  hasPasted: false,
24
  cut: false,
25
  dragging: false,
26
  model: new Model([]),
27
  selected: new EmptySelection(),
28
  copied: null,
29
  lastCommit: null,
30
};
31

32
export default function reducer(
9✔
33
  state: Types.StoreState,
34
  action: Actions.Action
35
): Types.StoreState {
36
  switch (action.type) {
41✔
37
    case Actions.SET_DATA: {
41!
38
      const { data } = action.payload;
2✔
39
      const nextActive =
40
        state.active && Matrix.has(state.active, data) ? state.active : null;
2!
41
      const nextSelected = state.selected.normalizeTo(data);
2✔
42
      return {
2✔
43
        ...state,
44
        model: new Model(data),
45
        active: nextActive,
46
        selected: nextSelected,
47
      };
48
    }
49
    case Actions.SELECT_ENTIRE_ROW: {
50
      const { row, extend } = action.payload;
3✔
51
      const { active } = state;
3✔
52

53
      return {
3✔
54
        ...state,
55
        selected:
56
          extend && active
5✔
57
            ? new EntireRowsSelection(active.row, row)
3✔
58
            : new EntireRowsSelection(row, row),
59
        active: extend && active ? active : { ...Point.ORIGIN, row },
8✔
60
        mode: "view",
61
      };
62
    }
63
    case Actions.SELECT_ENTIRE_COLUMN: {
64
      const { column, extend } = action.payload;
2✔
65
      const { active } = state;
2✔
66

67
      return {
2✔
68
        ...state,
69
        selected:
70
          extend && active
3✔
71
            ? new EntireColumnsSelection(active.column, column)
2✔
72
            : new EntireColumnsSelection(column, column),
73
        active: extend && active ? active : { ...Point.ORIGIN, column },
5✔
74
        mode: "view",
75
      };
76
    }
77
    case Actions.SELECT_ENTIRE_WORKSHEET: {
78
      return {
1✔
79
        ...state,
80
        selected: new EntireWorksheetSelection(),
81
        active: Point.ORIGIN,
82
        mode: "view",
83
      };
84
    }
85
    case Actions.SELECT: {
86
      const { point } = action.payload;
2✔
87
      if (state.active && !isActive(state.active, point)) {
2✔
88
        return {
2✔
89
          ...state,
90
          selected: new RangeSelection(new PointRange(point, state.active)),
91
          mode: "view",
92
        };
93
      }
94
      return state;
×
95
    }
96
    case Actions.ACTIVATE: {
97
      const { point } = action.payload;
5✔
98
      return {
5✔
99
        ...state,
100
        selected: new RangeSelection(new PointRange(point, point)),
101
        active: point,
102
        mode: isActive(state.active, point) ? "edit" : "view",
5!
103
      };
104
    }
105
    case Actions.SET_CELL_DATA: {
106
      const { active, data: cellData } = action.payload;
2✔
107
      if (isActiveReadOnly(state)) {
2!
108
        return state;
×
109
      }
110
      return {
2✔
111
        ...state,
112
        model: updateCellValue(state.model, active, cellData),
113
        lastChanged: active,
114
      };
115
    }
116
    case Actions.SET_CELL_DIMENSIONS: {
117
      const { point, dimensions } = action.payload;
16✔
118
      const prevRowDimensions = state.rowDimensions[point.row];
16✔
119
      const prevColumnDimensions = state.columnDimensions[point.column];
16✔
120
      if (
16✔
121
        prevRowDimensions &&
66✔
122
        prevColumnDimensions &&
123
        prevRowDimensions.top === dimensions.top &&
124
        prevRowDimensions.height === dimensions.height &&
125
        prevColumnDimensions.left === dimensions.left &&
126
        prevColumnDimensions.width === dimensions.width
127
      ) {
128
        return state;
10✔
129
      }
130
      return {
6✔
131
        ...state,
132
        rowDimensions: {
133
          ...state.rowDimensions,
134
          [point.row]: { top: dimensions.top, height: dimensions.height },
135
        },
136
        columnDimensions: {
137
          ...state.columnDimensions,
138
          [point.column]: { left: dimensions.left, width: dimensions.width },
139
        },
140
      };
141
    }
142
    case Actions.COPY:
143
    case Actions.CUT: {
144
      const selectedRange = state.selected.toRange(state.model.data);
×
145
      return {
×
146
        ...state,
147
        copied: selectedRange,
148
        cut: action.type === Actions.CUT,
149
        hasPasted: false,
150
      };
151
    }
152

153
    case Actions.PASTE: {
154
      const { data: text } = action.payload;
×
155
      const { active } = state;
×
156
      if (!active) {
×
157
        return state;
×
158
      }
159
      const copied = Matrix.split(text, (value) => ({ value }));
×
160
      const copiedSize = Matrix.getSize(copied);
×
161

162
      const selectedRange = state.selected.toRange(state.model.data);
×
163
      if (selectedRange && copiedSize.rows === 1 && copiedSize.columns === 1) {
×
164
        const cell = Matrix.get({ row: 0, column: 0 }, copied);
×
165
        let newData =
166
          state.cut && state.copied
×
167
            ? Matrix.unset(state.copied.start, state.model.data)
×
168
            : state.model.data;
169
        const commit: Types.StoreState["lastCommit"] = [];
×
170
        for (const point of selectedRange || []) {
×
171
          const currentCell = Matrix.get(point, state.model.data);
×
172
          commit.push({
×
173
            prevCell: currentCell || null,
×
174
            nextCell: cell || null,
×
175
          });
176
          newData = Matrix.set(point, cell, newData);
×
177
        }
178

179
        return {
×
180
          ...state,
181
          model: new Model(newData),
182
          copied: null,
183
          cut: false,
184
          hasPasted: true,
185
          mode: "view",
186
          lastCommit: commit,
187
        };
188
      }
189

190
      const requiredSize: Matrix.Size = {
×
191
        rows: active.row + copiedSize.rows,
192
        columns: active.column + copiedSize.columns,
193
      };
194
      const paddedData = Matrix.pad(state.model.data, requiredSize);
×
195

196
      let acc: {
197
        data: Types.StoreState["model"]["data"];
198
        commit: Types.StoreState["lastCommit"];
199
      } = { data: paddedData, commit: [] };
×
200
      for (const [point, cell] of Matrix.entries(copied)) {
×
201
        let commit = acc.commit || [];
×
202
        const nextPoint: Point.Point = {
×
203
          row: point.row + active.row,
204
          column: point.column + active.column,
205
        };
206

207
        let nextData = acc.data;
×
208

209
        if (state.cut) {
×
210
          if (state.copied) {
×
211
            const prevPoint: Point.Point = {
×
212
              row: point.row + state.copied.start.row,
213
              column: point.column + state.copied.start.column,
214
            };
215
            nextData = Matrix.unset(prevPoint, acc.data);
×
216
          }
217

218
          commit = [...commit, { prevCell: cell || null, nextCell: null }];
×
219
        }
220

221
        if (!Matrix.has(nextPoint, paddedData)) {
×
222
          acc = { data: nextData, commit };
×
223
        }
224

225
        const currentCell = Matrix.get(nextPoint, nextData) || null;
×
226

227
        commit = [
×
228
          ...commit,
229
          {
230
            prevCell: currentCell,
231
            nextCell: cell || null,
×
232
          },
233
        ];
234

235
        acc.data = Matrix.set(
×
236
          nextPoint,
237
          { value: undefined, ...currentCell, ...cell },
238
          nextData
239
        );
240
        acc.commit = commit;
×
241
      }
242

243
      return {
×
244
        ...state,
245
        model: new Model(acc.data),
246
        selected: new RangeSelection(
247
          new PointRange(active, {
248
            row: active.row + copiedSize.rows - 1,
249
            column: active.column + copiedSize.columns - 1,
250
          })
251
        ),
252
        copied: null,
253
        cut: false,
254
        hasPasted: true,
255
        mode: "view",
256
        lastCommit: acc.commit,
257
      };
258
    }
259

260
    case Actions.EDIT: {
261
      return edit(state);
1✔
262
    }
263

264
    case Actions.VIEW: {
265
      return view(state);
1✔
266
    }
267

268
    case Actions.CLEAR: {
269
      return clear(state);
×
270
    }
271

272
    case Actions.BLUR: {
273
      return blur(state);
1✔
274
    }
275

276
    case Actions.KEY_PRESS: {
277
      const { event } = action.payload;
×
278
      if (isActiveReadOnly(state) || event.metaKey) {
×
279
        return state;
×
280
      }
281
      if (state.mode === "view" && state.active) {
×
282
        return edit(state);
×
283
      }
284
      return state;
×
285
    }
286

287
    case Actions.KEY_DOWN: {
288
      const { event } = action.payload;
3✔
289
      const handler = getKeyDownHandler(state, event);
3✔
290
      if (handler) {
3✔
291
        return { ...state, ...handler(state, event) };
2✔
292
      }
293
      return state;
1✔
294
    }
295

296
    case Actions.DRAG_START: {
297
      return { ...state, dragging: true };
1✔
298
    }
299

300
    case Actions.DRAG_END: {
301
      return { ...state, dragging: false };
1✔
302
    }
303

304
    case Actions.COMMIT: {
305
      const { changes } = action.payload;
×
306
      return { ...state, ...commit(changes) };
×
307
    }
308
  }
309
}
310

311
// const reducer = createReducer(INITIAL_STATE, (builder) => {
312
//   builder.addMatcher(
313
//     (action) =>
314
//       action.type === Actions.copy.type || action.type === Actions.cut.type,
315
//     (state, action) => {
316

317
//     }
318
//   );
319
// });
320

321
// // Shared reducers
322

323
function edit(state: Types.StoreState): Types.StoreState {
324
  if (isActiveReadOnly(state)) {
3!
325
    return state;
×
326
  }
327
  return { ...state, mode: "edit" };
3✔
328
}
329

330
function clear(state: Types.StoreState): Types.StoreState {
331
  if (!state.active) {
×
332
    return state;
×
333
  }
334

335
  const canClearCell = (cell: Types.CellBase | undefined) =>
×
336
    cell && !cell.readOnly;
×
337
  const clearCell = (cell: Types.CellBase | undefined) => {
×
338
    if (!canClearCell(cell)) {
×
339
      return cell;
×
340
    }
341
    return Object.assign({}, cell, { value: undefined });
×
342
  };
343

344
  const selectedRange = state.selected.toRange(state.model.data);
×
345

346
  const changes: Types.CommitChanges = [];
×
347
  let newData = state.model.data;
×
348

349
  for (const point of selectedRange || []) {
×
350
    const cell = Matrix.get(point, state.model.data);
×
351
    const clearedCell = clearCell(cell);
×
352
    changes.push({
×
353
      prevCell: cell || null,
×
354
      nextCell: clearedCell || null,
×
355
    });
356
    newData = Matrix.set(point, clearedCell, newData);
×
357
  }
358

359
  return {
×
360
    ...state,
361
    model: new Model(newData),
362
    ...commit(changes),
363
  };
364
}
365

366
function blur(state: Types.StoreState): Types.StoreState {
367
  return { ...state, active: null, selected: new EmptySelection() };
1✔
368
}
369

370
function view(state: Types.StoreState): Types.StoreState {
371
  return { ...state, mode: "view" };
1✔
372
}
373

374
function commit(changes: Types.CommitChanges): Partial<Types.StoreState> {
375
  return { lastCommit: changes };
×
376
}
377

378
// Utility
379

380
export const go =
9✔
381
  (rowDelta: number, columnDelta: number): KeyDownHandler =>
9✔
382
  (state) => {
63✔
383
    if (!state.active) {
×
384
      return;
×
385
    }
386
    const nextActive = {
×
387
      row: state.active.row + rowDelta,
388
      column: state.active.column + columnDelta,
389
    };
390
    if (!Matrix.has(nextActive, state.model.data)) {
×
391
      return { ...state, mode: "view" };
×
392
    }
393
    return {
×
394
      ...state,
395
      active: nextActive,
396
      selected: new RangeSelection(new PointRange(nextActive, nextActive)),
397
      mode: "view",
398
    };
399
  };
400

401
// Key Bindings
402

403
export type KeyDownHandler = (
404
  state: Types.StoreState,
405
  event: React.KeyboardEvent
406
) => Types.StoreState | void;
407

408
type KeyDownHandlers = {
409
  [K in string]: KeyDownHandler;
410
};
411

412
const keyDownHandlers: KeyDownHandlers = {
9✔
413
  ArrowUp: go(-1, 0),
414
  ArrowDown: go(+1, 0),
415
  ArrowLeft: go(0, -1),
416
  ArrowRight: go(0, +1),
417
  Tab: go(0, +1),
418
  Enter: edit,
419
  Backspace: clear,
420
  Delete: clear,
421
  Escape: blur,
422
};
423

424
const editKeyDownHandlers: KeyDownHandlers = {
9✔
425
  Escape: view,
426
  Tab: keyDownHandlers.Tab,
427
  Enter: keyDownHandlers.ArrowDown,
428
};
429

430
const editShiftKeyDownHandlers: KeyDownHandlers = {
9✔
431
  Tab: go(0, -1),
432
};
433

434
const shiftKeyDownHandlers: KeyDownHandlers = {
9✔
435
  ArrowUp: (state) => ({
×
436
    ...state,
437
    selected: modifyEdge(
438
      state.selected,
439
      state.active,
440
      state.model.data,
441
      Direction.Top
442
    ),
443
  }),
444
  ArrowDown: (state) => ({
×
445
    ...state,
446
    selected: modifyEdge(
447
      state.selected,
448
      state.active,
449
      state.model.data,
450
      Direction.Bottom
451
    ),
452
  }),
453
  ArrowLeft: (state) => ({
×
454
    ...state,
455
    selected: modifyEdge(
456
      state.selected,
457
      state.active,
458
      state.model.data,
459
      Direction.Left
460
    ),
461
  }),
462
  ArrowRight: (state) => ({
×
463
    ...state,
464
    selected: modifyEdge(
465
      state.selected,
466
      state.active,
467
      state.model.data,
468
      Direction.Right
469
    ),
470
  }),
471
  Tab: go(0, -1),
472
};
473

474
const shiftMetaKeyDownHandlers: KeyDownHandlers = {};
9✔
475
const metaKeyDownHandlers: KeyDownHandlers = {};
9✔
476

477
export function getKeyDownHandler(
9✔
478
  state: Types.StoreState,
479
  event: React.KeyboardEvent
480
): KeyDownHandler | undefined {
481
  const { key } = event;
12✔
482
  let handlers;
483
  // Order matters
484
  if (state.mode === "edit") {
12✔
485
    if (event.shiftKey) {
2!
486
      handlers = editShiftKeyDownHandlers;
×
487
    } else {
488
      handlers = editKeyDownHandlers;
2✔
489
    }
490
  } else if (event.shiftKey && event.metaKey) {
10!
491
    handlers = shiftMetaKeyDownHandlers;
×
492
  } else if (event.shiftKey) {
10!
493
    handlers = shiftKeyDownHandlers;
×
494
  } else if (event.metaKey) {
10!
495
    handlers = metaKeyDownHandlers;
×
496
  } else {
497
    handlers = keyDownHandlers;
10✔
498
  }
499

500
  return handlers[key];
12✔
501
}
502

503
/** Returns whether the reducer has a handler for the given keydown event */
504
export function hasKeyDownHandler(
9✔
505
  state: Types.StoreState,
506
  event: React.KeyboardEvent
507
): boolean {
508
  return getKeyDownHandler(state, event) !== undefined;
9✔
509
}
510

511
/** Returns whether the active cell is read only */
512
export function isActiveReadOnly(state: Types.StoreState): boolean {
9✔
513
  const activeCell = getActive(state);
8✔
514
  return Boolean(activeCell?.readOnly);
8✔
515
}
516

517
/** Gets active cell from given state */
518
export function getActive(state: Types.StoreState): Types.CellBase | null {
9✔
519
  const activeCell = state.active && Matrix.get(state.active, state.model.data);
8✔
520
  return activeCell || null;
8✔
521
}
522

523
export enum Direction {
9✔
524
  Left = "Left",
9✔
525
  Right = "Right",
9✔
526
  Top = "Top",
9✔
527
  Bottom = "Bottom",
9✔
528
}
529

530
/** Modify given edge according to given active point and data */
531
export function modifyEdge<T extends Selection>(
9✔
532
  selection: T,
533
  active: Point.Point | null,
534
  data: Matrix.Matrix<unknown>,
535
  direction: Direction
536
): T {
537
  if (!active) {
4!
538
    return selection;
×
539
  }
540
  if (selection instanceof RangeSelection) {
4✔
541
    const nextSelection = modifyRangeSelectionEdge(
1✔
542
      selection,
543
      active,
544
      data,
545
      direction
546
    );
547
    // @ts-expect-error
548
    return nextSelection;
1✔
549
  }
550
  if (selection instanceof EntireColumnsSelection) {
3✔
551
    // @ts-expect-error
552
    return modifyEntireColumnsSelection(selection, active, data, direction);
1✔
553
  }
554
  if (selection instanceof EntireRowsSelection) {
2✔
555
    // @ts-expect-error
556
    return modifyEntireRowsSelection(selection, active, data, direction);
1✔
557
  }
558
  return selection;
1✔
559
}
560

561
export function modifyRangeSelectionEdge(
9✔
562
  rangeSelection: RangeSelection,
563
  active: Point.Point,
564
  data: Matrix.Matrix<unknown>,
565
  edge: Direction
566
): RangeSelection {
567
  const field =
568
    edge === Direction.Left || edge === Direction.Right ? "column" : "row";
13✔
569

570
  const key =
571
    edge === Direction.Left || edge === Direction.Top ? "start" : "end";
13✔
572
  const delta = key === "start" ? -1 : 1;
13✔
573

574
  const edgeOffsets = rangeSelection.range.has({
13✔
575
    ...active,
576
    [field]: active[field] + delta * -1,
577
  });
578

579
  const keyToModify = edgeOffsets ? (key === "start" ? "end" : "start") : key;
13✔
580

581
  const nextRange = new PointRange(
13✔
582
    rangeSelection.range.start,
583
    rangeSelection.range.end
584
  );
585

586
  nextRange[keyToModify][field] += delta;
13✔
587

588
  const nextSelection = new RangeSelection(nextRange).normalizeTo(data);
13✔
589

590
  return nextSelection;
13✔
591
}
592

593
export function modifyEntireRowsSelection(
9✔
594
  selection: EntireRowsSelection,
595
  active: Point.Point,
596
  data: Matrix.Matrix<unknown>,
597
  edge: Direction
598
): EntireRowsSelection {
599
  if (edge === Direction.Left || edge === Direction.Right) {
9✔
600
    return selection;
2✔
601
  }
602
  const delta = edge === Direction.Top ? -1 : 1;
7✔
603
  const property = edge === Direction.Top ? "start" : "end";
7✔
604
  const oppositeProperty = property === "start" ? "end" : "start";
7✔
605
  const newSelectionData = { ...selection };
7✔
606
  if (
7✔
607
    edge === Direction.Top
608
      ? selection.end > active.row
7✔
609
      : selection.start < active.row
610
  ) {
611
    newSelectionData[oppositeProperty] = selection[oppositeProperty] + delta;
2✔
612
  } else {
613
    newSelectionData[property] = selection[property] + delta;
5✔
614
  }
615
  const nextSelection = new EntireRowsSelection(
7✔
616
    Math.max(newSelectionData.start, 0),
617
    Math.max(newSelectionData.end, 0)
618
  );
619
  return nextSelection.normalizeTo(data);
7✔
620
}
621

622
export function modifyEntireColumnsSelection(
9✔
623
  selection: EntireColumnsSelection,
624
  active: Point.Point,
625
  data: Matrix.Matrix<unknown>,
626
  edge: Direction
627
): EntireColumnsSelection {
628
  if (edge === Direction.Top || edge === Direction.Bottom) {
9✔
629
    return selection;
2✔
630
  }
631
  const delta = edge === Direction.Left ? -1 : 1;
7✔
632
  const property = edge === Direction.Left ? "start" : "end";
7✔
633
  const oppositeProperty = property === "start" ? "end" : "start";
7✔
634
  const newSelectionData = { ...selection };
7✔
635
  if (
7✔
636
    edge === Direction.Left
637
      ? selection.end > active.row
7✔
638
      : selection.start < active.row
639
  ) {
640
    newSelectionData[oppositeProperty] = selection[oppositeProperty] + delta;
2✔
641
  } else {
642
    newSelectionData[property] = selection[property] + delta;
5✔
643
  }
644
  const nextSelection = new EntireColumnsSelection(
7✔
645
    Math.max(newSelectionData.start, 0),
646
    Math.max(newSelectionData.end, 0)
647
  );
648
  return nextSelection.normalizeTo(data);
7✔
649
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc