UPDATE: Implement copy/paste in the UI
This commit is contained in:
committed by
Nicolás Hatcher Andrés
parent
843d8beb02
commit
cd54389e91
112
Cargo.lock
generated
112
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
114
base/src/test/user_model/test_paste_csv.rs
Normal file
114
base/src/test/user_model/test_paste_csv.rs
Normal 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), ©.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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -10,3 +10,4 @@ pub use common::UserModel;
|
||||
pub use ui::SelectedView;
|
||||
|
||||
pub use common::BorderArea;
|
||||
pub use common::ClipboardData;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
2
webapp/src/components/clipboard.ts
Normal file
2
webapp/src/components/clipboard.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const getNewClipboardId = () => new Date().toISOString();
|
||||
export const CLIPBOARD_ID_SESSION_STORAGE_KEY = "IronCalc-Clipboard";
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user