From 04d8c658abd3c936da452f2cc3748ee02395d970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Sat, 26 Oct 2024 22:39:10 +0200 Subject: [PATCH] UPDATE: Adds cut/paste --- base/src/test/user_model/test_paste_csv.rs | 4 +- base/src/user_model/common.rs | 51 ++++++++++++++++-- bindings/wasm/fix_types.py | 6 ++- bindings/wasm/src/lib.rs | 3 +- .../WorksheetCanvas/worksheetCanvas.ts | 30 +++++++++++ .../src/components/useKeyboardNavigation.ts | 4 ++ webapp/src/components/workbook.tsx | 54 +++++++++++++++++-- webapp/src/components/workbookState.ts | 22 ++++++++ 8 files changed, 162 insertions(+), 12 deletions(-) diff --git a/base/src/test/user_model/test_paste_csv.rs b/base/src/test/user_model/test_paste_csv.rs index 8edad9e..24c9754 100644 --- a/base/src/test/user_model/test_paste_csv.rs +++ b/base/src/test/user_model/test_paste_csv.rs @@ -15,6 +15,7 @@ fn csv_paste() { width: 1, height: 1, }; + model.set_selected_cell(4, 2).unwrap(); model.paste_csv_string(&area, csv).unwrap(); assert_eq!( @@ -38,6 +39,7 @@ fn tsv_crlf_paste() { width: 1, height: 1, }; + model.set_selected_cell(4, 2).unwrap(); model.paste_csv_string(&area, csv).unwrap(); assert_eq!( @@ -79,7 +81,7 @@ fn copy_paste_internal() { // paste in cell D4 (4, 4) model - .paste_from_clipboard((1, 1, 2, 2), ©.data) + .paste_from_clipboard((1, 1, 2, 2), ©.data, false) .unwrap(); assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string())); diff --git a/base/src/user_model/common.rs b/base/src/user_model/common.rs index 76dcf86..1403fb0 100644 --- a/base/src/user_model/common.rs +++ b/base/src/user_model/common.rs @@ -1275,18 +1275,31 @@ impl UserModel { &mut self, source_range: ClipboardTuple, clipboard: &ClipboardData, + is_cut: bool, ) -> Result<(), String> { let mut diff_list = Vec::new(); let view = self.get_selected_view(); - let (source_first_row, source_first_column, _, _) = source_range; + let (source_first_row, source_first_column, source_last_row, source_last_column) = + source_range; let sheet = view.sheet; let [selected_row, selected_column, _, _] = view.range; + let mut max_row = selected_row; + let mut max_column = selected_column; + let area = &Area { + sheet, + row: source_first_row, + column: source_first_column, + width: source_last_column - source_first_column + 1, + height: source_last_row - source_first_row + 1, + }; for (source_row, data_row) in clipboard { let delta_row = source_row - source_first_row; let target_row = selected_row + delta_row; + max_row = max_row.max(target_row); for (source_column, value) in data_row { let delta_column = source_column - source_first_column; let target_column = selected_column + delta_column; + max_column = max_column.max(target_column); // We are copying the value in // (source_row, source_column) to (target_row , target_column) @@ -1303,9 +1316,13 @@ impl UserModel { column: target_column, row: target_row, }; - let new_value = self - .model - .extend_copied_value(&value.text, source, target)?; + let new_value = if is_cut { + self.model + .move_cell_value_to_area(&value.text, source, target, area)? + } else { + self.model + .extend_copied_value(&value.text, source, target)? + }; let old_value = self .model @@ -1340,7 +1357,28 @@ impl UserModel { }); } } + if is_cut { + for row in source_first_row..=source_last_row { + for column in source_first_column..=source_last_column { + let old_value = self + .model + .workbook + .worksheet(sheet)? + .cell(row, column) + .cloned(); + diff_list.push(Diff::CellClearContents { + sheet, + row, + column, + old_value: Box::new(old_value), + }); + self.model.cell_clear_contents(sheet, row, column)?; + } + } + } self.push_diff_list(diff_list); + // select the pasted area + self.set_selected_range(selected_row, selected_column, max_row, max_column)?; self.evaluate_if_not_paused(); Ok(()) } @@ -1350,6 +1388,7 @@ impl UserModel { let mut diff_list = Vec::new(); let sheet = area.sheet; let mut row = area.row; + let mut column = area.column; // Create a sniffer with default settings let mut sniffer = Sniffer::new(); let mut csv_reader = Cursor::new(csv); @@ -1367,7 +1406,7 @@ impl UserModel { for record in reader.records() { match record { Ok(r) => { - let mut column = area.column; + column = area.column; for value in &r { let old_value = self .model @@ -1397,6 +1436,8 @@ impl UserModel { row += 1; } self.push_diff_list(diff_list); + // select the pasted area + self.set_selected_range(area.row, area.column, row, column)?; self.evaluate_if_not_paused(); Ok(()) } diff --git a/bindings/wasm/fix_types.py b/bindings/wasm/fix_types.py index d5db3b8..27e9e1a 100644 --- a/bindings/wasm/fix_types.py +++ b/bindings/wasm/fix_types.py @@ -171,16 +171,18 @@ paste_from_clipboard = r""" /** * @param {any} source_range * @param {any} clipboard +* @param {boolean} is_cut */ - pasteFromClipboard(source_range: any, clipboard: any): void; + pasteFromClipboard(source_range: any, clipboard: any, is_cut: boolean): void; """ paste_from_clipboard_types = r""" /** * @param {[number, number, number, number]} source_range * @param {ClipboardData} clipboard +* @param {boolean} is_cut */ - pasteFromClipboard(source_range: [number, number, number, number], clipboard: ClipboardData): void; + pasteFromClipboard(source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void; """ def fix_types(text): diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index 200e138..c1fd0c1 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -512,13 +512,14 @@ impl Model { &mut self, source_range: JsValue, clipboard: JsValue, + is_cut: bool, ) -> Result<(), JsError> { let source_range: (i32, i32, i32, i32) = serde_wasm_bindgen::from_value(source_range).map_err(|e| to_js_error(e.to_string()))?; let clipboard: ClipboardData = serde_wasm_bindgen::from_value(clipboard).map_err(|e| to_js_error(e.to_string()))?; self.model - .paste_from_clipboard(source_range, &clipboard) + .paste_from_clipboard(source_range, &clipboard, is_cut) .map_err(|e| to_js_error(e.to_string())) } diff --git a/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts b/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts index c83fd69..3fb371d 100644 --- a/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts +++ b/webapp/src/components/WorksheetCanvas/worksheetCanvas.ts @@ -1239,6 +1239,35 @@ export default class WorksheetCanvas { cellOutlineHandle.style.top = `${handleY - handleHeight / 2 - 1}px`; } + private drawCutRange(): void { + const range = this.workbookState.getCutRange() || null; + if (!range) { + return; + } + const selectedSheet = this.model.getSelectedSheet(); + if (range.sheet !== selectedSheet) { + return; + } + const ctx = this.ctx; + ctx.setLineDash([2, 2]); + + const [xStart, yStart] = this.getCoordinatesByCell( + range.rowStart, + range.columnStart, + ); + const [xEnd, yEnd] = this.getCoordinatesByCell( + range.rowEnd + 1, + range.columnEnd + 1, + ); + ctx.strokeStyle = "red"; + ctx.lineWidth = 1; + ctx.strokeRect(xStart, yStart, xEnd - xStart, yEnd - yStart); + // ctx.fillStyle = hexToRGBA10Percent(range.color); + // ctx.fillRect(xStart, yStart, xEnd - xStart, yEnd - yStart); + + ctx.setLineDash([]); + } + private drawActiveRanges(topLeftCell: Cell, bottomRightCell: Cell): void { let activeRanges = this.workbookState.getActiveRanges(); const ctx = this.ctx; @@ -1437,5 +1466,6 @@ export default class WorksheetCanvas { this.drawCellEditor(); this.drawExtendToArea(); this.drawActiveRanges(topLeftCell, bottomRightCell); + this.drawCutRange(); } } diff --git a/webapp/src/components/useKeyboardNavigation.ts b/webapp/src/components/useKeyboardNavigation.ts index c96711a..47c46b3 100644 --- a/webapp/src/components/useKeyboardNavigation.ts +++ b/webapp/src/components/useKeyboardNavigation.ts @@ -31,6 +31,7 @@ interface Options { onRedo: () => void; onNextSheet: () => void; onPreviousSheet: () => void; + onEscape: () => void; root: RefObject; } @@ -212,6 +213,9 @@ const useKeyboardNavigation = ( break; } + case "Escape": { + options.onEscape(); + } // No default } event.stopPropagation(); diff --git a/webapp/src/components/workbook.tsx b/webapp/src/components/workbook.tsx index 0a1fbf8..a2b2b1d 100644 --- a/webapp/src/components/workbook.tsx +++ b/webapp/src/components/workbook.tsx @@ -28,7 +28,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { const rootRef = useRef(null); // Calling `setRedrawId((id) => id + 1);` forces a redraw - // This is needed because `model` can change without React being aware of it + // This is needed because `model` or `workbookState` can change without React being aware of it const setRedrawId = useState(0)[1]; const info = model .getWorksheetsProperties() @@ -293,6 +293,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { model.setSelectedSheet(nextSheet); } }, + onEscape: (): void => { + workbookState.clearCutRange(); + setRedrawId((id) => id + 1); + }, root: rootRef, }); @@ -346,6 +350,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { } }} onPaste={(event: React.ClipboardEvent) => { + workbookState.clearCutRange(); const { items } = event.clipboardData; if (!items) { return; @@ -385,7 +390,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { } data.set(Number.parseInt(row, 10), rowMap); } - model.pasteFromClipboard(source.area, data); + model.pasteFromClipboard(source.area, data, source.type === "cut"); setRedrawId((id) => id + 1); } else if (mimeType === "text/plain") { const { @@ -445,7 +450,50 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { event.preventDefault(); event.stopPropagation(); }} - onCut={() => {}} + onCut={(event: React.ClipboardEvent) => { + const data = model.copyToClipboard(); + // '2024-10-18T14:07:37.599Z' + + let clipboardId = sessionStorage.getItem( + CLIPBOARD_ID_SESSION_STORAGE_KEY, + ); + if (!clipboardId) { + clipboardId = getNewClipboardId(); + sessionStorage.setItem(CLIPBOARD_ID_SESSION_STORAGE_KEY, clipboardId); + } + const sheetData: { + [row: number]: { + [column: number]: ClipboardCell; + }; + } = {}; + data.data.forEach((value, row) => { + const rowData: { + [column: number]: ClipboardCell; + } = {}; + value.forEach((val, column) => { + rowData[column] = val; + }); + sheetData[row] = rowData; + }); + const clipboardJsonStr = JSON.stringify({ + type: "cut", + area: data.range, + sheetData, + clipboardId, + }); + event.clipboardData.setData("text/plain", data.csv); + event.clipboardData.setData("application/json", clipboardJsonStr); + workbookState.setCutRange({ + sheet: model.getSelectedSheet(), + rowStart: data.range[0], + rowEnd: data.range[2], + columnStart: data.range[1], + columnEnd: data.range[3], + }); + event.preventDefault(); + event.stopPropagation(); + setRedrawId((id) => id + 1); + }} >