UPDATE: Adds cut/paste

This commit is contained in:
Nicolás Hatcher
2024-10-26 22:39:10 +02:00
committed by Nicolás Hatcher Andrés
parent dad4755b16
commit 04d8c658ab
8 changed files with 162 additions and 12 deletions

View File

@@ -15,6 +15,7 @@ fn csv_paste() {
width: 1, width: 1,
height: 1, height: 1,
}; };
model.set_selected_cell(4, 2).unwrap();
model.paste_csv_string(&area, csv).unwrap(); model.paste_csv_string(&area, csv).unwrap();
assert_eq!( assert_eq!(
@@ -38,6 +39,7 @@ fn tsv_crlf_paste() {
width: 1, width: 1,
height: 1, height: 1,
}; };
model.set_selected_cell(4, 2).unwrap();
model.paste_csv_string(&area, csv).unwrap(); model.paste_csv_string(&area, csv).unwrap();
assert_eq!( assert_eq!(
@@ -79,7 +81,7 @@ fn copy_paste_internal() {
// paste in cell D4 (4, 4) // paste in cell D4 (4, 4)
model model
.paste_from_clipboard((1, 1, 2, 2), &copy.data) .paste_from_clipboard((1, 1, 2, 2), &copy.data, false)
.unwrap(); .unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string())); assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));

View File

@@ -1275,18 +1275,31 @@ impl UserModel {
&mut self, &mut self,
source_range: ClipboardTuple, source_range: ClipboardTuple,
clipboard: &ClipboardData, clipboard: &ClipboardData,
is_cut: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let mut diff_list = Vec::new(); let mut diff_list = Vec::new();
let view = self.get_selected_view(); 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 sheet = view.sheet;
let [selected_row, selected_column, _, _] = view.range; 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 { for (source_row, data_row) in clipboard {
let delta_row = source_row - source_first_row; let delta_row = source_row - source_first_row;
let target_row = selected_row + delta_row; let target_row = selected_row + delta_row;
max_row = max_row.max(target_row);
for (source_column, value) in data_row { for (source_column, value) in data_row {
let delta_column = source_column - source_first_column; let delta_column = source_column - source_first_column;
let target_column = selected_column + delta_column; let target_column = selected_column + delta_column;
max_column = max_column.max(target_column);
// We are copying the value in // We are copying the value in
// (source_row, source_column) to (target_row , target_column) // (source_row, source_column) to (target_row , target_column)
@@ -1303,9 +1316,13 @@ impl UserModel {
column: target_column, column: target_column,
row: target_row, row: target_row,
}; };
let new_value = self let new_value = if is_cut {
.model self.model
.extend_copied_value(&value.text, source, target)?; .move_cell_value_to_area(&value.text, source, target, area)?
} else {
self.model
.extend_copied_value(&value.text, source, target)?
};
let old_value = self let old_value = self
.model .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); 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(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }
@@ -1350,6 +1388,7 @@ impl UserModel {
let mut diff_list = Vec::new(); let mut diff_list = Vec::new();
let sheet = area.sheet; let sheet = area.sheet;
let mut row = area.row; let mut row = area.row;
let mut column = area.column;
// Create a sniffer with default settings // Create a sniffer with default settings
let mut sniffer = Sniffer::new(); let mut sniffer = Sniffer::new();
let mut csv_reader = Cursor::new(csv); let mut csv_reader = Cursor::new(csv);
@@ -1367,7 +1406,7 @@ impl UserModel {
for record in reader.records() { for record in reader.records() {
match record { match record {
Ok(r) => { Ok(r) => {
let mut column = area.column; column = area.column;
for value in &r { for value in &r {
let old_value = self let old_value = self
.model .model
@@ -1397,6 +1436,8 @@ impl UserModel {
row += 1; row += 1;
} }
self.push_diff_list(diff_list); 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(); self.evaluate_if_not_paused();
Ok(()) Ok(())
} }

View File

@@ -171,16 +171,18 @@ paste_from_clipboard = r"""
/** /**
* @param {any} source_range * @param {any} source_range
* @param {any} clipboard * @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""" paste_from_clipboard_types = r"""
/** /**
* @param {[number, number, number, number]} source_range * @param {[number, number, number, number]} source_range
* @param {ClipboardData} clipboard * @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): def fix_types(text):

View File

@@ -512,13 +512,14 @@ impl Model {
&mut self, &mut self,
source_range: JsValue, source_range: JsValue,
clipboard: JsValue, clipboard: JsValue,
is_cut: bool,
) -> Result<(), JsError> { ) -> Result<(), JsError> {
let source_range: (i32, i32, i32, i32) = let source_range: (i32, i32, i32, i32) =
serde_wasm_bindgen::from_value(source_range).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(source_range).map_err(|e| to_js_error(e.to_string()))?;
let clipboard: ClipboardData = let clipboard: ClipboardData =
serde_wasm_bindgen::from_value(clipboard).map_err(|e| to_js_error(e.to_string()))?; serde_wasm_bindgen::from_value(clipboard).map_err(|e| to_js_error(e.to_string()))?;
self.model 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())) .map_err(|e| to_js_error(e.to_string()))
} }

View File

@@ -1239,6 +1239,35 @@ export default class WorksheetCanvas {
cellOutlineHandle.style.top = `${handleY - handleHeight / 2 - 1}px`; 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 { private drawActiveRanges(topLeftCell: Cell, bottomRightCell: Cell): void {
let activeRanges = this.workbookState.getActiveRanges(); let activeRanges = this.workbookState.getActiveRanges();
const ctx = this.ctx; const ctx = this.ctx;
@@ -1437,5 +1466,6 @@ export default class WorksheetCanvas {
this.drawCellEditor(); this.drawCellEditor();
this.drawExtendToArea(); this.drawExtendToArea();
this.drawActiveRanges(topLeftCell, bottomRightCell); this.drawActiveRanges(topLeftCell, bottomRightCell);
this.drawCutRange();
} }
} }

View File

@@ -31,6 +31,7 @@ interface Options {
onRedo: () => void; onRedo: () => void;
onNextSheet: () => void; onNextSheet: () => void;
onPreviousSheet: () => void; onPreviousSheet: () => void;
onEscape: () => void;
root: RefObject<HTMLDivElement>; root: RefObject<HTMLDivElement>;
} }
@@ -212,6 +213,9 @@ const useKeyboardNavigation = (
break; break;
} }
case "Escape": {
options.onEscape();
}
// No default // No default
} }
event.stopPropagation(); event.stopPropagation();

View File

@@ -28,7 +28,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
// Calling `setRedrawId((id) => id + 1);` forces a redraw // 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 setRedrawId = useState(0)[1];
const info = model const info = model
.getWorksheetsProperties() .getWorksheetsProperties()
@@ -293,6 +293,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
model.setSelectedSheet(nextSheet); model.setSelectedSheet(nextSheet);
} }
}, },
onEscape: (): void => {
workbookState.clearCutRange();
setRedrawId((id) => id + 1);
},
root: rootRef, root: rootRef,
}); });
@@ -346,6 +350,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
} }
}} }}
onPaste={(event: React.ClipboardEvent) => { onPaste={(event: React.ClipboardEvent) => {
workbookState.clearCutRange();
const { items } = event.clipboardData; const { items } = event.clipboardData;
if (!items) { if (!items) {
return; return;
@@ -385,7 +390,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
} }
data.set(Number.parseInt(row, 10), rowMap); data.set(Number.parseInt(row, 10), rowMap);
} }
model.pasteFromClipboard(source.area, data); model.pasteFromClipboard(source.area, data, source.type === "cut");
setRedrawId((id) => id + 1); setRedrawId((id) => id + 1);
} else if (mimeType === "text/plain") { } else if (mimeType === "text/plain") {
const { const {
@@ -445,7 +450,50 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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);
}}
> >
<Toolbar <Toolbar
canUndo={model.canUndo()} canUndo={model.canUndo()}

View File

@@ -15,6 +15,14 @@
import type { CellStyle } from "@ironcalc/wasm"; import type { CellStyle } from "@ironcalc/wasm";
export interface CutRange {
sheet: number;
rowStart: number;
rowEnd: number;
columnStart: number;
columnEnd: number;
}
export enum AreaType { export enum AreaType {
rowsDown = 0, rowsDown = 0,
columnsRight = 1, columnsRight = 1,
@@ -83,12 +91,14 @@ export class WorkbookState {
private extendToArea: Area | null; private extendToArea: Area | null;
private copyStyles: AreaStyles | null; private copyStyles: AreaStyles | null;
private cell: EditingCell | null; private cell: EditingCell | null;
private cutRange: CutRange | null;
constructor() { constructor() {
// the extendTo area is the area we are covering // the extendTo area is the area we are covering
this.extendToArea = null; this.extendToArea = null;
this.copyStyles = null; this.copyStyles = null;
this.cell = null; this.cell = null;
this.cutRange = null;
} }
getExtendToArea(): Area | null { getExtendToArea(): Area | null {
@@ -155,4 +165,16 @@ export class WorkbookState {
} }
return ""; return "";
} }
setCutRange(range: CutRange): void {
this.cutRange = range;
}
clearCutRange(): void {
this.cutRange = null;
}
getCutRange(): CutRange | null {
return this.cutRange;
}
} }