UPDATE: Implement copy/paste in the UI

This commit is contained in:
Nicolás Hatcher
2024-10-15 22:57:00 +02:00
committed by Nicolás Hatcher Andrés
parent 843d8beb02
commit cd54389e91
12 changed files with 648 additions and 15 deletions

112
Cargo.lock generated
View File

@@ -19,6 +19,15 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -76,6 +85,12 @@ dependencies = [
"syn",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -242,6 +257,40 @@ dependencies = [
"typenum",
]
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "csv-sniffer"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b8e952164bb270a505d6cb6136624174c34cfb9abd16e0011f5e53058317f39"
dependencies = [
"bitflags",
"csv",
"csv-core",
"memchr",
"regex 0.2.11",
]
[[package]]
name = "deranged"
version = "0.3.11"
@@ -375,10 +424,12 @@ dependencies = [
"bitcode",
"chrono",
"chrono-tz",
"csv",
"csv-sniffer",
"js-sys",
"once_cell",
"rand",
"regex",
"regex 1.10.4",
"ryu",
"serde",
"serde_json",
@@ -417,6 +468,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.153"
@@ -480,7 +537,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
dependencies = [
"regex",
"regex 1.10.4",
]
[[package]]
@@ -689,16 +746,29 @@ dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384"
dependencies = [
"aho-corasick 0.6.10",
"memchr",
"regex-syntax 0.5.6",
"thread_local",
"utf8-ranges",
]
[[package]]
name = "regex"
version = "1.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
dependencies = [
"aho-corasick",
"aho-corasick 1.1.3",
"memchr",
"regex-automata",
"regex-syntax",
"regex-syntax 0.8.3",
]
[[package]]
@@ -707,9 +777,18 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
dependencies = [
"aho-corasick",
"aho-corasick 1.1.3",
"memchr",
"regex-syntax",
"regex-syntax 0.8.3",
]
[[package]]
name = "regex-syntax"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
dependencies = [
"ucd-util",
]
[[package]]
@@ -849,6 +928,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
dependencies = [
"lazy_static",
]
[[package]]
name = "time"
version = "0.3.34"
@@ -874,6 +962,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "ucd-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
[[package]]
name = "unicode-ident"
version = "1.0.12"
@@ -886,6 +980,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
[[package]]
name = "utf8-ranges"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
[[package]]
name = "uuid"
version = "1.8.0"

View File

@@ -18,6 +18,8 @@ chrono-tz = "0.9"
regex = "1.0"
once_cell = "1.16.0"
bitcode = "0.6.0"
csv = "1.3.0"
csv-sniffer = "0.1"
[dev-dependencies]
serde_json = "1.0"

View File

@@ -58,4 +58,5 @@ pub mod mock_time;
pub use model::get_milliseconds_since_epoch;
pub use model::Model;
pub use user_model::BorderArea;
pub use user_model::ClipboardData;
pub use user_model::UserModel;

View File

@@ -11,6 +11,7 @@ mod test_keyboard_navigation;
mod test_on_area_selection;
mod test_on_expand_selected_range;
mod test_on_paste_styles;
mod test_paste_csv;
mod test_rename_sheet;
mod test_row_column;
mod test_styles;

View File

@@ -0,0 +1,114 @@
use crate::{expressions::types::Area, UserModel};
#[test]
fn csv_paste() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 7, 7, "=SUM(B4:D7)").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 7, 7), Ok("0".to_string()));
// paste some numbers in B4:C7
let csv = "1,2,3\n4,5,6";
let area = Area {
sheet: 0,
row: 4,
column: 2,
width: 1,
height: 1,
};
model.paste_csv_string(&area, csv).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string())
);
}
#[test]
fn tsv_crlf_paste() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 7, 7, "=SUM(B4:D7)").unwrap();
assert_eq!(model.get_formatted_cell_value(0, 7, 7), Ok("0".to_string()));
// paste some numbers in B4:C7
let csv = "1\t2\t3\r\n4\t5\t6";
let area = Area {
sheet: 0,
row: 4,
column: 2,
width: 1,
height: 1,
};
model.paste_csv_string(&area, csv).unwrap();
assert_eq!(
model.get_formatted_cell_value(0, 7, 7),
Ok("21".to_string())
);
}
#[test]
fn copy_paste_internal() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_user_input(0, 1, 2, "=A1*3+1").unwrap();
// set A1 bold
let range = Area {
sheet: 0,
row: 1,
column: 1,
width: 1,
height: 1,
};
model.update_range_style(&range, "font.b", "true").unwrap();
model
.set_user_input(0, 2, 1, "A season of faith, \"perfection\"")
.unwrap();
// Select A1:B2 and copy
model.set_selected_range(1, 1, 2, 2).unwrap();
let copy = model.copy_to_clipboard().unwrap();
assert_eq!(
copy.csv,
"42,127\n\"A season of faith, \"\"perfection\"\"\",\n"
);
assert_eq!(copy.range, (1, 1, 2, 2));
model.set_selected_cell(4, 4).unwrap();
// paste in cell D4 (4, 4)
model
.paste_from_clipboard((1, 1, 2, 2), &copy.data)
.unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
assert_eq!(model.get_cell_content(0, 4, 5), Ok("=D4*3+1".to_string()));
assert_eq!(
model.get_formatted_cell_value(0, 4, 5),
Ok("127".to_string())
);
// cell D4 must be bold
let style_d4 = model.get_cell_style(0, 4, 4).unwrap();
assert!(style_d4.font.b);
model.undo().unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("".to_string()));
assert_eq!(model.get_cell_content(0, 4, 5), Ok("".to_string()));
// cell D4 must not be bold
let style_d4 = model.get_cell_style(0, 4, 4).unwrap();
assert!(!style_d4.font.b);
model.redo().unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
assert_eq!(model.get_cell_content(0, 4, 5), Ok("=D4*3+1".to_string()));
assert_eq!(
model.get_formatted_cell_value(0, 4, 5),
Ok("127".to_string())
);
// cell D4 must be bold
let style_d4 = model.get_cell_style(0, 4, 4).unwrap();
assert!(style_d4.font.b);
}

View File

@@ -1,13 +1,15 @@
#![deny(missing_docs)]
use std::{collections::HashMap, fmt::Debug};
use std::{collections::HashMap, fmt::Debug, io::Cursor};
use csv::{ReaderBuilder, WriterBuilder};
use csv_sniffer::Sniffer;
use serde::{Deserialize, Serialize};
use crate::{
constants,
expressions::{
types::Area,
types::{Area, CellReferenceIndex},
utils::{is_valid_column_number, is_valid_row},
},
model::Model,
@@ -21,6 +23,23 @@ use crate::{
use crate::user_model::history::{
ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
};
/// Data for the clipboard
pub type ClipboardData = HashMap<i32, HashMap<i32, ClipboardCell>>;
pub type ClipboardTuple = (i32, i32, i32, i32);
#[derive(Serialize, Deserialize)]
pub struct ClipboardCell {
text: String,
style: Style,
}
#[derive(Serialize, Deserialize)]
pub struct Clipboard {
pub(crate) csv: String,
pub(crate) data: ClipboardData,
pub(crate) range: (i32, i32, i32, i32),
}
#[derive(Serialize, Deserialize)]
pub enum BorderType {
@@ -976,7 +995,7 @@ impl UserModel {
/// See also:
/// * [Model::get_style_for_cell]
#[inline]
pub fn get_cell_style(&mut self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
pub fn get_cell_style(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
self.model.get_style_for_cell(sheet, row, column)
}
@@ -1209,6 +1228,174 @@ impl UserModel {
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
}
/// Returns a copy of the selected area
pub fn copy_to_clipboard(&self) -> Result<Clipboard, String> {
let selected_area = self.get_selected_view();
let sheet = selected_area.sheet;
let mut wtr = WriterBuilder::new().from_writer(vec![]);
let mut data = HashMap::new();
let [row_start, column_start, row_end, column_end] = selected_area.range;
for row in row_start..=row_end {
let mut data_row = HashMap::new();
let mut text_row = Vec::new();
for column in column_start..=column_end {
let text = self.get_formatted_cell_value(sheet, row, column)?;
let content = self.get_cell_content(sheet, row, column)?;
let style = self.get_cell_style(sheet, row, column)?;
data_row.insert(
column,
ClipboardCell {
text: content,
style,
},
);
text_row.push(text);
}
wtr.write_record(text_row).unwrap();
data.insert(row, data_row);
}
let csv = String::from_utf8(wtr.into_inner().unwrap()).unwrap();
Ok(Clipboard {
csv,
data,
range: (row_start, column_start, row_end, column_end),
})
}
/// Paste text that we copied
pub fn paste_from_clipboard(
&mut self,
source_range: ClipboardTuple,
clipboard: &ClipboardData,
) -> Result<(), String> {
let mut diff_list = Vec::new();
let view = self.get_selected_view();
let (source_first_row, source_first_column, _, _) = source_range;
let sheet = view.sheet;
let [selected_row, selected_column, _, _] = view.range;
for (source_row, data_row) in clipboard {
let delta_row = source_row - source_first_row;
let target_row = selected_row + delta_row;
for (source_column, value) in data_row {
let delta_column = source_column - source_first_column;
let target_column = selected_column + delta_column;
// We are copying the value in
// (source_row, source_column) to (target_row , target_column)
// References in formulas are displaced
// remain in the copied area
let source = &CellReferenceIndex {
sheet,
column: *source_column,
row: *source_row,
};
let target = &CellReferenceIndex {
sheet,
column: target_column,
row: target_row,
};
let new_value = self
.model
.extend_copied_value(&value.text, source, target)?;
let old_value = self
.model
.workbook
.worksheet(sheet)?
.cell(target_row, target_column)
.cloned();
let old_style = self
.model
.get_style_for_cell(sheet, target_row, target_column)?;
self.model
.set_user_input(sheet, target_row, target_column, new_value.clone())?;
self.model
.set_cell_style(sheet, target_row, target_column, &value.style)?;
diff_list.push(Diff::SetCellValue {
sheet,
row: target_row,
column: target_column,
new_value,
old_value: Box::new(old_value),
});
diff_list.push(Diff::SetCellStyle {
sheet,
row: target_row,
column: target_column,
old_value: Box::new(old_style),
new_value: Box::new(value.style.clone()),
});
}
}
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
/// Paste a csv-string into the model
pub fn paste_csv_string(&mut self, area: &Area, csv: &str) -> Result<(), String> {
let mut diff_list = Vec::new();
let sheet = area.sheet;
let mut row = area.row;
// Create a sniffer with default settings
let mut sniffer = Sniffer::new();
let mut csv_reader = Cursor::new(csv);
// Sniff the CSV metadata
let metadata = sniffer
.sniff_reader(&mut csv_reader)
.map_err(|_| "Failed")?;
// Reset the cursor to the beginning after sniffing
csv_reader.set_position(0);
let mut reader = ReaderBuilder::new()
.delimiter(metadata.dialect.delimiter)
.has_headers(false)
.from_reader(csv_reader);
for record in reader.records() {
match record {
Ok(r) => {
let mut column = area.column;
for value in &r {
let old_value = self
.model
.workbook
.worksheet(sheet)?
.cell(row, column)
.cloned();
// let old_style = self.model.get_style_for_cell(sheet, row, column)?;
self.model
.set_user_input(sheet, row, column, value.to_string())?;
diff_list.push(Diff::SetCellValue {
sheet,
row,
column,
new_value: value.to_string(),
old_value: Box::new(old_value),
});
column += 1;
}
}
Err(_) => {
// skip
continue;
}
};
row += 1;
}
self.push_diff_list(diff_list);
self.evaluate_if_not_paused();
Ok(())
}
// **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) {

View File

@@ -10,3 +10,4 @@ pub use common::UserModel;
pub use ui::SelectedView;
pub use common::BorderArea;
pub use common::ClipboardData;

View File

@@ -137,6 +137,52 @@ set_area_border_types = r"""
setAreaWithBorder(area: Area, border_area: BorderArea): void;
"""
paste_csv_string = r"""
/**
* @param {any} area
* @param {string} csv
*/
pasteCsvText(area: any, csv: string): void;
"""
paste_csv_string_types = r"""
/**
* @param {Area} area
* @param {string} csv
*/
pasteCsvText(area: Area, csv: string): void;
"""
clipboard = r"""
/**
* @returns {any}
*/
copyToClipboard(): any;
"""
clipboard_types = r"""
/**
* @returns {Clipboard}
*/
copyToClipboard(): Clipboard;
"""
paste_from_clipboard = r"""
/**
* @param {any} source_range
* @param {any} clipboard
*/
pasteFromClipboard(source_range: any, clipboard: any): void;
"""
paste_from_clipboard_types = r"""
/**
* @param {[number, number, number, number]} source_range
* @param {ClipboardData} clipboard
*/
pasteFromClipboard(source_range: [number, number, number, number], clipboard: ClipboardData): void;
"""
def fix_types(text):
text = text.replace(get_tokens_str, get_tokens_str_types)
text = text.replace(update_style_str, update_style_str_types)
@@ -147,6 +193,9 @@ def fix_types(text):
text = text.replace(autofill_columns, autofill_columns_types)
text = text.replace(set_cell_style, set_cell_style_types)
text = text.replace(set_area_border, set_area_border_types)
text = text.replace(paste_csv_string, paste_csv_string_types)
text = text.replace(clipboard, clipboard_types)
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
with open("types.ts") as f:
types_str = f.read()
header_types = "{}\n\n{}".format(header, types_str)

View File

@@ -6,7 +6,7 @@ use wasm_bindgen::{
use ironcalc_base::{
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
types::{CellType, Style},
BorderArea, UserModel as BaseModel,
BorderArea, ClipboardData, UserModel as BaseModel,
};
fn to_js_error(error: String) -> JsError {
@@ -497,4 +497,37 @@ impl Model {
pub fn set_name(&mut self, name: &str) {
self.model.set_name(name);
}
#[wasm_bindgen(js_name = "copyToClipboard")]
pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
let data = self
.model
.copy_to_clipboard()
.map_err(|e| to_js_error(e.to_string()));
data.map(|x| serde_wasm_bindgen::to_value(&x).unwrap())
}
#[wasm_bindgen(js_name = "pasteFromClipboard")]
pub fn paste_from_clipboard(
&mut self,
source_range: JsValue,
clipboard: JsValue,
) -> 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)
.map_err(|e| to_js_error(e.to_string()))
}
#[wasm_bindgen(js_name = "pasteCsvText")]
pub fn paste_csv_string(&mut self, area: JsValue, csv: &str) -> Result<(), JsError> {
let range: Area =
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
self.model
.paste_csv_string(&range, csv)
.map_err(|e| to_js_error(e.to_string()))
}
}

View File

@@ -205,3 +205,23 @@ export interface SelectedView {
top_row: number;
left_column: number;
}
// type ClipboardData = {
// [row: number]: {
// [column: number]: ClipboardCell;
// };
// };
// type ClipboardData = Record<string, Record <string, ClipboardCell>>;
type ClipboardData = Map<number, Map <number, ClipboardCell>>;
export interface ClipboardCell {
text: string;
style: CellStyle;
}
export interface Clipboard {
csv: string;
data: ClipboardData;
range: [number, number, number, number];
}

View File

@@ -0,0 +1,2 @@
export const getNewClipboardId = () => new Date().toISOString();
export const CLIPBOARD_ID_SESSION_STORAGE_KEY = "IronCalc-Clipboard";

View File

@@ -1,4 +1,9 @@
import type { BorderOptions, Model, WorksheetProperties } from "@ironcalc/wasm";
import type {
BorderOptions,
ClipboardCell,
Model,
WorksheetProperties,
} from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { useCallback, useEffect, useRef, useState } from "react";
import {
@@ -6,6 +11,10 @@ import {
LAST_COLUMN,
ROW_HEIGH_SCALE,
} from "./WorksheetCanvas/constants";
import {
CLIPBOARD_ID_SESSION_STORAGE_KEY,
getNewClipboardId,
} from "./clipboard";
import FormulaBar from "./formulabar";
import Navigation from "./navigation/navigation";
import Toolbar from "./toolbar";
@@ -26,7 +35,20 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
.map(({ name, color, sheet_id }: WorksheetProperties) => {
return { name, color: color ? color : "#FFF", sheetId: sheet_id };
});
const focusWorkbook = useCallback(() => {
if (rootRef.current) {
rootRef.current.focus();
// HACK: We need to select something inside the root for onCopy to work
const selection = window.getSelection();
if (selection) {
selection.empty();
const range = new Range();
range.setStart(rootRef.current.firstChild as Node, 0);
range.setEnd(rootRef.current.firstChild as Node, 0);
selection.addRange(range);
}
}
}, []);
const onRedo = () => {
model.redo();
setRedrawId((id) => id + 1);
@@ -279,7 +301,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
return;
}
if (!workbookState.getEditingCell()) {
rootRef.current.focus();
focusWorkbook();
}
});
@@ -318,11 +340,112 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
tabIndex={0}
onClick={(event) => {
if (!workbookState.getEditingCell()) {
rootRef.current?.focus();
focusWorkbook();
} else {
event.stopPropagation();
}
}}
onPaste={(event: React.ClipboardEvent) => {
const { items } = event.clipboardData;
if (!items) {
return;
}
const mimeTypes = [
"application/json",
"text/plain",
"text/csv",
"text/html",
];
let mimeType = null;
let value = null;
for (let index = 0; index < mimeTypes.length; index += 1) {
mimeType = mimeTypes[index];
value = event.clipboardData.getData(mimeType);
if (value) {
break;
}
}
if (!mimeType || !value) {
// No clipboard data to paste
return;
}
if (mimeType === "application/json") {
// We are copying from within the application
const source = JSON.parse(value);
// const clipboardId = sessionStorage.getItem(
// CLIPBOARD_ID_SESSION_STORAGE_KEY
// );
const data: Map<number, Map<number, ClipboardCell>> = new Map();
const sheetData = source.sheetData;
for (const row of Object.keys(sheetData)) {
const dataRow = sheetData[row];
const rowMap = new Map();
for (const column of Object.keys(dataRow)) {
rowMap.set(Number.parseInt(column, 10), dataRow[column]);
}
data.set(Number.parseInt(row, 10), rowMap);
}
model.pasteFromClipboard(source.area, data);
setRedrawId((id) => id + 1);
} else if (mimeType === "text/plain") {
const {
sheet,
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
const row = Math.min(rowStart, rowEnd);
const column = Math.min(columnStart, columnEnd);
const range = {
sheet,
row,
column,
width: Math.abs(columnEnd - columnStart) + 1,
height: Math.abs(rowEnd - rowStart) + 1,
};
model.pasteCsvText(range, value);
setRedrawId((id) => id + 1);
} else {
// NOT IMPLEMENTED
}
event.preventDefault();
event.stopPropagation();
}}
onCopy={(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: "copy",
area: data.range,
sheetData,
clipboardId,
});
event.clipboardData.setData("text/plain", data.csv);
event.clipboardData.setData("application/json", clipboardJsonStr);
event.preventDefault();
event.stopPropagation();
}}
onCut={() => {}}
>
<Toolbar
canUndo={model.canUndo()}
@@ -385,7 +508,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
formulaValue={formulaValue()}
onChange={() => {
setRedrawId((id) => id + 1);
rootRef.current?.focus();
focusWorkbook();
}}
onTextUpdated={() => {
setRedrawId((id) => id + 1);