Compare commits

..

13 Commits

Author SHA1 Message Date
Nicolás Hatcher
5c13f241c6 FIX: Fixes for the CI builds 2025-02-28 12:00:54 +01:00
Nicolás Hatcher
26b20eea43 UPDATE: Bump versions to 0.5 2025-02-28 01:00:50 +01:00
Nicolás Hatcher
b62256963a UPDATE: Adds wrapping! 2025-02-28 00:29:44 +01:00
Nicolás Hatcher
4f627b4363 FIX: More sensible decrease/increase font-size 2025-02-28 00:29:44 +01:00
Daniel
a9a8c4f615 UPDATE: Add a dialog when 'Share' buttons is clickled 2025-02-27 18:13:20 +01:00
Nicolás Hatcher
f9c9467e6c FIX: Correct height/width of cells with different font sizes 2025-02-26 23:44:08 +01:00
Nicolás Hatcher
409b77c210 FIX: Default size should be 13 pixels 2025-02-26 20:29:36 +01:00
Nicolás Hatcher
eecf6f3c3b UPDATE: Download to PNG the visible part of the selected area
This downloads only the visible part of the selected area.
To download the full selected area we would need to work a bit more
2025-02-26 19:27:56 +01:00
Nicolás Hatcher
ce7318840d UPDATE: We can now change the font size! 2025-02-26 19:11:38 +01:00
Nicolás Hatcher
7bc563ef29 FIX: Make biome happy 2025-02-26 18:03:15 +01:00
Nicolás Hatcher
8ed88e1445 FIX: Update versions in widget 2025-02-26 18:03:15 +01:00
Nicolás Hatcher
a1353e0817 FIX: More consistent naming conventions in widget 2025-02-26 18:03:15 +01:00
Nicolás Hatcher
c0fa55c5f7 FIX: Add "Apply" button to color picker 2025-02-24 19:00:05 +01:00
114 changed files with 2476 additions and 2511 deletions

View File

@@ -32,7 +32,7 @@ jobs:
manylinux: auto manylinux: auto
working-directory: bindings/python working-directory: bindings/python
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels
path: bindings/python/dist path: bindings/python/dist
@@ -56,7 +56,7 @@ jobs:
sccache: 'true' sccache: 'true'
working-directory: bindings/python working-directory: bindings/python
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels
path: bindings/python/dist path: bindings/python/dist
@@ -79,7 +79,7 @@ jobs:
sccache: 'true' sccache: 'true'
working-directory: bindings/python working-directory: bindings/python
- name: Upload wheels - name: Upload wheels
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels
path: bindings/python/dist path: bindings/python/dist
@@ -95,7 +95,7 @@ jobs:
args: --out dist args: --out dist
working-directory: bindings/python working-directory: bindings/python
- name: Upload sdist - name: Upload sdist
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: wheels name: wheels
path: bindings/python/dist path: bindings/python/dist

10
Cargo.lock generated
View File

@@ -414,7 +414,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc" name = "ironcalc"
version = "0.3.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -430,7 +430,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_base" name = "ironcalc_base"
version = "0.3.0" version = "0.5.0"
dependencies = [ dependencies = [
"bitcode", "bitcode",
"chrono", "chrono",
@@ -448,7 +448,7 @@ dependencies = [
[[package]] [[package]]
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.3.1" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"napi", "napi",
@@ -784,7 +784,7 @@ dependencies = [
[[package]] [[package]]
name = "pyroncalc" name = "pyroncalc"
version = "0.3.0" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc", "ironcalc",
"pyo3", "pyo3",
@@ -1070,7 +1070,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]] [[package]]
name = "wasm" name = "wasm"
version = "0.3.2" version = "0.5.0"
dependencies = [ dependencies = [
"ironcalc_base", "ironcalc_base",
"serde", "serde",

View File

@@ -77,7 +77,7 @@ And visit <http://0.0.0.0:8000/ironcalc/>
Add the dependency to `Cargo.toml`: Add the dependency to `Cargo.toml`:
```toml ```toml
[dependencies] [dependencies]
ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"} ironcalc = { git = "https://github.com/ironcalc/IronCalc", version = "0.5"}
``` ```
And then use this code in `main.rs`: And then use this code in `main.rs`:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
use super::{Node, move_formula::ref_is_in_area}; use super::{move_formula::ref_is_in_area, Node};
use crate::expressions::types::{Area, CellReferenceIndex}; 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(), text: "#VALUE!".to_owned(),
color: None, color: None,
error: Some(e), error: Some(e),
}; }
} }
}; };
for token in tokens { for token in tokens {
@@ -391,7 +391,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
if l_exp <= p.exponent_digit_count { if l_exp <= p.exponent_digit_count {
if !(number_index < 0 && digit.kind == '#') { if !(number_index < 0 && digit.kind == '#') {
let c = if number_index < 0 { let c = if number_index < 0 {
if digit.kind == '?' { ' ' } else { '0' } if digit.kind == '?' {
' '
} else {
'0'
}
} else { } else {
exponent_part[number_index as usize] exponent_part[number_index as usize]
}; };

View File

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

View File

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

View File

@@ -57,7 +57,7 @@
use std::f64::consts::FRAC_2_PI; use std::f64::consts::FRAC_2_PI;
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words}; use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
// R0/S0 on [0, 2.00] // R0/S0 on [0, 2.00]
const R02: f64 = 1.562_499_999_999_999_5e-2; // 0x3F8FFFFF, 0xFFFFFFFD 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 std::f64::consts::FRAC_2_PI;
use super::bessel_util::{FRAC_2_SQRT_PI, HUGE, high_word, split_words}; use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
// R0/S0 on [0,2] // R0/S0 on [0,2]
const R00: f64 = -6.25e-2; // 0xBFB00000, 0x00000000 const R00: f64 = -6.25e-2; // 0xBFB00000, 0x00000000

View File

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

View File

@@ -45,5 +45,9 @@ pub(crate) fn erf(x: f64) -> f64 {
} }
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd); 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, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
}; };
CalcResult::Number(ipmt) CalcResult::Number(ipmt)
@@ -762,7 +762,7 @@ impl Model {
error: error.0, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
}; };
CalcResult::Number(ppmt) CalcResult::Number(ppmt)
@@ -1075,7 +1075,7 @@ impl Model {
error, error,
origin: cell, origin: cell,
message, message,
}; }
} }
} }
}; };
@@ -1096,7 +1096,7 @@ impl Model {
error, error,
origin: cell, origin: cell,
message, message,
}; }
} }
} }
}; };
@@ -1634,7 +1634,7 @@ impl Model {
error: error.0, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
} }
} }
@@ -1702,7 +1702,7 @@ impl Model {
error: error.0, error: error.0,
origin: cell, origin: cell,
message: error.1, message: error.1,
}; }
} }
} }
} }
@@ -1750,7 +1750,11 @@ impl Model {
rate = 1.0 rate = 1.0
}; };
let value = if 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 { } else {
cost * (1.0 - rate).powf(period - 1.0) cost * (1.0 - rate).powf(period - 1.0)
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,14 +8,14 @@ use crate::{
expressions::{ expressions::{
lexer::LexerMode, lexer::LexerMode,
parser::{ parser::{
Parser,
stringify::{rename_sheet_in_node, to_rc_format}, stringify::{rename_sheet_in_node, to_rc_format},
Parser,
}, },
types::CellReferenceRC, types::CellReferenceRC,
}, },
language::get_language, language::get_language,
locale::get_locale, locale::get_locale,
model::{Model, ParsedDefinedName, get_milliseconds_since_epoch}, model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{ types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView, 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(), text: "#ERROR!".to_owned(),
color: None, color: None,
error: Some("Invalid locale".to_string()), error: Some("Invalid locale".to_string()),
}; }
} }
}; };
formatter::format::format_number(value, format_code, locale) formatter::format::format_number(value, format_code, locale)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
use crate::{ use crate::{
UserModel,
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN}, constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN},
test::util::new_empty_model, test::util::new_empty_model,
UserModel,
}; };
#[test] #[test]
@@ -170,7 +170,7 @@ fn row_heigh_increases_automatically() {
model model
.set_user_input(0, 1, 1, "My home in Canada had horses\nAnd monkeys!") .set_user_input(0, 1, 1, "My home in Canada had horses\nAnd monkeys!")
.unwrap(); .unwrap();
assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT)); assert_eq!(model.get_row_height(0, 1), Ok(40.5));
} }
#[test] #[test]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ use csv::{ReaderBuilder, WriterBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
constants::{self, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW}, constants::{self, LAST_COLUMN, LAST_ROW},
expressions::{ expressions::{
types::{Area, CellReferenceIndex}, types::{Area, CellReferenceIndex},
utils::{is_valid_column_number, is_valid_row}, utils::{is_valid_column_number, is_valid_row},
@@ -127,6 +127,17 @@ fn update_style(old_value: &Style, style_path: &str, value: &str) -> Result<Styl
"font.color" => { "font.color" => {
style.font.color = color(value)?; 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" => { "fill.bg_color" => {
style.fill.bg_color = color(value)?; style.fill.bg_color = color(value)?;
style.fill.pattern_type = "solid".to_string(); style.fill.pattern_type = "solid".to_string();
@@ -419,10 +430,14 @@ impl UserModel {
new_value: value.to_string(), new_value: value.to_string(),
old_value: Box::new(old_value), old_value: Box::new(old_value),
}]; }];
let style = self.model.get_style_for_cell(sheet, row, column)?;
let line_count = value.split('\n').count(); let line_count = value.split('\n').count() as f64;
let row_height = self.model.get_row_height(sheet, row)?; let row_height = self.model.get_row_height(sheet, row)?;
let cell_height = (line_count as f64) * DEFAULT_ROW_HEIGHT; // This is in sync with the front-end auto fit row
let font_size = style.font.sz as f64;
let line_height = font_size * 1.5;
let cell_height = (line_count - 1.0) * line_height + 8.0 + font_size;
if cell_height > row_height { if cell_height > row_height {
diff_list.push(Diff::SetRowHeight { diff_list.push(Diff::SetRowHeight {
sheet, sheet,

View File

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

View File

@@ -1,7 +1,7 @@
[package] [package]
edition = "2024" edition = "2021"
name = "ironcalc_nodejs" name = "ironcalc_nodejs"
version = "0.3.1" version = "0.5.0"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] } napi = { version = "2.12.2", default-features = false, features = ["napi4", "serde-json"] }
napi-derive = "2.12.2" napi-derive = "2.12.2"
ironcalc = { path = "../../xlsx", version = "0.3.0" } ironcalc = { path = "../../xlsx", version = "0.5.0" }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[build-dependencies] [build-dependencies]

View File

@@ -1,6 +1,6 @@
{ {
"name": "@ironcalc/nodejs", "name": "@ironcalc/nodejs",
"version": "0.3.1", "version": "0.5.1",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
"napi": { "napi": {

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "pyroncalc" name = "pyroncalc"
version = "0.3.0" version = "0.5.0"
edition = "2024" edition = "2021"
[lib] [lib]
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.3.0" } xlsx = { package= "ironcalc", path = "../../xlsx", version = "0.5.0" }
pyo3 = { version = "0.23", features = ["extension-module"] } pyo3 = { version = "0.23", features = ["extension-module"] }

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "ironcalc" name = "ironcalc"
version = "0.3.0" version = "0.5.0"
description = "Create, edit and evaluate Excel spreadsheets" description = "Create, edit and evaluate Excel spreadsheets"
requires-python = ">=3.10" requires-python = ">=3.10"
keywords = [ keywords = [

View File

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

View File

@@ -1,11 +1,11 @@
[package] [package]
name = "wasm" name = "wasm"
version = "0.3.2" version = "0.5.0"
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
description = "IronCalc Web bindings" description = "IronCalc Web bindings"
license = "MIT/Apache-2.0" license = "MIT/Apache-2.0"
repository = "https://github.com/ironcalc/ironcalc" repository = "https://github.com/ironcalc/ironcalc"
edition = "2024" edition = "2021"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
@@ -14,7 +14,7 @@ crate-type = ["cdylib"]
# Uses `../ironcalc/base` when used locally, and uses # Uses `../ironcalc/base` when used locally, and uses
# the inicated version from crates.io when published. # the inicated version from crates.io when published.
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations # https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
ironcalc_base = { path = "../../base", version = "0.3", features = ["use_regex_lite"] } ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = "0.2.92" wasm-bindgen = "0.2.92"
serde-wasm-bindgen = "0.4" serde-wasm-bindgen = "0.4"

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
name = "generate_locale" name = "generate_locale"
version = "0.1.0" version = "0.1.0"
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"] authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
edition = "2024" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # 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": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "^3.2.4", "@chromatic-com/storybook": "^3.2.4",
"@storybook/addon-essentials": "^8.5.3", "@storybook/addon-essentials": "^8.6.0",
"@storybook/addon-interactions": "^8.5.3", "@storybook/addon-interactions": "^8.6.0",
"@storybook/blocks": "^8.5.3", "@storybook/blocks": "^8.6.0",
"@storybook/react": "^8.5.3", "@storybook/react": "^8.6.0",
"@storybook/react-vite": "^8.5.3", "@storybook/react-vite": "^8.6.0",
"@storybook/test": "^8.5.3", "@storybook/test": "^8.6.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"storybook": "^8.5.3", "storybook": "^8.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^6.0.5", "vite": "^6.2.0",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vitest": "^2.0.5" "vitest": "^3.0.7"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0 || ^19.0.0", "@types/react": "^18.0.0 || ^19.0.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,10 +18,13 @@ import {
Grid2X2, Grid2X2,
Grid2x2Check, Grid2x2Check,
Grid2x2X, Grid2x2X,
ImageDown,
Italic, Italic,
Minus,
PaintBucket, PaintBucket,
PaintRoller, PaintRoller,
Percent, Percent,
Plus,
Redo2, Redo2,
RemoveFormatting, RemoveFormatting,
Strikethrough, Strikethrough,
@@ -29,6 +32,7 @@ import {
Type, Type,
Underline, Underline,
Undo2, Undo2,
WrapText,
} from "lucide-react"; } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -36,19 +40,19 @@ import {
ArrowMiddleFromLine, ArrowMiddleFromLine,
DecimalPlacesDecreaseIcon, DecimalPlacesDecreaseIcon,
DecimalPlacesIncreaseIcon, DecimalPlacesIncreaseIcon,
} from "../icons"; } from "../../icons";
import { theme } from "../theme"; import { theme } from "../../theme";
import NameManagerDialog from "./NameManagerDialog"; import BorderPicker from "../BorderPicker/BorderPicker";
import type { NameManagerProperties } from "./NameManagerDialog/NameManagerDialog"; import ColorPicker from "../ColorPicker/ColorPicker";
import BorderPicker from "./borderPicker"; import FormatMenu from "../FormatMenu/FormatMenu";
import ColorPicker from "./colorPicker";
import { TOOLBAR_HEIGHT } from "./constants";
import FormatMenu from "./formatMenu";
import { import {
NumberFormats, NumberFormats,
decreaseDecimalPlaces, decreaseDecimalPlaces,
increaseDecimalPlaces, increaseDecimalPlaces,
} from "./formatUtil"; } from "../FormatMenu/formatUtil";
import NameManagerDialog from "../NameManagerDialog";
import type { NameManagerProperties } from "../NameManagerDialog/NameManagerDialog";
import { TOOLBAR_HEIGHT } from "../constants";
type ToolbarProperties = { type ToolbarProperties = {
canUndo: boolean; canUndo: boolean;
@@ -61,20 +65,25 @@ type ToolbarProperties = {
onToggleStrike: (v: boolean) => void; onToggleStrike: (v: boolean) => void;
onToggleHorizontalAlign: (v: string) => void; onToggleHorizontalAlign: (v: string) => void;
onToggleVerticalAlign: (v: string) => void; onToggleVerticalAlign: (v: string) => void;
onToggleWrapText: (v: boolean) => void;
onCopyStyles: () => void; onCopyStyles: () => void;
onTextColorPicked: (hex: string) => void; onTextColorPicked: (hex: string) => void;
onFillColorPicked: (hex: string) => void; onFillColorPicked: (hex: string) => void;
onNumberFormatPicked: (numberFmt: string) => void; onNumberFormatPicked: (numberFmt: string) => void;
onBorderChanged: (border: BorderOptions) => void; onBorderChanged: (border: BorderOptions) => void;
onClearFormatting: () => void; onClearFormatting: () => void;
onIncreaseFontSize: (delta: number) => void;
onDownloadPNG: () => void;
fillColor: string; fillColor: string;
fontColor: string; fontColor: string;
fontSize: number;
bold: boolean; bold: boolean;
underline: boolean; underline: boolean;
italic: boolean; italic: boolean;
strike: boolean; strike: boolean;
horizontalAlign: HorizontalAlignment; horizontalAlign: HorizontalAlignment;
verticalAlign: VerticalAlignment; verticalAlign: VerticalAlignment;
wrapText: boolean;
canEdit: boolean; canEdit: boolean;
numFmt: string; numFmt: string;
showGridLines: boolean; showGridLines: boolean;
@@ -200,6 +209,30 @@ function Toolbar(properties: ToolbarProperties) {
</StyledButton> </StyledButton>
</FormatMenu> </FormatMenu>
<Divider /> <Divider />
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(-1);
}}
title={t("toolbar.decrease_font_size")}
>
<Minus />
</StyledButton>
<FontSizeBox>{properties.fontSize}</FontSizeBox>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onIncreaseFontSize(1);
}}
title={t("toolbar.increase_font_size")}
>
<Plus />
</StyledButton>
<Divider />
<StyledButton <StyledButton
type="button" type="button"
$pressed={properties.bold} $pressed={properties.bold}
@@ -336,6 +369,17 @@ function Toolbar(properties: ToolbarProperties) {
> >
<ArrowDownToLine /> <ArrowDownToLine />
</StyledButton> </StyledButton>
<StyledButton
type="button"
$pressed={properties.wrapText === true}
onClick={() => {
properties.onToggleWrapText(!properties.wrapText);
}}
disabled={!canEdit}
title={t("toolbar.wrap_text")}
>
<WrapText />
</StyledButton>
<Divider /> <Divider />
<StyledButton <StyledButton
@@ -374,6 +418,17 @@ function Toolbar(properties: ToolbarProperties) {
> >
<RemoveFormatting /> <RemoveFormatting />
</StyledButton> </StyledButton>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onDownloadPNG();
}}
title={t("toolbar.selected_png")}
>
<ImageDown />
</StyledButton>
<ColorPicker <ColorPicker
color={properties.fontColor} color={properties.fontColor}
@@ -496,4 +551,16 @@ const Divider = styled("div")({
margin: "0px 12px", margin: "0px 12px",
}); });
const FontSizeBox = styled("div")({
width: "24px",
height: "24px",
lineHeight: "24px",
textAlign: "center",
fontFamily: "Inter",
fontSize: "11px",
border: `1px solid ${theme.palette.grey["300"]}`,
borderRadius: "4px",
minWidth: "24px",
});
export default Toolbar; export default Toolbar;

View File

@@ -6,30 +6,35 @@ import type {
} from "@ironcalc/wasm"; } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import SheetTabBar from "./SheetTabBar/SheetTabBar"; import FormulaBar from "../FormulaBar/FormulaBar";
import SheetTabBar from "../SheetTabBar";
import Toolbar from "../Toolbar/Toolbar";
import Worksheet from "../Worksheet/Worksheet";
import { import {
COLUMN_WIDTH_SCALE, COLUMN_WIDTH_SCALE,
LAST_COLUMN, LAST_COLUMN,
ROW_HEIGH_SCALE, ROW_HEIGH_SCALE,
} from "./WorksheetCanvas/constants"; } from "../WorksheetCanvas/constants";
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
import { devicePixelRatio } from "../WorksheetCanvas/worksheetCanvas";
import { import {
CLIPBOARD_ID_SESSION_STORAGE_KEY, CLIPBOARD_ID_SESSION_STORAGE_KEY,
getNewClipboardId, getNewClipboardId,
} from "./clipboard"; } from "../clipboard";
import FormulaBar from "./formulabar";
import Toolbar from "./toolbar";
import useKeyboardNavigation from "./useKeyboardNavigation";
import { import {
type NavigationKey, type NavigationKey,
getCellAddress, getCellAddress,
getFullRangeToString, getFullRangeToString,
} from "./util"; } from "../util";
import type { WorkbookState } from "./workbookState"; import type { WorkbookState } from "../workbookState";
import Worksheet from "./worksheet"; import useKeyboardNavigation from "./useKeyboardNavigation";
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const { model, workbookState } = props; const { model, workbookState } = props;
const rootRef = useRef<HTMLDivElement | null>(null); const rootRef = useRef<HTMLDivElement | null>(null);
const worksheetRef = useRef<{
getCanvas: () => WorksheetCanvas | null;
}>(null);
// Calling `setRedrawId((id) => id + 1);` forces a redraw // Calling `setRedrawId((id) => id + 1);` forces a redraw
// This is needed because `model` or `workbookState` can change without React being aware of it // This is needed because `model` or `workbookState` can change without React being aware of it
@@ -107,6 +112,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
updateRangeStyle("alignment.vertical", value); updateRangeStyle("alignment.vertical", value);
}; };
const onToggleWrapText = (value: boolean) => {
updateRangeStyle("alignment.wrap_text", `${value}`);
};
const onTextColorPicked = (hex: string) => { const onTextColorPicked = (hex: string) => {
updateRangeStyle("font.color", hex); updateRangeStyle("font.color", hex);
}; };
@@ -119,6 +128,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
updateRangeStyle("num_fmt", numberFmt); updateRangeStyle("num_fmt", numberFmt);
}; };
const onIncreaseFontSize = (delta: number) => {
updateRangeStyle("font.size_delta", `${delta}`);
};
const onCopyStyles = () => { const onCopyStyles = () => {
const { const {
sheet, sheet,
@@ -523,6 +536,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
onToggleStrike={onToggleStrike} onToggleStrike={onToggleStrike}
onToggleHorizontalAlign={onToggleHorizontalAlign} onToggleHorizontalAlign={onToggleHorizontalAlign}
onToggleVerticalAlign={onToggleVerticalAlign} onToggleVerticalAlign={onToggleVerticalAlign}
onToggleWrapText={onToggleWrapText}
onCopyStyles={onCopyStyles} onCopyStyles={onCopyStyles}
onTextColorPicked={onTextColorPicked} onTextColorPicked={onTextColorPicked}
onFillColorPicked={onFillColorPicked} onFillColorPicked={onFillColorPicked}
@@ -541,6 +555,62 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
); );
setRedrawId((id) => id + 1); 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 => { onBorderChanged={(border: BorderOptions): void => {
const { const {
sheet, sheet,
@@ -563,6 +633,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}} }}
fillColor={style.fill.fg_color || "#FFFFFF"} fillColor={style.fill.fg_color || "#FFFFFF"}
fontColor={style.font.color} fontColor={style.font.color}
fontSize={style.font.sz}
bold={style.font.b} bold={style.font.b}
underline={style.font.u} underline={style.font.u}
italic={style.font.i} italic={style.font.i}
@@ -573,6 +644,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
verticalAlign={ verticalAlign={
style.alignment?.vertical ? style.alignment.vertical : "bottom" style.alignment?.vertical ? style.alignment.vertical : "bottom"
} }
wrapText={style.alignment?.wrap_text || false}
canEdit={true} canEdit={true}
numFmt={style.num_fmt} numFmt={style.num_fmt}
showGridLines={model.getShowGridLines(model.getSelectedSheet())} showGridLines={model.getShowGridLines(model.getSelectedSheet())}
@@ -633,6 +705,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
refresh={(): void => { refresh={(): void => {
setRedrawId((id) => id + 1); setRedrawId((id) => id + 1);
}} }}
ref={worksheetRef}
/> />
<SheetTabBar <SheetTabBar

View File

@@ -1,5 +1,5 @@
import { type KeyboardEvent, type RefObject, useCallback } from "react"; 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 { export enum Border {
Top = "top", Top = "top",

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import type { Model } from "@ironcalc/wasm"; import type { Model } from "@ironcalc/wasm";
import { columnNameFromNumber } 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 { Cell } from "../types";
import type { WorkbookState } from "../workbookState"; import type { WorkbookState } from "../workbookState";
import { import {
@@ -70,6 +70,52 @@ function hexToRGBA10Percent(colorHex: string): string {
return `rgba(${red}, ${green}, ${blue}, ${alpha})`; return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
} }
/**
* Splits the given text into multiple lines. If `wrapText` is true, it applies word-wrapping
* based on the specified canvas context, maximum width, and horizontal padding.
*
* - First, the text is split by newline characters so that explicit newlines are respected.
* - If wrapping is enabled, each line is further split into words and measured against the
* available width. Whenever adding an extra word would exceed
* this limit, a new line is started.
*
* @param text The text to split into lines.
* @param wrapText Whether to apply word-wrapping or just return text split by newlines.
* @param context The `CanvasRenderingContext2D` used for measuring text width.
* @param width The maximum width for each line.
* @returns An array of lines (strings), each fitting within the specified width if wrapping is enabled.
*/
function computeWrappedLines(
text: string,
wrapText: boolean,
context: CanvasRenderingContext2D,
width: number,
): string[] {
// Split the text into lines
const rawLines = text.split("\n");
if (!wrapText) {
// If there is no wrapping, return the raw lines
return rawLines;
}
const wrappedLines = [];
for (const line of rawLines) {
const words = line.split(" ");
let currentLine = words[0];
for (const word of words) {
const testLine = `${currentLine} ${word}`;
const textWidth = context.measureText(testLine).width;
if (textWidth < width) {
currentLine = testLine;
} else {
wrappedLines.push(currentLine);
currentLine = word;
}
}
wrappedLines.push(currentLine);
}
return wrappedLines;
}
export default class WorksheetCanvas { export default class WorksheetCanvas {
sheetWidth: number; sheetWidth: number;
@@ -353,7 +399,7 @@ export default class WorksheetCanvas {
? gridColor ? gridColor
: backgroundColor; : backgroundColor;
const fontSize = 13; const fontSize = style.font?.sz || 13;
let font = `${fontSize}px ${defaultCellFontFamily}`; let font = `${fontSize}px ${defaultCellFontFamily}`;
let textColor = defaultTextColor; let textColor = defaultTextColor;
if (style.font) { if (style.font) {
@@ -371,6 +417,7 @@ export default class WorksheetCanvas {
if (style.alignment?.vertical) { if (style.alignment?.vertical) {
verticalAlign = style.alignment.vertical; verticalAlign = style.alignment.vertical;
} }
const wrapText = style.alignment?.wrap_text || false;
const context = this.ctx; const context = this.ctx;
context.font = font; context.font = font;
@@ -496,9 +543,14 @@ export default class WorksheetCanvas {
context.rect(x, y, width, height); context.rect(x, y, width, height);
context.clip(); context.clip();
// Is there any better parameter? // Is there any better to determine the line height?
const lineHeight = 22; const lineHeight = fontSize * 1.5;
const lines = fullText.split("\n"); const lines = computeWrappedLines(
fullText,
wrapText,
context,
width - padding,
);
const lineCount = lines.length; const lineCount = lines.length;
lines.forEach((text, line) => { lines.forEach((text, line) => {
@@ -608,14 +660,16 @@ export default class WorksheetCanvas {
const sheet = this.model.getSelectedSheet(); const sheet = this.model.getSelectedSheet();
const rows = this.model.getRowsWithData(sheet, column); const rows = this.model.getRowsWithData(sheet, column);
let width = 0; let width = 0;
// This is a bit of a HACK. We should use the actual font size and weather is bold or not
const fontSize = 13;
this.ctx.font = `${fontSize}px ${defaultCellFontFamily}`;
for (const row of rows) { for (const row of rows) {
const fullText = this.model.getFormattedCellValue(sheet, row, column); const fullText = this.model.getFormattedCellValue(sheet, row, column);
if (fullText === "") { if (fullText === "") {
continue; continue;
} }
const style = this.model.getCellStyle(sheet, row, column);
const fontSize = style.font.sz;
let font = `${fontSize}px ${defaultCellFontFamily}`;
font = style.font.b ? `bold ${font}` : `400 ${font}`;
this.ctx.font = font;
const lines = fullText.split("\n"); const lines = fullText.split("\n");
for (const line of lines) { for (const line of lines) {
const textWidth = this.ctx.measureText(line).width; const textWidth = this.ctx.measureText(line).width;
@@ -675,18 +729,26 @@ export default class WorksheetCanvas {
const sheet = this.model.getSelectedSheet(); const sheet = this.model.getSelectedSheet();
const columns = this.model.getColumnsWithData(sheet, row); const columns = this.model.getColumnsWithData(sheet, row);
let height = 0; let height = 0;
const lineHeight = 22;
// This is a bit of a HACK. We should use the actual font size and weather is bold or not
const fontSize = 13;
this.ctx.font = `${fontSize}px ${defaultCellFontFamily}`;
for (const column of columns) { for (const column of columns) {
const fullText = this.model.getFormattedCellValue(sheet, row, column); const fullText = this.model.getFormattedCellValue(sheet, row, column);
if (fullText === "") { if (fullText === "") {
continue; continue;
} }
const lines = fullText.split("\n"); const width = this.getColumnWidth(sheet, column);
const style = this.model.getCellStyle(sheet, row, column);
const fontSize = style.font.sz;
const lineHeight = fontSize * 1.5;
let font = `${fontSize}px ${defaultCellFontFamily}`;
font = style.font.b ? `bold ${font}` : `400 ${font}`;
this.ctx.font = font;
const lines = computeWrappedLines(
fullText,
style.alignment?.wrap_text || false,
this.ctx,
width,
);
const lineCount = lines.length; const lineCount = lines.length;
// This si computed so that the y position of the text is independent of the vertical alignment // This is computed so that the y position of the text is independent of the vertical alignment
const textHeight = (lineCount - 1) * lineHeight + 8 + fontSize; const textHeight = (lineCount - 1) * lineHeight + 8 + fontSize;
height = Math.max(height, textHeight); height = Math.max(height, textHeight);
} }

View File

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

View File

@@ -1,659 +0,0 @@
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,6 +16,8 @@
"format_number": "Format number", "format_number": "Format number",
"font_color": "Font color", "font_color": "Font color",
"fill_color": "Fill 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_increase": "Increase decimal places",
"decimal_places_decrease": "Decrease decimal places", "decimal_places_decrease": "Decrease decimal places",
"show_hide_grid_lines": "Show/hide grid lines", "show_hide_grid_lines": "Show/hide grid lines",
@@ -23,6 +25,8 @@
"vertical_align_bottom": "Align bottom", "vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle", "vertical_align_middle": " Align middle",
"vertical_align_top": "Align top", "vertical_align_top": "Align top",
"selected_png": "Export Selected area as PNG",
"wrap_text": "Wrap text",
"format_menu": { "format_menu": {
"auto": "Auto", "auto": "Auto",
"number": "Number", "number": "Number",
@@ -115,5 +119,8 @@
"freeze": "Freeze", "freeze": "Freeze",
"insert_row": "Insert row", "insert_row": "Insert row",
"insert_column": "Insert column" "insert_column": "Insert column"
},
"color_picker": {
"apply": "Apply"
} }
} }

View File

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

View File

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

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