Compare commits
1 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
379c84f64a |
2
.github/workflows/test-coverage.yaml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Coverage
|
name: Coverage
|
||||||
|
|
||||||
on: [pull_request]
|
on: [pull_request, push]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
coverage:
|
coverage:
|
||||||
|
|||||||
@@ -58,4 +58,3 @@ 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::UserModel;
|
pub use user_model::UserModel;
|
||||||
pub use user_model::BorderArea;
|
|
||||||
|
|||||||
@@ -683,13 +683,6 @@ impl Model {
|
|||||||
Err(format!("Invalid color: {}", color))
|
Err(format!("Invalid color: {}", color))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes the grid lines in the sheet visible (`true`) or hidden (`false`)
|
|
||||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
|
||||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
|
||||||
worksheet.show_grid_lines = show_grid_lines;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
||||||
use Cell::*;
|
use Cell::*;
|
||||||
match cell {
|
match cell {
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ impl Model {
|
|||||||
color: Default::default(),
|
color: Default::default(),
|
||||||
frozen_columns: 0,
|
frozen_columns: 0,
|
||||||
frozen_rows: 0,
|
frozen_rows: 0,
|
||||||
show_grid_lines: true,
|
|
||||||
views,
|
views,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,14 +352,7 @@ impl Model {
|
|||||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||||
|
|
||||||
let mut views = HashMap::new();
|
let mut views = HashMap::new();
|
||||||
views.insert(
|
views.insert(0, WorkbookView { sheet: 0 });
|
||||||
0,
|
|
||||||
WorkbookView {
|
|
||||||
sheet: 0,
|
|
||||||
window_width: 800,
|
|
||||||
window_height: 600,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// String versions of the locale are added here to simplify the serialize/deserialize logic
|
// String versions of the locale are added here to simplify the serialize/deserialize logic
|
||||||
let workbook = Workbook {
|
let workbook = Workbook {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
mod test_add_delete_sheets;
|
mod test_add_delete_sheets;
|
||||||
mod test_autofill_columns;
|
|
||||||
mod test_autofill_rows;
|
|
||||||
mod test_clear_cells;
|
mod test_clear_cells;
|
||||||
mod test_diff_queue;
|
mod test_diff_queue;
|
||||||
mod test_evaluation;
|
mod test_evaluation;
|
||||||
mod test_general;
|
mod test_general;
|
||||||
mod test_grid_lines;
|
|
||||||
mod test_rename_sheet;
|
mod test_rename_sheet;
|
||||||
mod test_row_column;
|
mod test_row_column;
|
||||||
mod test_styles;
|
mod test_styles;
|
||||||
|
|||||||
@@ -1,404 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
|
||||||
use crate::expressions::types::Area;
|
|
||||||
use crate::test::util::new_empty_model;
|
|
||||||
use crate::UserModel;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_tests() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// This is cell A3
|
|
||||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
|
||||||
// We autofill from A3 to C3
|
|
||||||
model
|
|
||||||
.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 3,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
// B3
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 3, 2),
|
|
||||||
Ok("alpha".to_string())
|
|
||||||
);
|
|
||||||
// C3
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 3, 3),
|
|
||||||
Ok("alpha".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn one_cell_right() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
|
||||||
model
|
|
||||||
.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
// B1
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 2),
|
|
||||||
Ok("23".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_beta_gamma() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A1:B3
|
|
||||||
model.set_user_input(0, 1, 1, "Alpher").unwrap(); // A1
|
|
||||||
model.set_user_input(0, 1, 2, "Bethe").unwrap(); // B1
|
|
||||||
model.set_user_input(0, 1, 3, "Gamow").unwrap(); // C1
|
|
||||||
model.set_user_input(0, 2, 1, "=A1").unwrap(); // A2
|
|
||||||
model.set_user_input(0, 2, 2, "=B1").unwrap(); // B2
|
|
||||||
model.set_user_input(0, 2, 3, "=C1").unwrap(); // C2
|
|
||||||
|
|
||||||
// We autofill from A1:C2 to I2
|
|
||||||
model
|
|
||||||
.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 3,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
9,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// D1
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 4),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 5),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 6),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 7),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 8),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 9),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 2, 4),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 2, 5),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 2, 6),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 2, 7),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 2, 8),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 2, 9),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(model.get_cell_content(0, 2, 4), Ok("=D1".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn styles() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A1:C1
|
|
||||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
|
||||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
|
||||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
|
||||||
|
|
||||||
let b1 = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 2,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let c1 = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 3,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
model.update_range_style(&b1, "font.i", "true").unwrap();
|
|
||||||
model
|
|
||||||
.update_range_style(&c1, "fill.bg_color", "#334455")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
model
|
|
||||||
.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 3,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
9,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Check that cell E1 has B1 style
|
|
||||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
|
||||||
assert!(style.font.i);
|
|
||||||
// A6 would have the style of A3
|
|
||||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_cell_content(0, 1, 4), Ok("".to_string()));
|
|
||||||
// Check that cell A5 has A2 style
|
|
||||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
|
||||||
assert!(!style.font.i);
|
|
||||||
// A6 would have the style of A3
|
|
||||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, None);
|
|
||||||
|
|
||||||
model.redo().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 4),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
// Check that cell A5 has A2 style
|
|
||||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
|
||||||
assert!(style.font.i);
|
|
||||||
// A6 would have the style of A3
|
|
||||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn left() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A10:A12
|
|
||||||
model.set_user_input(0, 1, 10, "Alpher").unwrap();
|
|
||||||
model.set_user_input(0, 1, 11, "Bethe").unwrap();
|
|
||||||
model.set_user_input(0, 1, 12, "Gamow").unwrap();
|
|
||||||
|
|
||||||
// We fill upwards to row 5
|
|
||||||
model
|
|
||||||
.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 10,
|
|
||||||
width: 3,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 9),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 8),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 7),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn left_4() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A10:A13
|
|
||||||
model.set_user_input(0, 1, 10, "Margaret Burbidge").unwrap();
|
|
||||||
model.set_user_input(0, 1, 11, "Geoffrey Burbidge").unwrap();
|
|
||||||
model.set_user_input(0, 1, 12, "Willy Fowler").unwrap();
|
|
||||||
model.set_user_input(0, 1, 13, "Fred Hoyle").unwrap();
|
|
||||||
|
|
||||||
// We fill left to row 5
|
|
||||||
model
|
|
||||||
.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 10,
|
|
||||||
width: 4,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 9),
|
|
||||||
Ok("Fred Hoyle".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 8),
|
|
||||||
Ok("Willy Fowler".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 5),
|
|
||||||
Ok("Fred Hoyle".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn errors() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
|
|
||||||
model.set_user_input(0, 1, 4, "Margaret Burbidge").unwrap();
|
|
||||||
|
|
||||||
// Invalid sheet
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 3,
|
|
||||||
row: 1,
|
|
||||||
column: 4,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid worksheet index: '3'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// invalid column
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: -1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid column: '-1'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// invalid column
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: LAST_COLUMN - 1,
|
|
||||||
width: 10,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid column: '16392'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: LAST_ROW + 1,
|
|
||||||
column: 1,
|
|
||||||
width: 10,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid row: '1048577'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: LAST_ROW - 2,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 10,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid row: '1048583'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 5,
|
|
||||||
width: 10,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
-10,
|
|
||||||
),
|
|
||||||
Err("Invalid row: '-10'".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_parameters() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_columns(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 2,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
Err("Invalid parameters for autofill".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
|
||||||
use crate::expressions::types::Area;
|
|
||||||
use crate::test::util::new_empty_model;
|
|
||||||
use crate::UserModel;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_tests() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// This is cell A3
|
|
||||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
|
||||||
// We autofill from A3 to A5
|
|
||||||
model
|
|
||||||
.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 3,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 4, 1),
|
|
||||||
Ok("alpha".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 5, 1),
|
|
||||||
Ok("alpha".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn one_cell_down() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
|
||||||
model
|
|
||||||
.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 2, 1),
|
|
||||||
Ok("23".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alpha_beta_gamma() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A1:B3
|
|
||||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
|
||||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
|
||||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
|
||||||
model.set_user_input(0, 1, 2, "=A1").unwrap();
|
|
||||||
model.set_user_input(0, 2, 2, "=A2").unwrap();
|
|
||||||
model.set_user_input(0, 3, 2, "=A3").unwrap();
|
|
||||||
// We autofill from A1:B3 to A9
|
|
||||||
model
|
|
||||||
.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 2,
|
|
||||||
height: 3,
|
|
||||||
},
|
|
||||||
9,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 4, 1),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 5, 1),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 6, 1),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 7, 1),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 8, 1),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 9, 1),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 4, 2),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 5, 2),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 6, 2),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 7, 2),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 8, 2),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 9, 2),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(model.get_cell_content(0, 4, 2), Ok("=A4".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn styles() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A1:B3
|
|
||||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
|
||||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
|
||||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
|
||||||
|
|
||||||
let a2 = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 2,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let a3 = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 3,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
model.update_range_style(&a2, "font.i", "true").unwrap();
|
|
||||||
model
|
|
||||||
.update_range_style(&a3, "fill.bg_color", "#334455")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
model
|
|
||||||
.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 3,
|
|
||||||
},
|
|
||||||
9,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Check that cell A5 has A2 style
|
|
||||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
|
||||||
assert!(style.font.i);
|
|
||||||
// A6 would have the style of A3
|
|
||||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_cell_content(0, 4, 1), Ok("".to_string()));
|
|
||||||
// Check that cell A5 has A2 style
|
|
||||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
|
||||||
assert!(!style.font.i);
|
|
||||||
// A6 would have the style of A3
|
|
||||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, None);
|
|
||||||
|
|
||||||
model.redo().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 4, 1),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
// Check that cell A5 has A2 style
|
|
||||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
|
||||||
assert!(style.font.i);
|
|
||||||
// A6 would have the style of A3
|
|
||||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn upwards() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A10:A12
|
|
||||||
model.set_user_input(0, 10, 1, "Alpher").unwrap();
|
|
||||||
model.set_user_input(0, 11, 1, "Bethe").unwrap();
|
|
||||||
model.set_user_input(0, 12, 1, "Gamow").unwrap();
|
|
||||||
|
|
||||||
// We fill upwards to row 5
|
|
||||||
model
|
|
||||||
.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 10,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 3,
|
|
||||||
},
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 9, 1),
|
|
||||||
Ok("Gamow".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 8, 1),
|
|
||||||
Ok("Bethe".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 7, 1),
|
|
||||||
Ok("Alpher".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn upwards_4() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A10:A13
|
|
||||||
model.set_user_input(0, 10, 1, "Margaret Burbidge").unwrap();
|
|
||||||
model.set_user_input(0, 11, 1, "Geoffrey Burbidge").unwrap();
|
|
||||||
model.set_user_input(0, 12, 1, "Willy Fowler").unwrap();
|
|
||||||
model.set_user_input(0, 13, 1, "Fred Hoyle").unwrap();
|
|
||||||
|
|
||||||
// We fill upwards to row 5
|
|
||||||
model
|
|
||||||
.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 10,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 4,
|
|
||||||
},
|
|
||||||
5,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 9, 1),
|
|
||||||
Ok("Fred Hoyle".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 8, 1),
|
|
||||||
Ok("Willy Fowler".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 5, 1),
|
|
||||||
Ok("Fred Hoyle".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn errors() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// cells A10:A13
|
|
||||||
model.set_user_input(0, 4, 1, "Margaret Burbidge").unwrap();
|
|
||||||
|
|
||||||
// Invalid sheet
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 3,
|
|
||||||
row: 4,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid worksheet index: '3'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// invalid row
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: -1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid row: '-1'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// invalid row
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: LAST_ROW - 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 10,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid row: '1048584'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: LAST_COLUMN + 1,
|
|
||||||
width: 1,
|
|
||||||
height: 10,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid column: '16385'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: LAST_COLUMN - 2,
|
|
||||||
width: 10,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
),
|
|
||||||
Err("Invalid column: '16391'".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 5,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 10,
|
|
||||||
},
|
|
||||||
-10,
|
|
||||||
),
|
|
||||||
Err("Invalid row: '-10'".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_parameters() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.auto_fill_rows(
|
|
||||||
&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
Err("Invalid parameters for autofill".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::test::util::new_empty_model;
|
|
||||||
use crate::UserModel;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_tests() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.new_sheet();
|
|
||||||
|
|
||||||
// default sheet has show_grid_lines = true
|
|
||||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
|
||||||
|
|
||||||
// default new sheet has show_grid_lines = true
|
|
||||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
|
||||||
|
|
||||||
// wrong sheet number
|
|
||||||
assert_eq!(
|
|
||||||
model.get_show_grid_lines(2),
|
|
||||||
Err("Invalid sheet index".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// we can set it
|
|
||||||
model.set_show_grid_lines(1, false).unwrap();
|
|
||||||
assert_eq!(model.get_show_grid_lines(1), Ok(false));
|
|
||||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
|
||||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
|
||||||
|
|
||||||
model.redo().unwrap();
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
let mut model2 = UserModel::from_model(new_empty_model());
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model2.get_show_grid_lines(1), Ok(false));
|
|
||||||
assert_eq!(model2.get_show_grid_lines(0), Ok(true));
|
|
||||||
}
|
|
||||||
@@ -33,10 +33,6 @@ pub struct WorkbookSettings {
|
|||||||
pub struct WorkbookView {
|
pub struct WorkbookView {
|
||||||
/// The index of the currently selected sheet.
|
/// The index of the currently selected sheet.
|
||||||
pub sheet: u32,
|
pub sheet: u32,
|
||||||
/// The current width of the window
|
|
||||||
pub window_width: i64,
|
|
||||||
/// The current heigh of the window
|
|
||||||
pub window_height: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An internal representation of an IronCalc Workbook
|
/// An internal representation of an IronCalc Workbook
|
||||||
@@ -115,8 +111,6 @@ pub struct Worksheet {
|
|||||||
pub frozen_rows: i32,
|
pub frozen_rows: i32,
|
||||||
pub frozen_columns: i32,
|
pub frozen_columns: i32,
|
||||||
pub views: HashMap<u32, WorksheetView>,
|
pub views: HashMap<u32, WorksheetView>,
|
||||||
/// Whether or not to show the grid lines in the worksheet
|
|
||||||
pub show_grid_lines: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal representation of Excel's sheet_data
|
/// Internal representation of Excel's sheet_data
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::{collections::HashMap, fmt::Debug};
|
use std::{collections::HashMap, fmt::Debug};
|
||||||
|
|
||||||
|
use bitcode::{Decode, Encode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -12,35 +13,176 @@ use crate::{
|
|||||||
},
|
},
|
||||||
model::Model,
|
model::Model,
|
||||||
types::{
|
types::{
|
||||||
Alignment, BorderItem, BorderStyle, CellType, Col, HorizontalAlignment, SheetProperties,
|
Alignment, BorderItem, BorderStyle, Cell, CellType, Col, HorizontalAlignment, Row,
|
||||||
Style, VerticalAlignment,
|
SheetProperties, Style, VerticalAlignment,
|
||||||
},
|
},
|
||||||
utils::is_valid_hex_color,
|
utils::is_valid_hex_color,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::user_model::history::{
|
|
||||||
ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub enum BorderType {
|
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||||
All,
|
pub struct SelectedView {
|
||||||
Inner,
|
pub sheet: u32,
|
||||||
Outer,
|
pub row: i32,
|
||||||
Top,
|
pub column: i32,
|
||||||
Right,
|
pub range: [i32; 4],
|
||||||
Bottom,
|
pub top_row: i32,
|
||||||
Left,
|
pub left_column: i32,
|
||||||
CenterH,
|
|
||||||
CenterV,
|
|
||||||
None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is the struct for a border area
|
#[derive(Clone, Encode, Decode)]
|
||||||
#[derive(Serialize, Deserialize)]
|
struct RowData {
|
||||||
pub struct BorderArea {
|
row: Option<Row>,
|
||||||
item: BorderItem,
|
data: HashMap<i32, Cell>,
|
||||||
r#type: BorderType,
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
struct ColumnData {
|
||||||
|
column: Option<Col>,
|
||||||
|
data: HashMap<i32, Cell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
enum Diff {
|
||||||
|
// Cell diffs
|
||||||
|
SetCellValue {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
new_value: String,
|
||||||
|
old_value: Box<Option<Cell>>,
|
||||||
|
},
|
||||||
|
CellClearContents {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
old_value: Box<Option<Cell>>,
|
||||||
|
},
|
||||||
|
CellClearAll {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
old_value: Box<Option<Cell>>,
|
||||||
|
old_style: Box<Style>,
|
||||||
|
},
|
||||||
|
SetCellStyle {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
old_value: Box<Style>,
|
||||||
|
new_value: Box<Style>,
|
||||||
|
},
|
||||||
|
// Column and Row diffs
|
||||||
|
SetColumnWidth {
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
new_value: f64,
|
||||||
|
old_value: f64,
|
||||||
|
},
|
||||||
|
SetRowHeight {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
new_value: f64,
|
||||||
|
old_value: f64,
|
||||||
|
},
|
||||||
|
InsertRow {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
},
|
||||||
|
DeleteRow {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
old_data: Box<RowData>,
|
||||||
|
},
|
||||||
|
InsertColumn {
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
},
|
||||||
|
DeleteColumn {
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
old_data: Box<ColumnData>,
|
||||||
|
},
|
||||||
|
SetFrozenRowsCount {
|
||||||
|
sheet: u32,
|
||||||
|
new_value: i32,
|
||||||
|
old_value: i32,
|
||||||
|
},
|
||||||
|
SetFrozenColumnsCount {
|
||||||
|
sheet: u32,
|
||||||
|
new_value: i32,
|
||||||
|
old_value: i32,
|
||||||
|
},
|
||||||
|
DeleteSheet {
|
||||||
|
sheet: u32,
|
||||||
|
},
|
||||||
|
NewSheet {
|
||||||
|
index: u32,
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
RenameSheet {
|
||||||
|
index: u32,
|
||||||
|
old_value: String,
|
||||||
|
new_value: String,
|
||||||
|
},
|
||||||
|
SetSheetColor {
|
||||||
|
index: u32,
|
||||||
|
old_value: String,
|
||||||
|
new_value: String,
|
||||||
|
},
|
||||||
|
// FIXME: we are missing SetViewDiffs
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiffList = Vec<Diff>;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct History {
|
||||||
|
undo_stack: Vec<DiffList>,
|
||||||
|
redo_stack: Vec<DiffList>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl History {
|
||||||
|
fn push(&mut self, diff_list: DiffList) {
|
||||||
|
self.undo_stack.push(diff_list);
|
||||||
|
self.redo_stack = vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn undo(&mut self) -> Option<Vec<Diff>> {
|
||||||
|
match self.undo_stack.pop() {
|
||||||
|
Some(diff_list) => {
|
||||||
|
self.redo_stack.push(diff_list.clone());
|
||||||
|
Some(diff_list)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redo(&mut self) -> Option<Vec<Diff>> {
|
||||||
|
match self.redo_stack.pop() {
|
||||||
|
Some(diff_list) => {
|
||||||
|
self.undo_stack.push(diff_list.clone());
|
||||||
|
Some(diff_list)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.redo_stack = vec![];
|
||||||
|
self.undo_stack = vec![];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
enum DiffType {
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
struct QueueDiffs {
|
||||||
|
r#type: DiffType,
|
||||||
|
list: DiffList,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn boolean(value: &str) -> Result<bool, String> {
|
fn boolean(value: &str) -> Result<bool, String> {
|
||||||
@@ -146,7 +288,7 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
|
|||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct UserModel {
|
pub struct UserModel {
|
||||||
pub(crate) model: Model,
|
model: Model,
|
||||||
history: History,
|
history: History,
|
||||||
send_queue: Vec<QueueDiffs>,
|
send_queue: Vec<QueueDiffs>,
|
||||||
pause_evaluation: bool,
|
pause_evaluation: bool,
|
||||||
@@ -682,154 +824,6 @@ impl UserModel {
|
|||||||
self.model.set_frozen_columns(sheet, frozen_columns)
|
self.model.set_frozen_columns(sheet, frozen_columns)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paste `styles` in the selected area
|
|
||||||
pub fn on_paste_styles(&mut self, styles: &[Vec<Style>]) -> Result<(), String> {
|
|
||||||
let styles_heigh = styles.len() as i32;
|
|
||||||
let styles_width = styles[0].len() as i32;
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let range = if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
|
||||||
view.range
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the pasted area is smaller than the selected area we increase it
|
|
||||||
let [row_start, column_start, row_end, column_end] = range;
|
|
||||||
let last_row = row_end.max(row_start + styles_heigh - 1);
|
|
||||||
let last_column = column_end.max(column_start + styles_width - 1);
|
|
||||||
|
|
||||||
let mut diff_list = Vec::new();
|
|
||||||
for row in row_start..=last_row {
|
|
||||||
for column in column_start..=last_column {
|
|
||||||
let row_index = ((row - row_start) % styles_heigh) as usize;
|
|
||||||
let column_index = ((column - column_start) % styles_width) as usize;
|
|
||||||
let style = &styles[row_index][column_index];
|
|
||||||
let old_value = self.model.get_style_for_cell(sheet, row, column);
|
|
||||||
self.model.set_cell_style(sheet, row, column, style)?;
|
|
||||||
diff_list.push(Diff::SetCellStyle {
|
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
old_value: Box::new(old_value),
|
|
||||||
new_value: Box::new(style.clone()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.push_diff_list(diff_list);
|
|
||||||
|
|
||||||
// select the pasted range
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.range = [row_start, column_start, last_row, last_column];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the border
|
|
||||||
pub fn set_area_with_border(
|
|
||||||
&mut self,
|
|
||||||
range: &Area,
|
|
||||||
border_area: &BorderArea,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let sheet = range.sheet;
|
|
||||||
let mut diff_list = Vec::new();
|
|
||||||
let last_row = range.row + range.height - 1;
|
|
||||||
let last_column = range.column + range.width - 1;
|
|
||||||
for row in range.row..=last_row {
|
|
||||||
for column in range.column..=last_column {
|
|
||||||
let old_value = self.model.get_style_for_cell(sheet, row, column);
|
|
||||||
let mut style = old_value.clone();
|
|
||||||
|
|
||||||
// First remove all existing borders
|
|
||||||
style.border.top = None;
|
|
||||||
style.border.right = None;
|
|
||||||
style.border.bottom = None;
|
|
||||||
style.border.left = None;
|
|
||||||
|
|
||||||
match border_area.r#type {
|
|
||||||
BorderType::All => {
|
|
||||||
style.border.top = Some(border_area.item.clone());
|
|
||||||
style.border.right = Some(border_area.item.clone());
|
|
||||||
style.border.bottom = Some(border_area.item.clone());
|
|
||||||
style.border.left = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
BorderType::Inner => {
|
|
||||||
if row != range.row {
|
|
||||||
style.border.top = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if row != last_row {
|
|
||||||
style.border.bottom = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if column != range.column {
|
|
||||||
style.border.left = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if column != last_column {
|
|
||||||
style.border.right = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BorderType::Outer => {
|
|
||||||
if row == range.row {
|
|
||||||
style.border.top = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if row == last_row {
|
|
||||||
style.border.bottom = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if column == range.column {
|
|
||||||
style.border.left = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if column == last_column {
|
|
||||||
style.border.right = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BorderType::Top => style.border.top = Some(border_area.item.clone()),
|
|
||||||
BorderType::Right => style.border.right = Some(border_area.item.clone()),
|
|
||||||
BorderType::Bottom => style.border.bottom = Some(border_area.item.clone()),
|
|
||||||
BorderType::Left => style.border.left = Some(border_area.item.clone()),
|
|
||||||
BorderType::CenterH => {
|
|
||||||
if row != range.row {
|
|
||||||
style.border.top = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if row != last_row {
|
|
||||||
style.border.bottom = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BorderType::CenterV => {
|
|
||||||
if column != range.column {
|
|
||||||
style.border.left = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
if column != last_column {
|
|
||||||
style.border.right = Some(border_area.item.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BorderType::None => {
|
|
||||||
// noop, we already removed all the borders
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
|
||||||
diff_list.push(Diff::SetCellStyle {
|
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
old_value: Box::new(old_value),
|
|
||||||
new_value: Box::new(style),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.push_diff_list(diff_list);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the range with a cell style.
|
/// Updates the range with a cell style.
|
||||||
/// See also:
|
/// See also:
|
||||||
/// * [Model::set_cell_style]
|
/// * [Model::set_cell_style]
|
||||||
@@ -929,7 +923,7 @@ impl UserModel {
|
|||||||
column,
|
column,
|
||||||
old_value: Box::new(old_value),
|
old_value: Box::new(old_value),
|
||||||
new_value: Box::new(style),
|
new_value: Box::new(style),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.push_diff_list(diff_list);
|
self.push_diff_list(diff_list);
|
||||||
@@ -945,208 +939,6 @@ impl UserModel {
|
|||||||
Ok(self.model.get_style_for_cell(sheet, row, column))
|
Ok(self.model.get_style_for_cell(sheet, row, column))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fills the cells from `source_area` until `to_row`.
|
|
||||||
/// This simulates the user clicking on the cell outline handle and dragging it downwards (or upwards)
|
|
||||||
pub fn auto_fill_rows(&mut self, source_area: &Area, to_row: i32) -> Result<(), String> {
|
|
||||||
let mut diff_list = Vec::new();
|
|
||||||
let sheet = source_area.sheet;
|
|
||||||
let row1 = source_area.row;
|
|
||||||
let column1 = source_area.column;
|
|
||||||
let width1 = source_area.width;
|
|
||||||
let height1 = source_area.height;
|
|
||||||
|
|
||||||
// Check first all parameters are valid
|
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
|
||||||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_valid_column_number(column1) {
|
|
||||||
return Err(format!("Invalid column: '{column1}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_row(row1) {
|
|
||||||
return Err(format!("Invalid row: '{row1}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_column_number(column1 + width1 - 1) {
|
|
||||||
return Err(format!("Invalid column: '{}'", column1 + width1 - 1));
|
|
||||||
}
|
|
||||||
if !is_valid_row(row1 + height1 - 1) {
|
|
||||||
return Err(format!("Invalid row: '{}'", row1 + height1 - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_valid_row(to_row) {
|
|
||||||
return Err(format!("Invalid row: '{to_row}'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// anchor_row is the first row that repeats in each case.
|
|
||||||
let anchor_row;
|
|
||||||
let sign;
|
|
||||||
// this is the range of rows we are going to fill
|
|
||||||
let row_range: Vec<i32>;
|
|
||||||
|
|
||||||
if to_row >= row1 + height1 {
|
|
||||||
// we go downwards, we start from `row1 + height1` to `to_row`,
|
|
||||||
anchor_row = row1;
|
|
||||||
sign = 1;
|
|
||||||
row_range = (row1 + height1..to_row + 1).collect();
|
|
||||||
} else if to_row < row1 {
|
|
||||||
// we go upwards, starting from `row1 - `` all the way to `to_row`
|
|
||||||
anchor_row = row1 + height1 - 1;
|
|
||||||
sign = -1;
|
|
||||||
row_range = (to_row..row1).rev().collect();
|
|
||||||
} else {
|
|
||||||
return Err("Invalid parameters for autofill".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
for column in column1..column1 + width1 {
|
|
||||||
let mut index = 0;
|
|
||||||
for row_ref in &row_range {
|
|
||||||
// Save value and style first
|
|
||||||
let row = *row_ref;
|
|
||||||
let old_value = self
|
|
||||||
.model
|
|
||||||
.workbook
|
|
||||||
.worksheet(sheet)?
|
|
||||||
.cell(row, column)
|
|
||||||
.cloned();
|
|
||||||
let old_style = self.model.get_style_for_cell(sheet, row, column);
|
|
||||||
|
|
||||||
// compute the new value and set it
|
|
||||||
let source_row = anchor_row + index;
|
|
||||||
let target_value = self
|
|
||||||
.model
|
|
||||||
.extend_to(sheet, source_row, column, row, column)?;
|
|
||||||
self.model
|
|
||||||
.set_user_input(sheet, row, column, target_value.to_string());
|
|
||||||
|
|
||||||
// Compute the new style and set it
|
|
||||||
let new_style = self.model.get_style_for_cell(sheet, source_row, column);
|
|
||||||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
|
||||||
|
|
||||||
// Add the diffs
|
|
||||||
diff_list.push(Diff::SetCellStyle {
|
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
old_value: Box::new(old_style),
|
|
||||||
new_value: Box::new(new_style),
|
|
||||||
});
|
|
||||||
diff_list.push(Diff::SetCellValue {
|
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
new_value: target_value.to_string(),
|
|
||||||
old_value: Box::new(old_value),
|
|
||||||
});
|
|
||||||
|
|
||||||
index = (index + sign) % height1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.push_diff_list(diff_list);
|
|
||||||
self.evaluate();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fills the cells from `source_area` until `to_column`.
|
|
||||||
/// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left)
|
|
||||||
pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> {
|
|
||||||
let mut diff_list = Vec::new();
|
|
||||||
let sheet = source_area.sheet;
|
|
||||||
let row1 = source_area.row;
|
|
||||||
let column1 = source_area.column;
|
|
||||||
let width1 = source_area.width;
|
|
||||||
let height1 = source_area.height;
|
|
||||||
|
|
||||||
// Check first all parameters are valid
|
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
|
||||||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_valid_column_number(column1) {
|
|
||||||
return Err(format!("Invalid column: '{column1}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_row(row1) {
|
|
||||||
return Err(format!("Invalid row: '{row1}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_column_number(column1 + width1 - 1) {
|
|
||||||
return Err(format!("Invalid column: '{}'", column1 + width1 - 1));
|
|
||||||
}
|
|
||||||
if !is_valid_row(row1 + height1 - 1) {
|
|
||||||
return Err(format!("Invalid row: '{}'", row1 + height1 - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_valid_row(to_column) {
|
|
||||||
return Err(format!("Invalid row: '{to_column}'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// anchor_column is the first column that repeats in each case.
|
|
||||||
let anchor_column;
|
|
||||||
let sign;
|
|
||||||
// this is the range of columns we are going to fill
|
|
||||||
let column_range: Vec<i32>;
|
|
||||||
|
|
||||||
if to_column >= column1 + width1 {
|
|
||||||
// we go right, we start from `1 + width` to `to_column`,
|
|
||||||
anchor_column = column1;
|
|
||||||
sign = 1;
|
|
||||||
column_range = (column1 + width1..to_column + 1).collect();
|
|
||||||
} else if to_column < column1 {
|
|
||||||
// we go left, starting from `column1 - `` all the way to `to_column`
|
|
||||||
anchor_column = column1 + width1 - 1;
|
|
||||||
sign = -1;
|
|
||||||
column_range = (to_column..column1).rev().collect();
|
|
||||||
} else {
|
|
||||||
return Err("Invalid parameters for autofill".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
for row in row1..row1 + height1 {
|
|
||||||
let mut index = 0;
|
|
||||||
for column_ref in &column_range {
|
|
||||||
let column = *column_ref;
|
|
||||||
// Save value and style first
|
|
||||||
let old_value = self
|
|
||||||
.model
|
|
||||||
.workbook
|
|
||||||
.worksheet(sheet)?
|
|
||||||
.cell(row, column)
|
|
||||||
.cloned();
|
|
||||||
let old_style = self.model.get_style_for_cell(sheet, row, column);
|
|
||||||
|
|
||||||
// compute the new value and set it
|
|
||||||
let source_column = anchor_column + index;
|
|
||||||
let target_value = self
|
|
||||||
.model
|
|
||||||
.extend_to(sheet, row, source_column, row, column)?;
|
|
||||||
self.model
|
|
||||||
.set_user_input(sheet, row, column, target_value.to_string());
|
|
||||||
|
|
||||||
// Compute the new style and set it
|
|
||||||
let new_style = self.model.get_style_for_cell(sheet, row, source_column);
|
|
||||||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
|
||||||
|
|
||||||
// Add the diffs
|
|
||||||
diff_list.push(Diff::SetCellStyle {
|
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
old_value: Box::new(old_style),
|
|
||||||
new_value: Box::new(new_style),
|
|
||||||
});
|
|
||||||
diff_list.push(Diff::SetCellValue {
|
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
new_value: target_value.to_string(),
|
|
||||||
old_value: Box::new(old_value),
|
|
||||||
});
|
|
||||||
|
|
||||||
index = (index + sign) % width1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.push_diff_list(diff_list);
|
|
||||||
self.evaluate();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns information about the sheets
|
/// Returns information about the sheets
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@@ -1156,24 +948,165 @@ impl UserModel {
|
|||||||
self.model.get_worksheets_properties()
|
self.model.get_worksheets_properties()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the gid lines in the worksheet to visible (`true`) or hidden (`false`)
|
/// Returns the selected sheet index
|
||||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
pub fn get_selected_sheet(&self) -> u32 {
|
||||||
let old_value = self.model.workbook.worksheet(sheet)?.show_grid_lines;
|
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||||
self.model.set_show_grid_lines(sheet, show_grid_lines)?;
|
view.sheet
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.push_diff_list(vec![Diff::SetShowGridLines {
|
/// Returns the selected cell
|
||||||
sheet,
|
pub fn get_selected_cell(&self) -> (u32, i32, i32) {
|
||||||
new_value: show_grid_lines,
|
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||||
old_value,
|
view.sheet
|
||||||
}]);
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||||
|
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||||
|
return (sheet, view.row, view.column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return a safe default
|
||||||
|
(0, 1, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns selected view
|
||||||
|
pub fn get_selected_view(&self) -> SelectedView {
|
||||||
|
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||||
|
view.sheet
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||||
|
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||||
|
return SelectedView {
|
||||||
|
sheet,
|
||||||
|
row: view.row,
|
||||||
|
column: view.column,
|
||||||
|
range: view.range,
|
||||||
|
top_row: view.top_row,
|
||||||
|
left_column: view.left_column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return a safe default
|
||||||
|
SelectedView {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
range: [1, 1, 1, 1],
|
||||||
|
top_row: 1,
|
||||||
|
left_column: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the the selected sheet
|
||||||
|
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||||||
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
|
}
|
||||||
|
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
||||||
|
view.sheet = sheet;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true in the grid lines for
|
/// Sets the selected cell
|
||||||
pub fn get_show_grid_lines(&self, sheet: u32) -> Result<bool, String> {
|
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
|
||||||
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||||
|
view.sheet
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if !is_valid_column_number(column) {
|
||||||
|
return Err(format!("Invalid column: '{column}'"));
|
||||||
|
}
|
||||||
|
if !is_valid_column_number(row) {
|
||||||
|
return Err(format!("Invalid row: '{row}'"));
|
||||||
|
}
|
||||||
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
|
}
|
||||||
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
|
view.row = row;
|
||||||
|
view.column = column;
|
||||||
|
view.range = [row, column, row, column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the selected range
|
||||||
|
pub fn set_selected_range(
|
||||||
|
&mut self,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
end_row: i32,
|
||||||
|
end_column: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||||
|
view.sheet
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_valid_column_number(start_column) {
|
||||||
|
return Err(format!("Invalid column: '{start_column}'"));
|
||||||
|
}
|
||||||
|
if !is_valid_column_number(start_row) {
|
||||||
|
return Err(format!("Invalid row: '{start_row}'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_valid_column_number(end_column) {
|
||||||
|
return Err(format!("Invalid column: '{end_column}'"));
|
||||||
|
}
|
||||||
|
if !is_valid_column_number(end_row) {
|
||||||
|
return Err(format!("Invalid row: '{end_row}'"));
|
||||||
|
}
|
||||||
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
|
}
|
||||||
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
|
view.range = [start_row, start_column, end_row, end_column];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value of the first visible cell
|
||||||
|
pub fn set_top_left_visible_cell(
|
||||||
|
&mut self,
|
||||||
|
top_row: i32,
|
||||||
|
left_column: i32,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||||
|
view.sheet
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
if !is_valid_column_number(left_column) {
|
||||||
|
return Err(format!("Invalid column: '{left_column}'"));
|
||||||
|
}
|
||||||
|
if !is_valid_column_number(top_row) {
|
||||||
|
return Err(format!("Invalid row: '{top_row}'"));
|
||||||
|
}
|
||||||
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
|
}
|
||||||
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
|
view.top_row = top_row;
|
||||||
|
view.left_column = left_column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
// **** Private methods ****** //
|
// **** Private methods ****** //
|
||||||
|
|
||||||
fn push_diff_list(&mut self, diff_list: DiffList) {
|
fn push_diff_list(&mut self, diff_list: DiffList) {
|
||||||
@@ -1337,13 +1270,6 @@ impl UserModel {
|
|||||||
} => {
|
} => {
|
||||||
self.model.set_sheet_color(*index, old_value)?;
|
self.model.set_sheet_color(*index, old_value)?;
|
||||||
}
|
}
|
||||||
Diff::SetShowGridLines {
|
|
||||||
sheet,
|
|
||||||
old_value,
|
|
||||||
new_value: _,
|
|
||||||
} => {
|
|
||||||
self.model.set_show_grid_lines(*sheet, *old_value)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if needs_evaluation {
|
if needs_evaluation {
|
||||||
@@ -1464,13 +1390,6 @@ impl UserModel {
|
|||||||
} => {
|
} => {
|
||||||
self.model.set_sheet_color(*index, new_value)?;
|
self.model.set_sheet_color(*index, new_value)?;
|
||||||
}
|
}
|
||||||
Diff::SetShowGridLines {
|
|
||||||
sheet,
|
|
||||||
old_value: _,
|
|
||||||
new_value,
|
|
||||||
} => {
|
|
||||||
self.model.set_show_grid_lines(*sheet, *new_value)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1485,7 +1404,7 @@ impl UserModel {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{HorizontalAlignment, VerticalAlignment},
|
types::{HorizontalAlignment, VerticalAlignment},
|
||||||
user_model::common::{horizontal, vertical},
|
user_model::{horizontal, vertical},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use bitcode::{Decode, Encode};
|
|
||||||
|
|
||||||
use crate::types::{Cell, Col, Row, Style};
|
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
|
||||||
pub(crate) struct RowData {
|
|
||||||
pub(crate) row: Option<Row>,
|
|
||||||
pub(crate) data: HashMap<i32, Cell>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
|
||||||
pub(crate) struct ColumnData {
|
|
||||||
pub(crate) column: Option<Col>,
|
|
||||||
pub(crate) data: HashMap<i32, Cell>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
|
||||||
pub(crate) enum Diff {
|
|
||||||
// Cell diffs
|
|
||||||
SetCellValue {
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
new_value: String,
|
|
||||||
old_value: Box<Option<Cell>>,
|
|
||||||
},
|
|
||||||
CellClearContents {
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
old_value: Box<Option<Cell>>,
|
|
||||||
},
|
|
||||||
CellClearAll {
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
old_value: Box<Option<Cell>>,
|
|
||||||
old_style: Box<Style>,
|
|
||||||
},
|
|
||||||
SetCellStyle {
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
old_value: Box<Style>,
|
|
||||||
new_value: Box<Style>,
|
|
||||||
},
|
|
||||||
// Column and Row diffs
|
|
||||||
SetColumnWidth {
|
|
||||||
sheet: u32,
|
|
||||||
column: i32,
|
|
||||||
new_value: f64,
|
|
||||||
old_value: f64,
|
|
||||||
},
|
|
||||||
SetRowHeight {
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
new_value: f64,
|
|
||||||
old_value: f64,
|
|
||||||
},
|
|
||||||
InsertRow {
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
},
|
|
||||||
DeleteRow {
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
old_data: Box<RowData>,
|
|
||||||
},
|
|
||||||
InsertColumn {
|
|
||||||
sheet: u32,
|
|
||||||
column: i32,
|
|
||||||
},
|
|
||||||
DeleteColumn {
|
|
||||||
sheet: u32,
|
|
||||||
column: i32,
|
|
||||||
old_data: Box<ColumnData>,
|
|
||||||
},
|
|
||||||
SetFrozenRowsCount {
|
|
||||||
sheet: u32,
|
|
||||||
new_value: i32,
|
|
||||||
old_value: i32,
|
|
||||||
},
|
|
||||||
SetFrozenColumnsCount {
|
|
||||||
sheet: u32,
|
|
||||||
new_value: i32,
|
|
||||||
old_value: i32,
|
|
||||||
},
|
|
||||||
DeleteSheet {
|
|
||||||
sheet: u32,
|
|
||||||
},
|
|
||||||
NewSheet {
|
|
||||||
index: u32,
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
RenameSheet {
|
|
||||||
index: u32,
|
|
||||||
old_value: String,
|
|
||||||
new_value: String,
|
|
||||||
},
|
|
||||||
SetSheetColor {
|
|
||||||
index: u32,
|
|
||||||
old_value: String,
|
|
||||||
new_value: String,
|
|
||||||
},
|
|
||||||
SetShowGridLines {
|
|
||||||
sheet: u32,
|
|
||||||
old_value: bool,
|
|
||||||
new_value: bool,
|
|
||||||
}, // FIXME: we are missing SetViewDiffs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) type DiffList = Vec<Diff>;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct History {
|
|
||||||
pub(crate) undo_stack: Vec<DiffList>,
|
|
||||||
pub(crate) redo_stack: Vec<DiffList>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl History {
|
|
||||||
pub fn push(&mut self, diff_list: DiffList) {
|
|
||||||
self.undo_stack.push(diff_list);
|
|
||||||
self.redo_stack = vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn undo(&mut self) -> Option<Vec<Diff>> {
|
|
||||||
match self.undo_stack.pop() {
|
|
||||||
Some(diff_list) => {
|
|
||||||
self.redo_stack.push(diff_list.clone());
|
|
||||||
Some(diff_list)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn redo(&mut self) -> Option<Vec<Diff>> {
|
|
||||||
match self.redo_stack.pop() {
|
|
||||||
Some(diff_list) => {
|
|
||||||
self.undo_stack.push(diff_list.clone());
|
|
||||||
Some(diff_list)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.redo_stack = vec![];
|
|
||||||
self.undo_stack = vec![];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
|
||||||
pub enum DiffType {
|
|
||||||
Undo,
|
|
||||||
Redo,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
|
||||||
pub struct QueueDiffs {
|
|
||||||
pub r#type: DiffType,
|
|
||||||
pub list: DiffList,
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#![deny(missing_docs)]
|
|
||||||
|
|
||||||
mod common;
|
|
||||||
mod history;
|
|
||||||
mod ui;
|
|
||||||
|
|
||||||
pub use common::UserModel;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub use ui::SelectedView;
|
|
||||||
|
|
||||||
pub use common::BorderArea;
|
|
||||||
@@ -1,671 +0,0 @@
|
|||||||
#![deny(missing_docs)]
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::expressions::utils::{is_valid_column_number, is_valid_row};
|
|
||||||
|
|
||||||
use super::common::UserModel;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
|
||||||
pub struct SelectedView {
|
|
||||||
pub sheet: u32,
|
|
||||||
pub row: i32,
|
|
||||||
pub column: i32,
|
|
||||||
pub range: [i32; 4],
|
|
||||||
pub top_row: i32,
|
|
||||||
pub left_column: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserModel {
|
|
||||||
/// Returns the selected sheet index
|
|
||||||
pub fn get_selected_sheet(&self) -> u32 {
|
|
||||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the selected cell
|
|
||||||
pub fn get_selected_cell(&self) -> (u32, i32, i32) {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
|
||||||
return (sheet, view.row, view.column);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return a safe default
|
|
||||||
(0, 1, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns selected view
|
|
||||||
pub fn get_selected_view(&self) -> SelectedView {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
|
||||||
return SelectedView {
|
|
||||||
sheet,
|
|
||||||
row: view.row,
|
|
||||||
column: view.column,
|
|
||||||
range: view.range,
|
|
||||||
top_row: view.top_row,
|
|
||||||
left_column: view.left_column,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return a safe default
|
|
||||||
SelectedView {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
range: [1, 1, 1, 1],
|
|
||||||
top_row: 1,
|
|
||||||
left_column: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the the selected sheet
|
|
||||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
|
||||||
}
|
|
||||||
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
|
||||||
view.sheet = sheet;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the selected cell
|
|
||||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
if !is_valid_column_number(column) {
|
|
||||||
return Err(format!("Invalid column: '{column}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_row(row) {
|
|
||||||
return Err(format!("Invalid row: '{row}'"));
|
|
||||||
}
|
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
|
||||||
}
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
|
||||||
view.row = row;
|
|
||||||
view.column = column;
|
|
||||||
view.range = [row, column, row, column];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the selected range
|
|
||||||
pub fn set_selected_range(
|
|
||||||
&mut self,
|
|
||||||
start_row: i32,
|
|
||||||
start_column: i32,
|
|
||||||
end_row: i32,
|
|
||||||
end_column: i32,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_valid_column_number(start_column) {
|
|
||||||
return Err(format!("Invalid column: '{start_column}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_row(start_row) {
|
|
||||||
return Err(format!("Invalid row: '{start_row}'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_valid_column_number(end_column) {
|
|
||||||
return Err(format!("Invalid column: '{end_column}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_row(end_row) {
|
|
||||||
return Err(format!("Invalid row: '{end_row}'"));
|
|
||||||
}
|
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
|
||||||
}
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
|
||||||
view.range = [start_row, start_column, end_row, end_column];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The selected range is expanded with the keyboard
|
|
||||||
pub fn on_expand_selected_range(&mut self, key: &str) -> Result<(), String> {
|
|
||||||
let (sheet, window_width, window_height) =
|
|
||||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
(
|
|
||||||
view.sheet,
|
|
||||||
view.window_width as f64,
|
|
||||||
view.window_height as f64,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let (selected_row, selected_column, range, top_row, left_column) =
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
|
||||||
(
|
|
||||||
view.row,
|
|
||||||
view.column,
|
|
||||||
view.range,
|
|
||||||
view.top_row,
|
|
||||||
view.left_column,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let [row_start, column_start, row_end, column_end] = range;
|
|
||||||
|
|
||||||
match key {
|
|
||||||
"ArrowRight" => {
|
|
||||||
if selected_column > column_start {
|
|
||||||
let new_column = column_start + 1;
|
|
||||||
if !(is_valid_column_number(new_column)) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
self.set_selected_range(row_start, new_column, row_end, column_end)?;
|
|
||||||
} else {
|
|
||||||
let new_column = column_end + 1;
|
|
||||||
if !is_valid_column_number(new_column) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// if the column is not fully visible we 'scroll' right until it is
|
|
||||||
let mut width = 0.0;
|
|
||||||
let mut c = left_column;
|
|
||||||
while c <= new_column {
|
|
||||||
width += self.model.get_column_width(sheet, c)?;
|
|
||||||
c += 1;
|
|
||||||
}
|
|
||||||
if width > window_width {
|
|
||||||
self.set_top_left_visible_cell(top_row, left_column + 1)?;
|
|
||||||
}
|
|
||||||
self.set_selected_range(row_start, column_start, row_end, column_end + 1)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"ArrowLeft" => {
|
|
||||||
if selected_column < column_end {
|
|
||||||
let new_column = column_end - 1;
|
|
||||||
if !is_valid_column_number(new_column) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if new_column < left_column {
|
|
||||||
self.set_top_left_visible_cell(top_row, new_column)?;
|
|
||||||
}
|
|
||||||
self.set_selected_range(row_start, column_start, row_end, new_column)?;
|
|
||||||
} else {
|
|
||||||
let new_column = column_start - 1;
|
|
||||||
if !is_valid_column_number(new_column) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if new_column < left_column {
|
|
||||||
self.set_top_left_visible_cell(top_row, new_column)?;
|
|
||||||
}
|
|
||||||
self.set_selected_range(row_start, new_column, row_end, column_end)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"ArrowUp" => {
|
|
||||||
if selected_row < row_end {
|
|
||||||
let new_row = row_end - 1;
|
|
||||||
if !is_valid_row(new_row) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
self.set_selected_range(row_start, column_start, new_row, column_end)?;
|
|
||||||
} else {
|
|
||||||
let new_row = row_start - 1;
|
|
||||||
if !is_valid_row(new_row) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if new_row < top_row {
|
|
||||||
self.set_top_left_visible_cell(new_row, left_column)?;
|
|
||||||
}
|
|
||||||
self.set_selected_range(new_row, column_start, row_end, column_end)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"ArrowDown" => {
|
|
||||||
if selected_row > row_start {
|
|
||||||
let new_row = row_start + 1;
|
|
||||||
if !is_valid_row(new_row) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
self.set_selected_range(new_row, column_start, row_end, column_end)?;
|
|
||||||
} else {
|
|
||||||
let new_row = row_end + 1;
|
|
||||||
if !is_valid_row(new_row) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let mut height = 0.0;
|
|
||||||
let mut r = top_row;
|
|
||||||
while r <= new_row + 1 {
|
|
||||||
height += self.model.get_row_height(sheet, r)?;
|
|
||||||
r += 1;
|
|
||||||
}
|
|
||||||
if height >= window_height {
|
|
||||||
self.set_top_left_visible_cell(top_row + 1, left_column)?;
|
|
||||||
}
|
|
||||||
self.set_selected_range(row_start, column_start, new_row, column_end)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the value of the first visible cell
|
|
||||||
pub fn set_top_left_visible_cell(
|
|
||||||
&mut self,
|
|
||||||
top_row: i32,
|
|
||||||
left_column: i32,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_valid_column_number(left_column) {
|
|
||||||
return Err(format!("Invalid column: '{left_column}'"));
|
|
||||||
}
|
|
||||||
if !is_valid_row(top_row) {
|
|
||||||
return Err(format!("Invalid row: '{top_row}'"));
|
|
||||||
}
|
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
|
||||||
return Err(format!("Invalid worksheet index {}", sheet));
|
|
||||||
}
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
|
||||||
view.top_row = top_row;
|
|
||||||
view.left_column = left_column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the width of the window
|
|
||||||
pub fn set_window_width(&mut self, window_width: f64) {
|
|
||||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
|
||||||
view.window_width = window_width as i64;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the width of the window
|
|
||||||
pub fn get_window_width(&mut self) -> Result<i64, String> {
|
|
||||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
|
||||||
return Ok(view.window_width);
|
|
||||||
};
|
|
||||||
Err("View not found".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the height of the window
|
|
||||||
pub fn set_window_height(&mut self, window_height: f64) {
|
|
||||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
|
||||||
view.window_height = window_height as i64;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the height of the window
|
|
||||||
pub fn get_window_height(&mut self) -> Result<i64, String> {
|
|
||||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
|
||||||
return Ok(view.window_height);
|
|
||||||
};
|
|
||||||
Err("View not found".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User presses right arrow
|
|
||||||
pub fn on_arrow_right(&mut self) -> Result<(), String> {
|
|
||||||
let (sheet, window_width) =
|
|
||||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
(view.sheet, view.window_width)
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
let new_column = view.column + 1;
|
|
||||||
if !is_valid_column_number(new_column) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// if the column is not fully visible we 'scroll' right until it is
|
|
||||||
let mut width = 0.0;
|
|
||||||
let mut column = view.left_column;
|
|
||||||
while column <= new_column {
|
|
||||||
width += self.model.get_column_width(sheet, column)?;
|
|
||||||
column += 1;
|
|
||||||
}
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.column = new_column;
|
|
||||||
view.range = [view.row, new_column, view.row, new_column];
|
|
||||||
if width > window_width as f64 {
|
|
||||||
view.left_column += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User presses left arrow
|
|
||||||
pub fn on_arrow_left(&mut self) -> Result<(), String> {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
let new_column = view.column - 1;
|
|
||||||
if !is_valid_column_number(new_column) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// if the column is not fully visible we 'scroll' right until it is
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.column = new_column;
|
|
||||||
view.range = [view.row, new_column, view.row, new_column];
|
|
||||||
if new_column < view.left_column {
|
|
||||||
view.left_column = new_column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User presses up arrow key
|
|
||||||
pub fn on_arrow_up(&mut self) -> Result<(), String> {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
let new_row = view.row - 1;
|
|
||||||
if !is_valid_row(new_row) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// if the column is not fully visible we 'scroll' right until it is
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.row = new_row;
|
|
||||||
view.range = [new_row, view.column, new_row, view.column];
|
|
||||||
if new_row < view.top_row {
|
|
||||||
view.top_row = new_row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User presses down arrow key
|
|
||||||
pub fn on_arrow_down(&mut self) -> Result<(), String> {
|
|
||||||
let (sheet, window_height) =
|
|
||||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
(view.sheet, view.window_height)
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
let new_row = view.row + 1;
|
|
||||||
if !is_valid_row(new_row) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
// if the row is not fully visible we 'scroll' down until it is
|
|
||||||
let mut height = 0.0;
|
|
||||||
let mut row = view.top_row;
|
|
||||||
while row <= new_row + 1 {
|
|
||||||
height += self.model.get_row_height(sheet, row)?;
|
|
||||||
row += 1;
|
|
||||||
}
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.row = new_row;
|
|
||||||
view.range = [new_row, view.column, new_row, view.column];
|
|
||||||
if height > window_height as f64 {
|
|
||||||
view.top_row += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This function should be memoized
|
|
||||||
/// Returns the x-coordinate of the cell in the top left corner
|
|
||||||
pub fn get_scroll_x(&self) -> Result<f64, String> {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
let mut scroll_x = 0.0;
|
|
||||||
for column in 1..view.left_column {
|
|
||||||
scroll_x += self.model.get_column_width(sheet, column)?;
|
|
||||||
}
|
|
||||||
Ok(scroll_x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: This function should be memoized
|
|
||||||
/// Returns the y-coordinate of the cell in the top left corner
|
|
||||||
pub fn get_scroll_y(&self) -> Result<f64, String> {
|
|
||||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
view.sheet
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
let mut scroll_y = 0.0;
|
|
||||||
for row in 1..view.top_row {
|
|
||||||
scroll_y += self.model.get_row_height(sheet, row)?;
|
|
||||||
}
|
|
||||||
Ok(scroll_y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// User presses page down
|
|
||||||
pub fn on_page_down(&mut self) -> Result<(), String> {
|
|
||||||
let (sheet, window_height) =
|
|
||||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
(view.sheet, view.window_height)
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut height = 0.0;
|
|
||||||
let mut last_row = view.top_row;
|
|
||||||
while height <= window_height as f64 {
|
|
||||||
height += self.model.get_row_height(sheet, last_row)?;
|
|
||||||
last_row += 1;
|
|
||||||
}
|
|
||||||
if !is_valid_row(last_row) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let row_delta = view.row - view.top_row;
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.top_row = last_row;
|
|
||||||
view.row = view.top_row + row_delta;
|
|
||||||
view.range = [view.row, view.column, view.row, view.column];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// On page up
|
|
||||||
pub fn on_page_up(&mut self) -> Result<(), String> {
|
|
||||||
let (sheet, window_height) =
|
|
||||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
(view.sheet, view.window_height)
|
|
||||||
} else {
|
|
||||||
return Err("View not found".to_string());
|
|
||||||
};
|
|
||||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return Err("Worksheet not found".to_string()),
|
|
||||||
};
|
|
||||||
let view = match worksheet.views.get(&self.model.view_id) {
|
|
||||||
Some(s) => s,
|
|
||||||
None => return Err("View not found".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut height = 0.0;
|
|
||||||
let mut last_row = view.top_row;
|
|
||||||
while height <= window_height as f64 && last_row > 1 {
|
|
||||||
height += self.model.get_row_height(sheet, last_row)?;
|
|
||||||
last_row -= 1;
|
|
||||||
}
|
|
||||||
let row_delta = view.row - view.top_row;
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.top_row = last_row;
|
|
||||||
view.row = view.top_row + row_delta;
|
|
||||||
view.range = [view.row, view.column, view.row, view.column];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// We extend the selection to cell (target_row, target_column)
|
|
||||||
pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<(), String> {
|
|
||||||
let (sheet, window_width, window_height) =
|
|
||||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
|
||||||
(
|
|
||||||
view.sheet,
|
|
||||||
view.window_width as f64,
|
|
||||||
view.window_height as f64,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let (selected_row, selected_column, range, top_row, left_column) =
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
|
||||||
(
|
|
||||||
view.row,
|
|
||||||
view.column,
|
|
||||||
view.range,
|
|
||||||
view.top_row,
|
|
||||||
view.left_column,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let [row_start, column_start, _row_end, _column_end] = range;
|
|
||||||
|
|
||||||
let mut new_left_column = left_column;
|
|
||||||
if target_column >= selected_column {
|
|
||||||
let mut width = 0.0;
|
|
||||||
let mut column = left_column;
|
|
||||||
while column <= target_column {
|
|
||||||
width += self.model.get_column_width(sheet, column)?;
|
|
||||||
column += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
while width > window_width {
|
|
||||||
width -= self.model.get_column_width(sheet, new_left_column)?;
|
|
||||||
new_left_column += 1;
|
|
||||||
}
|
|
||||||
} else if target_column < new_left_column {
|
|
||||||
new_left_column = target_column;
|
|
||||||
}
|
|
||||||
let mut new_top_row = top_row;
|
|
||||||
if target_row >= selected_row {
|
|
||||||
let mut height = 0.0;
|
|
||||||
let mut row = top_row;
|
|
||||||
while row <= target_row {
|
|
||||||
height += self.model.get_row_height(sheet, row)?;
|
|
||||||
row += 1;
|
|
||||||
}
|
|
||||||
while height > window_height {
|
|
||||||
height -= self.model.get_row_height(sheet, new_top_row)?;
|
|
||||||
new_top_row += 1;
|
|
||||||
}
|
|
||||||
} else if target_row < new_top_row {
|
|
||||||
new_top_row = target_row;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
|
||||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
|
||||||
view.range = [row_start, column_start, target_row, target_column];
|
|
||||||
if new_top_row != top_row {
|
|
||||||
view.top_row = new_top_row;
|
|
||||||
}
|
|
||||||
if new_left_column != left_column {
|
|
||||||
view.left_column = new_left_column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -63,97 +63,15 @@ style_types = r"""
|
|||||||
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
view = r"""
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
getSelectedView(): any;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
view_types = r"""
|
|
||||||
* @returns {CellStyle}
|
|
||||||
*/
|
|
||||||
getSelectedView(): SelectedView;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
autofill_rows = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} source_area
|
|
||||||
* @param {number} to_row
|
|
||||||
*/
|
|
||||||
autoFillRows(source_area: any, to_row: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
autofill_rows_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} source_area
|
|
||||||
* @param {number} to_row
|
|
||||||
*/
|
|
||||||
autoFillRows(source_area: Area, to_row: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
autofill_columns = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} source_area
|
|
||||||
* @param {number} to_column
|
|
||||||
*/
|
|
||||||
autoFillColumns(source_area: any, to_column: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
autofill_columns_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} source_area
|
|
||||||
* @param {number} to_column
|
|
||||||
*/
|
|
||||||
autoFillColumns(source_area: Area, to_column: number): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_cell_style = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} styles
|
|
||||||
*/
|
|
||||||
onPasteStyles(styles: any): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_cell_style_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {CellStyle[][]} styles
|
|
||||||
*/
|
|
||||||
onPasteStyles(styles: CellStyle[][]): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_area_border = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} area
|
|
||||||
* @param {any} border_area
|
|
||||||
*/
|
|
||||||
setAreaWithBorder(area: any, border_area: any): void;
|
|
||||||
"""
|
|
||||||
|
|
||||||
set_area_border_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} area
|
|
||||||
* @param {BorderArea} border_area
|
|
||||||
*/
|
|
||||||
setAreaWithBorder(area: Area, border_area: BorderArea): 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)
|
||||||
text = text.replace(properties, properties_types)
|
text = text.replace(properties, properties_types)
|
||||||
text = text.replace(style, style_types)
|
text = text.replace(style, style_types)
|
||||||
text = text.replace(view, view_types)
|
|
||||||
text = text.replace(autofill_rows, autofill_rows_types)
|
|
||||||
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)
|
|
||||||
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)
|
||||||
text = text.replace(header, header_types)
|
text = text.replace(header, header_types)
|
||||||
if text.find("any") != -1:
|
|
||||||
print("There are 'unfixed' types. Please check.")
|
|
||||||
exit(1)
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use wasm_bindgen::{
|
|||||||
|
|
||||||
use ironcalc_base::{
|
use ironcalc_base::{
|
||||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
||||||
types::{CellType, Style},
|
types::CellType,
|
||||||
BorderArea, UserModel as BaseModel,
|
UserModel as BaseModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn to_js_error(error: String) -> JsError {
|
fn to_js_error(error: String) -> JsError {
|
||||||
@@ -102,13 +102,6 @@ impl Model {
|
|||||||
self.model.rename_sheet(sheet, name).map_err(to_js_error)
|
self.model.rename_sheet(sheet, name).map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setSheetColor")]
|
|
||||||
pub fn set_sheet_color(&mut self, sheet: u32, color: &str) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.set_sheet_color(sheet, color)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "rangeClearAll")]
|
#[wasm_bindgen(js_name = "rangeClearAll")]
|
||||||
pub fn range_clear_all(
|
pub fn range_clear_all(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -271,12 +264,6 @@ impl Model {
|
|||||||
.map(|x| serde_wasm_bindgen::to_value(&x).unwrap())
|
.map(|x| serde_wasm_bindgen::to_value(&x).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onPasteStyles")]
|
|
||||||
pub fn on_paste_styles(&mut self, styles: JsValue) -> Result<(), JsError> {
|
|
||||||
let styles: &Vec<Vec<Style>> = &serde_wasm_bindgen::from_value(styles).unwrap();
|
|
||||||
self.model.on_paste_styles(styles).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getCellType")]
|
#[wasm_bindgen(js_name = "getCellType")]
|
||||||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32, JsError> {
|
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32, JsError> {
|
||||||
Ok(
|
Ok(
|
||||||
@@ -351,126 +338,4 @@ impl Model {
|
|||||||
.set_top_left_visible_cell(top_row, top_column)
|
.set_top_left_visible_cell(top_row, top_column)
|
||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setShowGridLines")]
|
|
||||||
pub fn set_show_grid_lines(
|
|
||||||
&mut self,
|
|
||||||
sheet: u32,
|
|
||||||
show_grid_lines: bool,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.set_show_grid_lines(sheet, show_grid_lines)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getShowGridLines")]
|
|
||||||
pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result<bool, JsError> {
|
|
||||||
self.model.get_show_grid_lines(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "autoFillRows")]
|
|
||||||
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
|
|
||||||
let area: Area =
|
|
||||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
self.model
|
|
||||||
.auto_fill_rows(&area, to_row)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "autoFillColumns")]
|
|
||||||
pub fn auto_fill_columns(
|
|
||||||
&mut self,
|
|
||||||
source_area: JsValue,
|
|
||||||
to_column: i32,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
let area: Area =
|
|
||||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
self.model
|
|
||||||
.auto_fill_columns(&area, to_column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onArrowRight")]
|
|
||||||
pub fn on_arrow_right(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.on_arrow_right().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onArrowLeft")]
|
|
||||||
pub fn on_arrow_left(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.on_arrow_left().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onArrowUp")]
|
|
||||||
pub fn on_arrow_up(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.on_arrow_up().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onArrowDown")]
|
|
||||||
pub fn on_arrow_down(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.on_arrow_down().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onPageDown")]
|
|
||||||
pub fn on_page_down(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.on_page_down().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onPageUp")]
|
|
||||||
pub fn on_page_up(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.on_page_up().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setWindowWidth")]
|
|
||||||
pub fn set_window_width(&mut self, window_width: f64) {
|
|
||||||
self.model.set_window_width(window_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setWindowHeight")]
|
|
||||||
pub fn set_window_height(&mut self, window_height: f64) {
|
|
||||||
self.model.set_window_height(window_height);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getScrollX")]
|
|
||||||
pub fn get_scroll_x(&self) -> Result<f64, JsError> {
|
|
||||||
self.model.get_scroll_x().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getScrollY")]
|
|
||||||
pub fn get_scroll_y(&self) -> Result<f64, JsError> {
|
|
||||||
self.model.get_scroll_y().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onExpandSelectedRange")]
|
|
||||||
pub fn on_expand_selected_range(&mut self, key: &str) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.on_expand_selected_range(key)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onAreaSelecting")]
|
|
||||||
pub fn on_area_selecting(
|
|
||||||
&mut self,
|
|
||||||
target_row: i32,
|
|
||||||
target_column: i32,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.on_area_selecting(target_row, target_column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setAreaWithBorder")]
|
|
||||||
pub fn set_area_with_border(
|
|
||||||
&mut self,
|
|
||||||
area: JsValue,
|
|
||||||
border_area: JsValue,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
let range: Area =
|
|
||||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
let border: BorderArea =
|
|
||||||
serde_wasm_bindgen::from_value(border_area).map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
self.model
|
|
||||||
.set_area_with_border(&range, &border)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,14 +119,5 @@ test("floating column numbers get truncated", () => {
|
|||||||
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("autofill", () => {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
model.setUserInput(0, 1, 1, "23");
|
|
||||||
model.autoFillRows({sheet: 0, row: 1, column: 1, width: 1, height: 1}, 2);
|
|
||||||
|
|
||||||
const result = model.getFormattedCellValue(0, 2, 1);
|
|
||||||
assert.strictEqual(result, "23");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,24 +6,6 @@ export interface Area {
|
|||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BorderType {
|
|
||||||
All = "All",
|
|
||||||
Inner = "Inner",
|
|
||||||
Outer = "Outer",
|
|
||||||
Top = "Top",
|
|
||||||
Right = "Right",
|
|
||||||
Bottom = "Bottom",
|
|
||||||
Left = "Left",
|
|
||||||
CenterH = "CenterH",
|
|
||||||
CenterV = "CenterV",
|
|
||||||
None = "None",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BorderArea {
|
|
||||||
item: BorderItem;
|
|
||||||
type: BorderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrorType =
|
type ErrorType =
|
||||||
| "REF"
|
| "REF"
|
||||||
| "NAME"
|
| "NAME"
|
||||||
@@ -133,19 +115,19 @@ interface CellStyleFont {
|
|||||||
scheme: string;
|
scheme: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export enum BorderType {
|
export enum BorderType {
|
||||||
// BorderAll,
|
BorderAll,
|
||||||
// BorderInner,
|
BorderInner,
|
||||||
// BorderCenterH,
|
BorderCenterH,
|
||||||
// BorderCenterV,
|
BorderCenterV,
|
||||||
// BorderOuter,
|
BorderOuter,
|
||||||
// BorderNone,
|
BorderNone,
|
||||||
// BorderTop,
|
BorderTop,
|
||||||
// BorderRight,
|
BorderRight,
|
||||||
// BorderBottom,
|
BorderBottom,
|
||||||
// BorderLeft,
|
BorderLeft,
|
||||||
// None,
|
None,
|
||||||
// }
|
}
|
||||||
|
|
||||||
export interface BorderOptions {
|
export interface BorderOptions {
|
||||||
color: string;
|
color: string;
|
||||||
@@ -210,12 +192,3 @@ export interface CellStyle {
|
|||||||
num_fmt: string;
|
num_fmt: string;
|
||||||
alignment?: Alignment;
|
alignment?: Alignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectedView {
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
range: [number, number, number, number];
|
|
||||||
top_row: number;
|
|
||||||
left_column: number;
|
|
||||||
}
|
|
||||||
|
|||||||
1
webapp/.gitignore → solidjs_app/.gitignore
vendored
@@ -1,3 +1,2 @@
|
|||||||
node_modules/*
|
node_modules/*
|
||||||
dist/*
|
dist/*
|
||||||
example.json
|
|
||||||
19
solidjs_app/.storybook/main.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { StorybookConfig } from "storybook-solidjs-vite";
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||||
|
addons: [
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"@chromatic-com/storybook",
|
||||||
|
"@storybook/addon-interactions",
|
||||||
|
],
|
||||||
|
framework: {
|
||||||
|
name: "storybook-solidjs-vite",
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: "tag",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
12
solidjs_app/.storybook/preview.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
controls: {
|
||||||
|
matchers: {
|
||||||
|
color: /(background|color)$/i,
|
||||||
|
date: /Date$/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
8
solidjs_app/Makefile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
lint:
|
||||||
|
pnpm biome lint *
|
||||||
|
|
||||||
|
format:
|
||||||
|
pnpm biome format *
|
||||||
|
build:
|
||||||
|
pnpm run build
|
||||||
70
solidjs_app/README.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# Web IronCalc
|
||||||
|
|
||||||
|
## Widgets
|
||||||
|
|
||||||
|
Toolbar
|
||||||
|
NavigationBar
|
||||||
|
FormulaBar
|
||||||
|
ColorPicker
|
||||||
|
Number Formatter
|
||||||
|
Border Picker
|
||||||
|
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
Vite
|
||||||
|
TypeScript
|
||||||
|
SolidJs
|
||||||
|
Lucide Icons
|
||||||
|
BiomeJs
|
||||||
|
Storybook
|
||||||
|
pnpm
|
||||||
|
|
||||||
|
## Recreate
|
||||||
|
|
||||||
|
Install nodejs
|
||||||
|
Activate pnpm
|
||||||
|
corepack enable pnpm
|
||||||
|
Create app
|
||||||
|
pnpm create vite
|
||||||
|
pnpm install
|
||||||
|
add biomejs
|
||||||
|
pnpm add --save-dev --save-exact @biomejs/biome
|
||||||
|
pnpm biome init
|
||||||
|
add solidjs
|
||||||
|
add storybook
|
||||||
|
pnpm dlx storybook@latest init
|
||||||
|
add i18n
|
||||||
|
pnpm add @solid-primitives/i18n
|
||||||
|
(https://github.com/jfgodoy/vite-plugin-solid-svg)
|
||||||
|
add vite-plugin-solid-svg
|
||||||
|
add script: "restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pnpm install # or npm install or yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
### `pnpm run dev`
|
||||||
|
|
||||||
|
Runs the app in the development mode.<br>
|
||||||
|
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
|
||||||
|
|
||||||
|
### `pnpm run build`
|
||||||
|
|
||||||
|
Builds the app for production to the `dist` folder.<br>
|
||||||
|
It correctly bundles Solid in production mode and optimizes the build for the best performance.
|
||||||
|
|
||||||
|
The build is minified and the filenames include the hashes.<br>
|
||||||
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)
|
||||||
15
solidjs_app/biome.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
|
||||||
|
"organizeImports": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"indentStyle": "space"
|
||||||
|
}
|
||||||
|
}
|
||||||
13
solidjs_app/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/ironcalc_icon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Solid + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
solidjs_app/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"build-storybook": "storybook build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
|
||||||
|
"@solid-primitives/i18n": "^2.1.1",
|
||||||
|
"lucide-solid": "^0.379.0",
|
||||||
|
"solid-js": "^1.8.15"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "1.7.0",
|
||||||
|
"@chromatic-com/storybook": "^1.3.3",
|
||||||
|
"@storybook/addon-essentials": "^8.0.8",
|
||||||
|
"@storybook/addon-interactions": "^8.0.8",
|
||||||
|
"@storybook/addon-links": "^8.0.8",
|
||||||
|
"@storybook/blocks": "^8.0.8",
|
||||||
|
"storybook": "^8.0.8",
|
||||||
|
"storybook-solidjs": "^1.0.0-beta.2",
|
||||||
|
"storybook-solidjs-vite": "^1.0.0-beta.2",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.2.0",
|
||||||
|
"vite-plugin-solid": "^2.10.2",
|
||||||
|
"vite-plugin-solid-svg": "^0.8.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
6864
solidjs_app/pnpm-lock.yaml
generated
Normal file
8
solidjs_app/public/ironcalc_icon.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="600" height="600" rx="20" fill="#F2994A"/>
|
||||||
|
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
|
||||||
|
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
|
||||||
|
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
7
solidjs_app/src/App.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
29
solidjs_app/src/App.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Show, createResource } from "solid-js";
|
||||||
|
// import "./App.css";
|
||||||
|
// import solidLogo from "./assets/solid.svg";
|
||||||
|
|
||||||
|
import init, { Model } from "@ironcalc/wasm";
|
||||||
|
import Workbook from "./components/Workbook";
|
||||||
|
|
||||||
|
const fetchModel = async () => {
|
||||||
|
await init();
|
||||||
|
// const model_bytes = new Uint8Array(
|
||||||
|
// await (await fetch("./example.ic")).arrayBuffer(),
|
||||||
|
// );
|
||||||
|
// const _model = Model.from_bytes(model_bytes);*/
|
||||||
|
const model = new Model("en", "UTC");
|
||||||
|
model.setUserInput(0, 1, 1, "=1+1");
|
||||||
|
return model;
|
||||||
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [model] = createResource(fetchModel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={model()} fallback={<div>Loading...</div>}>
|
||||||
|
{(model) => <Workbook model={model()} />}
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
BIN
solidjs_app/src/assets/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
solidjs_app/src/assets/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
solidjs_app/src/assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
solidjs_app/src/assets/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 441 B |
BIN
solidjs_app/src/assets/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 729 B |
BIN
solidjs_app/src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
solidjs_app/src/assets/ironcalc_icon.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
8
solidjs_app/src/assets/ironcalc_icon.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="600" height="600" rx="20" fill="#F2994A"/>
|
||||||
|
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
|
||||||
|
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
|
||||||
|
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
solidjs_app/src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
23
solidjs_app/src/components/Workbook.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Model } from "@ironcalc/wasm";
|
||||||
|
import styles from "./workbook.module.css";
|
||||||
|
import Toolbar from "./toolbar/Toolbar";
|
||||||
|
import Navigation from "./navigation/Navigation";
|
||||||
|
import FormulaBar from "./formulabar/FormulaBar";
|
||||||
|
import Worksheet from "./Worksheet/Worksheet";
|
||||||
|
|
||||||
|
function Workbook(props: { model: Model }) {
|
||||||
|
const onkeydown = (event: KeyboardEvent) => {
|
||||||
|
console.log("key pressed: ", event);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div class={styles.workbook} onkeydown={onkeydown} tabIndex={0}>
|
||||||
|
<Toolbar></Toolbar>
|
||||||
|
{/* {props.model.getFormattedCellValue(0, 1, 1)} */}
|
||||||
|
<FormulaBar></FormulaBar>
|
||||||
|
<Worksheet></Worksheet>
|
||||||
|
<Navigation></Navigation>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Workbook;
|
||||||
14
solidjs_app/src/components/Worksheet/Worksheet.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import styles from "./worksheet.module.css";
|
||||||
|
|
||||||
|
function Worksheet() {
|
||||||
|
const onkeydown = (event: KeyboardEvent) => {
|
||||||
|
console.log("key pressed: ", event);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div class={styles.worksheet} onkeydown={onkeydown} tabIndex={0}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Worksheet;
|
||||||
14
solidjs_app/src/components/formulabar/FormulaBar.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import styles from "./formulabar.module.css";
|
||||||
|
|
||||||
|
function FormulaBar() {
|
||||||
|
const onkeydown = (event: KeyboardEvent) => {
|
||||||
|
console.log("key pressed: ", event);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div class={styles.toolbar} onkeydown={onkeydown} tabIndex={0}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormulaBar;
|
||||||
14
solidjs_app/src/components/navigation/Navigation.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import styles from "./navigation.module.css";
|
||||||
|
|
||||||
|
function Navigation() {
|
||||||
|
const onkeydown = (event: KeyboardEvent) => {
|
||||||
|
console.log("key pressed: ", event);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div class={styles.navigation} onkeydown={onkeydown} tabIndex={0}>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navigation;
|
||||||
299
solidjs_app/src/components/toolbar/Toolbar.tsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import { JSX } from "solid-js/jsx-runtime";
|
||||||
|
import styles from "./toolbar.module.css";
|
||||||
|
import {
|
||||||
|
AlignCenter,
|
||||||
|
AlignLeft,
|
||||||
|
AlignRight,
|
||||||
|
ArrowDownToLine,
|
||||||
|
ArrowUpToLine,
|
||||||
|
Bold,
|
||||||
|
ChevronDown,
|
||||||
|
Euro,
|
||||||
|
Grid2X2,
|
||||||
|
Italic,
|
||||||
|
Paintbrush2,
|
||||||
|
PaintBucket,
|
||||||
|
Percent,
|
||||||
|
Redo2,
|
||||||
|
Strikethrough,
|
||||||
|
Type,
|
||||||
|
Underline,
|
||||||
|
Undo2,
|
||||||
|
} from "lucide-solid";
|
||||||
|
import { DecimalPlacesDecreaseIcon, DecimalPlacesIncreaseIcon, ArrowMiddleFromLine } from "../../icons";
|
||||||
|
|
||||||
|
function Toolbar() {
|
||||||
|
const onkeydown = (event: KeyboardEvent) => {
|
||||||
|
console.log("key pressed: ", event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = (s: string): string => s;
|
||||||
|
|
||||||
|
const properties = {
|
||||||
|
onUndo: () => {},
|
||||||
|
canUndo: true,
|
||||||
|
onRedo: () => {},
|
||||||
|
canRedo: true,
|
||||||
|
onCopyStyles: () => {},
|
||||||
|
canEdit: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={styles.toolbar} onkeydown={onkeydown} tabIndex={0}>
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
onClick={properties.onUndo}
|
||||||
|
disabled={!properties.canUndo}
|
||||||
|
title={t("toolbar.undo")}
|
||||||
|
>
|
||||||
|
<Undo2 />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
onClick={properties.onRedo}
|
||||||
|
disabled={!properties.canRedo}
|
||||||
|
title={t("toolbar.redo")}
|
||||||
|
>
|
||||||
|
<Redo2 />
|
||||||
|
</StyledButton>
|
||||||
|
<div class={styles.divider} />
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
onClick={properties.onCopyStyles}
|
||||||
|
title={t("toolbar.copy_styles")}
|
||||||
|
>
|
||||||
|
<Paintbrush2 />
|
||||||
|
</StyledButton>
|
||||||
|
<div class={styles.divider} />
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
// onClick={(): void => {
|
||||||
|
// properties.onNumberFormatPicked(NumberFormats.CURRENCY_EUR);
|
||||||
|
// }}
|
||||||
|
title={t("toolbar.euro")}
|
||||||
|
>
|
||||||
|
<Euro />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
// onClick={(): void => {
|
||||||
|
// properties.onNumberFormatPicked(NumberFormats.PERCENTAGE);
|
||||||
|
// }}
|
||||||
|
title={t("toolbar.percentage")}
|
||||||
|
>
|
||||||
|
<Percent />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
// onClick={(): void => {
|
||||||
|
// properties.onNumberFormatPicked(
|
||||||
|
// decreaseDecimalPlaces(properties.numFmt)
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
title={t("toolbar.decimal_places_decrease")}
|
||||||
|
>
|
||||||
|
<div><DecimalPlacesDecreaseIcon /></div>
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
// onClick={(): void => {
|
||||||
|
// properties.onNumberFormatPicked(
|
||||||
|
// increaseDecimalPlaces(properties.numFmt)
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
title={t("toolbar.decimal_places_increase")}
|
||||||
|
>
|
||||||
|
<DecimalPlacesIncreaseIcon />
|
||||||
|
</StyledButton>
|
||||||
|
{/* // <FormatMenu
|
||||||
|
// numFmt={properties.numFmt}
|
||||||
|
// onChange={(numberFmt): void => {
|
||||||
|
// properties.onNumberFormatPicked(numberFmt);
|
||||||
|
// }}
|
||||||
|
// onExited={(): void => {}}
|
||||||
|
// anchorOrigin={{
|
||||||
|
// horizontal: 20, // Aligning the menu to the middle of FormatButton
|
||||||
|
// vertical: "bottom",
|
||||||
|
// }}
|
||||||
|
// >*/
|
||||||
|
<StyledButton
|
||||||
|
|
||||||
|
pressed={false}
|
||||||
|
|
||||||
|
title={t("toolbar.format_number")}
|
||||||
|
>
|
||||||
|
<div class={styles.format_menu}>{"123"}<ChevronDown /></div>
|
||||||
|
|
||||||
|
</StyledButton>
|
||||||
|
/* </FormatMenu> */}
|
||||||
|
<div class={styles.divider} />
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.bold}
|
||||||
|
// onClick={() => properties.onToggleBold(!properties.bold)}
|
||||||
|
title={t("toolbar.bold")}
|
||||||
|
>
|
||||||
|
<Bold />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.italic}
|
||||||
|
// onClick={() => properties.onToggleItalic(!properties.italic)}
|
||||||
|
title={t("toolbar.italic")}
|
||||||
|
>
|
||||||
|
<Italic />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.underline}
|
||||||
|
// onClick={() => properties.onToggleUnderline(!properties.underline)}
|
||||||
|
title={t("toolbar.underline")}
|
||||||
|
>
|
||||||
|
<Underline />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.strike}
|
||||||
|
// onClick={() => properties.onToggleStrike(!properties.strike)}
|
||||||
|
title={t("toolbar.strike_trough")}
|
||||||
|
>
|
||||||
|
<Strikethrough />
|
||||||
|
</StyledButton>
|
||||||
|
<div class={styles.divider} />
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
title={t("toolbar.font_color")}
|
||||||
|
// ref={fontColorButton}
|
||||||
|
// underlinedColor={properties.fontColor}
|
||||||
|
// onClick={() => setFontColorPickerOpen(true)}
|
||||||
|
>
|
||||||
|
<Type />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
title={t("toolbar.fill_color")}
|
||||||
|
// ref={fillColorButton}
|
||||||
|
// underlinedColor={properties.fillColor}
|
||||||
|
// onClick={() => setFillColorPickerOpen(true)}
|
||||||
|
>
|
||||||
|
<PaintBucket />
|
||||||
|
</StyledButton>
|
||||||
|
<div class={styles.divider} />
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.horizontalAlign === "left"}
|
||||||
|
// onClick={() =>
|
||||||
|
// properties.onToggleHorizontalAlign(
|
||||||
|
// properties.horizontalAlign === "left" ? "general" : "left"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
title={t("toolbar.align_left")}
|
||||||
|
>
|
||||||
|
<AlignLeft />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.horizontalAlign === "center"}
|
||||||
|
// onClick={() =>
|
||||||
|
// properties.onToggleHorizontalAlign(
|
||||||
|
// properties.horizontalAlign === "center" ? "general" : "center"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
title={t("toolbar.align_center")}
|
||||||
|
>
|
||||||
|
<AlignCenter />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.horizontalAlign === "right"}
|
||||||
|
// onClick={() =>
|
||||||
|
// properties.onToggleHorizontalAlign(
|
||||||
|
// properties.horizontalAlign === "right" ? "general" : "right"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
title={t("toolbar.align_right")}
|
||||||
|
>
|
||||||
|
<AlignRight />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.verticalAlign === "top"}
|
||||||
|
// onClick={() =>
|
||||||
|
// properties.onToggleVerticalAlign(
|
||||||
|
// properties.verticalAlign === "top" ? "bottom" : "top"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
title={t("toolbar.vertical_align_top")}
|
||||||
|
>
|
||||||
|
<ArrowUpToLine />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.verticalAlign === "center"}
|
||||||
|
// onClick={() =>
|
||||||
|
// properties.onToggleVerticalAlign(
|
||||||
|
// properties.verticalAlign === "center" ? "bottom" : "center"
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
title={t("toolbar.vertical_align_center")}
|
||||||
|
>
|
||||||
|
<ArrowMiddleFromLine />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
// pressed={properties.verticalAlign === "bottom"}
|
||||||
|
// onClick={() => properties.onToggleVerticalAlign("bottom")}
|
||||||
|
title={t("toolbar.vertical_align_bottom")}
|
||||||
|
>
|
||||||
|
<ArrowDownToLine />
|
||||||
|
</StyledButton>
|
||||||
|
<div class={styles.divider} />
|
||||||
|
<StyledButton
|
||||||
|
pressed={false}
|
||||||
|
// onClick={() => setBorderPickerOpen(true)}
|
||||||
|
// ref={borderButton}
|
||||||
|
title={t("toolbar.borders")}
|
||||||
|
>
|
||||||
|
<Grid2X2 />
|
||||||
|
</StyledButton>
|
||||||
|
{/* // <ColorPicker
|
||||||
|
// color={properties.fontColor}
|
||||||
|
// onChange={(color): void => {
|
||||||
|
// properties.onTextColorPicked(color);
|
||||||
|
// setFontColorPickerOpen(false);
|
||||||
|
// }}
|
||||||
|
// anchorEl={fontColorButton}
|
||||||
|
// open={fontColorPickerOpen}
|
||||||
|
// />
|
||||||
|
// <ColorPicker
|
||||||
|
// color={properties.fillColor}
|
||||||
|
// onChange={(color): void => {
|
||||||
|
// properties.onFillColorPicked(color);
|
||||||
|
// setFillColorPickerOpen(false);
|
||||||
|
// }}
|
||||||
|
// anchorEl={fillColorButton}
|
||||||
|
// open={fillColorPickerOpen}
|
||||||
|
// />
|
||||||
|
// <BorderPicker
|
||||||
|
// onChange={(border): void => {
|
||||||
|
// properties.onBorderChanged(border);
|
||||||
|
// setBorderPickerOpen(false);
|
||||||
|
// }}
|
||||||
|
// anchorEl={borderButton}
|
||||||
|
// open={borderPickerOpen}
|
||||||
|
// /> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StyledButton(props: {
|
||||||
|
children: JSX.Element;
|
||||||
|
title: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
pressed?: boolean;
|
||||||
|
underlinedColor?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
disabled={props.disabled || false}
|
||||||
|
onClick={props.onClick}
|
||||||
|
title={props.title}
|
||||||
|
class={styles.button}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Toolbar;
|
||||||
67
solidjs_app/src/components/toolbar/toolbar.module.css
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
/* ${({ theme }) => theme.palette.background.paper}; */
|
||||||
|
background: #fff;
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
/* theme.palette.grey["600"] */
|
||||||
|
border-bottom: 1px solid #757575;
|
||||||
|
font-family: Inter;
|
||||||
|
border-radius: 4px 4px 0px 0px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 26px;
|
||||||
|
border: 0px solid #fff;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 5px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: white;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
color: grey;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:not(disabled) {
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
color: #21243a;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #f1f2f8;
|
||||||
|
border-top-color: #f1f2f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 0px;
|
||||||
|
height: 10px;
|
||||||
|
border-left: 1px solid #d3d6e9;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format_menu {
|
||||||
|
width: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
6
solidjs_app/src/components/workbook.module.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* @import './theme.css'; */
|
||||||
|
|
||||||
|
.workbook {
|
||||||
|
/* background-color: var(--main-bg-color); */
|
||||||
|
/* color: var(--other); */
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 869 B After Width: | Height: | Size: 869 B |
|
Before Width: | Height: | Size: 538 B After Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 498 B |
|
Before Width: | Height: | Size: 498 B After Width: | Height: | Size: 498 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 539 B |
|
Before Width: | Height: | Size: 513 B After Width: | Height: | Size: 513 B |
|
Before Width: | Height: | Size: 542 B After Width: | Height: | Size: 542 B |
|
Before Width: | Height: | Size: 538 B After Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 744 B |
|
Before Width: | Height: | Size: 539 B After Width: | Height: | Size: 539 B |
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 659 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1004 B After Width: | Height: | Size: 1004 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 929 B After Width: | Height: | Size: 929 B |
46
solidjs_app/src/icons/index.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import DecimalPlacesDecreaseIcon from "./decrease-decimal.svg";
|
||||||
|
import DecimalPlacesIncreaseIcon from "./increase-decimal.svg";
|
||||||
|
|
||||||
|
import BorderBottomIcon from "./border-bottom.svg";
|
||||||
|
import BorderCenterHIcon from "./border-center-h.svg";
|
||||||
|
import BorderCenterVIcon from "./border-center-v.svg";
|
||||||
|
import BorderInnerIcon from "./border-inner.svg";
|
||||||
|
import BorderLeftIcon from "./border-left.svg";
|
||||||
|
import BorderOuterIcon from "./border-outer.svg";
|
||||||
|
import BorderRightIcon from "./border-right.svg";
|
||||||
|
import BorderTopIcon from "./border-top.svg";
|
||||||
|
import BorderNoneIcon from "./border-none.svg";
|
||||||
|
import BorderStyleIcon from "./border-style.svg";
|
||||||
|
|
||||||
|
import DeleteColumnIcon from "./delete-column.svg";
|
||||||
|
import DeleteRowIcon from "./delete-row.svg";
|
||||||
|
import InsertColumnLeftIcon from "./insert-column-left.svg";
|
||||||
|
import InsertColumnRightIcon from "./insert-column-right.svg";
|
||||||
|
import InsertRowAboveIcon from "./insert-row-above.svg";
|
||||||
|
import InsertRowBelow from "./insert-row-below.svg";
|
||||||
|
import ArrowMiddleFromLine from "./arrow-middle-from-line.svg";
|
||||||
|
|
||||||
|
import Fx from "./fx.svg";
|
||||||
|
|
||||||
|
export {
|
||||||
|
ArrowMiddleFromLine,
|
||||||
|
DecimalPlacesDecreaseIcon,
|
||||||
|
DecimalPlacesIncreaseIcon,
|
||||||
|
BorderBottomIcon,
|
||||||
|
BorderCenterHIcon,
|
||||||
|
BorderCenterVIcon,
|
||||||
|
BorderInnerIcon,
|
||||||
|
BorderLeftIcon,
|
||||||
|
BorderOuterIcon,
|
||||||
|
BorderRightIcon,
|
||||||
|
BorderTopIcon,
|
||||||
|
BorderNoneIcon,
|
||||||
|
BorderStyleIcon,
|
||||||
|
DeleteColumnIcon,
|
||||||
|
DeleteRowIcon,
|
||||||
|
InsertColumnLeftIcon,
|
||||||
|
InsertColumnRightIcon,
|
||||||
|
InsertRowAboveIcon,
|
||||||
|
InsertRowBelow,
|
||||||
|
Fx,
|
||||||
|
};
|
||||||
|
Before Width: | Height: | Size: 726 B After Width: | Height: | Size: 726 B |
|
Before Width: | Height: | Size: 725 B After Width: | Height: | Size: 725 B |
|
Before Width: | Height: | Size: 706 B After Width: | Height: | Size: 706 B |
|
Before Width: | Height: | Size: 706 B After Width: | Height: | Size: 706 B |
68
solidjs_app/src/index.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
solidjs_app/src/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/* @refresh reload */
|
||||||
|
import { render } from "solid-js/web";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
// import "./index.css";
|
||||||
|
import "./theme.css";
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
render(() => <App />, root);
|
||||||
|
}
|
||||||
4
solidjs_app/src/theme.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
:root {
|
||||||
|
--main-bg-color: brown;
|
||||||
|
--other: blue;
|
||||||
|
}
|
||||||
1
solidjs_app/src/typings.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module "*.module.css";
|
||||||
1
solidjs_app/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
@@ -12,14 +12,18 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "preserve",
|
||||||
|
"jsxImportSource": "solid-js",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"esModuleInterop": true
|
"types": [
|
||||||
|
"vite-plugin-solid-svg/types-component-solid",
|
||||||
|
"vite/client"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
8
solidjs_app/vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import solid from "vite-plugin-solid";
|
||||||
|
import solidPlugin from "vite-plugin-solid";
|
||||||
|
import solidSvg from "vite-plugin-solid-svg";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [solid(), solidPlugin(), solidSvg()],
|
||||||
|
});
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { StorybookConfig } from '@storybook/react-vite';
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
|
||||||
addons: [
|
|
||||||
'@storybook/addon-links',
|
|
||||||
'@storybook/addon-essentials',
|
|
||||||
'@storybook/addon-onboarding',
|
|
||||||
'@storybook/addon-interactions',
|
|
||||||
'@storybook/addon-mdx-gfm',
|
|
||||||
'@chromatic-com/storybook'
|
|
||||||
],
|
|
||||||
framework: {
|
|
||||||
name: '@storybook/react-vite',
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
autodocs: 'tag',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { Preview } from '@storybook/react';
|
|
||||||
import i18n from '../src/i18n';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
|
|
||||||
const withI18next = (Story: any) => {
|
|
||||||
return (
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<Story />
|
|
||||||
</I18nextProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
parameters: {
|
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/i,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const decorators = [withI18next];
|
|
||||||
export default preview;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# IronCalc Web App
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<!-- <meta name="theme-color" content="#1bb566"> -->
|
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#F2994A" />
|
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
|
||||||
<title>IronCalc Spreadsheet</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { Config } from "jest";
|
|
||||||
// import {defaults} from 'jest-config';
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
// testMatch:["**.jest.mjs"],
|
|
||||||
moduleFileExtensions: ["js", "ts", "mts", "mjs"],
|
|
||||||
transform: {
|
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
|
||||||
},
|
|
||||||
moduleNameMapper: {
|
|
||||||
"^@ironcalc/wasm$": "<rootDir>/node_modules/@ironcalc/nodejs/"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
16992
webapp/package-lock.json
generated
@@ -1,57 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.1.3",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
|
|
||||||
"dev": "vite",
|
|
||||||
"test": "jest",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"storybook": "storybook dev -p 6006",
|
|
||||||
"build-storybook": "storybook build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@emotion/react": "^11.11.4",
|
|
||||||
"@emotion/styled": "^11.11.5",
|
|
||||||
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
|
|
||||||
"@mui/material": "^5.15.15",
|
|
||||||
"@storybook/test": "^8.0.8",
|
|
||||||
"i18next": "^23.11.1",
|
|
||||||
"lucide-react": "^0.375.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-colorful": "^5.6.1",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-i18next": "^13.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@chromatic-com/storybook": "^1.3.2",
|
|
||||||
"@storybook/addon-essentials": "^8.0.8",
|
|
||||||
"@storybook/addon-interactions": "^8.0.8",
|
|
||||||
"@storybook/addon-links": "^8.0.8",
|
|
||||||
"@storybook/addon-mdx-gfm": "^8.0.8",
|
|
||||||
"@storybook/addon-onboarding": "^8.0.8",
|
|
||||||
"@storybook/blocks": "^8.0.8",
|
|
||||||
"@storybook/react": "^8.0.8",
|
|
||||||
"@storybook/react-vite": "^8.0.8",
|
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/react": "^18.2.75",
|
|
||||||
"@types/react-dom": "^18.2.24",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
|
||||||
"eslint-plugin-storybook": "^0.6.15",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"storybook": "^8.0.8",
|
|
||||||
"ts-jest": "^29.1.2",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.4.5",
|
|
||||||
"vite": "^5.2.8",
|
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#root {
|
|
||||||
position: absolute;
|
|
||||||
inset: 10px;
|
|
||||||
border: 1px solid #AAA;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import "./App.css";
|
|
||||||
import Workbook from "./components/workbook";
|
|
||||||
import "./i18n";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import init, { Model } from "@ironcalc/wasm";
|
|
||||||
import { WorkbookState } from "./components/workbookState";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [model, setModel] = useState<Model | null>(null);
|
|
||||||
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
async function start() {
|
|
||||||
await init();
|
|
||||||
const model_bytes = new Uint8Array(await (await fetch("./example.ic")).arrayBuffer());
|
|
||||||
const _model = Model.from_bytes(model_bytes);
|
|
||||||
// const _model = new Model("en", "UTC");
|
|
||||||
if (!model) setModel(_model);
|
|
||||||
if (!workbookState) setWorkbookState(new WorkbookState());
|
|
||||||
}
|
|
||||||
start();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!model || !workbookState) {
|
|
||||||
return <div>Loading</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could use context for model, but the problem is that it should initialized to null.
|
|
||||||
// Passing the property down makes sure it is always defined.
|
|
||||||
return (
|
|
||||||
<Workbook model={model} workbookState={workbookState} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Keyboard and mouse events architecture
|
|
||||||
|
|
||||||
This document describes the architecture of the keyboard navigation and mouse events in IronCalc Web
|
|
||||||
|
|
||||||
There are two modes for mouse events:
|
|
||||||
|
|
||||||
* Normal mode: clicking a cell selects it, clicking on a sheet opens it
|
|
||||||
* Browse mode: clicking on a cell updates the formula, etc
|
|
||||||
|
|
||||||
While in browse mode some mouse events might end the browse mode
|
|
||||||
|
|
||||||
We follow Excel's way of navigating a spreadsheet
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
export const headerCornerBackground = '#FFF';
|
|
||||||
export const headerTextColor = '#333';
|
|
||||||
export const headerBackground = '#FFF';
|
|
||||||
export const headerGlobalSelectorColor = '#EAECF4';
|
|
||||||
export const headerSelectedBackground = '#EEEEEE';
|
|
||||||
export const headerFullSelectedBackground = '#D3D6E9';
|
|
||||||
export const headerSelectedColor = '#333';
|
|
||||||
export const headerBorderColor = '#DEE0EF';
|
|
||||||
|
|
||||||
export const gridColor = '#D3D6E9';
|
|
||||||
export const gridSeparatorColor = '#D3D6E9';
|
|
||||||
export const defaultTextColor = '#2E414D';
|
|
||||||
|
|
||||||
export const outlineColor = '#F2994A';
|
|
||||||
export const outlineBackgroundColor = '#F2994A1A';
|
|
||||||
|
|
||||||
export const LAST_COLUMN = 16_384;
|
|
||||||
export const LAST_ROW = 1_048_576;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
export interface Cell {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SheetArea extends Area {
|
|
||||||
sheet: number;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AreaWithBorderInterface extends Area {
|
|
||||||
border: "left" | "top" | "right" | "bottom";
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
|
||||||
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
const letters = [
|
|
||||||
'A',
|
|
||||||
'B',
|
|
||||||
'C',
|
|
||||||
'D',
|
|
||||||
'E',
|
|
||||||
'F',
|
|
||||||
'G',
|
|
||||||
'H',
|
|
||||||
'I',
|
|
||||||
'J',
|
|
||||||
'K',
|
|
||||||
'L',
|
|
||||||
'M',
|
|
||||||
'N',
|
|
||||||
'O',
|
|
||||||
'P',
|
|
||||||
'Q',
|
|
||||||
'R',
|
|
||||||
'S',
|
|
||||||
'T',
|
|
||||||
'U',
|
|
||||||
'V',
|
|
||||||
'W',
|
|
||||||
'X',
|
|
||||||
'Y',
|
|
||||||
'Z',
|
|
||||||
];
|
|
||||||
interface Reference {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
absoluteRow: boolean;
|
|
||||||
absoluteColumn: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function referenceToString(rf: Reference): string {
|
|
||||||
const absC = rf.absoluteColumn ? '$' : '';
|
|
||||||
const absR = rf.absoluteRow ? '$' : '';
|
|
||||||
return absC + columnNameFromNumber(rf.column) + absR + rf.row;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNameFromNumber(column: number): string {
|
|
||||||
let columnName = '';
|
|
||||||
let index = column;
|
|
||||||
while (index > 0) {
|
|
||||||
columnName = `${letters[(index - 1) % 26]}${columnName}`;
|
|
||||||
index = Math.floor((index - 1) / 26);
|
|
||||||
}
|
|
||||||
return columnName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNumberFromName(columnName: string): number {
|
|
||||||
let column = 0;
|
|
||||||
for (const character of columnName) {
|
|
||||||
const index = (character.codePointAt(0) ?? 0) - 64;
|
|
||||||
column = column * 26 + index;
|
|
||||||
}
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EqualTo Color Palette
|
|
||||||
export function getColor(index: number, alpha = 1): string {
|
|
||||||
const colors = [
|
|
||||||
{
|
|
||||||
name: 'Cyan',
|
|
||||||
rgba: [89, 185, 188, 1],
|
|
||||||
hex: '#59B9BC',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Flamingo',
|
|
||||||
rgba: [236, 87, 83, 1],
|
|
||||||
hex: '#EC5753',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#3358B7',
|
|
||||||
rgba: [51, 88, 183, 1],
|
|
||||||
name: 'Blue',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#F8CD3C',
|
|
||||||
rgba: [248, 205, 60, 1],
|
|
||||||
name: 'Yellow',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#3BB68A',
|
|
||||||
rgba: [59, 182, 138, 1],
|
|
||||||
name: 'Emerald',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#523E93',
|
|
||||||
rgba: [82, 62, 147, 1],
|
|
||||||
name: 'Violet',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#A23C52',
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: 'Burgundy',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#8CB354',
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: 'Wasabi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#D03627',
|
|
||||||
rgba: [208, 54, 39, 1],
|
|
||||||
name: 'Red',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#1B717E',
|
|
||||||
rgba: [27, 113, 126, 1],
|
|
||||||
name: 'Teal',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (alpha === 1) {
|
|
||||||
return colors[index % 10].hex;
|
|
||||||
}
|
|
||||||
const { rgba } = colors[index % 10];
|
|
||||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergedAreas(area1: Area, area2: Area): Area {
|
|
||||||
return {
|
|
||||||
rowStart: Math.min(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
|
||||||
rowEnd: Math.max(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
|
||||||
columnStart: Math.min(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
|
||||||
columnEnd: Math.max(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getExpandToArea(area: Area, cell: Cell): AreaWithBorder {
|
|
||||||
let { rowStart, rowEnd, columnStart, columnEnd } = area;
|
|
||||||
if (rowStart > rowEnd) {
|
|
||||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
|
||||||
}
|
|
||||||
if (columnStart > columnEnd) {
|
|
||||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
|
||||||
}
|
|
||||||
const { row, column } = cell;
|
|
||||||
if (row <= rowEnd && row >= rowStart && column >= columnStart && column <= columnEnd) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Two rules:
|
|
||||||
// * The extendTo area must be larger than the selected area
|
|
||||||
// * The extendTo area must be of the same width or the same height as the selected area
|
|
||||||
if (row >= rowEnd && column >= columnStart) {
|
|
||||||
// Normal case: we are expanding down and right
|
|
||||||
if (row - rowEnd > column - columnEnd) {
|
|
||||||
// Expanding by rows (down)
|
|
||||||
return {
|
|
||||||
rowStart: rowEnd + 1,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'top',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// expanding by columns (right)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: columnEnd + 1,
|
|
||||||
columnEnd: column,
|
|
||||||
border: 'left',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (row >= rowEnd && column <= columnStart) {
|
|
||||||
// We are expanding down and left
|
|
||||||
if (row - rowEnd > columnStart - column) {
|
|
||||||
// Expanding by rows (down)
|
|
||||||
return {
|
|
||||||
rowStart: rowEnd + 1,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'top',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Expanding by columns (left)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: columnStart - 1,
|
|
||||||
border: 'right',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (row <= rowEnd && column >= columnEnd) {
|
|
||||||
// We are expanding up and right
|
|
||||||
if (rowStart - row > column - columnEnd) {
|
|
||||||
// Expanding by rows (up)
|
|
||||||
return {
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: rowStart - 1,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'bottom',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Expanding by columns (right)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: columnEnd + 1,
|
|
||||||
columnEnd: column,
|
|
||||||
border: 'left',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (row <= rowEnd && column <= columnStart) {
|
|
||||||
// We are expanding up and left
|
|
||||||
if (rowStart - row > columnStart - column) {
|
|
||||||
// Expanding by rows (up)
|
|
||||||
return {
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: rowStart - 1,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'bottom',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Expanding by columns (left)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: columnStart - 1,
|
|
||||||
border: 'right',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the keypress should start editing
|
|
||||||
*/
|
|
||||||
export function isEditingKey(key: string): boolean {
|
|
||||||
if (key.length !== 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const code = key.codePointAt(0) ?? 0;
|
|
||||||
if (code > 0 && code < 255) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// / Common types
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AreaWithBorderInterface extends Area {
|
|
||||||
border: 'left' | 'top' | 'right' | 'bottom';
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
|
||||||
|
|
||||||
export interface Cell {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScrollPosition {
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StateSettings {
|
|
||||||
selectedCell: Cell;
|
|
||||||
selectedArea: Area;
|
|
||||||
scrollPosition: ScrollPosition;
|
|
||||||
extendToArea: AreaWithBorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Dispatch<A> = (value: A) => void;
|
|
||||||
export type SetStateAction<S> = S | ((prevState: S) => S);
|
|
||||||
|
|
||||||
export enum FocusType {
|
|
||||||
Cell = 'cell',
|
|
||||||
FormulaBar = 'formula-bar',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In Excel there are two "modes" of editing
|
|
||||||
* * `init`: When you start typing in a cell. In this mode arrow keys will move away from the cell
|
|
||||||
* * `edit`: If you double click on a cell or click in the cell while editing.
|
|
||||||
* In this mode arrow keys will move within the cell.
|
|
||||||
*
|
|
||||||
* In a formula bar mode is always `edit`.
|
|
||||||
*/
|
|
||||||
export type CellEditMode = 'init' | 'edit';
|
|
||||||
export interface CellEditingType {
|
|
||||||
/**
|
|
||||||
* ID of cell editing. Useful when one edit transforms into another and some code needs to run
|
|
||||||
* when target changes.
|
|
||||||
*
|
|
||||||
* Due to problems with focus management (see #339) it's possible to start a new cell editing
|
|
||||||
* without properly cleaning up previous one (lose focus in workbook, regain focus NOT in
|
|
||||||
* the input and then use the keyboard.
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
text: string;
|
|
||||||
base: string;
|
|
||||||
mode: CellEditMode;
|
|
||||||
focus: FocusType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NavigationKey = 'ArrowRight' | 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';
|
|
||||||
|
|
||||||
export const isNavigationKey = (key: string): key is NavigationKey =>
|
|
||||||
['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key);
|
|
||||||
|
|
||||||
function nameNeedsQuoting(name: string): boolean {
|
|
||||||
const chars = [' ', '(', ')', "'", '$', ',', ';', '-', '+', '{', '}'];
|
|
||||||
const l = chars.length;
|
|
||||||
for (let index = 0; index < l; index += 1) {
|
|
||||||
if (name.includes(chars[index])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: We should use the function of a similar name in the rust code.
|
|
||||||
export const quoteSheetName = (name: string): string => {
|
|
||||||
if (nameNeedsQuoting(name)) {
|
|
||||||
return `'${name.replace("'", "''")}'`;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function cellReprToRowColumn(cellRepr: string): { row: number; column: number } {
|
|
||||||
let row = 0;
|
|
||||||
let column = 0;
|
|
||||||
for (const character of cellRepr) {
|
|
||||||
if (Number.isNaN(Number.parseInt(character, 10))) {
|
|
||||||
column *= 26;
|
|
||||||
const characterCode = character.codePointAt(0);
|
|
||||||
const ACharacterCode = 'A'.codePointAt(0);
|
|
||||||
if (typeof characterCode === 'undefined' || typeof ACharacterCode === 'undefined') {
|
|
||||||
throw new TypeError('Failed to find character code');
|
|
||||||
}
|
|
||||||
const deltaCodes = characterCode - ACharacterCode;
|
|
||||||
if (deltaCodes < 0) {
|
|
||||||
throw new Error('Incorrect character');
|
|
||||||
}
|
|
||||||
column += deltaCodes + 1;
|
|
||||||
} else {
|
|
||||||
row *= 10;
|
|
||||||
row += Number.parseInt(character, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { row, column };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMessageCellText = (
|
|
||||||
cell: string,
|
|
||||||
getMessageSheetNumber: (sheet: string) => number | undefined,
|
|
||||||
getCellText?: (sheet: number, row: number, column: number) => string | undefined,
|
|
||||||
) => {
|
|
||||||
const messageMatch = /^=?(?<sheet>\w+)!(?<cell>\w+)/.exec(cell);
|
|
||||||
if (messageMatch && messageMatch.groups) {
|
|
||||||
const messageSheet = getMessageSheetNumber(messageMatch.groups.sheet);
|
|
||||||
const dynamicIconCell = cellReprToRowColumn(messageMatch.groups.cell);
|
|
||||||
if (messageSheet !== undefined && getCellText) {
|
|
||||||
return getCellText(messageSheet, dynamicIconCell.row, dynamicIconCell.column) || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCellAddress = (selectedArea: Area, selectedCell?: Cell) => {
|
|
||||||
const isSingleCell =
|
|
||||||
selectedArea.rowStart === selectedArea.rowEnd &&
|
|
||||||
selectedArea.columnEnd === selectedArea.columnStart;
|
|
||||||
|
|
||||||
return isSingleCell && selectedCell
|
|
||||||
? `${columnNameFromNumber(selectedCell.column)}${selectedCell.row}`
|
|
||||||
: `${columnNameFromNumber(selectedArea.columnStart)}${
|
|
||||||
selectedArea.rowStart
|
|
||||||
}:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum Border {
|
|
||||||
Top = 'top',
|
|
||||||
Bottom = 'bottom',
|
|
||||||
Right = 'right',
|
|
||||||
Left = 'left',
|
|
||||||
}
|
|
||||||
@@ -1,566 +0,0 @@
|
|||||||
import React, { useRef, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
BorderBottomIcon,
|
|
||||||
BorderCenterHIcon,
|
|
||||||
BorderCenterVIcon,
|
|
||||||
BorderInnerIcon,
|
|
||||||
BorderLeftIcon,
|
|
||||||
BorderOuterIcon,
|
|
||||||
BorderRightIcon,
|
|
||||||
BorderTopIcon,
|
|
||||||
BorderNoneIcon,
|
|
||||||
BorderStyleIcon,
|
|
||||||
} from "../icons";
|
|
||||||
import ColorPicker from "./colorPicker";
|
|
||||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
|
||||||
import {
|
|
||||||
Check,
|
|
||||||
ChevronDown,
|
|
||||||
Grid2X2 as BorderAllIcon,
|
|
||||||
PencilLine,
|
|
||||||
ChevronRight,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import { theme } from "../theme";
|
|
||||||
import { BorderOptions, BorderStyle, BorderType } from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
type BorderPickerProps = {
|
|
||||||
className?: string;
|
|
||||||
onChange: (border: BorderOptions) => void;
|
|
||||||
anchorEl: React.RefObject<HTMLElement>;
|
|
||||||
anchorOrigin?: PopoverOrigin;
|
|
||||||
transformOrigin?: PopoverOrigin;
|
|
||||||
open: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BorderPicker = (properties: BorderPickerProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [borderSelected, setBorderSelected] = useState(BorderType.None);
|
|
||||||
const [borderColor, setBorderColor] = useState("#000000");
|
|
||||||
const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin);
|
|
||||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
|
||||||
const [stylePickerOpen, setStylePickerOpen] = useState(false);
|
|
||||||
const closePicker = (): void => {
|
|
||||||
properties.onChange({
|
|
||||||
color: borderColor,
|
|
||||||
style: borderStyle,
|
|
||||||
border: borderSelected,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const borderColorButton = useRef(null);
|
|
||||||
const borderStyleButton = useRef(null);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledPopover
|
|
||||||
open={properties.open}
|
|
||||||
onClose={(): void => closePicker()}
|
|
||||||
anchorEl={properties.anchorEl.current}
|
|
||||||
anchorOrigin={
|
|
||||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
|
||||||
}
|
|
||||||
transformOrigin={
|
|
||||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BorderPickerDialog>
|
|
||||||
<Borders>
|
|
||||||
<Line>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.All}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.All) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.All);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderAllIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.Inner}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.Inner) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.Inner);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderInnerIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.CenterH}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.CenterH) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.CenterH);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderCenterHIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.CenterV}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.CenterV) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.CenterV);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderCenterVIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.Outer}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.Outer) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.Outer);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderOuterIcon />
|
|
||||||
</Button>
|
|
||||||
</Line>
|
|
||||||
<Line>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.None}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.None) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderNoneIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.Top}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.Top) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.Top);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderTopIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.Right}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.Right) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.Right);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderRightIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.Bottom}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.Bottom) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.Bottom);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderBottomIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.Left}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.Left) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.Left);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderLeftIcon />
|
|
||||||
</Button>
|
|
||||||
</Line>
|
|
||||||
</Borders>
|
|
||||||
<Divider />
|
|
||||||
<Styles>
|
|
||||||
<ButtonWrapper onClick={() => setColorPickerOpen(true)}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
disabled={false}
|
|
||||||
ref={borderColorButton}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<PencilLine />
|
|
||||||
</Button>
|
|
||||||
<div style={{flexGrow:2}}>Border color</div>
|
|
||||||
<ChevronRightStyled />
|
|
||||||
</ButtonWrapper>
|
|
||||||
<ButtonWrapper onClick={() => setStylePickerOpen(true)} ref={borderStyleButton}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderStyleIcon />
|
|
||||||
</Button>
|
|
||||||
<div style={{flexGrow:2}}>Border style</div>
|
|
||||||
<ChevronRightStyled />
|
|
||||||
</ButtonWrapper>
|
|
||||||
</Styles>
|
|
||||||
</BorderPickerDialog>
|
|
||||||
<ColorPicker
|
|
||||||
color={borderColor}
|
|
||||||
onChange={(color): void => {
|
|
||||||
setBorderColor(color);
|
|
||||||
setColorPickerOpen(false);
|
|
||||||
}}
|
|
||||||
anchorEl={borderColorButton}
|
|
||||||
open={colorPickerOpen}
|
|
||||||
/>
|
|
||||||
<StyledPopover
|
|
||||||
open={stylePickerOpen}
|
|
||||||
onClose={(): void => {
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
anchorEl={borderStyleButton.current}
|
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
|
||||||
transformOrigin={{ vertical: 38, horizontal: -6 }}
|
|
||||||
>
|
|
||||||
<BorderStyleDialog>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dashed);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.None}
|
|
||||||
>
|
|
||||||
<BorderDescription>None</BorderDescription>
|
|
||||||
<NoneLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Thin);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Thin}
|
|
||||||
>
|
|
||||||
<BorderDescription>Thin</BorderDescription>
|
|
||||||
<SolidLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Medium);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Medium}
|
|
||||||
>
|
|
||||||
<BorderDescription>Medium</BorderDescription>
|
|
||||||
<MediumLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Thick);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Thick}
|
|
||||||
>
|
|
||||||
<BorderDescription>Thick</BorderDescription>
|
|
||||||
<ThickLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dotted);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Dotted}
|
|
||||||
>
|
|
||||||
<BorderDescription>Dotted</BorderDescription>
|
|
||||||
<DottedLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dashed);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Dashed}
|
|
||||||
>
|
|
||||||
<BorderDescription>Dashed</BorderDescription>
|
|
||||||
<DashedLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dashed);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Double}
|
|
||||||
>
|
|
||||||
<BorderDescription>Double</BorderDescription>
|
|
||||||
<DoubleLine />
|
|
||||||
</LineWrapper>
|
|
||||||
</BorderStyleDialog>
|
|
||||||
</StyledPopover>
|
|
||||||
</StyledPopover>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type LineWrapperProperties = { $checked: boolean };
|
|
||||||
const LineWrapper = styled("div")<LineWrapperProperties>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
background-color: ${({ $checked }): string => {
|
|
||||||
if ($checked) {
|
|
||||||
return '#EEEEEE;';
|
|
||||||
} else {
|
|
||||||
return 'inherit;';
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid #EEEEEE;
|
|
||||||
}
|
|
||||||
padding:8px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid white;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CheckIconWrapper = styled("div")`
|
|
||||||
width: 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type CheckIconProperties = { $checked: boolean };
|
|
||||||
const CheckIcon = styled("div")<CheckIconProperties>`
|
|
||||||
width: 2px;
|
|
||||||
background-color: #EEE;
|
|
||||||
height: 28px;
|
|
||||||
visibility: ${({ $checked }): string => {
|
|
||||||
if ($checked) {
|
|
||||||
return "visible";
|
|
||||||
}
|
|
||||||
return "hidden";
|
|
||||||
}};
|
|
||||||
`;
|
|
||||||
const NoneLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px solid #E0E0E0;
|
|
||||||
`;
|
|
||||||
const SolidLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px solid #333333;
|
|
||||||
`;
|
|
||||||
const MediumLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 2px solid #333333;
|
|
||||||
`;
|
|
||||||
const ThickLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 3px solid #333333;
|
|
||||||
`;
|
|
||||||
const DashedLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px dashed #333333;
|
|
||||||
`;
|
|
||||||
const DottedLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px dotted #333333;
|
|
||||||
`;
|
|
||||||
const DoubleLine = styled('div')`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 3px double #333333;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Divider = styled("div")`
|
|
||||||
display: inline-flex;
|
|
||||||
heigh: 1px;
|
|
||||||
border-bottom: 1px solid #EEE;
|
|
||||||
margin-left: 0px;
|
|
||||||
margin-right: 0px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Borders = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Styles = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Line = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ButtonWrapper = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
&:hover {
|
|
||||||
background-color: #EEE;
|
|
||||||
border-top-color: ${(): string => theme.palette.grey["400"]};
|
|
||||||
}
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BorderStyleDialog = styled("div")`
|
|
||||||
background: ${({ theme }): string => theme.palette.background.default};
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledPopover = styled(Popover)`
|
|
||||||
.MuiPopover-paper {
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 0px solid ${({ theme }): string => theme.palette.background.default};
|
|
||||||
box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
|
||||||
}
|
|
||||||
.MuiPopover-padding {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
.MuiList-padding {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
|
||||||
font-size: 13px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BorderPickerDialog = styled("div")`
|
|
||||||
background: ${({ theme }): string => theme.palette.background.default};
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BorderDescription = styled("div")`
|
|
||||||
width: 70px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
|
||||||
// const Button = styled.button<TypeButtonProperties>`
|
|
||||||
// width: 23px;
|
|
||||||
// height: 23px;
|
|
||||||
// display: inline-flex;
|
|
||||||
// align-items: center;
|
|
||||||
// justify-content: center;
|
|
||||||
// font-size: 14px;
|
|
||||||
// border-radius: 2px;
|
|
||||||
// margin-right: 5px;
|
|
||||||
// transition: all 0.2s;
|
|
||||||
|
|
||||||
// ${({ theme, disabled, $pressed, $underlinedColor }): string => {
|
|
||||||
// if (disabled) {
|
|
||||||
// return `
|
|
||||||
// color: ${theme.palette.grey['600']};
|
|
||||||
// cursor: default;
|
|
||||||
// `;
|
|
||||||
// }
|
|
||||||
// return `
|
|
||||||
// border-top: ${$underlinedColor ? '3px solid #FFF' : 'none'};
|
|
||||||
// border-bottom: ${$underlinedColor ? `3px solid ${$underlinedColor}` : 'none'};
|
|
||||||
// color: ${theme.palette.text.primary};
|
|
||||||
// background-color: ${$pressed ? theme.palette.grey['600'] : '#FFF'};
|
|
||||||
// &:hover {
|
|
||||||
// background-color: ${theme.palette.grey['400']};
|
|
||||||
// border-top-color: ${theme.palette.grey['400']};
|
|
||||||
// }
|
|
||||||
// `;
|
|
||||||
// }}
|
|
||||||
// `;
|
|
||||||
|
|
||||||
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
|
||||||
const Button = styled("button")<TypeButtonProperties>(
|
|
||||||
({ disabled, $pressed, $underlinedColor }) => {
|
|
||||||
let result: Record<string, any> = {
|
|
||||||
width: "24px",
|
|
||||||
height: "24px",
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
// fontSize: "26px",
|
|
||||||
border: "0px solid #fff",
|
|
||||||
borderRadius: "2px",
|
|
||||||
marginRight: "5px",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "0px",
|
|
||||||
};
|
|
||||||
if (disabled) {
|
|
||||||
result.color = theme.palette.grey["600"];
|
|
||||||
result.cursor = "default";
|
|
||||||
} else {
|
|
||||||
result.borderTop = $underlinedColor ? "3px solid #FFF" : "none";
|
|
||||||
result.borderBottom = $underlinedColor
|
|
||||||
? `3px solid ${$underlinedColor}`
|
|
||||||
: "none";
|
|
||||||
(result.color = "#21243A"),
|
|
||||||
(result.backgroundColor = $pressed
|
|
||||||
? theme.palette.grey["600"]
|
|
||||||
: "inherit");
|
|
||||||
result["&:hover"] = {
|
|
||||||
backgroundColor: "#F1F2F8",
|
|
||||||
borderTopColor: "#F1F2F8",
|
|
||||||
};
|
|
||||||
result["svg"] = {
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const ChevronRightStyled = styled(ChevronRight)`
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default BorderPicker;
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
import styled from "@emotion/styled";
|
|
||||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
|
||||||
import { theme } from "../theme";
|
|
||||||
|
|
||||||
type ColorPickerProps = {
|
|
||||||
className?: string;
|
|
||||||
color: string;
|
|
||||||
onChange: (color: string) => void;
|
|
||||||
anchorEl: React.RefObject<HTMLElement>;
|
|
||||||
anchorOrigin?: PopoverOrigin;
|
|
||||||
transformOrigin?: PopoverOrigin;
|
|
||||||
open: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorPickerWidth = 240;
|
|
||||||
const colorfulHeight = 185; // 150 + 15 + 20
|
|
||||||
|
|
||||||
const ColorPicker = (properties: ColorPickerProps) => {
|
|
||||||
const [color, setColor] = useState<string>(properties.color);
|
|
||||||
const recentColors = useRef<string[]>([]);
|
|
||||||
|
|
||||||
const closePicker = (newColor: string): void => {
|
|
||||||
const maxRecentColors = 14;
|
|
||||||
properties.onChange(newColor);
|
|
||||||
const colors = recentColors.current.filter((c) => c !== newColor);
|
|
||||||
recentColors.current = [newColor, ...colors].slice(0, maxRecentColors);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setColor(properties.color);
|
|
||||||
}, [properties.color]);
|
|
||||||
|
|
||||||
const presetColors = [
|
|
||||||
"#FFFFFF",
|
|
||||||
"#1B717E",
|
|
||||||
"#59B9BC",
|
|
||||||
"#3BB68A",
|
|
||||||
"#8CB354",
|
|
||||||
"#F8CD3C",
|
|
||||||
"#EC5753",
|
|
||||||
"#A23C52",
|
|
||||||
"#D03627",
|
|
||||||
"#523E93",
|
|
||||||
"#3358B7",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={properties.open}
|
|
||||||
onClose={(): void => closePicker(color)}
|
|
||||||
anchorEl={properties.anchorEl.current}
|
|
||||||
anchorOrigin={
|
|
||||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
|
||||||
}
|
|
||||||
transformOrigin={
|
|
||||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ColorPickerDialog>
|
|
||||||
<HexColorPicker
|
|
||||||
color={color}
|
|
||||||
onChange={(newColor): void => {
|
|
||||||
setColor(newColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ColorPickerInput>
|
|
||||||
<HexWrapper>
|
|
||||||
<HexLabel>{"Hex"}</HexLabel>
|
|
||||||
<HexColorInputBox>
|
|
||||||
<HashLabel>{"#"}</HashLabel>
|
|
||||||
<HexColorInput
|
|
||||||
color={color}
|
|
||||||
onChange={(newColor): void => {
|
|
||||||
setColor(newColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</HexColorInputBox>
|
|
||||||
</HexWrapper>
|
|
||||||
<Swatch $color={color} onClick={(): void => {
|
|
||||||
closePicker(color);
|
|
||||||
}} />
|
|
||||||
</ColorPickerInput>
|
|
||||||
<HorizontalDivider />
|
|
||||||
<ColorList>
|
|
||||||
{presetColors.map((presetColor) => (
|
|
||||||
<Button
|
|
||||||
key={presetColor}
|
|
||||||
$color={presetColor}
|
|
||||||
onClick={(): void => {
|
|
||||||
closePicker(presetColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ColorList>
|
|
||||||
<HorizontalDivider />
|
|
||||||
<RecentLabel>{"Recent"}</RecentLabel>
|
|
||||||
<ColorList>
|
|
||||||
{recentColors.current.map((recentColor) => (
|
|
||||||
<Button
|
|
||||||
key={recentColor}
|
|
||||||
$color={recentColor}
|
|
||||||
onClick={(): void => {
|
|
||||||
closePicker(recentColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ColorList>
|
|
||||||
</ColorPickerDialog>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RecentLabel = styled.div`
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${theme.palette.text.secondary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColorList = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Button = styled.button<{ $color: string }>`
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
${({ $color }): string => {
|
|
||||||
if ($color.toUpperCase() === "#FFFFFF") {
|
|
||||||
return `border: 1px solid ${theme.palette.grey["600"]};`;
|
|
||||||
}
|
|
||||||
return `border: 1px solid ${$color};`;
|
|
||||||
}}
|
|
||||||
background-color: ${({ $color }): string => {
|
|
||||||
return $color;
|
|
||||||
}};
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HorizontalDivider = styled.div`
|
|
||||||
height: 0px;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px solid ${theme.palette.grey["400"]};
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// const StyledPopover = styled(Popover)`
|
|
||||||
// .MuiPopover-paper {
|
|
||||||
// border-radius: 10px;
|
|
||||||
// border: 0px solid ${theme.palette.background.default};
|
|
||||||
// box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
|
||||||
// }
|
|
||||||
// .MuiPopover-padding {
|
|
||||||
// padding: 0px;
|
|
||||||
// }
|
|
||||||
// .MuiList-padding {
|
|
||||||
// padding: 0px;
|
|
||||||
// }
|
|
||||||
// `;
|
|
||||||
|
|
||||||
const ColorPickerDialog = styled.div`
|
|
||||||
background: ${theme.palette.background.default};
|
|
||||||
width: ${colorPickerWidth}px;
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
& .react-colorful {
|
|
||||||
height: ${colorfulHeight}px;
|
|
||||||
width: ${colorPickerWidth}px;
|
|
||||||
}
|
|
||||||
& .react-colorful__saturation {
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
& .react-colorful__hue {
|
|
||||||
height: 20px;
|
|
||||||
margin-top: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
& .react-colorful__saturation-pointer {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
& .react-colorful__hue-pointer {
|
|
||||||
width: 7px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HashLabel = styled.div`
|
|
||||||
margin: auto 0px auto 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #7d8ec2;
|
|
||||||
font-family: ${theme.typography.button.fontFamily};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HexLabel = styled.div`
|
|
||||||
margin: auto 10px auto 0px;
|
|
||||||
font-size: 12px;
|
|
||||||
display: inline-flex;
|
|
||||||
font-family: ${theme.typography.button.fontFamily};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HexColorInputBox = styled.div`
|
|
||||||
display: inline-flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 140px;
|
|
||||||
height: 28px;
|
|
||||||
border: 1px solid ${theme.palette.grey["600"]};
|
|
||||||
border-radius: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HexWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
& input {
|
|
||||||
min-width: 0px;
|
|
||||||
border: 0px;
|
|
||||||
background: ${theme.palette.background.default};
|
|
||||||
outline: none;
|
|
||||||
font-family: ${theme.typography.button.fontFamily};
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& input:focus {
|
|
||||||
border-color: #4298ef;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Swatch = styled.div<{ $color: string }>`
|
|
||||||
display: inline-flex;
|
|
||||||
${({ $color }): string => {
|
|
||||||
if ($color.toUpperCase() === "#FFFFFF") {
|
|
||||||
return `border: 1px solid ${theme.palette.grey["600"]};`;
|
|
||||||
}
|
|
||||||
return `border: 1px solid ${$color};`;
|
|
||||||
}}
|
|
||||||
background-color: ${({ $color }): string => $color};
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColorPickerInput = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default ColorPicker;
|
|
||||||
@@ -1,420 +0,0 @@
|
|||||||
import {
|
|
||||||
CSSProperties,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
KeyboardEvent,
|
|
||||||
useContext,
|
|
||||||
} from "react";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import EditorContext, { Area } from "./editorContext";
|
|
||||||
import { getStringRange } from "./util";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the Cell Editor for IronCalc
|
|
||||||
* I uses a transparent textarea and a styled mask. What you see is the HTML styled content of the mask
|
|
||||||
* and the caret from the textarea. The alternative would be to have a 'contenteditable' div.
|
|
||||||
* That turns out to be a much more difficult implementation.
|
|
||||||
*
|
|
||||||
* The editor grows horizontally with text if it fits in the screen.
|
|
||||||
* If it doesn't fit, it wraps and grows vertically. If it doesn't fit vertically it scrolls.
|
|
||||||
*
|
|
||||||
* Many keyboard and mouse events are handled gracefully by the textarea in full or in part.
|
|
||||||
* For example letter key strokes like 'q' or '1' are handled full by the textarea.
|
|
||||||
* Some keyboard events like "RightArrow" might need to be handled separately and let them bubble up,
|
|
||||||
* or might be handled by the textarea, depending on the "editor mode".
|
|
||||||
* Some other like "Enter" we need to intercept and change the normal behaviour.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const commonCSS: CSSProperties = {
|
|
||||||
fontWeight: "inherit",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
fontSize: "inherit",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
whiteSpace: "pre",
|
|
||||||
width: "100%",
|
|
||||||
padding: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Cell {
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EditorOptions {
|
|
||||||
minimalWidth: number;
|
|
||||||
minimalHeight: number;
|
|
||||||
textColor: string;
|
|
||||||
originalText: string;
|
|
||||||
getStyledText: (
|
|
||||||
text: string,
|
|
||||||
insertRangeText: string
|
|
||||||
) => {
|
|
||||||
html: JSX.Element[];
|
|
||||||
isInReferenceMode: boolean;
|
|
||||||
};
|
|
||||||
onEditEnd: (text: string) => void;
|
|
||||||
display: boolean;
|
|
||||||
cell: Cell;
|
|
||||||
sheetNames: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can either be editing a formula or content.
|
|
||||||
// When editing content (behaviour is common to Excel and Google Sheets):
|
|
||||||
// * If you start editing by typing you are in *accept* mode
|
|
||||||
// * If you start editing by F2 you are in *cruise* mode
|
|
||||||
// * If you start editing by double click you are in *cruise* mode
|
|
||||||
// In Google Sheets "Enter" starts editing and puts you in *cruise* mode. We do not do that
|
|
||||||
// Once you are in cruise mode it is not possible to switch to accept mode
|
|
||||||
// The only way to go from accept mode to cruise mode is clicking in the content somewhere
|
|
||||||
|
|
||||||
// When editing a formula.
|
|
||||||
// In Google Sheets you are either in insert mode or cruise mode.
|
|
||||||
// You can get back to accept mode if you delete the whole formula
|
|
||||||
// In Excel you can be either in insert or accept but if you click in the formula body
|
|
||||||
// you switch to cruise mode. Once in cruise mode you can go to insert mode by selecting a range.
|
|
||||||
// Then you are back in accept/insert modes
|
|
||||||
|
|
||||||
const Editor = (options: EditorOptions) => {
|
|
||||||
const {
|
|
||||||
minimalWidth,
|
|
||||||
minimalHeight,
|
|
||||||
textColor,
|
|
||||||
onEditEnd,
|
|
||||||
originalText,
|
|
||||||
display,
|
|
||||||
cell,
|
|
||||||
sheetNames,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const [width, setWidth] = useState(minimalWidth);
|
|
||||||
const [height, setHeight] = useState(minimalHeight);
|
|
||||||
|
|
||||||
const { editorContext, setEditorContext } = useContext(EditorContext);
|
|
||||||
|
|
||||||
const setBaseText = (newText: string) => {
|
|
||||||
console.log('Calling setBaseText');
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
baseText: newText,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const insertRangeText = editorContext.insertRange
|
|
||||||
? getStringRange(editorContext.insertRange, sheetNames)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const baseText = editorContext.baseText;
|
|
||||||
const text = baseText + insertRangeText;
|
|
||||||
// console.log('baseText', baseText, 'insertRange:', insertRangeText);
|
|
||||||
|
|
||||||
const formulaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const maskRef = useRef<HTMLDivElement>(null);
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// setBaseText(originalText);
|
|
||||||
// }, [cell]);
|
|
||||||
|
|
||||||
const { html: styledFormula, isInReferenceMode } = options.getStyledText(
|
|
||||||
baseText,
|
|
||||||
insertRangeText
|
|
||||||
);
|
|
||||||
|
|
||||||
if (display && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formulaRef.current) {
|
|
||||||
const scrollWidth = formulaRef.current.scrollWidth;
|
|
||||||
if (scrollWidth > width) {
|
|
||||||
setWidth(scrollWidth);
|
|
||||||
} else if (scrollWidth <= minimalWidth) {
|
|
||||||
setWidth(minimalWidth);
|
|
||||||
}
|
|
||||||
const scrollHeight = formulaRef.current.scrollHeight;
|
|
||||||
if (scrollHeight > height) {
|
|
||||||
setHeight(scrollHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [text]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInReferenceMode) {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
mode: "insert",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
mode: "cruise",
|
|
||||||
insertRange: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [isInReferenceMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (display && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [display]);
|
|
||||||
|
|
||||||
// console.log("Ok, this is running", text, editorContext.id);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
const { key, shiftKey, altKey } = event;
|
|
||||||
const textarea = textareaRef.current;
|
|
||||||
const mode = editorContext.mode;
|
|
||||||
if (!textarea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (key) {
|
|
||||||
case "Enter": {
|
|
||||||
if (altKey) {
|
|
||||||
// new line
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const newText = text.slice(0, start) + "\n" + text.slice(end);
|
|
||||||
setBaseText(newText);
|
|
||||||
setTimeout(() => {
|
|
||||||
textarea.setSelectionRange(start + 1, start + 1);
|
|
||||||
}, 1);
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// end edit
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
// event bubbles up
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Escape": {
|
|
||||||
setBaseText(originalText);
|
|
||||||
textarea.blur();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowLeft": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
// event bubbles up
|
|
||||||
return;
|
|
||||||
} else if (mode == "insert") {
|
|
||||||
if (shiftKey) {
|
|
||||||
// increase the inserted range to the left
|
|
||||||
if (!editorContext.insertRange) {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
insertRange: {
|
|
||||||
absoluteColumnEnd: false,
|
|
||||||
absoluteColumnStart: false,
|
|
||||||
absoluteRowEnd: false,
|
|
||||||
absoluteRowStart: false,
|
|
||||||
sheet: cell.sheet,
|
|
||||||
rowStart: cell.row,
|
|
||||||
rowEnd: cell.row,
|
|
||||||
columnStart: cell.column,
|
|
||||||
columnEnd: cell.column,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// const r = insertRage;
|
|
||||||
// r.columnStart = Math.max(r.columnStart - 1, 1);
|
|
||||||
// setInsertRange(r);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// move inserted cell to the left
|
|
||||||
if (!editorContext.insertRange) {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
insertRange: {
|
|
||||||
absoluteColumnEnd: false,
|
|
||||||
absoluteColumnStart: false,
|
|
||||||
absoluteRowEnd: false,
|
|
||||||
absoluteRowStart: false,
|
|
||||||
sheet: cell.sheet,
|
|
||||||
rowStart: cell.row,
|
|
||||||
rowEnd: cell.row,
|
|
||||||
columnStart: cell.column,
|
|
||||||
columnEnd: cell.column,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
const range = c.insertRange as Area;
|
|
||||||
const row = range.rowStart;
|
|
||||||
let column = range.columnStart - 1;
|
|
||||||
if (column < 1) {
|
|
||||||
column = 1;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
insertRange: {
|
|
||||||
absoluteColumnEnd: false,
|
|
||||||
absoluteColumnStart: false,
|
|
||||||
absoluteRowEnd: false,
|
|
||||||
absoluteRowStart: false,
|
|
||||||
sheet: range.sheet,
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: column,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We don't do anything in "cruise mode" and rely on the textarea default behaviour
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowDown": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowRight": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowUp": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Tab": {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
// event bubbles up
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (editorContext.mode === "insert") {
|
|
||||||
setBaseText(text);
|
|
||||||
setEditorContext((context) => {
|
|
||||||
return {
|
|
||||||
...context,
|
|
||||||
mode: "cruise",
|
|
||||||
insertRange: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[text, editorContext]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
overflow: "hidden",
|
|
||||||
background: "#FFF",
|
|
||||||
display: display ? "block" : "none",
|
|
||||||
}}
|
|
||||||
onClick={(_event) => {
|
|
||||||
console.log("Click on wrapper");
|
|
||||||
}}
|
|
||||||
onPointerDown={() => {
|
|
||||||
console.log("On pointer down wrapper");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={maskRef}
|
|
||||||
style={{
|
|
||||||
...commonCSS,
|
|
||||||
textAlign: "left",
|
|
||||||
pointerEvents: "none",
|
|
||||||
height,
|
|
||||||
}}
|
|
||||||
onClick={(_event) => {
|
|
||||||
console.log("Click on mask");
|
|
||||||
}}
|
|
||||||
onPointerDown={() => {
|
|
||||||
console.log("On pointer down mask");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div ref={formulaRef}>{styledFormula}</div>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
rows={1}
|
|
||||||
style={{
|
|
||||||
...commonCSS,
|
|
||||||
color: "transparent",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
caretColor: textColor,
|
|
||||||
outline: "none",
|
|
||||||
resize: "none",
|
|
||||||
border: "none",
|
|
||||||
height,
|
|
||||||
}}
|
|
||||||
spellCheck="false"
|
|
||||||
value={text}
|
|
||||||
onChange={(event) => {
|
|
||||||
console.log("onChange", event.target.value);
|
|
||||||
setBaseText(event.target.value);
|
|
||||||
}}
|
|
||||||
onScroll={() => {
|
|
||||||
if (maskRef.current && textareaRef.current) {
|
|
||||||
maskRef.current.style.left = `-${textareaRef.current.scrollLeft}px`;
|
|
||||||
maskRef.current.style.top = `-${textareaRef.current.scrollTop}px`;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onClick={(event) => {
|
|
||||||
console.log("Setting mode");
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
mode: "cruise",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
console.log("here");
|
|
||||||
// if (display) {
|
|
||||||
event.stopPropagation();
|
|
||||||
// }
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
// on blur
|
|
||||||
}}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Editor;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Dispatch, SetStateAction, createContext } from "react";
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
sheet: number | null;
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
absoluteRowStart: boolean;
|
|
||||||
absoluteRowEnd: boolean;
|
|
||||||
absoluteColumnStart: boolean;
|
|
||||||
absoluteColumnEnd: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrow keys behave in different ways depending on the "edit mode":
|
|
||||||
// * In _cruise_ mode arrowy keys navigate within the editor
|
|
||||||
// * In _accept_ mode pressing an arrow key will end editing
|
|
||||||
// * In _insert_ mode arrow keys will change the selected range
|
|
||||||
export type EditorMode = "cruise" | "accept" | "insert";
|
|
||||||
|
|
||||||
export interface EditorState {
|
|
||||||
mode: EditorMode;
|
|
||||||
insertRange: null | Area;
|
|
||||||
baseText: string;
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EditorContextType {
|
|
||||||
editorContext: EditorState;
|
|
||||||
setEditorContext: Dispatch<
|
|
||||||
SetStateAction<{ mode: EditorMode; insertRange: null | Area }>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditorContext = createContext<EditorContextType>({
|
|
||||||
editorContext: {
|
|
||||||
mode: "accept",
|
|
||||||
insertRange: null,
|
|
||||||
baseText: '',
|
|
||||||
id: Math.floor(Math.random()*1000),
|
|
||||||
},
|
|
||||||
setEditorContext: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default EditorContext;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { default } from './editor';
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
type ErrorType =
|
|
||||||
| 'REF'
|
|
||||||
| 'NAME'
|
|
||||||
| 'VALUE'
|
|
||||||
| 'DIV'
|
|
||||||
| 'NA'
|
|
||||||
| 'NUM'
|
|
||||||
| 'ERROR'
|
|
||||||
| 'NIMPL'
|
|
||||||
| 'SPILL'
|
|
||||||
| 'CALC'
|
|
||||||
| 'CIRC';
|
|
||||||
|
|
||||||
type OpCompareType =
|
|
||||||
| 'LessThan'
|
|
||||||
| 'GreaterThan'
|
|
||||||
| 'Equal'
|
|
||||||
| 'LessOrEqualThan'
|
|
||||||
| 'GreaterOrEqualThan'
|
|
||||||
| 'NonEqual';
|
|
||||||
|
|
||||||
type OpSumType = 'Add' | 'Minus';
|
|
||||||
|
|
||||||
type OpProductType = 'Times' | 'Divide';
|
|
||||||
|
|
||||||
interface ReferenceType {
|
|
||||||
sheet: string | null;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
absolute_column: boolean;
|
|
||||||
absolute_row: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParsedReferenceType {
|
|
||||||
column: number;
|
|
||||||
row: number;
|
|
||||||
absolute_column: boolean;
|
|
||||||
absolute_row: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Reference {
|
|
||||||
Reference: ReferenceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Range {
|
|
||||||
Range: {
|
|
||||||
sheet: string | null;
|
|
||||||
left: ParsedReferenceType;
|
|
||||||
right: ParsedReferenceType;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TokenType =
|
|
||||||
| 'Illegal'
|
|
||||||
| 'Eof'
|
|
||||||
| { Ident: string }
|
|
||||||
| { String: string }
|
|
||||||
| { Boolean: boolean }
|
|
||||||
| { Number: number }
|
|
||||||
| { ERROR: ErrorType }
|
|
||||||
| { COMPARE: OpCompareType }
|
|
||||||
| { SUM: OpSumType }
|
|
||||||
| { PRODUCT: OpProductType }
|
|
||||||
| 'POWER'
|
|
||||||
| 'LPAREN'
|
|
||||||
| 'RPAREN'
|
|
||||||
| 'COLON'
|
|
||||||
| 'SEMICOLON'
|
|
||||||
| 'LBRACKET'
|
|
||||||
| 'RBRACKET'
|
|
||||||
| 'LBRACE'
|
|
||||||
| 'RBRACE'
|
|
||||||
| 'COMMA'
|
|
||||||
| 'BANG'
|
|
||||||
| 'PERCENT'
|
|
||||||
| 'AND'
|
|
||||||
| Reference
|
|
||||||
| Range;
|
|
||||||
|
|
||||||
export interface MarkedToken {
|
|
||||||
token: TokenType;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tokenIsReferenceType(token: TokenType): token is Reference {
|
|
||||||
return typeof token === 'object' && 'Reference' in token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tokenIsRangeType(token: TokenType): token is Range {
|
|
||||||
return typeof token === 'object' && 'Range' in token;
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { useCallback, KeyboardEvent } from "react";
|
|
||||||
import { WorkbookState } from "../workbookState";
|
|
||||||
import { Model } from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
// onMoveCaretToStart: () => void;
|
|
||||||
// onMoveCaretToEnd: () => void;
|
|
||||||
// onEditEnd: (delta: { deltaRow: number; deltaColumn: number }) => void;
|
|
||||||
// onEditEscape: () => void;
|
|
||||||
// onReferenceCycle: () => void;
|
|
||||||
// text: string;
|
|
||||||
// setText: (text: string) => void;
|
|
||||||
model: Model;
|
|
||||||
state: WorkbookState;
|
|
||||||
refresh: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useEditorKeydown = (
|
|
||||||
options: Options
|
|
||||||
): {
|
|
||||||
onKeyDown: (event: KeyboardEvent) => void;
|
|
||||||
} => {
|
|
||||||
const { state, model } = options;
|
|
||||||
const onKeyDown = useCallback((event: KeyboardEvent) => {
|
|
||||||
const { key, shiftKey } = event;
|
|
||||||
const { mode, text } = state.getEditor() ?? { mode: "init", text: "" };
|
|
||||||
switch (key) {
|
|
||||||
// case "Enter":
|
|
||||||
// // options.onEditEnd({ deltaRow: 1, deltaColumn: 0 });
|
|
||||||
// const { row, column } = state.getSelectedCell();
|
|
||||||
// const sheet = state.getSelectedSheet();
|
|
||||||
// model.setUserInput(sheet, row, column, text);
|
|
||||||
// state.selectCell({ row: row + 1, column });
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// options.refresh();
|
|
||||||
// break;
|
|
||||||
// case 'ArrowUp': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: -1, deltaColumn: 0 });
|
|
||||||
// } else {
|
|
||||||
// options.onMoveCaretToStart();
|
|
||||||
// }
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'ArrowDown': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: 1, deltaColumn: 0 });
|
|
||||||
// } else {
|
|
||||||
// options.onMoveCaretToEnd();
|
|
||||||
// }
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'Tab': {
|
|
||||||
// if (event.shiftKey) {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: -1 });
|
|
||||||
// } else {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: 1 });
|
|
||||||
// }
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'Escape': {
|
|
||||||
// options.onEditEscape();
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'ArrowLeft': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: -1 });
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'ArrowRight': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: 1 });
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'F4': {
|
|
||||||
// options.onReferenceCycle();
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, [model, state]);
|
|
||||||
return { onKeyDown };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useEditorKeydown;
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
import { getTokens } from "@ironcalc/wasm";
|
|
||||||
import { tokenIsRangeType, tokenIsReferenceType } from "./tokenTypes";
|
|
||||||
import { Area } from "./editorContext";
|
|
||||||
|
|
||||||
const letters = [
|
|
||||||
"A",
|
|
||||||
"B",
|
|
||||||
"C",
|
|
||||||
"D",
|
|
||||||
"E",
|
|
||||||
"F",
|
|
||||||
"G",
|
|
||||||
"H",
|
|
||||||
"I",
|
|
||||||
"J",
|
|
||||||
"K",
|
|
||||||
"L",
|
|
||||||
"M",
|
|
||||||
"N",
|
|
||||||
"O",
|
|
||||||
"P",
|
|
||||||
"Q",
|
|
||||||
"R",
|
|
||||||
"S",
|
|
||||||
"T",
|
|
||||||
"U",
|
|
||||||
"V",
|
|
||||||
"W",
|
|
||||||
"X",
|
|
||||||
"Y",
|
|
||||||
"Z",
|
|
||||||
];
|
|
||||||
interface Reference {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
absoluteRow: boolean;
|
|
||||||
absoluteColumn: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function referenceToString(rf: Reference): string {
|
|
||||||
const absC = rf.absoluteColumn ? "$" : "";
|
|
||||||
const absR = rf.absoluteRow ? "$" : "";
|
|
||||||
return absC + columnNameFromNumber(rf.column) + absR + rf.row;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNameFromNumber(column: number): string {
|
|
||||||
let columnName = "";
|
|
||||||
let index = column;
|
|
||||||
while (index > 0) {
|
|
||||||
columnName = `${letters[(index - 1) % 26]}${columnName}`;
|
|
||||||
index = Math.floor((index - 1) / 26);
|
|
||||||
}
|
|
||||||
return columnName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNumberFromName(columnName: string): number {
|
|
||||||
let column = 0;
|
|
||||||
for (const character of columnName) {
|
|
||||||
const index = (character.codePointAt(0) ?? 0) - 64;
|
|
||||||
column = column * 26 + index;
|
|
||||||
}
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
interface Range {
|
|
||||||
sheet: number | null;
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
absoluteRowStart: boolean;
|
|
||||||
absoluteRowEnd: boolean;
|
|
||||||
absoluteColumnStart: boolean;
|
|
||||||
absoluteColumnEnd: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStringRange(range: Range, sheetNames: string[]) {
|
|
||||||
const name = range.sheet ? `${sheetNames[range.sheet]}!` : "";
|
|
||||||
const left = referenceToString({
|
|
||||||
row: range.rowStart,
|
|
||||||
column: range.columnStart,
|
|
||||||
absoluteRow: range.absoluteRowStart,
|
|
||||||
absoluteColumn: range.absoluteColumnStart,
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
range.rowStart === range.rowEnd &&
|
|
||||||
range.columnStart === range.columnEnd
|
|
||||||
) {
|
|
||||||
return `${name}${left}`;
|
|
||||||
}
|
|
||||||
const right = referenceToString({
|
|
||||||
row: range.rowEnd,
|
|
||||||
column: range.columnEnd,
|
|
||||||
absoluteRow: range.absoluteRowEnd,
|
|
||||||
absoluteColumn: range.absoluteColumnEnd,
|
|
||||||
});
|
|
||||||
return `${name}${left}:${right}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActiveRange {
|
|
||||||
sheet: number;
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IronCalc Color Palette
|
|
||||||
export function getColor(index: number, alpha = 1): string {
|
|
||||||
const colors = [
|
|
||||||
{
|
|
||||||
name: "Cyan",
|
|
||||||
rgba: [89, 185, 188, 1],
|
|
||||||
hex: "#59B9BC",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Flamingo",
|
|
||||||
rgba: [236, 87, 83, 1],
|
|
||||||
hex: "#EC5753",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#3358B7",
|
|
||||||
rgba: [51, 88, 183, 1],
|
|
||||||
name: "Blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#F8CD3C",
|
|
||||||
rgba: [248, 205, 60, 1],
|
|
||||||
name: "Yellow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#3BB68A",
|
|
||||||
rgba: [59, 182, 138, 1],
|
|
||||||
name: "Emerald",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#523E93",
|
|
||||||
rgba: [82, 62, 147, 1],
|
|
||||||
name: "Violet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#A23C52",
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: "Burgundy",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#8CB354",
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: "Wasabi",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#D03627",
|
|
||||||
rgba: [208, 54, 39, 1],
|
|
||||||
name: "Red",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#1B717E",
|
|
||||||
rgba: [27, 113, 126, 1],
|
|
||||||
name: "Teal",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (alpha === 1) {
|
|
||||||
return colors[index % 10].hex;
|
|
||||||
}
|
|
||||||
const { rgba } = colors[index % 10];
|
|
||||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* This function get a formula like `=A1*SUM(B5:C6)` and transforms it to:
|
|
||||||
*
|
|
||||||
* `<span>=</span><span>A1</span><span>SUM</span><span>(</span><span>B5:C6</span><span>)</span>`
|
|
||||||
*
|
|
||||||
* While also returning the set of ranges [A1, B5:C6] with specific color assignments for each range
|
|
||||||
*/
|
|
||||||
export function getFormulaHTML(
|
|
||||||
text: string,
|
|
||||||
sheet: number,
|
|
||||||
sheetList: string[],
|
|
||||||
insertRage: Area | null,
|
|
||||||
insertRangeText: string
|
|
||||||
): {
|
|
||||||
html: JSX.Element[];
|
|
||||||
activeRanges: ActiveRange[];
|
|
||||||
isInReferenceMode: boolean;
|
|
||||||
} {
|
|
||||||
let html = [];
|
|
||||||
const activeRanges: ActiveRange[] = [];
|
|
||||||
let colorCount = 0;
|
|
||||||
if (text.startsWith("=")) {
|
|
||||||
const formula = text.slice(1);
|
|
||||||
|
|
||||||
const tokens = getTokens(formula);
|
|
||||||
const tokenCount = tokens.length;
|
|
||||||
const usedColors: Record<string, string> = {};
|
|
||||||
for (let index = 0; index < tokenCount; index += 1) {
|
|
||||||
const { token, start, end } = tokens[index];
|
|
||||||
if (tokenIsReferenceType(token)) {
|
|
||||||
const { sheet: refSheet, row, column } = token.Reference;
|
|
||||||
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
|
||||||
const key = `${sheetIndex}-${row}-${column}`;
|
|
||||||
let color = usedColors[key];
|
|
||||||
if (!color) {
|
|
||||||
color = getColor(colorCount);
|
|
||||||
usedColors[key] = color;
|
|
||||||
colorCount += 1;
|
|
||||||
}
|
|
||||||
html.push(
|
|
||||||
<span key={index} style={{ color }}>
|
|
||||||
{formula.slice(start, end)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
activeRanges.push({
|
|
||||||
sheet: sheetIndex,
|
|
||||||
rowStart: row,
|
|
||||||
columnStart: column,
|
|
||||||
rowEnd: row,
|
|
||||||
columnEnd: column,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
} else if (tokenIsRangeType(token)) {
|
|
||||||
let {
|
|
||||||
sheet: refSheet,
|
|
||||||
left: { row: rowStart, column: columnStart },
|
|
||||||
right: { row: rowEnd, column: columnEnd },
|
|
||||||
} = token.Range;
|
|
||||||
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
|
||||||
|
|
||||||
const key = `${sheetIndex}-${rowStart}-${columnStart}:${rowEnd}-${columnEnd}`;
|
|
||||||
let color = usedColors[key];
|
|
||||||
if (!color) {
|
|
||||||
color = getColor(colorCount);
|
|
||||||
usedColors[key] = color;
|
|
||||||
colorCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowStart > rowEnd) {
|
|
||||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
|
||||||
}
|
|
||||||
if (columnStart > columnEnd) {
|
|
||||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
|
||||||
}
|
|
||||||
html.push(
|
|
||||||
<span key={index} style={{ color }}>
|
|
||||||
{formula.slice(start, end)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
colorCount += 1;
|
|
||||||
|
|
||||||
activeRanges.push({
|
|
||||||
sheet: sheetIndex,
|
|
||||||
rowStart,
|
|
||||||
columnStart,
|
|
||||||
rowEnd,
|
|
||||||
columnEnd,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
html.push(<span key={index}>{formula.slice(start, end)}</span>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tokenCount > 0) {
|
|
||||||
const lastToken = tokens[tokens.length - 1];
|
|
||||||
if (lastToken.end < text.length - 1) {
|
|
||||||
html.push(
|
|
||||||
<span key="rest">{text.slice(lastToken.end + 1, text.length)}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html = [<span key="equals">=</span>].concat(html);
|
|
||||||
} else {
|
|
||||||
html = [<span key="single">{text}</span>];
|
|
||||||
}
|
|
||||||
const isRefMode = isInReferenceMode(text, text.length);
|
|
||||||
if (isRefMode) {
|
|
||||||
if (insertRage) {
|
|
||||||
const color = getColor(colorCount);
|
|
||||||
activeRanges.push({
|
|
||||||
sheet: insertRage.sheet || sheet,
|
|
||||||
rowStart: insertRage.rowStart,
|
|
||||||
rowEnd: insertRage.rowEnd,
|
|
||||||
columnStart: insertRage.columnStart,
|
|
||||||
columnEnd: insertRage.columnEnd,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
colorCount += 1;
|
|
||||||
html.push(
|
|
||||||
<span key="insert-range" style={{ color, textDecoration: "underline" }}>
|
|
||||||
{insertRangeText}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
html.push(
|
|
||||||
<span
|
|
||||||
key="insert-cue"
|
|
||||||
style={{
|
|
||||||
border: "1px solid #d5d5d5",
|
|
||||||
height: "2px",
|
|
||||||
width: "7px",
|
|
||||||
borderTop: 0,
|
|
||||||
display: "inline-block",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We add a clickable element that spans the rest of the available space
|
|
||||||
html.push(<span key="spacer" style={{ flexGrow: 1 }}></span>);
|
|
||||||
return { html, activeRanges, isInReferenceMode: isRefMode };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isInReferenceMode(text: string, cursor: number): boolean {
|
|
||||||
// FIXME
|
|
||||||
// This is a gross oversimplification
|
|
||||||
// Returns true if both are true:
|
|
||||||
// 1. Cursor is at the end
|
|
||||||
// 2. Last char is one of [',', '(', '+', '*', '-', '/', '<', '>', '=', '&']
|
|
||||||
// This has many false positives like '="1+' and also likely some false negatives
|
|
||||||
// The right way of doing this is to have a partial parse of the formula tree
|
|
||||||
// and check if the next token could be a reference
|
|
||||||
if (!text.startsWith("=")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (text === "=") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const l = text.length;
|
|
||||||
const chars = [",", "(", "+", "*", "-", "/", "<", ">", "=", "&"];
|
|
||||||
if (cursor === l && chars.includes(text[l - 1])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||