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",
|
"cpufeatures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "0.6.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.3"
|
||||||
@@ -76,6 +85,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -242,6 +257,40 @@ dependencies = [
|
|||||||
"typenum",
|
"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]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@@ -375,10 +424,12 @@ dependencies = [
|
|||||||
"bitcode",
|
"bitcode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
|
"csv",
|
||||||
|
"csv-sniffer",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex 1.10.4",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -417,6 +468,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.153"
|
version = "0.2.153"
|
||||||
@@ -480,7 +537,7 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"regex",
|
"regex 1.10.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -689,16 +746,29 @@ dependencies = [
|
|||||||
"getrandom",
|
"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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.4"
|
version = "1.10.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 1.1.3",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata",
|
"regex-automata",
|
||||||
"regex-syntax",
|
"regex-syntax 0.8.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -707,9 +777,18 @@ version = "0.4.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick 1.1.3",
|
||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
@@ -849,6 +928,15 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -874,6 +962,12 @@ version = "1.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ucd-util"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
@@ -886,6 +980,12 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
|
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-ranges"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ chrono-tz = "0.9"
|
|||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
once_cell = "1.16.0"
|
once_cell = "1.16.0"
|
||||||
bitcode = "0.6.0"
|
bitcode = "0.6.0"
|
||||||
|
csv = "1.3.0"
|
||||||
|
csv-sniffer = "0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
@@ -58,4 +58,5 @@ pub mod mock_time;
|
|||||||
pub use model::get_milliseconds_since_epoch;
|
pub use model::get_milliseconds_since_epoch;
|
||||||
pub use model::Model;
|
pub use model::Model;
|
||||||
pub use user_model::BorderArea;
|
pub use user_model::BorderArea;
|
||||||
|
pub use user_model::ClipboardData;
|
||||||
pub use user_model::UserModel;
|
pub use user_model::UserModel;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ mod test_keyboard_navigation;
|
|||||||
mod test_on_area_selection;
|
mod test_on_area_selection;
|
||||||
mod test_on_expand_selected_range;
|
mod test_on_expand_selected_range;
|
||||||
mod test_on_paste_styles;
|
mod test_on_paste_styles;
|
||||||
|
mod test_paste_csv;
|
||||||
mod test_rename_sheet;
|
mod test_rename_sheet;
|
||||||
mod test_row_column;
|
mod test_row_column;
|
||||||
mod test_styles;
|
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)]
|
#![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 serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants,
|
constants,
|
||||||
expressions::{
|
expressions::{
|
||||||
types::Area,
|
types::{Area, CellReferenceIndex},
|
||||||
utils::{is_valid_column_number, is_valid_row},
|
utils::{is_valid_column_number, is_valid_row},
|
||||||
},
|
},
|
||||||
model::Model,
|
model::Model,
|
||||||
@@ -21,6 +23,23 @@ use crate::{
|
|||||||
use crate::user_model::history::{
|
use crate::user_model::history::{
|
||||||
ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
|
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)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum BorderType {
|
pub enum BorderType {
|
||||||
@@ -976,7 +995,7 @@ impl UserModel {
|
|||||||
/// See also:
|
/// See also:
|
||||||
/// * [Model::get_style_for_cell]
|
/// * [Model::get_style_for_cell]
|
||||||
#[inline]
|
#[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)
|
self.model.get_style_for_cell(sheet, row, column)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1209,6 +1228,174 @@ impl UserModel {
|
|||||||
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
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 ****** //
|
// **** Private methods ****** //
|
||||||
|
|
||||||
fn push_diff_list(&mut self, diff_list: DiffList) {
|
fn push_diff_list(&mut self, diff_list: DiffList) {
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ pub use common::UserModel;
|
|||||||
pub use ui::SelectedView;
|
pub use ui::SelectedView;
|
||||||
|
|
||||||
pub use common::BorderArea;
|
pub use common::BorderArea;
|
||||||
|
pub use common::ClipboardData;
|
||||||
|
|||||||
@@ -137,6 +137,52 @@ set_area_border_types = r"""
|
|||||||
setAreaWithBorder(area: Area, border_area: BorderArea): void;
|
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):
|
def fix_types(text):
|
||||||
text = text.replace(get_tokens_str, get_tokens_str_types)
|
text = text.replace(get_tokens_str, get_tokens_str_types)
|
||||||
text = text.replace(update_style_str, update_style_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(autofill_columns, autofill_columns_types)
|
||||||
text = text.replace(set_cell_style, set_cell_style_types)
|
text = text.replace(set_cell_style, set_cell_style_types)
|
||||||
text = text.replace(set_area_border, set_area_border_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:
|
with open("types.ts") as f:
|
||||||
types_str = f.read()
|
types_str = f.read()
|
||||||
header_types = "{}\n\n{}".format(header, types_str)
|
header_types = "{}\n\n{}".format(header, types_str)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use wasm_bindgen::{
|
|||||||
use ironcalc_base::{
|
use ironcalc_base::{
|
||||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
|
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
|
||||||
types::{CellType, Style},
|
types::{CellType, Style},
|
||||||
BorderArea, UserModel as BaseModel,
|
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn to_js_error(error: String) -> JsError {
|
fn to_js_error(error: String) -> JsError {
|
||||||
@@ -497,4 +497,37 @@ impl Model {
|
|||||||
pub fn set_name(&mut self, name: &str) {
|
pub fn set_name(&mut self, name: &str) {
|
||||||
self.model.set_name(name);
|
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;
|
top_row: number;
|
||||||
left_column: 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 { styled } from "@mui/material/styles";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
@@ -6,6 +11,10 @@ import {
|
|||||||
LAST_COLUMN,
|
LAST_COLUMN,
|
||||||
ROW_HEIGH_SCALE,
|
ROW_HEIGH_SCALE,
|
||||||
} from "./WorksheetCanvas/constants";
|
} from "./WorksheetCanvas/constants";
|
||||||
|
import {
|
||||||
|
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
||||||
|
getNewClipboardId,
|
||||||
|
} from "./clipboard";
|
||||||
import FormulaBar from "./formulabar";
|
import FormulaBar from "./formulabar";
|
||||||
import Navigation from "./navigation/navigation";
|
import Navigation from "./navigation/navigation";
|
||||||
import Toolbar from "./toolbar";
|
import Toolbar from "./toolbar";
|
||||||
@@ -26,7 +35,20 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
.map(({ name, color, sheet_id }: WorksheetProperties) => {
|
.map(({ name, color, sheet_id }: WorksheetProperties) => {
|
||||||
return { name, color: color ? color : "#FFF", sheetId: sheet_id };
|
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 = () => {
|
const onRedo = () => {
|
||||||
model.redo();
|
model.redo();
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
@@ -279,7 +301,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!workbookState.getEditingCell()) {
|
if (!workbookState.getEditingCell()) {
|
||||||
rootRef.current.focus();
|
focusWorkbook();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,11 +340,112 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (!workbookState.getEditingCell()) {
|
if (!workbookState.getEditingCell()) {
|
||||||
rootRef.current?.focus();
|
focusWorkbook();
|
||||||
} else {
|
} else {
|
||||||
event.stopPropagation();
|
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
|
<Toolbar
|
||||||
canUndo={model.canUndo()}
|
canUndo={model.canUndo()}
|
||||||
@@ -385,7 +508,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
formulaValue={formulaValue()}
|
formulaValue={formulaValue()}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
rootRef.current?.focus();
|
focusWorkbook();
|
||||||
}}
|
}}
|
||||||
onTextUpdated={() => {
|
onTextUpdated={() => {
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user