Compare commits

..

1 Commits

Author SHA1 Message Date
Nicolás Hatcher
ce3f0f33c2 UPDATE: Update to Rust 2024 edition 2025-02-23 12:41:36 +01:00
108 changed files with 2466 additions and 2330 deletions

View File

@@ -2,7 +2,7 @@
name = "ironcalc_base"
version = "0.3.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021"
edition = "2024"
homepage = "https://www.ironcalc.com"
repository = "https://github.com/ironcalc/ironcalc/"
description = "Open source spreadsheet engine"

View File

@@ -1,4 +1,4 @@
use ironcalc_base::{types::CellType, Model};
use ironcalc_base::{Model, types::CellType};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?;

View File

@@ -1,4 +1,4 @@
use ironcalc_base::{cell::CellValue, Model};
use ironcalc_base::{Model, cell::CellValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut model = Model::new_empty("hello-world", "en", "UTC")?;

View File

@@ -2,7 +2,7 @@ use crate::{
expressions::{
parser::{
move_formula::ref_is_in_area,
stringify::{to_string, to_string_displaced, DisplaceData},
stringify::{DisplaceData, to_string, to_string_displaced},
walk::forward_references,
},
types::{Area, CellReferenceIndex, CellReferenceRC},

View File

@@ -149,14 +149,16 @@ impl Lexer {
Ok(n) => n,
Err(_) => {
return Err(self
.set_error(&format!("Failed parsing row {}", row_left), position))
.set_error(&format!("Failed parsing row {}", row_left), position));
}
};
let row_right = match row_right.parse::<i32>() {
Ok(n) => n,
Err(_) => {
return Err(self
.set_error(&format!("Failed parsing row {}", row_right), position))
return Err(self.set_error(
&format!("Failed parsing row {}", row_right),
position,
));
}
};
if row_left > LAST_ROW {

View File

@@ -1,6 +1,6 @@
use super::{
stringify::{stringify_reference, DisplaceData},
Node, Reference,
stringify::{DisplaceData, stringify_reference},
};
use crate::{
constants::{LAST_COLUMN, LAST_ROW},

View File

@@ -4,7 +4,7 @@ use std::collections::HashMap;
use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::{
to_rc_format, to_string, to_string_displaced, DisplaceData,
DisplaceData, to_rc_format, to_string, to_string_displaced,
};
use crate::expressions::parser::{Node, Parser};
use crate::expressions::types::CellReferenceRC;

View File

@@ -2,8 +2,8 @@
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::types::CellReferenceRC;
#[test]

View File

@@ -1,7 +1,7 @@
use std::collections::HashMap;
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
use crate::expressions::parser::Parser;
use crate::expressions::parser::move_formula::{MoveContext, move_formula};
use crate::expressions::types::{Area, CellReferenceRC};
#[test]

View File

@@ -2,8 +2,8 @@ use std::collections::HashMap;
use crate::expressions::lexer::LexerMode;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::parser::Parser;
use crate::expressions::parser::stringify::{to_rc_format, to_string};
use crate::expressions::types::CellReferenceRC;
struct Formula<'a> {

View File

@@ -2,8 +2,8 @@
use std::collections::HashMap;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::parser::Parser;
use crate::expressions::parser::stringify::to_string;
use crate::expressions::types::CellReferenceRC;
#[test]

View File

@@ -1,4 +1,4 @@
use super::{move_formula::ref_is_in_area, Node};
use super::{Node, move_formula::ref_is_in_area};
use crate::expressions::types::{Area, CellReferenceIndex};

View File

@@ -161,7 +161,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
text: "#VALUE!".to_owned(),
color: None,
error: Some(e),
}
};
}
};
for token in tokens {
@@ -391,11 +391,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if l_exp <= p.exponent_digit_count {
if !(number_index < 0 && digit.kind == '#') {
let c = if number_index < 0 {
if digit.kind == '?' {
' '
} else {
'0'
}
if digit.kind == '?' { ' ' } else { '0' }
} else {
exponent_part[number_index as usize]
};

View File

@@ -2,7 +2,7 @@
use crate::{
formatter::format::format_number,
locale::{get_locale, Locale},
locale::{Locale, get_locale},
};
fn get_default_locale() -> &'static Locale {

View File

@@ -31,7 +31,7 @@ impl Model {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
};
}
};
let day = date.day() as f64;
@@ -54,7 +54,7 @@ impl Model {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
};
}
};
let month = date.month() as f64;
@@ -87,7 +87,7 @@ impl Model {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
};
}
};
if serial_number > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
@@ -192,7 +192,7 @@ impl Model {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
};
}
};
let year = date.year() as f64;
@@ -216,7 +216,7 @@ impl Model {
error: Error::NUM,
origin: cell,
message: "Out of range parameters for date".to_string(),
}
};
}
};
@@ -266,7 +266,7 @@ impl Model {
error: Error::ERROR,
origin: cell,
message: "Invalid date".to_string(),
}
};
}
};
// 693_594 is computed as:
@@ -296,7 +296,7 @@ impl Model {
error: Error::ERROR,
origin: cell,
message: "Invalid date".to_string(),
}
};
}
};
// 693_594 is computed as:

View File

@@ -57,7 +57,7 @@
use std::f64::consts::FRAC_2_PI;
use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words};
// R0/S0 on [0, 2.00]
const R02: f64 = 1.562_499_999_999_999_5e-2; // 0x3F8FFFFF, 0xFFFFFFFD

View File

@@ -56,7 +56,7 @@
use std::f64::consts::FRAC_2_PI;
use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words};
// R0/S0 on [0,2]
const R00: f64 = -6.25e-2; // 0xBFB00000, 0x00000000

View File

@@ -40,7 +40,7 @@
use super::{
bessel_j0_y0::{j0, y0},
bessel_j1_y1::{j1, y1},
bessel_util::{split_words, FRAC_2_SQRT_PI},
bessel_util::{FRAC_2_SQRT_PI, split_words},
};
// Special cases are:
@@ -232,11 +232,7 @@ pub(crate) fn jn(n: i32, x: f64) -> f64 {
}
}
};
if sign == 1 {
-b
} else {
b
}
if sign == 1 { -b } else { b }
}
// Yn returns the order-n Bessel function of the second kind.
@@ -321,9 +317,5 @@ pub(crate) fn yn(n: i32, x: f64) -> f64 {
}
b
};
if sign > 0 {
b
} else {
-b
}
if sign > 0 { b } else { -b }
}

View File

@@ -45,9 +45,5 @@ pub(crate) fn erf(x: f64) -> f64 {
}
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
if x < 0.0 {
res - 1.0
} else {
1.0 - res
}
if x < 0.0 { res - 1.0 } else { 1.0 - res }
}

View File

@@ -698,7 +698,7 @@ impl Model {
error: error.0,
origin: cell,
message: error.1,
}
};
}
};
CalcResult::Number(ipmt)
@@ -762,7 +762,7 @@ impl Model {
error: error.0,
origin: cell,
message: error.1,
}
};
}
};
CalcResult::Number(ppmt)
@@ -1075,7 +1075,7 @@ impl Model {
error,
origin: cell,
message,
}
};
}
}
};
@@ -1096,7 +1096,7 @@ impl Model {
error,
origin: cell,
message,
}
};
}
}
};
@@ -1634,7 +1634,7 @@ impl Model {
error: error.0,
origin: cell,
message: error.1,
}
};
}
}
}
@@ -1702,7 +1702,7 @@ impl Model {
error: error.0,
origin: cell,
message: error.1,
}
};
}
}
}
@@ -1750,11 +1750,7 @@ impl Model {
rate = 1.0
};
let value = if rate == 1.0 {
if period == 1.0 {
cost
} else {
0.0
}
if period == 1.0 { cost } else { 0.0 }
} else {
cost * (1.0 - rate).powf(period - 1.0)
};

View File

@@ -257,10 +257,10 @@ impl Model {
{
match defined_name {
ParsedDefinedName::CellReference(reference) => {
return CalcResult::Number(reference.sheet as f64 + 1.0)
return CalcResult::Number(reference.sheet as f64 + 1.0);
}
ParsedDefinedName::RangeReference(range) => {
return CalcResult::Number(range.left.sheet as f64 + 1.0)
return CalcResult::Number(range.left.sheet as f64 + 1.0);
}
ParsedDefinedName::InvalidDefinedNameFormula => {
return CalcResult::Error {
@@ -296,7 +296,7 @@ impl Model {
error: Error::NAME,
origin: cell,
message: format!("Name not found: {name}"),
}
};
}
arg => {
// Now it should be the name of a sheet

View File

@@ -388,7 +388,7 @@ impl Model {
Error::ERROR,
cell,
format!("Invalid worksheet index: '{}'", first_range.left.sheet),
)
);
}
};
let max_row = dimension.max_row;

View File

@@ -1,7 +1,7 @@
use crate::{
calc_result::CalcResult,
expressions::{
parser::{parse_range, Node},
parser::{Node, parse_range},
token::Error,
types::CellReferenceIndex,
},

View File

@@ -8,7 +8,7 @@ use crate::{
};
use super::{
text_util::{substitute, text_after, text_before, Case},
text_util::{Case, substitute, text_after, text_before},
util::from_wildcard_to_regex,
};
@@ -368,7 +368,7 @@ impl Model {
error: Error::VALUE,
origin: cell,
message: "Empty cell".to_string(),
}
};
}
};
@@ -629,7 +629,7 @@ impl Model {
error: Error::VALUE,
origin: cell,
message: "Expecting number".to_string(),
}
};
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {

View File

@@ -57,8 +57,8 @@ mod test;
#[cfg(test)]
pub mod mock_time;
pub use model::get_milliseconds_since_epoch;
pub use model::Model;
pub use model::get_milliseconds_since_epoch;
pub use user_model::BorderArea;
pub use user_model::ClipboardData;
pub use user_model::UserModel;

View File

@@ -10,11 +10,11 @@ use crate::{
expressions::{
lexer::LexerMode,
parser::{
move_formula::{move_formula, MoveContext},
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
Node, Parser,
move_formula::{MoveContext, move_formula},
stringify::{rename_defined_name_in_node, to_rc_format, to_string},
},
token::{get_error_by_name, Error, OpCompare, OpProduct, OpSum, OpUnary},
token::{Error, OpCompare, OpProduct, OpSum, OpUnary, get_error_by_name},
types::*,
utils::{self, is_valid_column_number, is_valid_identifier, is_valid_row},
},
@@ -24,8 +24,8 @@ use crate::{
},
functions::util::compare_values,
implicit_intersection::implicit_intersection,
language::{get_language, Language},
locale::{get_locale, Currency, Locale},
language::{Language, get_language},
locale::{Currency, Locale, get_locale},
types::*,
utils as common,
};

View File

@@ -8,14 +8,14 @@ use crate::{
expressions::{
lexer::LexerMode,
parser::{
stringify::{rename_sheet_in_node, to_rc_format},
Parser,
stringify::{rename_sheet_in_node, to_rc_format},
},
types::CellReferenceRC,
},
language::get_language,
locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
model::{Model, ParsedDefinedName, get_milliseconds_since_epoch},
types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
},

View File

@@ -150,7 +150,7 @@ pub fn format_number(value: f64, format_code: &str, locale: &str) -> Formatted {
text: "#ERROR!".to_owned(),
color: None,
error: Some("Invalid locale".to_string()),
}
};
}
};
formatter::format::format_number(value, format_code, locale)

View File

@@ -206,9 +206,11 @@ fn test_delete_column_width() {
let (sheet, column) = (0, 5);
let normal_width = model.get_column_width(sheet, column).unwrap();
// Set the width of one column to 5 times the normal width
assert!(model
.set_column_width(sheet, column, normal_width * 5.0)
.is_ok());
assert!(
model
.set_column_width(sheet, column, normal_width * 5.0)
.is_ok()
);
// delete it
assert!(model.delete_columns(sheet, column, 1).is_ok());

View File

@@ -179,52 +179,60 @@ fn test_move_formula_rectangle() {
width: 2,
height: 20,
};
assert!(model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 3,
row: 1,
},
target,
area
)
.is_err());
assert!(model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 2,
row: 1,
},
target,
area
)
.is_ok());
assert!(model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 1,
row: 20,
},
target,
area
)
.is_ok());
assert!(model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 1,
row: 21,
},
target,
area
)
.is_err());
assert!(
model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 3,
row: 1,
},
target,
area
)
.is_err()
);
assert!(
model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 2,
row: 1,
},
target,
area
)
.is_ok()
);
assert!(
model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 1,
row: 20,
},
target,
area
)
.is_ok()
);
assert!(
model
.move_cell_value_to_area(
value,
&CellReferenceIndex {
sheet: 0,
column: 1,
row: 21,
},
target,
area
)
.is_err()
);
}

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::{constants::DEFAULT_COLUMN_WIDTH, UserModel};
use crate::{UserModel, constants::DEFAULT_COLUMN_WIDTH};
#[test]
fn add_undo_redo() {

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
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() {

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
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() {

View File

@@ -1,10 +1,10 @@
#![allow(clippy::unwrap_used)]
use crate::{
BorderArea, UserModel,
constants::{LAST_COLUMN, LAST_ROW},
expressions::{types::Area, utils::number_to_column},
types::{Border, BorderItem, BorderStyle},
BorderArea, UserModel,
};
// checks there are no borders in the sheet

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::{expressions::types::Area, UserModel};
use crate::{UserModel, expressions::types::Area};
#[test]
fn basic() {

View File

@@ -1,8 +1,8 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
use crate::expressions::types::Area;
use crate::UserModel;
#[test]
fn column_width() {

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW},
expressions::types::Area,
UserModel,
};
#[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT},
test::util::new_empty_model,
UserModel,
};
#[test]
@@ -157,7 +157,9 @@ fn new_sheet() {
#[test]
fn wrong_diffs_handled() {
let mut model = UserModel::from_model(new_empty_model());
assert!(model
.apply_external_diffs("Hello world".as_bytes())
.is_err());
assert!(
model
.apply_external_diffs("Hello world".as_bytes())
.is_err()
);
}

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::constants::{LAST_COLUMN, LAST_ROW};
use crate::test::util::new_empty_model;
use crate::types::CellType;
use crate::UserModel;
#[test]
fn set_user_input_errors() {

View File

@@ -1,7 +1,7 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::UserModel;
use crate::test::util::new_empty_model;
#[test]
fn basic_tests() {

View File

@@ -1,12 +1,12 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
constants::{
DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH,
LAST_COLUMN,
},
test::util::new_empty_model,
UserModel,
};
#[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH},
test::util::new_empty_model,
UserModel,
};
#[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_WINDOW_WIDTH, LAST_COLUMN},
test::util::new_empty_model,
UserModel,
};
#[test]

View File

@@ -1,8 +1,8 @@
#![allow(clippy::unwrap_used)]
use crate::UserModel;
use crate::test::util::new_empty_model;
use crate::types::Fill;
use crate::UserModel;
#[test]
fn simple_pasting() {

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::{expressions::types::Area, UserModel};
use crate::{UserModel, expressions::types::Area};
#[test]
fn csv_paste() {

View File

@@ -1,7 +1,7 @@
#![allow(clippy::unwrap_used)]
use crate::{
constants::LAST_ROW, expressions::types::Area, test::util::new_empty_model, UserModel,
UserModel, constants::LAST_ROW, expressions::types::Area, test::util::new_empty_model,
};
#[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN},
test::util::new_empty_model,
UserModel,
};
#[test]

View File

@@ -1,7 +1,7 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::UserModel;
use crate::test::util::new_empty_model;
#[test]
fn basic_tests() {

View File

@@ -1,7 +1,7 @@
#![allow(clippy::unwrap_used)]
use crate::test::util::new_empty_model;
use crate::UserModel;
use crate::test::util::new_empty_model;
#[test]
fn basic_undo_redo() {

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
expressions::types::Area,
types::{Alignment, HorizontalAlignment, VerticalAlignment},
UserModel,
};
#[test]

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::{test::util::new_empty_model, UserModel};
use crate::{UserModel, test::util::new_empty_model};
#[test]
fn basic() {

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::{test::util::new_empty_model, UserModel};
use crate::{UserModel, test::util::new_empty_model};
#[test]
fn simple_undo_redo() {

View File

@@ -3,10 +3,10 @@
use std::collections::HashMap;
use crate::{
UserModel,
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
user_model::SelectedView,
UserModel,
};
#[test]

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)]
use crate::{
UserModel,
constants::{DEFAULT_ROW_HEIGHT, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH},
test::util::new_empty_model,
UserModel,
};
#[test]

View File

@@ -1,6 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::{expressions::types::Area, types::Border, BorderArea, UserModel};
use crate::{BorderArea, UserModel, expressions::types::Area, types::Border};
impl UserModel {
pub fn _set_cell_border(&mut self, cell: &str, color: &str) {

View File

@@ -407,7 +407,7 @@ impl Default for Font {
u: false,
b: false,
i: false,
sz: 13,
sz: 11,
color: Some("#000000".to_string()),
name: "Calibri".to_string(),
family: 2,

View File

@@ -4,7 +4,7 @@ use crate::{
};
use super::{
border_utils::is_max_border, common::BorderType, history::Diff, BorderArea, UserModel,
BorderArea, UserModel, border_utils::is_max_border, common::BorderType, history::Diff,
};
impl UserModel {

View File

@@ -127,17 +127,6 @@ fn update_style(old_value: &Style, style_path: &str, value: &str) -> Result<Styl
"font.color" => {
style.font.color = color(value)?;
}
"font.size_delta" => {
// This is a special case, we need to add the value to the current size
let size_delta: i32 = value
.parse()
.map_err(|_| format!("Invalid value for font size: '{value}'."))?;
let new_size = style.font.sz + size_delta;
if new_size < 1 {
return Err(format!("Invalid value for font size: '{new_size}'."));
}
style.font.sz = new_size;
}
"fill.bg_color" => {
style.fill.bg_color = color(value)?;
style.fill.pattern_type = "solid".to_string();

View File

@@ -156,7 +156,7 @@ mod tests {
use super::*;
use crate::language::get_language;
use crate::locale::{get_locale, Locale};
use crate::locale::{Locale, get_locale};
fn get_test_locale() -> &'static Locale {
#![allow(clippy::unwrap_used)]

View File

@@ -1,5 +1,5 @@
[package]
edition = "2021"
edition = "2024"
name = "ironcalc_nodejs"
version = "0.3.1"

View File

@@ -1,2 +1,2 @@
tab_spaces = 2
edition = "2021"
edition = "2024"

View File

@@ -1,12 +1,12 @@
#![deny(clippy::all)]
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use napi::{self, JsUnknown, Result, bindgen_prelude::*};
use serde::Serialize;
use ironcalc::{
base::{
types::{CellType, Style},
Model as BaseModel,
types::{CellType, Style},
},
error::XlsxError,
export::{save_to_icalc, save_to_xlsx},

View File

@@ -2,12 +2,12 @@
use serde::Serialize;
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
use napi::{self, JsUnknown, Result, bindgen_prelude::*};
use ironcalc::base::{
BorderArea, ClipboardData, UserModel as BaseModel,
expressions::types::Area,
types::{CellType, Style},
BorderArea, ClipboardData, UserModel as BaseModel,
};
#[derive(Serialize)]

View File

@@ -1,7 +1,7 @@
[package]
name = "pyroncalc"
version = "0.3.0"
edition = "2021"
edition = "2024"
[lib]

View File

@@ -2,8 +2,8 @@ use pyo3::exceptions::PyException;
use pyo3::{create_exception, prelude::*, wrap_pyfunction};
use types::{PySheetProperty, PyStyle};
use xlsx::base::types::Style;
use xlsx::base::Model;
use xlsx::base::types::Style;
use xlsx::export::{save_to_icalc, save_to_xlsx};
use xlsx::import;

View File

@@ -5,7 +5,7 @@ authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
description = "IronCalc Web bindings"
license = "MIT/Apache-2.0"
repository = "https://github.com/ironcalc/ironcalc"
edition = "2021"
edition = "2024"
[lib]
crate-type = ["cdylib"]

View File

@@ -1,13 +1,13 @@
use serde::Serialize;
use wasm_bindgen::{
prelude::{wasm_bindgen, JsError},
JsValue,
prelude::{JsError, wasm_bindgen},
};
use ironcalc_base::{
BorderArea, ClipboardData, UserModel as BaseModel,
expressions::{lexer::util::get_tokens as tokenizer, types::Area, utils::number_to_column},
types::{CellType, Style},
BorderArea, ClipboardData, UserModel as BaseModel,
};
fn to_js_error(error: String) -> JsError {

View File

@@ -49,7 +49,7 @@ test('Styles work', () => {
num_fmt: 'general',
fill: { pattern_type: 'none' },
font: {
sz: 13,
sz: 11,
color: '#000000',
name: 'Calibri',
family: 2,
@@ -64,7 +64,7 @@ test('Styles work', () => {
num_fmt: 'general',
fill: { pattern_type: 'none' },
font: {
sz: 13,
sz: 11,
color: '#000000',
name: 'Calibri',
family: 2,

View File

@@ -2,7 +2,7 @@
name = "generate_locale"
version = "0.1.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

File diff suppressed because it is too large Load Diff

View File

@@ -28,21 +28,21 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "^3.2.4",
"@storybook/addon-essentials": "^8.6.0",
"@storybook/addon-interactions": "^8.6.0",
"@storybook/blocks": "^8.6.0",
"@storybook/react": "^8.6.0",
"@storybook/react-vite": "^8.6.0",
"@storybook/test": "^8.6.0",
"@storybook/addon-essentials": "^8.5.3",
"@storybook/addon-interactions": "^8.5.3",
"@storybook/blocks": "^8.5.3",
"@storybook/react": "^8.5.3",
"@storybook/react-vite": "^8.5.3",
"@storybook/test": "^8.5.3",
"@vitejs/plugin-react": "^4.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"storybook": "^8.6.0",
"storybook": "^8.5.3",
"ts-node": "^10.9.2",
"typescript": "~5.6.2",
"vite": "^6.2.0",
"vite": "^6.0.5",
"vite-plugin-svgr": "^4.2.0",
"vitest": "^3.0.7"
"vitest": "^2.0.5"
},
"peerDependencies": {
"@types/react": "^18.0.0 || ^19.0.0",

View File

@@ -1,7 +1,7 @@
import "./index.css";
import type { Model } from "@ironcalc/wasm";
import ThemeProvider from "@mui/material/styles/ThemeProvider";
import Workbook from "./components/Workbook/Workbook.tsx";
import Workbook from "./components/workbook.tsx";
import { WorkbookState } from "./components/workbookState.ts";
import { theme } from "./theme.ts";
import "./i18n";

View File

@@ -8,7 +8,7 @@ import {
} from "lucide-react";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import { theme } from "../theme";
const red_color = theme.palette.error.main;

View File

@@ -3,8 +3,8 @@ import type { MenuItemProps } from "@mui/material";
import { ChevronDown } from "lucide-react";
import { useRef, useState } from "react";
import { theme } from "../../theme";
import ColorPicker from "../ColorPicker/ColorPicker";
import { isInReferenceMode } from "../Editor/util";
import ColorPicker from "../colorPicker";
import { isInReferenceMode } from "../editor/util";
import type { WorkbookState } from "../workbookState";
import SheetDeleteDialog from "./SheetDeleteDialog";
import SheetRenameDialog from "./SheetRenameDialog";

View File

@@ -3,8 +3,8 @@ import { Menu, Plus } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import { StyledButton } from "../Toolbar/Toolbar";
import { NAVIGATION_HEIGHT } from "../constants";
import { StyledButton } from "../toolbar";
import type { WorkbookState } from "../workbookState";
import SheetListMenu from "./SheetListMenu";
import SheetTab from "./SheetTab";

View File

@@ -1,676 +0,0 @@
import { type Model, columnNameFromNumber } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import {
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState,
} from "react";
import Editor from "../Editor/Editor";
import {
COLUMN_WIDTH_SCALE,
LAST_COLUMN,
LAST_ROW,
ROW_HEIGH_SCALE,
outlineBackgroundColor,
outlineColor,
} from "../WorksheetCanvas/constants";
import WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
import {
FORMULA_BAR_HEIGHT,
NAVIGATION_HEIGHT,
TOOLBAR_HEIGHT,
} from "../constants";
import type { Cell } from "../types";
import { AreaType, type WorkbookState } from "../workbookState";
import CellContextMenu from "./CellContextMenu";
import usePointer from "./usePointer";
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener("resize", updateSize);
updateSize();
return () => window.removeEventListener("resize", updateSize);
}, []);
return size;
}
const Worksheet = forwardRef(
(
props: {
model: Model;
workbookState: WorkbookState;
refresh: () => void;
},
ref,
) => {
const canvasElement = useRef<HTMLCanvasElement>(null);
const worksheetElement = useRef<HTMLDivElement>(null);
const scrollElement = useRef<HTMLDivElement>(null);
const editorElement = useRef<HTMLDivElement>(null);
const spacerElement = useRef<HTMLDivElement>(null);
const cellOutline = useRef<HTMLDivElement>(null);
const areaOutline = useRef<HTMLDivElement>(null);
const cellOutlineHandle = useRef<HTMLDivElement>(null);
const extendToOutline = useRef<HTMLDivElement>(null);
const columnResizeGuide = useRef<HTMLDivElement>(null);
const rowResizeGuide = useRef<HTMLDivElement>(null);
const columnHeaders = useRef<HTMLDivElement>(null);
const worksheetCanvas = useRef<WorksheetCanvas | null>(null);
const [contextMenuOpen, setContextMenuOpen] = useState(false);
const ignoreScrollEventRef = useRef(false);
const { model, workbookState, refresh } = props;
const [clientWidth, clientHeight] = useWindowSize();
useImperativeHandle(ref, () => ({
getCanvas: () => worksheetCanvas.current,
}));
useEffect(() => {
const canvasRef = canvasElement.current;
const columnGuideRef = columnResizeGuide.current;
const rowGuideRef = rowResizeGuide.current;
const columnHeadersRef = columnHeaders.current;
const worksheetRef = worksheetElement.current;
const outline = cellOutline.current;
const handle = cellOutlineHandle.current;
const area = areaOutline.current;
const extendTo = extendToOutline.current;
const editor = editorElement.current;
if (
!canvasRef ||
!columnGuideRef ||
!rowGuideRef ||
!columnHeadersRef ||
!worksheetRef ||
!outline ||
!handle ||
!area ||
!extendTo ||
!scrollElement.current ||
!editor
)
return;
// FIXME: This two need to be computed.
model.setWindowWidth(clientWidth - 37);
model.setWindowHeight(clientHeight - 190);
const canvas = new WorksheetCanvas({
width: worksheetRef.clientWidth,
height: worksheetRef.clientHeight,
model,
workbookState,
elements: {
canvas: canvasRef,
columnGuide: columnGuideRef,
rowGuide: rowGuideRef,
columnHeaders: columnHeadersRef,
cellOutline: outline,
cellOutlineHandle: handle,
areaOutline: area,
extendToOutline: extendTo,
editor: editor,
},
onColumnWidthChanges(sheet, column, width) {
if (width < 0) {
return;
}
const { range } = model.getSelectedView();
let columnStart = column;
let columnEnd = column;
const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
if (
fullColumn &&
column >= range[1] &&
column <= range[3] &&
!fullRow
) {
columnStart = Math.min(range[1], column, range[3]);
columnEnd = Math.max(range[1], column, range[3]);
}
model.setColumnsWidth(sheet, columnStart, columnEnd, width);
worksheetCanvas.current?.renderSheet();
},
onRowHeightChanges(sheet, row, height) {
if (height < 0) {
return;
}
const { range } = model.getSelectedView();
let rowStart = row;
let rowEnd = row;
const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
if (fullRow && row >= range[0] && row <= range[2] && !fullColumn) {
rowStart = Math.min(range[0], row, range[2]);
rowEnd = Math.max(range[0], row, range[2]);
}
model.setRowsHeight(sheet, rowStart, rowEnd, height);
worksheetCanvas.current?.renderSheet();
},
refresh,
});
const scrollX = model.getScrollX();
const scrollY = model.getScrollY();
const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000];
if (spacerElement.current) {
spacerElement.current.style.height = `${sheetHeight}px`;
spacerElement.current.style.width = `${sheetWidth}px`;
}
const left = scrollElement.current.scrollLeft;
const top = scrollElement.current.scrollTop;
if (scrollX !== left) {
ignoreScrollEventRef.current = true;
scrollElement.current.scrollLeft = scrollX;
setTimeout(() => {
ignoreScrollEventRef.current = false;
}, 0);
}
if (scrollY !== top) {
ignoreScrollEventRef.current = true;
scrollElement.current.scrollTop = scrollY;
setTimeout(() => {
ignoreScrollEventRef.current = false;
}, 0);
}
canvas.renderSheet();
worksheetCanvas.current = canvas;
});
const { onPointerMove, onPointerDown, onPointerHandleDown, onPointerUp } =
usePointer({
model,
workbookState,
refresh,
onColumnSelected: (column: number, shift: boolean) => {
let firstColumn = column;
let lastColumn = column;
if (shift) {
const { range } = model.getSelectedView();
firstColumn = Math.min(range[1], column, range[3]);
lastColumn = Math.max(range[3], column, range[1]);
}
model.setSelectedCell(1, firstColumn);
model.setSelectedRange(1, firstColumn, LAST_ROW, lastColumn);
refresh();
},
onRowSelected: (row: number, shift: boolean) => {
let firstRow = row;
let lastRow = row;
if (shift) {
const { range } = model.getSelectedView();
firstRow = Math.min(range[0], row, range[2]);
lastRow = Math.max(range[2], row, range[0]);
}
model.setSelectedCell(firstRow, 1);
model.setSelectedRange(firstRow, 1, lastRow, LAST_COLUMN);
refresh();
},
onAllSheetSelected: () => {
model.setSelectedCell(1, 1);
model.setSelectedRange(1, 1, LAST_ROW, LAST_COLUMN);
},
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
model.setSelectedCell(cell.row, cell.column);
refresh();
},
onAreaSelecting: (cell: Cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
model.onAreaSelecting(row, column);
canvas.renderSheet();
refresh();
},
onAreaSelected: () => {
const styles = workbookState.getCopyStyles();
if (styles?.length) {
model.onPasteStyles(styles);
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
canvas.renderSheet();
}
workbookState.setCopyStyles(null);
if (worksheetElement.current) {
worksheetElement.current.style.cursor = "auto";
}
refresh();
},
onExtendToCell: (cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
// We are either extending by rows or by columns
// And we could be doing it in the positive direction (downwards or right)
// or the negative direction (upwards or left)
if (
row > rowEnd &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < row - rowEnd) ||
(column > columnEnd && column - columnEnd < row - rowEnd))
) {
// rows downwards
const area = {
type: AreaType.rowsDown,
rowStart: rowEnd + 1,
rowEnd: row,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
row < rowStart &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < rowStart - row) ||
(column > columnEnd && column - columnEnd < rowStart - row))
) {
// rows upwards
const area = {
type: AreaType.rowsUp,
rowStart: row,
rowEnd: rowStart,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column > columnEnd &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < column - columnEnd) ||
(row > rowEnd && row - rowEnd < column - columnEnd))
) {
// columns right
const area = {
type: AreaType.columnsRight,
rowStart,
rowEnd,
columnStart: columnEnd + 1,
columnEnd: column,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column < columnStart &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < columnStart - column) ||
(row > rowEnd && row - rowEnd < columnStart - column))
) {
// columns left
const area = {
type: AreaType.columnsLeft,
rowStart,
rowEnd,
columnStart: column,
columnEnd: columnStart,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
}
},
onExtendToEnd: () => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { sheet, range } = model.getSelectedView();
const extendedArea = workbookState.getExtendToArea();
if (!extendedArea) {
return;
}
const rowStart = Math.min(range[0], range[2]);
const height = Math.abs(range[2] - range[0]) + 1;
const width = Math.abs(range[3] - range[1]) + 1;
const columnStart = Math.min(range[1], range[3]);
const area = {
sheet,
row: rowStart,
column: columnStart,
width,
height,
};
switch (extendedArea.type) {
case AreaType.rowsDown:
model.autoFillRows(area, extendedArea.rowEnd);
break;
case AreaType.rowsUp: {
model.autoFillRows(area, extendedArea.rowStart);
break;
}
case AreaType.columnsRight: {
model.autoFillColumns(area, extendedArea.columnEnd);
break;
}
case AreaType.columnsLeft: {
model.autoFillColumns(area, extendedArea.columnStart);
break;
}
}
model.setSelectedRange(
Math.min(rowStart, extendedArea.rowStart),
Math.min(columnStart, extendedArea.columnStart),
Math.max(rowStart + height - 1, extendedArea.rowEnd),
Math.max(columnStart + width - 1, extendedArea.columnEnd),
);
workbookState.clearExtendToArea();
canvas.renderSheet();
},
canvasElement,
worksheetElement,
worksheetCanvas,
});
const onScroll = (): void => {
if (!scrollElement.current || !worksheetCanvas.current) {
return;
}
if (ignoreScrollEventRef.current) {
// Programmatic scroll ignored
return;
}
const left = scrollElement.current.scrollLeft;
const top = scrollElement.current.scrollTop;
worksheetCanvas.current.setScrollPosition({ left, top });
worksheetCanvas.current.renderSheet();
};
return (
<Wrapper ref={scrollElement} onScroll={onScroll} className="scroll">
<Spacer ref={spacerElement} />
<SheetContainer
className="sheet-container"
ref={worksheetElement}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onContextMenu={(event) => {
event.preventDefault();
event.stopPropagation();
setContextMenuOpen(true);
}}
onDoubleClick={(event) => {
// Starts editing cell
const { sheet, row, column } = model.getSelectedView();
const text = model.getCellContent(sheet, row, column);
const editorWidth =
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
const editorHeight =
model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
workbookState.setEditingCell({
sheet,
row,
column,
text,
cursorStart: text.length,
cursorEnd: text.length,
focus: "cell",
referencedRange: null,
activeRanges: [],
mode: "accept",
editorWidth,
editorHeight,
});
event.stopPropagation();
// event.preventDefault();
props.refresh();
}}
>
<SheetCanvas ref={canvasElement} />
<CellOutline ref={cellOutline} />
<EditorWrapper ref={editorElement}>
<Editor
originalText={workbookState.getEditingText()}
onEditEnd={(): void => {
props.refresh();
}}
onTextUpdated={(): void => {
props.refresh();
}}
model={model}
workbookState={workbookState}
type={"cell"}
/>
</EditorWrapper>
<AreaOutline ref={areaOutline} />
<ExtendToOutline ref={extendToOutline} />
<CellOutlineHandle
ref={cellOutlineHandle}
onPointerDown={onPointerHandleDown}
/>
<ColumnResizeGuide ref={columnResizeGuide} />
<RowResizeGuide ref={rowResizeGuide} />
<ColumnHeaders ref={columnHeaders} />
</SheetContainer>
<CellContextMenu
open={contextMenuOpen}
onClose={() => setContextMenuOpen(false)}
anchorEl={cellOutline.current}
onInsertRowAbove={(): void => {
const view = model.getSelectedView();
model.insertRow(view.sheet, view.row);
setContextMenuOpen(false);
}}
onInsertRowBelow={(): void => {
const view = model.getSelectedView();
model.insertRow(view.sheet, view.row + 1);
setContextMenuOpen(false);
}}
onInsertColumnLeft={(): void => {
const view = model.getSelectedView();
model.insertColumn(view.sheet, view.column);
setContextMenuOpen(false);
}}
onInsertColumnRight={(): void => {
const view = model.getSelectedView();
model.insertColumn(view.sheet, view.column + 1);
setContextMenuOpen(false);
}}
onFreezeColumns={(): void => {
const view = model.getSelectedView();
model.setFrozenColumnsCount(view.sheet, view.column);
setContextMenuOpen(false);
}}
onFreezeRows={(): void => {
const view = model.getSelectedView();
model.setFrozenRowsCount(view.sheet, view.row);
setContextMenuOpen(false);
}}
onUnfreezeColumns={(): void => {
const sheet = model.getSelectedSheet();
model.setFrozenColumnsCount(sheet, 0);
setContextMenuOpen(false);
}}
onUnfreezeRows={(): void => {
const sheet = model.getSelectedSheet();
model.setFrozenRowsCount(sheet, 0);
setContextMenuOpen(false);
}}
onDeleteRow={(): void => {
const view = model.getSelectedView();
model.deleteRow(view.sheet, view.row);
setContextMenuOpen(false);
}}
onDeleteColumn={(): void => {
const view = model.getSelectedView();
model.deleteColumn(view.sheet, view.column);
setContextMenuOpen(false);
}}
row={model.getSelectedView().row}
column={columnNameFromNumber(model.getSelectedView().column)}
/>
</Wrapper>
);
},
);
const Spacer = styled("div")`
position: absolute;
height: 5000px;
width: 5000px;
`;
const SheetContainer = styled("div")`
position: sticky;
top: 0px;
left: 0px;
height: 100%;
.column-resize-handle {
position: absolute;
top: 0px;
width: 3px;
opacity: 0;
background: ${outlineColor};
border-radius: 5px;
cursor: col-resize;
}
.column-resize-handle:hover {
opacity: 1;
}
.row-resize-handle {
position: absolute;
left: 0px;
height: 3px;
opacity: 0;
background: ${outlineColor};
border-radius: 5px;
cursor: row-resize;
}
.row-resize-handle:hover {
opacity: 1;
}
`;
const Wrapper = styled("div")({
position: "absolute",
overflow: "scroll",
top: TOOLBAR_HEIGHT + FORMULA_BAR_HEIGHT + 1,
left: 0,
right: 0,
bottom: NAVIGATION_HEIGHT + 1,
overscrollBehavior: "none",
});
const SheetCanvas = styled("canvas")`
position: relative;
top: 0px;
left: 0px;
right: 0px;
bottom: 40px;
`;
const ColumnResizeGuide = styled("div")`
position: absolute;
top: 0px;
display: none;
height: 100%;
width: 0px;
border-left: 1px dashed ${outlineColor};
`;
const ColumnHeaders = styled("div")`
position: absolute;
left: 0px;
top: 0px;
overflow: hidden;
display: flex;
& .column-header {
display: inline-block;
text-align: center;
overflow: hidden;
height: 100%;
user-select: none;
}
`;
const RowResizeGuide = styled("div")`
position: absolute;
display: none;
left: 0px;
height: 0px;
width: 100%;
border-top: 1px dashed ${outlineColor};
`;
const AreaOutline = styled("div")`
position: absolute;
border: 1px solid ${outlineColor};
border-radius: 3px;
background-color: ${outlineBackgroundColor};
`;
const CellOutline = styled("div")`
position: absolute;
border: 2px solid ${outlineColor};
border-radius: 3px;
word-break: break-word;
font-size: 13px;
display: flex;
`;
const CellOutlineHandle = styled("div")`
position: absolute;
width: 5px;
height: 5px;
background: ${outlineColor};
cursor: crosshair;
border-radius: 1px;
`;
const ExtendToOutline = styled("div")`
position: absolute;
border: 1px dashed ${outlineColor};
border-radius: 3px;
`;
const EditorWrapper = styled("div")`
position: absolute;
width: 100%;
padding: 0px;
border-width: 0px;
outline: none;
resize: none;
white-space: pre-wrap;
vertical-align: bottom;
overflow: hidden;
text-align: left;
span {
min-width: 1px;
}
font-family: monospace;
border: 2px solid ${outlineColor};
`;
export default Worksheet;

View File

@@ -1,6 +1,6 @@
import type { Model } from "@ironcalc/wasm";
import { columnNameFromNumber } from "@ironcalc/wasm";
import { getColor } from "../Editor/util";
import { getColor } from "../editor/util";
import type { Cell } from "../types";
import type { WorkbookState } from "../workbookState";
import {
@@ -353,7 +353,7 @@ export default class WorksheetCanvas {
? gridColor
: backgroundColor;
const fontSize = style.font?.sz || 13;
const fontSize = 13;
let font = `${fontSize}px ${defaultCellFontFamily}`;
let textColor = defaultTextColor;
if (style.font) {

View File

@@ -20,9 +20,9 @@ import {
BorderRightIcon,
BorderStyleIcon,
BorderTopIcon,
} from "../../icons";
import { theme } from "../../theme";
import ColorPicker from "../ColorPicker/ColorPicker";
} from "../icons";
import { theme } from "../theme";
import ColorPicker from "./colorPicker";
type BorderPickerProps = {
className?: string;

View File

@@ -1,13 +1,12 @@
import styled from "@emotion/styled";
import Popover, { type PopoverOrigin } from "@mui/material/Popover";
import { Check } from "lucide-react";
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { HexColorInput, HexColorPicker } from "react-colorful";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import { theme } from "../theme";
type ColorPickerProps = {
className?: string;
color: string;
onChange: (color: string) => void;
onClose: () => void;
@@ -18,19 +17,17 @@ type ColorPickerProps = {
};
const colorPickerWidth = 240;
const colorfulHeight = 240;
const colorfulHeight = 185; // 150 + 15 + 20
const ColorPicker = (properties: ColorPickerProps) => {
const [color, setColor] = useState<string>(properties.color);
const recentColors = useRef<string[]>([]);
const { t } = useTranslation();
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);
properties.onChange(newColor);
};
const handleClose = (): void => {
@@ -88,16 +85,21 @@ const ColorPicker = (properties: ColorPickerProps) => {
/>
</HexColorInputBox>
</HexWrapper>
<Swatch $color={color} />
<Swatch
$color={color}
onClick={(): void => {
closePicker(color);
}}
/>
</ColorPickerInput>
<HorizontalDivider />
<ColorList>
{presetColors.map((presetColor) => (
<RecentColorButton
<Button
key={presetColor}
$color={presetColor}
onClick={(): void => {
setColor(presetColor);
closePicker(presetColor);
}}
/>
))}
@@ -109,11 +111,11 @@ const ColorPicker = (properties: ColorPickerProps) => {
<RecentLabel>{"Recent"}</RecentLabel>
<ColorList>
{recentColors.current.map((recentColor) => (
<RecentColorButton
<Button
key={recentColor}
$color={recentColor}
onClick={(): void => {
setColor(recentColor);
closePicker(recentColor);
}}
/>
))}
@@ -122,46 +124,11 @@ const ColorPicker = (properties: ColorPickerProps) => {
) : (
<div />
)}
<Buttons>
<StyledButton
onClick={(): void => {
closePicker(color);
}}
>
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
{t("color_picker.apply")}
</StyledButton>
</Buttons>
</ColorPickerDialog>
</Popover>
);
};
const Buttons = styled.div`
display: flex;
justify-content: flex-end;
margin: 8px;
`;
const StyledButton = styled("div")`
cursor: pointer;
color: #ffffff;
background: #f2994a;
padding: 0px 10px;
height: 36px;
line-height: 36px;
border-radius: 4px;
display: flex;
align-items: center;
font-family: "Inter";
font-size: 14px;
&:hover {
background: #d68742;
}
`;
const RecentLabel = styled.div`
font-family: "Inter";
font-size: 12px;
@@ -179,7 +146,7 @@ const ColorList = styled.div`
gap: 4.7px;
`;
const RecentColorButton = styled.button<{ $color: string }>`
const Button = styled.button<{ $color: string }>`
width: 16px;
height: 16px;
${({ $color }): string => {
@@ -207,6 +174,20 @@ const HorizontalDivider = styled.div`
border-top: 1px solid ${theme.palette.grey["200"]};
`;
// 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;

View File

@@ -1,7 +1,7 @@
import { Menu, MenuItem, styled } from "@mui/material";
import { type ComponentProps, useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import FormatPicker from "./FormatPicker";
import FormatPicker from "./formatPicker";
import { NumberFormats } from "./formatUtil";
type FormatMenuProps = {

View File

@@ -3,7 +3,7 @@ import { Dialog, TextField } from "@mui/material";
import { Check, X } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import { theme } from "../theme";
type FormatPickerProps = {
className?: string;

View File

@@ -1,14 +1,14 @@
import type { Model } from "@ironcalc/wasm";
import { styled } from "@mui/material";
import { Fx } from "../../icons";
import { theme } from "../../theme";
import Editor from "../Editor/Editor";
import { Fx } from "../icons";
import { theme } from "../theme";
import {
COLUMN_WIDTH_SCALE,
ROW_HEIGH_SCALE,
} from "../WorksheetCanvas/constants";
import { FORMULA_BAR_HEIGHT } from "../constants";
import type { WorkbookState } from "../workbookState";
} from "./WorksheetCanvas/constants";
import { FORMULA_BAR_HEIGHT } from "./constants";
import Editor from "./editor/editor";
import type { WorkbookState } from "./workbookState";
type FormulaBarProps = {
cellAddress: string;

View File

@@ -1,10 +1,7 @@
import { readFile } from "node:fs/promises";
import { type SelectedView, initSync } from "@ironcalc/wasm";
import { expect, test } from "vitest";
import {
decreaseDecimalPlaces,
increaseDecimalPlaces,
} from "../FormatMenu/formatUtil";
import { decreaseDecimalPlaces, increaseDecimalPlaces } from "../formatUtil";
import { getFullRangeToString, isNavigationKey } from "../util";
test("checks arrow left is a navigation key", () => {

View File

@@ -7,8 +7,6 @@ import type {
import { styled } from "@mui/material/styles";
import type {} from "@mui/system";
import {
AArrowDown,
AArrowUp,
AlignCenter,
AlignLeft,
AlignRight,
@@ -20,7 +18,6 @@ import {
Grid2X2,
Grid2x2Check,
Grid2x2X,
ImageDown,
Italic,
PaintBucket,
PaintRoller,
@@ -39,19 +36,19 @@ import {
ArrowMiddleFromLine,
DecimalPlacesDecreaseIcon,
DecimalPlacesIncreaseIcon,
} from "../../icons";
import { theme } from "../../theme";
import BorderPicker from "../BorderPicker/BorderPicker";
import ColorPicker from "../ColorPicker/ColorPicker";
import FormatMenu from "../FormatMenu/FormatMenu";
} from "../icons";
import { theme } from "../theme";
import NameManagerDialog from "./NameManagerDialog";
import type { NameManagerProperties } from "./NameManagerDialog/NameManagerDialog";
import BorderPicker from "./borderPicker";
import ColorPicker from "./colorPicker";
import { TOOLBAR_HEIGHT } from "./constants";
import FormatMenu from "./formatMenu";
import {
NumberFormats,
decreaseDecimalPlaces,
increaseDecimalPlaces,
} from "../FormatMenu/formatUtil";
import NameManagerDialog from "../NameManagerDialog";
import type { NameManagerProperties } from "../NameManagerDialog/NameManagerDialog";
import { TOOLBAR_HEIGHT } from "../constants";
} from "./formatUtil";
type ToolbarProperties = {
canUndo: boolean;
@@ -70,8 +67,6 @@ type ToolbarProperties = {
onNumberFormatPicked: (numberFmt: string) => void;
onBorderChanged: (border: BorderOptions) => void;
onClearFormatting: () => void;
onIncreaseFontSize: (delta: number) => void;
onDownloadPNG: () => void;
fillColor: string;
fontColor: string;
bold: boolean;
@@ -253,28 +248,6 @@ function Toolbar(properties: ToolbarProperties) {
>
<Type />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(1);
}}
title={t("toolbar.increase_font_size")}
>
<AArrowUp />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(-1);
}}
title={t("toolbar.decrease_font_size")}
>
<AArrowDown />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
@@ -401,17 +374,6 @@ function Toolbar(properties: ToolbarProperties) {
>
<RemoveFormatting />
</StyledButton>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onDownloadPNG();
}}
title={t("toolbar.selected_png")}
>
<ImageDown />
</StyledButton>
<ColorPicker
color={properties.fontColor}

View File

@@ -1,5 +1,5 @@
import { type KeyboardEvent, type RefObject, useCallback } from "react";
import { type NavigationKey, isEditingKey, isNavigationKey } from "../util";
import { type NavigationKey, isEditingKey, isNavigationKey } from "./util";
export enum Border {
Top = "top",

View File

@@ -1,14 +1,14 @@
import type { Model } from "@ironcalc/wasm";
import { type PointerEvent, type RefObject, useCallback, useRef } from "react";
import { isInReferenceMode } from "../Editor/util";
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
import type WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
import {
headerColumnWidth,
headerRowHeight,
} from "../WorksheetCanvas/worksheetCanvas";
import type { Cell } from "../types";
import { rangeToStr } from "../util";
import type { WorkbookState } from "../workbookState";
} from "./WorksheetCanvas/worksheetCanvas";
import { isInReferenceMode } from "./editor/util";
import type { Cell } from "./types";
import { rangeToStr } from "./util";
import type { WorkbookState } from "./workbookState";
interface PointerSettings {
canvasElement: RefObject<HTMLCanvasElement | null>;

View File

@@ -6,35 +6,30 @@ import type {
} from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { useCallback, useEffect, useRef, useState } from "react";
import FormulaBar from "../FormulaBar/FormulaBar";
import SheetTabBar from "../SheetTabBar";
import Toolbar from "../Toolbar/Toolbar";
import Worksheet from "../Worksheet/Worksheet";
import SheetTabBar from "./SheetTabBar/SheetTabBar";
import {
COLUMN_WIDTH_SCALE,
LAST_COLUMN,
ROW_HEIGH_SCALE,
} from "../WorksheetCanvas/constants";
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
import { devicePixelRatio } from "../WorksheetCanvas/worksheetCanvas";
} from "./WorksheetCanvas/constants";
import {
CLIPBOARD_ID_SESSION_STORAGE_KEY,
getNewClipboardId,
} from "../clipboard";
} from "./clipboard";
import FormulaBar from "./formulabar";
import Toolbar from "./toolbar";
import useKeyboardNavigation from "./useKeyboardNavigation";
import {
type NavigationKey,
getCellAddress,
getFullRangeToString,
} from "../util";
import type { WorkbookState } from "../workbookState";
import useKeyboardNavigation from "./useKeyboardNavigation";
} from "./util";
import type { WorkbookState } from "./workbookState";
import Worksheet from "./worksheet";
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const { model, workbookState } = props;
const rootRef = useRef<HTMLDivElement | null>(null);
const worksheetRef = useRef<{
getCanvas: () => WorksheetCanvas | null;
}>(null);
// Calling `setRedrawId((id) => id + 1);` forces a redraw
// This is needed because `model` or `workbookState` can change without React being aware of it
@@ -124,10 +119,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
updateRangeStyle("num_fmt", numberFmt);
};
const onIncreaseFontSize = (delta: number) => {
updateRangeStyle("font.size_delta", `${delta}`);
};
const onCopyStyles = () => {
const {
sheet,
@@ -550,62 +541,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
);
setRedrawId((id) => id + 1);
}}
onIncreaseFontSize={(delta: number) => {
onIncreaseFontSize(delta);
}}
onDownloadPNG={() => {
// creates a new canvas element in the visible part of the the selected area
const worksheetCanvas = worksheetRef.current?.getCanvas();
if (!worksheetCanvas) {
return;
}
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
const { topLeftCell, bottomRightCell } =
worksheetCanvas.getVisibleCells();
const firstRow = Math.max(rowStart, topLeftCell.row);
const firstColumn = Math.max(columnStart, topLeftCell.column);
const lastRow = Math.min(rowEnd, bottomRightCell.row);
const lastColumn = Math.min(columnEnd, bottomRightCell.column);
let [x, y] = worksheetCanvas.getCoordinatesByCell(
firstRow,
firstColumn,
);
const [x1, y1] = worksheetCanvas.getCoordinatesByCell(
lastRow + 1,
lastColumn + 1,
);
const width = (x1 - x) * devicePixelRatio;
const height = (y1 - y) * devicePixelRatio;
x *= devicePixelRatio;
y *= devicePixelRatio;
const capturedCanvas = document.createElement("canvas");
capturedCanvas.width = width;
capturedCanvas.height = height;
const ctx = capturedCanvas.getContext("2d");
if (!ctx) {
return;
}
ctx.drawImage(
worksheetCanvas.canvas,
x,
y,
width,
height,
0,
0,
width,
height,
);
const downloadLink = document.createElement("a");
downloadLink.href = capturedCanvas.toDataURL("image/png");
downloadLink.download = "ironcalc.png";
downloadLink.click();
}}
onBorderChanged={(border: BorderOptions): void => {
const {
sheet,
@@ -698,7 +633,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
refresh={(): void => {
setRedrawId((id) => id + 1);
}}
ref={worksheetRef}
/>
<SheetTabBar

View File

@@ -0,0 +1,659 @@
import { type Model, columnNameFromNumber } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import CellContextMenu from "./CellContextMenu";
import {
COLUMN_WIDTH_SCALE,
LAST_COLUMN,
LAST_ROW,
ROW_HEIGH_SCALE,
outlineBackgroundColor,
outlineColor,
} from "./WorksheetCanvas/constants";
import WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
import {
FORMULA_BAR_HEIGHT,
NAVIGATION_HEIGHT,
TOOLBAR_HEIGHT,
} from "./constants";
import Editor from "./editor/editor";
import type { Cell } from "./types";
import usePointer from "./usePointer";
import { AreaType, type WorkbookState } from "./workbookState";
function useWindowSize() {
const [size, setSize] = useState([0, 0]);
useLayoutEffect(() => {
function updateSize() {
setSize([window.innerWidth, window.innerHeight]);
}
window.addEventListener("resize", updateSize);
updateSize();
return () => window.removeEventListener("resize", updateSize);
}, []);
return size;
}
function Worksheet(props: {
model: Model;
workbookState: WorkbookState;
refresh: () => void;
}) {
const canvasElement = useRef<HTMLCanvasElement>(null);
const worksheetElement = useRef<HTMLDivElement>(null);
const scrollElement = useRef<HTMLDivElement>(null);
const editorElement = useRef<HTMLDivElement>(null);
const spacerElement = useRef<HTMLDivElement>(null);
const cellOutline = useRef<HTMLDivElement>(null);
const areaOutline = useRef<HTMLDivElement>(null);
const cellOutlineHandle = useRef<HTMLDivElement>(null);
const extendToOutline = useRef<HTMLDivElement>(null);
const columnResizeGuide = useRef<HTMLDivElement>(null);
const rowResizeGuide = useRef<HTMLDivElement>(null);
const columnHeaders = useRef<HTMLDivElement>(null);
const worksheetCanvas = useRef<WorksheetCanvas | null>(null);
const [contextMenuOpen, setContextMenuOpen] = useState(false);
const ignoreScrollEventRef = useRef(false);
const { model, workbookState, refresh } = props;
const [clientWidth, clientHeight] = useWindowSize();
useEffect(() => {
const canvasRef = canvasElement.current;
const columnGuideRef = columnResizeGuide.current;
const rowGuideRef = rowResizeGuide.current;
const columnHeadersRef = columnHeaders.current;
const worksheetRef = worksheetElement.current;
const outline = cellOutline.current;
const handle = cellOutlineHandle.current;
const area = areaOutline.current;
const extendTo = extendToOutline.current;
const editor = editorElement.current;
if (
!canvasRef ||
!columnGuideRef ||
!rowGuideRef ||
!columnHeadersRef ||
!worksheetRef ||
!outline ||
!handle ||
!area ||
!extendTo ||
!scrollElement.current ||
!editor
)
return;
// FIXME: This two need to be computed.
model.setWindowWidth(clientWidth - 37);
model.setWindowHeight(clientHeight - 190);
const canvas = new WorksheetCanvas({
width: worksheetRef.clientWidth,
height: worksheetRef.clientHeight,
model,
workbookState,
elements: {
canvas: canvasRef,
columnGuide: columnGuideRef,
rowGuide: rowGuideRef,
columnHeaders: columnHeadersRef,
cellOutline: outline,
cellOutlineHandle: handle,
areaOutline: area,
extendToOutline: extendTo,
editor: editor,
},
onColumnWidthChanges(sheet, column, width) {
if (width < 0) {
return;
}
const { range } = model.getSelectedView();
let columnStart = column;
let columnEnd = column;
const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
if (
fullColumn &&
column >= range[1] &&
column <= range[3] &&
!fullRow
) {
columnStart = Math.min(range[1], column, range[3]);
columnEnd = Math.max(range[1], column, range[3]);
}
model.setColumnsWidth(sheet, columnStart, columnEnd, width);
worksheetCanvas.current?.renderSheet();
},
onRowHeightChanges(sheet, row, height) {
if (height < 0) {
return;
}
const { range } = model.getSelectedView();
let rowStart = row;
let rowEnd = row;
const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
if (fullRow && row >= range[0] && row <= range[2] && !fullColumn) {
rowStart = Math.min(range[0], row, range[2]);
rowEnd = Math.max(range[0], row, range[2]);
}
model.setRowsHeight(sheet, rowStart, rowEnd, height);
worksheetCanvas.current?.renderSheet();
},
refresh,
});
const scrollX = model.getScrollX();
const scrollY = model.getScrollY();
const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000];
if (spacerElement.current) {
spacerElement.current.style.height = `${sheetHeight}px`;
spacerElement.current.style.width = `${sheetWidth}px`;
}
const left = scrollElement.current.scrollLeft;
const top = scrollElement.current.scrollTop;
if (scrollX !== left) {
ignoreScrollEventRef.current = true;
scrollElement.current.scrollLeft = scrollX;
setTimeout(() => {
ignoreScrollEventRef.current = false;
}, 0);
}
if (scrollY !== top) {
ignoreScrollEventRef.current = true;
scrollElement.current.scrollTop = scrollY;
setTimeout(() => {
ignoreScrollEventRef.current = false;
}, 0);
}
canvas.renderSheet();
worksheetCanvas.current = canvas;
});
const { onPointerMove, onPointerDown, onPointerHandleDown, onPointerUp } =
usePointer({
model,
workbookState,
refresh,
onColumnSelected: (column: number, shift: boolean) => {
let firstColumn = column;
let lastColumn = column;
if (shift) {
const { range } = model.getSelectedView();
firstColumn = Math.min(range[1], column, range[3]);
lastColumn = Math.max(range[3], column, range[1]);
}
model.setSelectedCell(1, firstColumn);
model.setSelectedRange(1, firstColumn, LAST_ROW, lastColumn);
refresh();
},
onRowSelected: (row: number, shift: boolean) => {
let firstRow = row;
let lastRow = row;
if (shift) {
const { range } = model.getSelectedView();
firstRow = Math.min(range[0], row, range[2]);
lastRow = Math.max(range[2], row, range[0]);
}
model.setSelectedCell(firstRow, 1);
model.setSelectedRange(firstRow, 1, lastRow, LAST_COLUMN);
refresh();
},
onAllSheetSelected: () => {
model.setSelectedCell(1, 1);
model.setSelectedRange(1, 1, LAST_ROW, LAST_COLUMN);
},
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
model.setSelectedCell(cell.row, cell.column);
refresh();
},
onAreaSelecting: (cell: Cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
model.onAreaSelecting(row, column);
canvas.renderSheet();
refresh();
},
onAreaSelected: () => {
const styles = workbookState.getCopyStyles();
if (styles?.length) {
model.onPasteStyles(styles);
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
canvas.renderSheet();
}
workbookState.setCopyStyles(null);
if (worksheetElement.current) {
worksheetElement.current.style.cursor = "auto";
}
refresh();
},
onExtendToCell: (cell) => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { row, column } = cell;
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
// We are either extending by rows or by columns
// And we could be doing it in the positive direction (downwards or right)
// or the negative direction (upwards or left)
if (
row > rowEnd &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < row - rowEnd) ||
(column > columnEnd && column - columnEnd < row - rowEnd))
) {
// rows downwards
const area = {
type: AreaType.rowsDown,
rowStart: rowEnd + 1,
rowEnd: row,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
row < rowStart &&
((column <= columnEnd && column >= columnStart) ||
(column < columnStart && columnStart - column < rowStart - row) ||
(column > columnEnd && column - columnEnd < rowStart - row))
) {
// rows upwards
const area = {
type: AreaType.rowsUp,
rowStart: row,
rowEnd: rowStart,
columnStart,
columnEnd,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column > columnEnd &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < column - columnEnd) ||
(row > rowEnd && row - rowEnd < column - columnEnd))
) {
// columns right
const area = {
type: AreaType.columnsRight,
rowStart,
rowEnd,
columnStart: columnEnd + 1,
columnEnd: column,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
} else if (
column < columnStart &&
((row <= rowEnd && row >= rowStart) ||
(row < rowStart && rowStart - row < columnStart - column) ||
(row > rowEnd && row - rowEnd < columnStart - column))
) {
// columns left
const area = {
type: AreaType.columnsLeft,
rowStart,
rowEnd,
columnStart: column,
columnEnd: columnStart,
};
workbookState.setExtendToArea(area);
canvas.renderSheet();
}
},
onExtendToEnd: () => {
const canvas = worksheetCanvas.current;
if (!canvas) {
return;
}
const { sheet, range } = model.getSelectedView();
const extendedArea = workbookState.getExtendToArea();
if (!extendedArea) {
return;
}
const rowStart = Math.min(range[0], range[2]);
const height = Math.abs(range[2] - range[0]) + 1;
const width = Math.abs(range[3] - range[1]) + 1;
const columnStart = Math.min(range[1], range[3]);
const area = {
sheet,
row: rowStart,
column: columnStart,
width,
height,
};
switch (extendedArea.type) {
case AreaType.rowsDown:
model.autoFillRows(area, extendedArea.rowEnd);
break;
case AreaType.rowsUp: {
model.autoFillRows(area, extendedArea.rowStart);
break;
}
case AreaType.columnsRight: {
model.autoFillColumns(area, extendedArea.columnEnd);
break;
}
case AreaType.columnsLeft: {
model.autoFillColumns(area, extendedArea.columnStart);
break;
}
}
model.setSelectedRange(
Math.min(rowStart, extendedArea.rowStart),
Math.min(columnStart, extendedArea.columnStart),
Math.max(rowStart + height - 1, extendedArea.rowEnd),
Math.max(columnStart + width - 1, extendedArea.columnEnd),
);
workbookState.clearExtendToArea();
canvas.renderSheet();
},
canvasElement,
worksheetElement,
worksheetCanvas,
});
const onScroll = (): void => {
if (!scrollElement.current || !worksheetCanvas.current) {
return;
}
if (ignoreScrollEventRef.current) {
// Programmatic scroll ignored
return;
}
const left = scrollElement.current.scrollLeft;
const top = scrollElement.current.scrollTop;
worksheetCanvas.current.setScrollPosition({ left, top });
worksheetCanvas.current.renderSheet();
};
return (
<Wrapper ref={scrollElement} onScroll={onScroll} className="scroll">
<Spacer ref={spacerElement} />
<SheetContainer
className="sheet-container"
ref={worksheetElement}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onContextMenu={(event) => {
event.preventDefault();
event.stopPropagation();
setContextMenuOpen(true);
}}
onDoubleClick={(event) => {
// Starts editing cell
const { sheet, row, column } = model.getSelectedView();
const text = model.getCellContent(sheet, row, column);
const editorWidth =
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
workbookState.setEditingCell({
sheet,
row,
column,
text,
cursorStart: text.length,
cursorEnd: text.length,
focus: "cell",
referencedRange: null,
activeRanges: [],
mode: "accept",
editorWidth,
editorHeight,
});
event.stopPropagation();
// event.preventDefault();
props.refresh();
}}
>
<SheetCanvas ref={canvasElement} />
<CellOutline ref={cellOutline} />
<EditorWrapper ref={editorElement}>
<Editor
originalText={workbookState.getEditingText()}
onEditEnd={(): void => {
props.refresh();
}}
onTextUpdated={(): void => {
props.refresh();
}}
model={model}
workbookState={workbookState}
type={"cell"}
/>
</EditorWrapper>
<AreaOutline ref={areaOutline} />
<ExtendToOutline ref={extendToOutline} />
<CellOutlineHandle
ref={cellOutlineHandle}
onPointerDown={onPointerHandleDown}
/>
<ColumnResizeGuide ref={columnResizeGuide} />
<RowResizeGuide ref={rowResizeGuide} />
<ColumnHeaders ref={columnHeaders} />
</SheetContainer>
<CellContextMenu
open={contextMenuOpen}
onClose={() => setContextMenuOpen(false)}
anchorEl={cellOutline.current}
onInsertRowAbove={(): void => {
const view = model.getSelectedView();
model.insertRow(view.sheet, view.row);
setContextMenuOpen(false);
}}
onInsertRowBelow={(): void => {
const view = model.getSelectedView();
model.insertRow(view.sheet, view.row + 1);
setContextMenuOpen(false);
}}
onInsertColumnLeft={(): void => {
const view = model.getSelectedView();
model.insertColumn(view.sheet, view.column);
setContextMenuOpen(false);
}}
onInsertColumnRight={(): void => {
const view = model.getSelectedView();
model.insertColumn(view.sheet, view.column + 1);
setContextMenuOpen(false);
}}
onFreezeColumns={(): void => {
const view = model.getSelectedView();
model.setFrozenColumnsCount(view.sheet, view.column);
setContextMenuOpen(false);
}}
onFreezeRows={(): void => {
const view = model.getSelectedView();
model.setFrozenRowsCount(view.sheet, view.row);
setContextMenuOpen(false);
}}
onUnfreezeColumns={(): void => {
const sheet = model.getSelectedSheet();
model.setFrozenColumnsCount(sheet, 0);
setContextMenuOpen(false);
}}
onUnfreezeRows={(): void => {
const sheet = model.getSelectedSheet();
model.setFrozenRowsCount(sheet, 0);
setContextMenuOpen(false);
}}
onDeleteRow={(): void => {
const view = model.getSelectedView();
model.deleteRow(view.sheet, view.row);
setContextMenuOpen(false);
}}
onDeleteColumn={(): void => {
const view = model.getSelectedView();
model.deleteColumn(view.sheet, view.column);
setContextMenuOpen(false);
}}
row={model.getSelectedView().row}
column={columnNameFromNumber(model.getSelectedView().column)}
/>
</Wrapper>
);
}
const Spacer = styled("div")`
position: absolute;
height: 5000px;
width: 5000px;
`;
const SheetContainer = styled("div")`
position: sticky;
top: 0px;
left: 0px;
height: 100%;
.column-resize-handle {
position: absolute;
top: 0px;
width: 3px;
opacity: 0;
background: ${outlineColor};
border-radius: 5px;
cursor: col-resize;
}
.column-resize-handle:hover {
opacity: 1;
}
.row-resize-handle {
position: absolute;
left: 0px;
height: 3px;
opacity: 0;
background: ${outlineColor};
border-radius: 5px;
cursor: row-resize;
}
.row-resize-handle:hover {
opacity: 1;
}
`;
const Wrapper = styled("div")({
position: "absolute",
overflow: "scroll",
top: TOOLBAR_HEIGHT + FORMULA_BAR_HEIGHT + 1,
left: 0,
right: 0,
bottom: NAVIGATION_HEIGHT + 1,
overscrollBehavior: "none",
});
const SheetCanvas = styled("canvas")`
position: relative;
top: 0px;
left: 0px;
right: 0px;
bottom: 40px;
`;
const ColumnResizeGuide = styled("div")`
position: absolute;
top: 0px;
display: none;
height: 100%;
width: 0px;
border-left: 1px dashed ${outlineColor};
`;
const ColumnHeaders = styled("div")`
position: absolute;
left: 0px;
top: 0px;
overflow: hidden;
display: flex;
& .column-header {
display: inline-block;
text-align: center;
overflow: hidden;
height: 100%;
user-select: none;
}
`;
const RowResizeGuide = styled("div")`
position: absolute;
display: none;
left: 0px;
height: 0px;
width: 100%;
border-top: 1px dashed ${outlineColor};
`;
const AreaOutline = styled("div")`
position: absolute;
border: 1px solid ${outlineColor};
border-radius: 3px;
background-color: ${outlineBackgroundColor};
`;
const CellOutline = styled("div")`
position: absolute;
border: 2px solid ${outlineColor};
border-radius: 3px;
word-break: break-word;
font-size: 13px;
display: flex;
`;
const CellOutlineHandle = styled("div")`
position: absolute;
width: 5px;
height: 5px;
background: ${outlineColor};
cursor: crosshair;
border-radius: 1px;
`;
const ExtendToOutline = styled("div")`
position: absolute;
border: 1px dashed ${outlineColor};
border-radius: 3px;
`;
const EditorWrapper = styled("div")`
position: absolute;
width: 100%;
padding: 0px;
border-width: 0px;
outline: none;
resize: none;
white-space: pre-wrap;
vertical-align: bottom;
overflow: hidden;
text-align: left;
span {
min-width: 1px;
}
font-family: monospace;
border: 2px solid ${outlineColor};
`;
export default Worksheet;

View File

@@ -16,8 +16,6 @@
"format_number": "Format number",
"font_color": "Font color",
"fill_color": "Fill color",
"increase_font_size": "Increase font size",
"decrease_font_size": "Decrease font size",
"decimal_places_increase": "Increase decimal places",
"decimal_places_decrease": "Decrease decimal places",
"show_hide_grid_lines": "Show/hide grid lines",
@@ -25,7 +23,6 @@
"vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle",
"vertical_align_top": "Align top",
"selected_png": "Export Selected area as PNG",
"format_menu": {
"auto": "Auto",
"number": "Number",
@@ -118,8 +115,5 @@
"freeze": "Freeze",
"insert_row": "Insert row",
"insert_column": "Insert column"
},
"color_picker": {
"apply": "Apply"
}
}

View File

@@ -13,7 +13,6 @@
"@ironcalc/workbook": "file:../../IronCalc/",
"@mui/material": "^6.4",
"lucide-react": "^0.473.0",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@@ -44,21 +43,21 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "^3.2.4",
"@storybook/addon-essentials": "^8.6.0",
"@storybook/addon-interactions": "^8.6.0",
"@storybook/blocks": "^8.6.0",
"@storybook/react": "^8.6.0",
"@storybook/react-vite": "^8.6.0",
"@storybook/test": "^8.6.0",
"@storybook/addon-essentials": "^8.5.3",
"@storybook/addon-interactions": "^8.5.3",
"@storybook/blocks": "^8.5.3",
"@storybook/react": "^8.5.3",
"@storybook/react-vite": "^8.5.3",
"@storybook/test": "^8.5.3",
"@vitejs/plugin-react": "^4.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"storybook": "^8.6.0",
"storybook": "^8.5.3",
"ts-node": "^10.9.2",
"typescript": "~5.6.2",
"vite": "^6.2.0",
"vite": "^6.0.5",
"vite-plugin-svgr": "^4.2.0",
"vitest": "^3.0.7"
"vitest": "^2.0.5"
},
"peerDependencies": {
"@types/react": "^18.0.0 || ^19.0.0",
@@ -2488,14 +2487,6 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",

View File

@@ -16,7 +16,6 @@
"@ironcalc/workbook": "file:../../IronCalc/",
"@mui/material": "^6.4",
"lucide-react": "^0.473.0",
"qrcode.react": "^4.2.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},

View File

@@ -1,12 +1,13 @@
import styled from "@emotion/styled";
import type { Model } from "@ironcalc/workbook";
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
import { CircleCheck } from "lucide-react";
import { useRef, useState } from "react";
// import { IronCalcIcon, IronCalcLogo } from "./../icons";
import { FileMenu } from "./FileMenu";
import { ShareButton } from "./ShareButton";
import ShareWorkbookDialog from "./ShareWorkbookDialog";
import { WorkbookTitle } from "./WorkbookTitle";
import { downloadModel } from "./rpc";
import { downloadModel, shareModel } from "./rpc";
import { updateNameSelectedWorkbook } from "./storage";
export function FileBar(properties: {
@@ -17,8 +18,7 @@ export function FileBar(properties: {
onDelete: () => void;
}) {
const hiddenInputRef = useRef<HTMLInputElement>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [toast, setToast] = useState(false);
return (
<FileBarWrapper>
<StyledDesktopLogo />
@@ -53,17 +53,37 @@ export function FileBar(properties: {
type="text"
style={{ position: "absolute", left: -9999, top: -9999 }}
/>
<div style={{ marginLeft: "auto" }} />
<DialogContainer>
<ShareButton onClick={() => setIsDialogOpen(true)} />
{isDialogOpen && (
<ShareWorkbookDialog
onClose={() => setIsDialogOpen(false)}
onModelUpload={properties.onModelUpload}
model={properties.model}
/>
<div style={{ marginLeft: "auto" }}>
{toast ? (
<Toast>
<CircleCheck style={{ width: 12 }} />
<span
style={{ marginLeft: 8, marginRight: 12, fontFamily: "Inter" }}
>
URL copied to clipboard
</span>
</Toast>
) : (
""
)}
</DialogContainer>
</div>
<ShareButton
onClick={async () => {
const model = properties.model;
const bytes = model.toBytes();
const fileName = model.getName();
const hash = await shareModel(bytes, fileName);
const value = `${location.origin}/?model=${hash}`;
if (hiddenInputRef.current) {
hiddenInputRef.current.value = value;
hiddenInputRef.current.select();
document.execCommand("copy");
setToast(true);
setTimeout(() => setToast(false), 5000);
}
console.log(value);
}}
/>
</FileBarWrapper>
);
}
@@ -97,6 +117,14 @@ const HelpButton = styled("div")`
}
`;
const Toast = styled("div")`
font-weight: 400;
font-size: 12px;
color: #9e9e9e;
display: flex;
align-items: center;
`;
const Divider = styled("div")`
margin: 0px 8px 0px 16px;
height: 12px;
@@ -113,17 +141,3 @@ const FileBarWrapper = styled("div")`
position: relative;
justify-content: space-between;
`;
const DialogContainer = styled("div")`
position: relative;
display: inline-block;
button {
margin-bottom: 8px;
}
.MuiDialog-root {
position: absolute;
top: 100%;
left: 0;
transform: translateY(8px);
}
`;

View File

@@ -1,202 +0,0 @@
import type { Model } from "@ironcalc/workbook";
import { Button, Dialog, TextField, styled } from "@mui/material";
import { Check, Copy, GlobeLock } from "lucide-react";
import { QRCodeSVG } from "qrcode.react";
import { useEffect, useState } from "react";
import { shareModel } from "./rpc";
function ShareWorkbookDialog(properties: {
onClose: () => void;
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
model?: Model;
}) {
const [url, setUrl] = useState<string>("");
const [copied, setCopied] = useState(false);
useEffect(() => {
const generateUrl = async () => {
if (properties.model) {
const bytes = properties.model.toBytes();
const fileName = properties.model.getName();
const hash = await shareModel(bytes, fileName);
setUrl(`${location.origin}/?model=${hash}`);
}
};
generateUrl();
}, [properties.model]);
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>;
if (copied) {
timeoutId = setTimeout(() => {
setCopied(false);
}, 2000);
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [copied]);
const handleClose = () => {
properties.onClose();
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(url);
setCopied(true);
} catch (err) {
console.error("Failed to copy text: ", err);
}
};
return (
<DialogWrapper
open={true}
tabIndex={0}
onClose={handleClose}
onKeyDown={(event) => {
if (event.code === "Escape") {
handleClose();
}
}}
>
<DialogContent>
<QRCodeWrapper>
<QRCodeSVG value={url} size={80} />{" "}
</QRCodeWrapper>
<URLWrapper>
<StyledTextField
hiddenLabel
disabled
value={url}
variant="outlined"
fullWidth
margin="normal"
size="small"
/>
<StyledButton
variant="contained"
color="primary"
size="small"
onClick={handleCopy}
>
{copied ? <StyledCheck /> : <StyledCopy />}
{copied ? "Copied!" : "Copy URL"}
</StyledButton>
</URLWrapper>
</DialogContent>
<UploadFooter>
<GlobeLock />
Anyone with the link will be able to access a copy of this workbook
</UploadFooter>
</DialogWrapper>
);
}
const DialogWrapper = styled(Dialog)`
.MuiDialog-paper {
width: 440px;
position: absolute;
top: 44px;
right: 0px;
margin: 10px;
max-width: calc(100% - 20px);
}
.MuiBackdrop-root {
background-color: transparent;
}
`;
const DialogContent = styled("div")`
padding: 20px;
display: flex;
flex-direction: row;
gap: 12px;
height: 80px;
`;
const URLWrapper = styled("div")`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
justify-content: space-between;
`;
const StyledTextField = styled(TextField)`
margin: 0px;
.MuiInputBase-root {
max-height: 36px;
font-size: 14px;
padding-top: 0px;
}
.MuiOutlinedInput-input {
text-overflow: ellipsis;
padding: 8px;
}
`;
const StyledButton = styled(Button)`
display: flex;
flex-direction: row;
gap: 4px;
background-color: #eeeeee;
height: 36px;
color: #616161;
box-shadow: none;
font-size: 14px;
text-transform: capitalize;
gap: 10px;
&:hover {
background-color: #e0e0e0;
box-shadow: none;
}
&:active {
background-color: #d4d4d4;
box-shadow: none;
}
`;
const StyledCopy = styled(Copy)`
width: 16px;
`;
const StyledCheck = styled(Check)`
width: 16px;
`;
const QRCodeWrapper = styled("div")`
min-height: 80px;
min-width: 80px;
background-color: grey;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
@media (max-width: 600px) {
display: none;
}
`;
const UploadFooter = styled("div")`
height: 44px;
border-top: 1px solid #e0e0e0;
font-size: 12px;
font-weight: 400;
color: #757575;
display: flex;
align-items: center;
font-family: Inter;
gap: 8px;
padding: 0px 12px;
svg {
max-width: 16px;
}
`;
export default ShareWorkbookDialog;

View File

@@ -1,7 +1,7 @@
[package]
name = "ironcalc_server"
version = "0.3.0"
edition = "2021"
edition = "2024"
[dependencies]
rocket = "0.5"

View File

@@ -2,7 +2,7 @@
name = "ironcalc"
version = "0.3.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2021"
edition = "2024"
homepage = "https://www.ironcalc.com"
repository = "https://github.com/ironcalc/ironcalc/"
description = "The democratization of spreadsheets"

View File

@@ -1,5 +1,5 @@
use ironcalc::{
base::{expressions::utils::number_to_column, Model},
base::{Model, expressions::utils::number_to_column},
export::save_to_xlsx,
};

Some files were not shown because too many files have changed in this diff Show More