UPDATE: Adds cut/paste
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
dad4755b16
commit
04d8c658ab
@@ -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), ©.data)
|
.paste_from_clipboard((1, 1, 2, 2), ©.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()));
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user