Compare commits
1 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd12881972 |
2
.github/workflows/test-coverage.yaml
vendored
2
.github/workflows/test-coverage.yaml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Coverage
|
||||
|
||||
on: [pull_request]
|
||||
on: [pull_request, push]
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
|
||||
@@ -69,21 +69,26 @@ impl Model {
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let source_cell = self
|
||||
if let Some(source_cell) = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(source_row, source_column)
|
||||
.ok_or("Expected Cell to exist")?;
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.get_cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
||||
{
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.get_cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| {
|
||||
source_cell.get_text(&self.workbook.shared_strings, &self.language)
|
||||
});
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
||||
} else {
|
||||
self.cell_clear_all(sheet, target_row, target_column)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -106,7 +111,7 @@ impl Model {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
|
||||
let last_column = dimensions.max_column + column_count;
|
||||
if last_column > LAST_COLUMN {
|
||||
return Err(
|
||||
@@ -263,7 +268,7 @@ impl Model {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// Check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
|
||||
let last_row = dimensions.max_row + row_count;
|
||||
if last_row > LAST_ROW {
|
||||
return Err(
|
||||
@@ -367,13 +372,162 @@ impl Model {
|
||||
}
|
||||
}
|
||||
self.workbook.worksheets[sheet as usize].rows = new_rows;
|
||||
self.displace_cells(
|
||||
&(DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: -row_count,
|
||||
}),
|
||||
);
|
||||
self.displace_cells(&DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: -row_count,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cells_and_shift_left(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_column = worksheet.get_dimension().max_column;
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column + 1..max_column + 1 {
|
||||
println!("{r}-{c}");
|
||||
self.move_cell(sheet, r, c, r, c - column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta: -column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells and shift right
|
||||
pub fn insert_cells_and_shift_right(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_column = worksheet.get_dimension().max_column;
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in (column..max_column + 1).rev() {
|
||||
self.move_cell(sheet, r, c, r, c + column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells and shift down
|
||||
pub fn insert_cells_and_shift_down(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_row = worksheet.get_dimension().max_row;
|
||||
|
||||
// Move all cells in the range
|
||||
for r in (row..row + max_row + 1).rev() {
|
||||
for c in column..column + column_delta {
|
||||
self.move_cell(sheet, r, c, r, c + column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cells_and_shift_up(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_row = worksheet.get_dimension().max_row;
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..max_row + 1 {
|
||||
for c in column + 1..column + column_delta {
|
||||
self.move_cell(sheet, r, c, r - row_delta, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta: -row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expressions::token;
|
||||
@@ -9,8 +11,11 @@ use super::{Lexer, LexerMode};
|
||||
/// A MarkedToken is a token together with its position on a formula
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MarkedToken {
|
||||
/// Token type (see [token::TokenType])
|
||||
pub token: token::TokenType,
|
||||
/// Position of the start of the token (in bytes)
|
||||
pub start: i32,
|
||||
/// Position of the end of the token (in bytes)
|
||||
pub end: i32,
|
||||
}
|
||||
|
||||
@@ -19,7 +24,7 @@ pub struct MarkedToken {
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ironcalc_base::expressions::{
|
||||
/// lexer::util::{get_tokens, MarkedToken},
|
||||
/// lexer::marked_token::{get_tokens, MarkedToken},
|
||||
/// token::{OpSum, TokenType},
|
||||
/// };
|
||||
///
|
||||
@@ -1,3 +1,5 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! A tokenizer for spreadsheet formulas.
|
||||
//!
|
||||
//! This is meant to feed a formula parser.
|
||||
@@ -7,8 +9,10 @@
|
||||
//! It supports two working modes:
|
||||
//!
|
||||
//! 1. A1 or display mode
|
||||
//!
|
||||
//! This is for user formulas. References are like `D4`, `D$4` or `F5:T10`
|
||||
//! 2. R1C1, internal or runtime mode
|
||||
//!
|
||||
//! A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
//! R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
//! It uses the 'en' locale and language.
|
||||
@@ -55,7 +59,8 @@ use super::token::{Error, TokenType};
|
||||
use super::types::*;
|
||||
use super::utils;
|
||||
|
||||
pub mod util;
|
||||
/// Returns an iterator over tokens together with their position in the byte string.
|
||||
pub mod marked_token;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
@@ -63,17 +68,28 @@ mod test;
|
||||
mod ranges;
|
||||
mod structured_references;
|
||||
|
||||
/// This is the TokenType we return if we cannot recognize a token
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LexerError {
|
||||
/// Position of the beginning of the token in the byte string.
|
||||
pub position: usize,
|
||||
/// Message describing what we think the error is.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub(super) type Result<T> = std::result::Result<T, LexerError>;
|
||||
|
||||
/// Whether we try to parse formulas in A1 mode or in the internal R1C1 mode
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LexerMode {
|
||||
/// Cell references are written `=S34`. This is the display mode
|
||||
A1,
|
||||
/// R1C1, internal or runtime mode
|
||||
///
|
||||
/// A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
/// R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
/// It uses the 'en' locale and language.
|
||||
/// This is used internally at runtime.
|
||||
R1C1,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod test_common;
|
||||
mod test_language;
|
||||
mod test_locale;
|
||||
mod test_marked_token;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
mod test_util;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::expressions::{
|
||||
lexer::util::get_tokens,
|
||||
lexer::marked_token::{get_tokens, MarkedToken},
|
||||
token::{OpCompare, OpSum, TokenType},
|
||||
};
|
||||
|
||||
@@ -22,6 +22,29 @@ fn test_get_tokens() {
|
||||
assert_eq!(l.end, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chinese_characters() {
|
||||
let formula = "\"你好\" & \"世界!\"";
|
||||
let marked_tokens = get_tokens(formula);
|
||||
assert_eq!(marked_tokens.len(), 3);
|
||||
let first_t = MarkedToken {
|
||||
token: TokenType::String("你好".to_string()),
|
||||
start: 0,
|
||||
end: 4,
|
||||
};
|
||||
let second_t = MarkedToken {
|
||||
token: TokenType::And,
|
||||
start: 4,
|
||||
end: 6,
|
||||
};
|
||||
let third_t = MarkedToken {
|
||||
token: TokenType::String("世界!".to_string()),
|
||||
start: 6,
|
||||
end: 12,
|
||||
};
|
||||
assert_eq!(marked_tokens, vec![first_t, second_t, third_t]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_tokens() {
|
||||
assert_eq!(
|
||||
@@ -1,4 +1,3 @@
|
||||
// public modules
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod token;
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
/*!
|
||||
# GRAMAR
|
||||
|
||||
<pre class="rust">
|
||||
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
opFactor => '*' | '/'
|
||||
unaryOp => '-' | '+'
|
||||
|
||||
expr => concat (opComp concat)*
|
||||
concat => term ('&' term)*
|
||||
term => factor (opFactor factor)*
|
||||
factor => prod (opProd prod)*
|
||||
prod => power ('^' power)*
|
||||
power => (unaryOp)* range '%'*
|
||||
range => primary (':' primary)?
|
||||
primary => '(' expr ')'
|
||||
=> number
|
||||
=> function '(' f_args ')'
|
||||
=> name
|
||||
=> string
|
||||
=> '{' a_args '}'
|
||||
=> bool
|
||||
=> bool()
|
||||
=> error
|
||||
|
||||
f_args => e (',' e)*
|
||||
</pre>
|
||||
*/
|
||||
//! # GRAMMAR
|
||||
//!
|
||||
//! <pre class="rust">
|
||||
//! opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
//! opFactor => '*' | '/'
|
||||
//! unaryOp => '-' | '+'
|
||||
//!
|
||||
//! expr => concat (opComp concat)*
|
||||
//! concat => term ('&' term)*
|
||||
//! term => factor (opFactor factor)*
|
||||
//! factor => prod (opProd prod)*
|
||||
//! prod => power ('^' power)*
|
||||
//! power => (unaryOp)* range '%'*
|
||||
//! range => primary (':' primary)?
|
||||
//! primary => '(' expr ')'
|
||||
//! => number
|
||||
//! => function '(' f_args ')'
|
||||
//! => name
|
||||
//! => string
|
||||
//! => '{' a_args '}'
|
||||
//! => bool
|
||||
//! => bool()
|
||||
//! => error
|
||||
//!
|
||||
//! f_args => e (',' e)*
|
||||
//! </pre>
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -44,21 +42,15 @@ use super::utils::number_to_column;
|
||||
|
||||
use token::OpCompare;
|
||||
|
||||
pub mod move_formula;
|
||||
pub(crate) mod move_formula;
|
||||
pub(crate) mod walk;
|
||||
|
||||
/// Produces a string representation of a formula from the AST.
|
||||
pub mod stringify;
|
||||
pub mod walk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ranges;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_move_formula;
|
||||
#[cfg(test)]
|
||||
mod test_tables;
|
||||
|
||||
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
|
||||
let mut lexer = lexer::Lexer::new(
|
||||
formula,
|
||||
|
||||
@@ -1,43 +1,75 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::token::OpUnary;
|
||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||
|
||||
/// Displaced data
|
||||
pub enum DisplaceData {
|
||||
/// Displaces columns (inserting or deleting columns)
|
||||
Column {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Column from which the data is displaced
|
||||
column: i32,
|
||||
/// Number of columns displaced (might be negative, e.g. when deleting columns)
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces rows (Inserting or deleting rows)
|
||||
Row {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Row from which the data is displaced
|
||||
row: i32,
|
||||
/// Number of rows displaced (might be negative, e.g. when deleting rows)
|
||||
delta: i32,
|
||||
},
|
||||
CellHorizontal {
|
||||
/// Displaces cells horizontally
|
||||
ShiftCellsRight {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Row of the to left corner
|
||||
row: i32,
|
||||
/// Column of the top left corner
|
||||
column: i32,
|
||||
delta: i32,
|
||||
/// Number of rows to be displaced
|
||||
row_delta: i32,
|
||||
/// Number of columns to be displaced (might be negative)
|
||||
column_delta: i32,
|
||||
},
|
||||
CellVertical {
|
||||
/// Displaces cells vertically
|
||||
ShiftCellsDown {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Row of the to left corner
|
||||
row: i32,
|
||||
/// Column of the top left corner
|
||||
column: i32,
|
||||
delta: i32,
|
||||
/// Number of rows displaced (might be negative)
|
||||
row_delta: i32,
|
||||
/// Number of columns to be displaced
|
||||
column_delta: i32,
|
||||
},
|
||||
/// Displaces data due to a column move from column to column + delta
|
||||
ColumnMove {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Column that is moved
|
||||
column: i32,
|
||||
/// The position of the new column is column + delta (might be negative)
|
||||
delta: i32,
|
||||
},
|
||||
/// Doesn't do any cell displacement
|
||||
None,
|
||||
}
|
||||
|
||||
/// Stringifies the AST formula in its internal R1C1 format
|
||||
pub fn to_rc_format(node: &Node) -> String {
|
||||
stringify(node, None, &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// Stringifies the formula applying the _displace_data_.
|
||||
pub fn to_string_displaced(
|
||||
node: &Node,
|
||||
context: &CellReferenceRC,
|
||||
@@ -46,10 +78,12 @@ pub fn to_string_displaced(
|
||||
stringify(node, Some(context), displace_data, false)
|
||||
}
|
||||
|
||||
/// Stringifies a formula from the AST
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// Stringifies the formula for Excel compatibility
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
@@ -116,41 +150,49 @@ pub(crate) fn stringify_reference(
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::CellHorizontal {
|
||||
DisplaceData::ShiftCellsRight {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
delta,
|
||||
column_delta,
|
||||
row_delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && displace_row == &row {
|
||||
if *delta < 0 {
|
||||
if sheet_index == *sheet
|
||||
&& displace_row >= &row
|
||||
&& *displace_row < row + *row_delta
|
||||
{
|
||||
if *column_delta < 0 {
|
||||
if &column >= displace_column {
|
||||
if column < displace_column - *delta {
|
||||
if column < displace_column - *column_delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
column += *delta;
|
||||
column += *column_delta;
|
||||
}
|
||||
} else if &column >= displace_column {
|
||||
column += *delta;
|
||||
column += *column_delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::CellVertical {
|
||||
DisplaceData::ShiftCellsDown {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
delta,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && displace_column == &column {
|
||||
if *delta < 0 {
|
||||
if sheet_index == *sheet
|
||||
&& displace_column >= &column
|
||||
&& *displace_column < column + *column_delta
|
||||
{
|
||||
if *row_delta < 0 {
|
||||
if &row >= displace_row {
|
||||
if row < displace_row - *delta {
|
||||
if row < displace_row - *row_delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
row += *delta;
|
||||
row += *row_delta;
|
||||
}
|
||||
} else if &row >= displace_row {
|
||||
row += *delta;
|
||||
row += *row_delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
base/src/expressions/parser/test/mod.rs
Normal file
4
base/src/expressions/parser/test/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
mod test_genertal;
|
||||
mod test_move_formula;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
@@ -1,17 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use super::{
|
||||
super::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node,
|
||||
},
|
||||
stringify::to_string_displaced,
|
||||
use crate::expressions::parser::stringify::{to_string_displaced, DisplaceData};
|
||||
use crate::expressions::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node, Parser,
|
||||
};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
struct Formula<'a> {
|
||||
initial: &'a str,
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
|
||||
use crate::expressions::types::Area;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::{Area, CellReferenceRC};
|
||||
|
||||
#[test]
|
||||
fn test_move_formula() {
|
||||
@@ -2,9 +2,9 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
|
||||
use super::super::parser::stringify::{to_rc_format, to_string};
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use crate::expressions::parser::stringify::{to_rc_format, to_string};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
struct Formula<'a> {
|
||||
formula_a1: &'a str,
|
||||
@@ -6,8 +6,8 @@ use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
fn create_test_table(
|
||||
table_name: &str,
|
||||
@@ -3,7 +3,7 @@ use std::fmt;
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
expressions::{
|
||||
lexer::util::get_tokens,
|
||||
lexer::marked_token::get_tokens,
|
||||
parser::Node,
|
||||
token::{Error, OpSum, TokenType},
|
||||
types::CellReferenceIndex,
|
||||
|
||||
@@ -221,7 +221,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -229,7 +229,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -284,7 +284,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -292,7 +292,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -360,7 +360,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -368,7 +368,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -866,7 +866,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -874,7 +874,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -132,7 +132,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -140,7 +140,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -199,7 +199,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -207,7 +207,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -385,7 +385,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(first_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension();
|
||||
.get_dimension();
|
||||
let max_row = dimension.max_row;
|
||||
let max_column = dimension.max_column;
|
||||
|
||||
@@ -530,7 +530,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if left_column == 1 && right_column == LAST_COLUMN {
|
||||
@@ -538,7 +538,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
|
||||
|
||||
@@ -892,7 +892,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -900,7 +900,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -255,7 +255,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -263,7 +263,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.get_dimension()
|
||||
.max_column;
|
||||
}
|
||||
let left = CellReferenceIndex {
|
||||
|
||||
@@ -58,4 +58,3 @@ pub mod mock_time;
|
||||
pub use model::get_milliseconds_since_epoch;
|
||||
pub use model::Model;
|
||||
pub use user_model::UserModel;
|
||||
pub use user_model::BorderArea;
|
||||
|
||||
@@ -118,8 +118,6 @@ pub struct Model {
|
||||
pub(crate) language: Language,
|
||||
/// The timezone used to evaluate the model
|
||||
pub(crate) tz: Tz,
|
||||
/// The view id. A view consist of a selected sheet and ranges.
|
||||
pub(crate) view_id: u32,
|
||||
}
|
||||
|
||||
// FIXME: Maybe this should be the same as CellReference
|
||||
@@ -683,13 +681,6 @@ impl Model {
|
||||
Err(format!("Invalid color: {}", color))
|
||||
}
|
||||
|
||||
/// Makes the grid lines in the sheet visible (`true`) or hidden (`false`)
|
||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||
worksheet.show_grid_lines = show_grid_lines;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cell_value(&self, cell: &Cell, cell_reference: CellReferenceIndex) -> CalcResult {
|
||||
use Cell::*;
|
||||
match cell {
|
||||
@@ -895,7 +886,6 @@ impl Model {
|
||||
language,
|
||||
locale,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
|
||||
model.parse_formulas();
|
||||
@@ -1798,7 +1788,7 @@ impl Model {
|
||||
/// Returns markup representation of the given `sheet`.
|
||||
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
let dimension = worksheet.dimension();
|
||||
let dimension = worksheet.get_dimension();
|
||||
|
||||
let mut rows = Vec::new();
|
||||
|
||||
|
||||
@@ -15,9 +15,7 @@ use crate::{
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
types::{
|
||||
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
|
||||
},
|
||||
types::{Metadata, Selection, SheetState, Workbook, WorkbookSettings, Worksheet},
|
||||
utils::ParsedReference,
|
||||
};
|
||||
|
||||
@@ -37,20 +35,7 @@ fn is_valid_sheet_name(name: &str) -> bool {
|
||||
|
||||
impl Model {
|
||||
/// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
|
||||
fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet {
|
||||
let mut views = HashMap::new();
|
||||
for id in view_ids {
|
||||
views.insert(
|
||||
**id,
|
||||
WorksheetView {
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet {
|
||||
Worksheet {
|
||||
cols: vec![],
|
||||
rows: vec![],
|
||||
@@ -65,8 +50,12 @@ impl Model {
|
||||
color: Default::default(),
|
||||
frozen_columns: 0,
|
||||
frozen_rows: 0,
|
||||
show_grid_lines: true,
|
||||
views,
|
||||
selection: Selection {
|
||||
is_selected: false,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +130,7 @@ impl Model {
|
||||
self.parsed_defined_names = parsed_defined_names;
|
||||
}
|
||||
|
||||
/// Reparses all formulas and defined names
|
||||
// Reparses all formulas and defined names
|
||||
pub(crate) fn reset_parsed_structures(&mut self) {
|
||||
self.parser
|
||||
.set_worksheets(self.workbook.get_worksheet_names());
|
||||
@@ -172,8 +161,7 @@ impl Model {
|
||||
let sheet_name = format!("{}{}", base_name, index);
|
||||
// Now we need a sheet_id
|
||||
let sheet_id = self.get_new_sheet_id();
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
|
||||
self.workbook.worksheets.push(worksheet);
|
||||
self.reset_parsed_structures();
|
||||
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
|
||||
@@ -204,8 +192,7 @@ impl Model {
|
||||
Some(id) => id,
|
||||
None => self.get_new_sheet_id(),
|
||||
};
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
|
||||
if sheet_index as usize > self.workbook.worksheets.len() {
|
||||
return Err("Sheet index out of range".to_string());
|
||||
}
|
||||
@@ -352,21 +339,11 @@ impl Model {
|
||||
// "2020-08-06T21:20:53Z
|
||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
let mut views = HashMap::new();
|
||||
views.insert(
|
||||
0,
|
||||
WorkbookView {
|
||||
sheet: 0,
|
||||
window_width: 800,
|
||||
window_height: 600,
|
||||
},
|
||||
);
|
||||
|
||||
// String versions of the locale are added here to simplify the serialize/deserialize logic
|
||||
let workbook = Workbook {
|
||||
shared_strings: vec![],
|
||||
defined_names: vec![],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)],
|
||||
styles: Default::default(),
|
||||
name: name.to_string(),
|
||||
settings: WorkbookSettings {
|
||||
@@ -382,7 +359,6 @@ impl Model {
|
||||
last_modified: now,
|
||||
},
|
||||
tables: HashMap::new(),
|
||||
views,
|
||||
};
|
||||
let parsed_formulas = Vec::new();
|
||||
let worksheets = &workbook.worksheets;
|
||||
@@ -403,7 +379,6 @@ impl Model {
|
||||
locale,
|
||||
language,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
model.parse_formulas();
|
||||
Ok(model)
|
||||
|
||||
@@ -76,16 +76,10 @@ fn fn_imconjugate() {
|
||||
fn fn_imcos() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMCOS("4+3i")"#);
|
||||
// In macos non intel this is "-6.58066304055116+7.58155274274655i"
|
||||
model._set("A2", r#"=COMPLEX(-6.58066304055116, 7.58155274274654)"#);
|
||||
model._set("A3", r#"=IMABS(IMSUB(A1, A2)) < G1"#);
|
||||
|
||||
// small number
|
||||
model._set("G1", "0.0000001");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A3"), "TRUE");
|
||||
assert_eq!(model._get_text("A1"), "-6.58066304055116+7.58155274274654i");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
18
base/src/test/functions/mod.rs
Normal file
18
base/src/test/functions/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
mod test_fn_choose;
|
||||
mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_if;
|
||||
mod test_fn_maxifs;
|
||||
mod test_fn_minifs;
|
||||
mod test_fn_offset;
|
||||
mod test_fn_product;
|
||||
mod test_fn_rept;
|
||||
mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_fn_type;
|
||||
@@ -1,3 +1,5 @@
|
||||
mod engineering;
|
||||
mod functions;
|
||||
mod test_actions;
|
||||
mod test_binary_search;
|
||||
mod test_cell;
|
||||
@@ -8,50 +10,30 @@ mod test_criteria;
|
||||
mod test_currency;
|
||||
mod test_date_and_time;
|
||||
mod test_error_propagation;
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
mod test_fn_choose;
|
||||
mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_if;
|
||||
mod test_fn_maxifs;
|
||||
mod test_fn_minifs;
|
||||
mod test_fn_product;
|
||||
mod test_fn_rept;
|
||||
mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_forward_references;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
mod test_get_cell_content;
|
||||
mod test_math;
|
||||
mod test_metadata;
|
||||
mod test_model_cell_clear_all;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_move_formula;
|
||||
mod test_number_format;
|
||||
mod test_percentage;
|
||||
mod test_quote_prefix;
|
||||
mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
mod test_shift_cells;
|
||||
mod test_styles;
|
||||
mod test_today;
|
||||
mod test_trigonometric;
|
||||
mod test_types;
|
||||
mod test_workbook;
|
||||
mod test_worksheet;
|
||||
pub(crate) mod util;
|
||||
|
||||
mod engineering;
|
||||
mod test_fn_offset;
|
||||
mod test_number_format;
|
||||
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_type;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_get_cell_content;
|
||||
mod test_percentage;
|
||||
mod test_today;
|
||||
mod test_types;
|
||||
mod user_model;
|
||||
pub(crate) mod util;
|
||||
|
||||
110
base/src/test/test_shift_cells.rs
Normal file
110
base/src/test/test_shift_cells.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn shift_cells_right() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 3); // C5
|
||||
model.set_user_input(sheet, row, column, "Hi".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "world".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "!".to_string());
|
||||
|
||||
model
|
||||
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
|
||||
assert_eq!(model.get_cell_content(0, 5, 4), Ok("Hi".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_right_with_formulas() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 3); // C5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "=C5*2".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "=A5+2".to_string());
|
||||
model.set_user_input(sheet, 20, 3, "=C5*A2".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column - 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, row, column), Ok("".to_string()));
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("42".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 2),
|
||||
Ok("=D5*2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 3),
|
||||
Ok("=A5+2".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, 20, 3), Ok("=D5*A2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_left() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 10); // J5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "Hi".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "honey!".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column - 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, row, column), Ok("Hi".to_string()));
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("honey!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_left_with_formulas() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 10); // J5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "33".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "=K5*A5".to_string());
|
||||
|
||||
model.set_user_input(sheet, row, column + 20, "=K5*A5".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("=J5*A5".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 19),
|
||||
Ok("=J5*A5".to_string())
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
fn test_worksheet_dimension_empty_sheet() {
|
||||
let model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -25,7 +25,7 @@ fn test_worksheet_dimension_single_cell() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("W11", "1");
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 23,
|
||||
@@ -41,7 +41,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
|
||||
model._set("W11", "1");
|
||||
model.cell_clear_contents(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 23,
|
||||
@@ -57,7 +57,7 @@ fn test_worksheet_dimension_single_cell_deleted() {
|
||||
model._set("W11", "1");
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -77,7 +77,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
||||
model._set("B19", "1");
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 2,
|
||||
@@ -91,7 +91,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
||||
fn test_worksheet_dimension_progressive() {
|
||||
let mut model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -102,7 +102,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 30, 50, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 30,
|
||||
min_column: 50,
|
||||
@@ -113,7 +113,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 10, 15, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 10,
|
||||
min_column: 15,
|
||||
@@ -124,7 +124,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 5, 25, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 5,
|
||||
min_column: 15,
|
||||
@@ -135,7 +135,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 10, 250, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 5,
|
||||
min_column: 15,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
mod test_add_delete_sheets;
|
||||
mod test_autofill_columns;
|
||||
mod test_autofill_rows;
|
||||
mod test_clear_cells;
|
||||
mod test_diff_queue;
|
||||
mod test_evaluation;
|
||||
mod test_general;
|
||||
mod test_grid_lines;
|
||||
mod test_rename_sheet;
|
||||
mod test_row_column;
|
||||
mod test_shift_cells;
|
||||
mod test_styles;
|
||||
mod test_to_from_bytes;
|
||||
mod test_undo_redo;
|
||||
mod test_view;
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is cell A3
|
||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
||||
// We autofill from A3 to C3
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
// B3
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 3, 2),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
// C3
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 3, 3),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_cell_right() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
// B1
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 2),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_beta_gamma() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap(); // A1
|
||||
model.set_user_input(0, 1, 2, "Bethe").unwrap(); // B1
|
||||
model.set_user_input(0, 1, 3, "Gamow").unwrap(); // C1
|
||||
model.set_user_input(0, 2, 1, "=A1").unwrap(); // A2
|
||||
model.set_user_input(0, 2, 2, "=B1").unwrap(); // B2
|
||||
model.set_user_input(0, 2, 3, "=C1").unwrap(); // C2
|
||||
|
||||
// We autofill from A1:C2 to I2
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 3,
|
||||
height: 2,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// D1
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 6),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 5),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 6),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 2, 4), Ok("=D1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:C1
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
|
||||
let b1 = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 2,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let c1 = Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 3,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model.update_range_style(&b1, "font.i", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&c1, "fill.bg_color", "#334455")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that cell E1 has B1 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 1, 4), Ok("".to_string()));
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(!style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 4),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 1, 5).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 1, 6).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A12
|
||||
model.set_user_input(0, 1, 10, "Alpher").unwrap();
|
||||
model.set_user_input(0, 1, 11, "Bethe").unwrap();
|
||||
model.set_user_input(0, 1, 12, "Gamow").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 10,
|
||||
width: 3,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 7),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_4() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 1, 10, "Margaret Burbidge").unwrap();
|
||||
model.set_user_input(0, 1, 11, "Geoffrey Burbidge").unwrap();
|
||||
model.set_user_input(0, 1, 12, "Willy Fowler").unwrap();
|
||||
model.set_user_input(0, 1, 13, "Fred Hoyle").unwrap();
|
||||
|
||||
// We fill left to row 5
|
||||
model
|
||||
.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 10,
|
||||
width: 4,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 9),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 8),
|
||||
Ok("Willy Fowler".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 1, 5),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
|
||||
model.set_user_input(0, 1, 4, "Margaret Burbidge").unwrap();
|
||||
|
||||
// Invalid sheet
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 3,
|
||||
row: 1,
|
||||
column: 4,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid worksheet index: '3'".to_string())
|
||||
);
|
||||
|
||||
// invalid column
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: -1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '-1'".to_string())
|
||||
);
|
||||
|
||||
// invalid column
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN - 1,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16392'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW + 1,
|
||||
column: 1,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048577'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW - 2,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048583'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 5,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
-10,
|
||||
),
|
||||
Err("Invalid row: '-10'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_parameters() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
assert_eq!(
|
||||
model.auto_fill_columns(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 2,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
),
|
||||
Err("Invalid parameters for autofill".to_string())
|
||||
);
|
||||
}
|
||||
@@ -1,399 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::types::Area;
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// This is cell A3
|
||||
model.set_user_input(0, 3, 1, "alpha").unwrap();
|
||||
// We autofill from A3 to A5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("alpha".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_cell_down() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
2,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 2, 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alpha_beta_gamma() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
model.set_user_input(0, 1, 2, "=A1").unwrap();
|
||||
model.set_user_input(0, 2, 2, "=A2").unwrap();
|
||||
model.set_user_input(0, 3, 2, "=A3").unwrap();
|
||||
// We autofill from A1:B3 to A9
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 2,
|
||||
height: 3,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 6, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 2),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 2),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 6, 2),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 2),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 2),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 2),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 2), Ok("=A4".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styles() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A1:B3
|
||||
model.set_user_input(0, 1, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 2, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 3, 1, "Gamow").unwrap();
|
||||
|
||||
let a2 = Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let a3 = Area {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
model.update_range_style(&a2, "font.i", "true").unwrap();
|
||||
model
|
||||
.update_range_style(&a3, "fill.bg_color", "#334455")
|
||||
.unwrap();
|
||||
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 3,
|
||||
},
|
||||
9,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 4, 1), Ok("".to_string()));
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(!style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
|
||||
model.redo().unwrap();
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 4, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
// Check that cell A5 has A2 style
|
||||
let style = model.get_cell_style(0, 5, 1).unwrap();
|
||||
assert!(style.font.i);
|
||||
// A6 would have the style of A3
|
||||
let style = model.get_cell_style(0, 6, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#334455".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upwards() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A12
|
||||
model.set_user_input(0, 10, 1, "Alpher").unwrap();
|
||||
model.set_user_input(0, 11, 1, "Bethe").unwrap();
|
||||
model.set_user_input(0, 12, 1, "Gamow").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 3,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Gamow".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Bethe".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 7, 1),
|
||||
Ok("Alpher".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upwards_4() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 10, 1, "Margaret Burbidge").unwrap();
|
||||
model.set_user_input(0, 11, 1, "Geoffrey Burbidge").unwrap();
|
||||
model.set_user_input(0, 12, 1, "Willy Fowler").unwrap();
|
||||
model.set_user_input(0, 13, 1, "Fred Hoyle").unwrap();
|
||||
|
||||
// We fill upwards to row 5
|
||||
model
|
||||
.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 10,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 4,
|
||||
},
|
||||
5,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 9, 1),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 8, 1),
|
||||
Ok("Willy Fowler".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_formatted_cell_value(0, 5, 1),
|
||||
Ok("Fred Hoyle".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// cells A10:A13
|
||||
model.set_user_input(0, 4, 1, "Margaret Burbidge").unwrap();
|
||||
|
||||
// Invalid sheet
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 3,
|
||||
row: 4,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid worksheet index: '3'".to_string())
|
||||
);
|
||||
|
||||
// invalid row
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: -1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '-1'".to_string())
|
||||
);
|
||||
|
||||
// invalid row
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: LAST_ROW - 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid row: '1048584'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN + 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16385'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: LAST_COLUMN - 2,
|
||||
width: 10,
|
||||
height: 1,
|
||||
},
|
||||
10,
|
||||
),
|
||||
Err("Invalid column: '16391'".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 10,
|
||||
},
|
||||
-10,
|
||||
),
|
||||
Err("Invalid row: '-10'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_parameters() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_user_input(0, 1, 1, "23").unwrap();
|
||||
assert_eq!(
|
||||
model.auto_fill_rows(
|
||||
&Area {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
width: 1,
|
||||
height: 2,
|
||||
},
|
||||
2,
|
||||
),
|
||||
Err("Invalid parameters for autofill".to_string())
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn basic_tests() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.new_sheet();
|
||||
|
||||
// default sheet has show_grid_lines = true
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
// default new sheet has show_grid_lines = true
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
||||
|
||||
// wrong sheet number
|
||||
assert_eq!(
|
||||
model.get_show_grid_lines(2),
|
||||
Err("Invalid sheet index".to_string())
|
||||
);
|
||||
|
||||
// we can set it
|
||||
model.set_show_grid_lines(1, false).unwrap();
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(false));
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
model.undo().unwrap();
|
||||
|
||||
assert_eq!(model.get_show_grid_lines(1), Ok(true));
|
||||
assert_eq!(model.get_show_grid_lines(0), Ok(true));
|
||||
|
||||
model.redo().unwrap();
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
let mut model2 = UserModel::from_model(new_empty_model());
|
||||
model2.apply_external_diffs(&send_queue).unwrap();
|
||||
|
||||
assert_eq!(model2.get_show_grid_lines(1), Ok(false));
|
||||
assert_eq!(model2.get_show_grid_lines(0), Ok(true));
|
||||
}
|
||||
23
base/src/test/user_model/test_shift_cells.rs
Normal file
23
base/src/test/user_model/test_shift_cells.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn shift_cells_general() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
// some reference value in A1
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
|
||||
// We put some values in row 5
|
||||
model.set_user_input(0, 5, 3, "=1 + 1").unwrap(); // C5
|
||||
model.set_user_input(0, 5, 7, "=C5*A1").unwrap();
|
||||
|
||||
// Insert one cell in C5 and push right
|
||||
model.insert_cells_and_shift_right(0, 5, 3, 1, 1).unwrap();
|
||||
|
||||
// C5 should now be empty
|
||||
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
|
||||
|
||||
// D5 should have 2
|
||||
assert_eq!(model.get_cell_content(0, 5, 4), Ok("=1+1".to_string()));
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
test::util::new_empty_model,
|
||||
user_model::SelectedView,
|
||||
UserModel,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn initial_view() {
|
||||
let model = new_empty_model();
|
||||
let model = UserModel::from_model(model);
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_the_cell_sets_the_range() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_selected_cell(5, 4).unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 5, 4));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 5,
|
||||
column: 4,
|
||||
range: [5, 4, 5, 4],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_the_range_does_not_set_the_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_selected_range(5, 4, 10, 6).unwrap();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [5, 4, 10, 6],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_new_sheet_and_back() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.new_sheet();
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
model.set_selected_cell(5, 4).unwrap();
|
||||
model.set_selected_sheet(1).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (1, 1, 1));
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (0, 5, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_selected_cell_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.set_selected_cell(-5, 4),
|
||||
Err("Invalid row: '-5'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_cell(5, -4),
|
||||
Err("Invalid column: '-4'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(-1, 1, 1, 1),
|
||||
Err("Invalid row: '-1'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 0, 1, 1),
|
||||
Err("Invalid column: '0'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 1, LAST_ROW + 1, 1),
|
||||
Err("Invalid row: '1048577'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(1, 1, 1, LAST_COLUMN + 1),
|
||||
Err("Invalid column: '16385'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_selected_cell_errors_wrong_sheet() {
|
||||
let mut model = new_empty_model();
|
||||
// forcefully set a wrong index
|
||||
model.workbook.views.get_mut(&0).unwrap().sheet = 2;
|
||||
let mut model = UserModel::from_model(model);
|
||||
// It's returning the wrong number
|
||||
assert_eq!(model.get_selected_sheet(), 2);
|
||||
|
||||
// But we can't set the selected cell anymore
|
||||
assert_eq!(
|
||||
model.set_selected_cell(3, 4),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_selected_range(3, 4, 5, 6),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(3, 4),
|
||||
Err("Invalid worksheet index 2".to_string())
|
||||
);
|
||||
|
||||
// we can fix it by setting the right cell
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
model.set_selected_cell(3, 4).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_visible_cell() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
model.set_top_left_visible_cell(100, 12).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 100,
|
||||
left_column: 12
|
||||
}
|
||||
);
|
||||
|
||||
let s = serde_json::to_string(&model.get_selected_view()).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<SelectedView>(&s).unwrap(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 100,
|
||||
left_column: 12
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_visible_cell_errors() {
|
||||
let model = new_empty_model();
|
||||
let mut model = UserModel::from_model(model);
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(-100, 12),
|
||||
Err("Invalid row: '-100'".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.set_top_left_visible_cell(100, -12),
|
||||
Err("Invalid column: '-12'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_no_views() {
|
||||
let mut model = new_empty_model();
|
||||
// forcefully remove the view
|
||||
model.workbook.views = HashMap::new();
|
||||
// also in the sheet
|
||||
model.workbook.worksheets[0].views = HashMap::new();
|
||||
let mut model = UserModel::from_model(model);
|
||||
// get methods will return defaults
|
||||
assert_eq!(model.get_selected_sheet(), 0);
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
assert_eq!(
|
||||
model.get_selected_view(),
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1
|
||||
}
|
||||
);
|
||||
|
||||
// set methods won't complain. but won't work either
|
||||
model.set_selected_sheet(0).unwrap();
|
||||
model.set_selected_cell(5, 6).unwrap();
|
||||
assert_eq!(model.get_selected_cell(), (0, 1, 1));
|
||||
}
|
||||
@@ -27,18 +27,6 @@ pub struct WorkbookSettings {
|
||||
pub tz: String,
|
||||
pub locale: String,
|
||||
}
|
||||
|
||||
/// A Workbook View tracks of the selected sheet for each view
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct WorkbookView {
|
||||
/// The index of the currently selected sheet.
|
||||
pub sheet: u32,
|
||||
/// The current width of the window
|
||||
pub window_width: i64,
|
||||
/// The current heigh of the window
|
||||
pub window_height: i64,
|
||||
}
|
||||
|
||||
/// An internal representation of an IronCalc Workbook
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Workbook {
|
||||
@@ -50,7 +38,6 @@ pub struct Workbook {
|
||||
pub settings: WorkbookSettings,
|
||||
pub metadata: Metadata,
|
||||
pub tables: HashMap<String, Table>,
|
||||
pub views: HashMap<u32, WorkbookView>,
|
||||
}
|
||||
|
||||
/// A defined name. The `sheet_id` is the sheet index in case the name is local
|
||||
@@ -61,6 +48,9 @@ pub struct DefinedName {
|
||||
pub sheet_id: Option<u32>,
|
||||
}
|
||||
|
||||
// TODO: Move to worksheet.rs make frozen_rows/columns private and u32
|
||||
/// Internal representation of a worksheet Excel object
|
||||
|
||||
/// * state:
|
||||
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
||||
/// hidden, veryHidden, visible
|
||||
@@ -81,21 +71,12 @@ impl Display for SheetState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of the worksheet as seen by the user. This includes
|
||||
/// details such as the currently selected cell, the visible range, and the
|
||||
/// position of the viewport.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct WorksheetView {
|
||||
/// The row index of the currently selected cell.
|
||||
pub struct Selection {
|
||||
pub is_selected: bool,
|
||||
pub row: i32,
|
||||
/// The column index of the currently selected cell.
|
||||
pub column: i32,
|
||||
/// The selected range in the worksheet, specified as [start_row, start_column, end_row, end_column].
|
||||
pub range: [i32; 4],
|
||||
/// The row index of the topmost visible cell in the worksheet view.
|
||||
pub top_row: i32,
|
||||
/// The column index of the leftmost visible cell in the worksheet view.
|
||||
pub left_column: i32,
|
||||
}
|
||||
|
||||
/// Internal representation of a worksheet Excel object
|
||||
@@ -114,9 +95,7 @@ pub struct Worksheet {
|
||||
pub comments: Vec<Comment>,
|
||||
pub frozen_rows: i32,
|
||||
pub frozen_columns: i32,
|
||||
pub views: HashMap<u32, WorksheetView>,
|
||||
/// Whether or not to show the grid lines in the worksheet
|
||||
pub show_grid_lines: bool,
|
||||
pub selection: Selection,
|
||||
}
|
||||
|
||||
/// Internal representation of Excel's sheet_data
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::{collections::HashMap, fmt::Debug};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use bitcode::{Decode, Encode};
|
||||
|
||||
use crate::{
|
||||
constants,
|
||||
@@ -12,35 +12,194 @@ use crate::{
|
||||
},
|
||||
model::Model,
|
||||
types::{
|
||||
Alignment, BorderItem, BorderStyle, CellType, Col, HorizontalAlignment, SheetProperties,
|
||||
Style, VerticalAlignment,
|
||||
Alignment, BorderItem, BorderStyle, Cell, CellType, Col, HorizontalAlignment, Row,
|
||||
SheetProperties, Style, VerticalAlignment,
|
||||
},
|
||||
utils::is_valid_hex_color,
|
||||
};
|
||||
|
||||
use crate::user_model::history::{
|
||||
ColumnData, Diff, DiffList, DiffType, History, QueueDiffs, RowData,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum BorderType {
|
||||
All,
|
||||
Inner,
|
||||
Outer,
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
CenterH,
|
||||
CenterV,
|
||||
None,
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
struct RowData {
|
||||
row: Option<Row>,
|
||||
data: HashMap<i32, Cell>,
|
||||
}
|
||||
|
||||
/// This is the struct for a border area
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BorderArea {
|
||||
item: BorderItem,
|
||||
r#type: BorderType,
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
struct ColumnData {
|
||||
column: Option<Col>,
|
||||
data: HashMap<i32, Cell>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
enum Diff {
|
||||
// Cell diffs
|
||||
SetCellValue {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
new_value: String,
|
||||
old_value: Box<Option<Cell>>,
|
||||
},
|
||||
CellClearContents {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Cell>>,
|
||||
},
|
||||
CellClearAll {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Cell>>,
|
||||
old_style: Box<Style>,
|
||||
},
|
||||
SetCellStyle {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Style>,
|
||||
new_value: Box<Style>,
|
||||
},
|
||||
// Column and Row diffs
|
||||
SetColumnWidth {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
new_value: f64,
|
||||
old_value: f64,
|
||||
},
|
||||
SetRowHeight {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
new_value: f64,
|
||||
old_value: f64,
|
||||
},
|
||||
InsertRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
},
|
||||
DeleteRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
old_data: Box<RowData>,
|
||||
},
|
||||
InsertColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
},
|
||||
DeleteColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
InsertCellsShiftRight {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
},
|
||||
InsertCellsShiftDown {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
},
|
||||
DeleteCellsShiftLeft {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
old_data: Vec<Vec<Option<Cell>>>,
|
||||
},
|
||||
DeleteCellsShiftUp {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
old_data: Vec<Vec<Option<Cell>>>,
|
||||
},
|
||||
SetFrozenRowsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
old_value: i32,
|
||||
},
|
||||
SetFrozenColumnsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
old_value: i32,
|
||||
},
|
||||
DeleteSheet {
|
||||
sheet: u32,
|
||||
},
|
||||
NewSheet {
|
||||
index: u32,
|
||||
name: String,
|
||||
},
|
||||
RenameSheet {
|
||||
index: u32,
|
||||
old_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
SetSheetColor {
|
||||
index: u32,
|
||||
old_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
}
|
||||
|
||||
type DiffList = Vec<Diff>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct History {
|
||||
undo_stack: Vec<DiffList>,
|
||||
redo_stack: Vec<DiffList>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
fn push(&mut self, diff_list: DiffList) {
|
||||
self.undo_stack.push(diff_list);
|
||||
self.redo_stack = vec![];
|
||||
}
|
||||
|
||||
fn undo(&mut self) -> Option<Vec<Diff>> {
|
||||
match self.undo_stack.pop() {
|
||||
Some(diff_list) => {
|
||||
self.redo_stack.push(diff_list.clone());
|
||||
Some(diff_list)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn redo(&mut self) -> Option<Vec<Diff>> {
|
||||
match self.redo_stack.pop() {
|
||||
Some(diff_list) => {
|
||||
self.undo_stack.push(diff_list.clone());
|
||||
Some(diff_list)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.redo_stack = vec![];
|
||||
self.undo_stack = vec![];
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
enum DiffType {
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
struct QueueDiffs {
|
||||
r#type: DiffType,
|
||||
list: DiffList,
|
||||
}
|
||||
|
||||
fn boolean(value: &str) -> Result<bool, String> {
|
||||
@@ -120,7 +279,7 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
|
||||
}
|
||||
|
||||
/// # A wrapper around [`Model`] for a spreadsheet end user.
|
||||
/// UserModel is a wrapper around Model with undo/redo history, _diffs_, automatic evaluation and view management.
|
||||
/// UserModel is a wrapper around Model with undo/redo history, _diffs_ and automatic evaluation.
|
||||
///
|
||||
/// A diff in this context (or more correctly a _user diff_) is a change created by a user.
|
||||
///
|
||||
@@ -146,7 +305,7 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct UserModel {
|
||||
pub(crate) model: Model,
|
||||
model: Model,
|
||||
history: History,
|
||||
send_queue: Vec<QueueDiffs>,
|
||||
pause_evaluation: bool,
|
||||
@@ -584,6 +743,123 @@ impl UserModel {
|
||||
self.model.delete_columns(sheet, column, 1)
|
||||
}
|
||||
|
||||
/// Insert cells in the area pushing the existing ones to the right
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::insert_cells_and_shift_right]
|
||||
pub fn insert_cells_and_shift_right(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertCellsShiftRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.insert_cells_and_shift_right(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells in the area pushing the existing ones down
|
||||
pub fn insert_cells_and_shift_down(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.insert_cells_and_shift_down(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete cells in the specified area and then shift cells left to fill the gap.
|
||||
pub fn delete_cells_and_shift_left(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut old_data = Vec::new();
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
for r in row..row + row_delta {
|
||||
let mut row_data = Vec::new();
|
||||
for c in column..column + column_delta {
|
||||
let cell = worksheet.cell(r, c);
|
||||
row_data.push(cell.cloned());
|
||||
}
|
||||
old_data.push(row_data);
|
||||
}
|
||||
let diff_list = vec![Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.delete_cells_and_shift_left(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete cells in the specified area and then shift cells upward to fill the gap.
|
||||
pub fn delete_cells_and_shift_up(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut old_data = Vec::new();
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
for r in row..row + row_delta {
|
||||
let mut row_data = Vec::new();
|
||||
for c in column..column + column_delta {
|
||||
let cell = worksheet.cell(r, c);
|
||||
row_data.push(cell.cloned());
|
||||
}
|
||||
old_data.push(row_data);
|
||||
}
|
||||
let diff_list = vec![Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.delete_cells_and_shift_up(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the width of a column
|
||||
///
|
||||
/// See also:
|
||||
@@ -682,154 +958,6 @@ impl UserModel {
|
||||
self.model.set_frozen_columns(sheet, frozen_columns)
|
||||
}
|
||||
|
||||
/// Paste `styles` in the selected area
|
||||
pub fn on_paste_styles(&mut self, styles: &[Vec<Style>]) -> Result<(), String> {
|
||||
let styles_heigh = styles.len() as i32;
|
||||
let styles_width = styles[0].len() as i32;
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let range = if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
view.range
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If the pasted area is smaller than the selected area we increase it
|
||||
let [row_start, column_start, row_end, column_end] = range;
|
||||
let last_row = row_end.max(row_start + styles_heigh - 1);
|
||||
let last_column = column_end.max(column_start + styles_width - 1);
|
||||
|
||||
let mut diff_list = Vec::new();
|
||||
for row in row_start..=last_row {
|
||||
for column in column_start..=last_column {
|
||||
let row_index = ((row - row_start) % styles_heigh) as usize;
|
||||
let column_index = ((column - column_start) % styles_width) as usize;
|
||||
let style = &styles[row_index][column_index];
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column);
|
||||
self.model.set_cell_style(sheet, row, column, style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(style.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
|
||||
// select the pasted range
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.range = [row_start, column_start, last_row, last_column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the border
|
||||
pub fn set_area_with_border(
|
||||
&mut self,
|
||||
range: &Area,
|
||||
border_area: &BorderArea,
|
||||
) -> Result<(), String> {
|
||||
let sheet = range.sheet;
|
||||
let mut diff_list = Vec::new();
|
||||
let last_row = range.row + range.height - 1;
|
||||
let last_column = range.column + range.width - 1;
|
||||
for row in range.row..=last_row {
|
||||
for column in range.column..=last_column {
|
||||
let old_value = self.model.get_style_for_cell(sheet, row, column);
|
||||
let mut style = old_value.clone();
|
||||
|
||||
// First remove all existing borders
|
||||
style.border.top = None;
|
||||
style.border.right = None;
|
||||
style.border.bottom = None;
|
||||
style.border.left = None;
|
||||
|
||||
match border_area.r#type {
|
||||
BorderType::All => {
|
||||
style.border.top = Some(border_area.item.clone());
|
||||
style.border.right = Some(border_area.item.clone());
|
||||
style.border.bottom = Some(border_area.item.clone());
|
||||
style.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
BorderType::Inner => {
|
||||
if row != range.row {
|
||||
style.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
style.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
if column != range.column {
|
||||
style.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
style.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Outer => {
|
||||
if row == range.row {
|
||||
style.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row == last_row {
|
||||
style.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
if column == range.column {
|
||||
style.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column == last_column {
|
||||
style.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::Top => style.border.top = Some(border_area.item.clone()),
|
||||
BorderType::Right => style.border.right = Some(border_area.item.clone()),
|
||||
BorderType::Bottom => style.border.bottom = Some(border_area.item.clone()),
|
||||
BorderType::Left => style.border.left = Some(border_area.item.clone()),
|
||||
BorderType::CenterH => {
|
||||
if row != range.row {
|
||||
style.border.top = Some(border_area.item.clone());
|
||||
}
|
||||
if row != last_row {
|
||||
style.border.bottom = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::CenterV => {
|
||||
if column != range.column {
|
||||
style.border.left = Some(border_area.item.clone());
|
||||
}
|
||||
if column != last_column {
|
||||
style.border.right = Some(border_area.item.clone());
|
||||
}
|
||||
}
|
||||
BorderType::None => {
|
||||
// noop, we already removed all the borders
|
||||
}
|
||||
}
|
||||
|
||||
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.push_diff_list(diff_list);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the range with a cell style.
|
||||
/// See also:
|
||||
/// * [Model::set_cell_style]
|
||||
@@ -929,7 +1057,7 @@ impl UserModel {
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(style),
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
@@ -945,208 +1073,6 @@ impl UserModel {
|
||||
Ok(self.model.get_style_for_cell(sheet, row, column))
|
||||
}
|
||||
|
||||
/// Fills the cells from `source_area` until `to_row`.
|
||||
/// This simulates the user clicking on the cell outline handle and dragging it downwards (or upwards)
|
||||
pub fn auto_fill_rows(&mut self, source_area: &Area, to_row: i32) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
let sheet = source_area.sheet;
|
||||
let row1 = source_area.row;
|
||||
let column1 = source_area.column;
|
||||
let width1 = source_area.width;
|
||||
let height1 = source_area.height;
|
||||
|
||||
// Check first all parameters are valid
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
||||
}
|
||||
|
||||
if !is_valid_column_number(column1) {
|
||||
return Err(format!("Invalid column: '{column1}'"));
|
||||
}
|
||||
if !is_valid_row(row1) {
|
||||
return Err(format!("Invalid row: '{row1}'"));
|
||||
}
|
||||
if !is_valid_column_number(column1 + width1 - 1) {
|
||||
return Err(format!("Invalid column: '{}'", column1 + width1 - 1));
|
||||
}
|
||||
if !is_valid_row(row1 + height1 - 1) {
|
||||
return Err(format!("Invalid row: '{}'", row1 + height1 - 1));
|
||||
}
|
||||
|
||||
if !is_valid_row(to_row) {
|
||||
return Err(format!("Invalid row: '{to_row}'"));
|
||||
}
|
||||
|
||||
// anchor_row is the first row that repeats in each case.
|
||||
let anchor_row;
|
||||
let sign;
|
||||
// this is the range of rows we are going to fill
|
||||
let row_range: Vec<i32>;
|
||||
|
||||
if to_row >= row1 + height1 {
|
||||
// we go downwards, we start from `row1 + height1` to `to_row`,
|
||||
anchor_row = row1;
|
||||
sign = 1;
|
||||
row_range = (row1 + height1..to_row + 1).collect();
|
||||
} else if to_row < row1 {
|
||||
// we go upwards, starting from `row1 - `` all the way to `to_row`
|
||||
anchor_row = row1 + height1 - 1;
|
||||
sign = -1;
|
||||
row_range = (to_row..row1).rev().collect();
|
||||
} else {
|
||||
return Err("Invalid parameters for autofill".to_string());
|
||||
}
|
||||
|
||||
for column in column1..column1 + width1 {
|
||||
let mut index = 0;
|
||||
for row_ref in &row_range {
|
||||
// Save value and style first
|
||||
let row = *row_ref;
|
||||
let old_value = self
|
||||
.model
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.cloned();
|
||||
let old_style = self.model.get_style_for_cell(sheet, row, column);
|
||||
|
||||
// compute the new value and set it
|
||||
let source_row = anchor_row + index;
|
||||
let target_value = self
|
||||
.model
|
||||
.extend_to(sheet, source_row, column, row, column)?;
|
||||
self.model
|
||||
.set_user_input(sheet, row, column, target_value.to_string());
|
||||
|
||||
// Compute the new style and set it
|
||||
let new_style = self.model.get_style_for_cell(sheet, source_row, column);
|
||||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
||||
|
||||
// Add the diffs
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_style),
|
||||
new_value: Box::new(new_style),
|
||||
});
|
||||
diff_list.push(Diff::SetCellValue {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
new_value: target_value.to_string(),
|
||||
old_value: Box::new(old_value),
|
||||
});
|
||||
|
||||
index = (index + sign) % height1;
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fills the cells from `source_area` until `to_column`.
|
||||
/// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left)
|
||||
pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> {
|
||||
let mut diff_list = Vec::new();
|
||||
let sheet = source_area.sheet;
|
||||
let row1 = source_area.row;
|
||||
let column1 = source_area.column;
|
||||
let width1 = source_area.width;
|
||||
let height1 = source_area.height;
|
||||
|
||||
// Check first all parameters are valid
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index: '{sheet}'"));
|
||||
}
|
||||
|
||||
if !is_valid_column_number(column1) {
|
||||
return Err(format!("Invalid column: '{column1}'"));
|
||||
}
|
||||
if !is_valid_row(row1) {
|
||||
return Err(format!("Invalid row: '{row1}'"));
|
||||
}
|
||||
if !is_valid_column_number(column1 + width1 - 1) {
|
||||
return Err(format!("Invalid column: '{}'", column1 + width1 - 1));
|
||||
}
|
||||
if !is_valid_row(row1 + height1 - 1) {
|
||||
return Err(format!("Invalid row: '{}'", row1 + height1 - 1));
|
||||
}
|
||||
|
||||
if !is_valid_row(to_column) {
|
||||
return Err(format!("Invalid row: '{to_column}'"));
|
||||
}
|
||||
|
||||
// anchor_column is the first column that repeats in each case.
|
||||
let anchor_column;
|
||||
let sign;
|
||||
// this is the range of columns we are going to fill
|
||||
let column_range: Vec<i32>;
|
||||
|
||||
if to_column >= column1 + width1 {
|
||||
// we go right, we start from `1 + width` to `to_column`,
|
||||
anchor_column = column1;
|
||||
sign = 1;
|
||||
column_range = (column1 + width1..to_column + 1).collect();
|
||||
} else if to_column < column1 {
|
||||
// we go left, starting from `column1 - `` all the way to `to_column`
|
||||
anchor_column = column1 + width1 - 1;
|
||||
sign = -1;
|
||||
column_range = (to_column..column1).rev().collect();
|
||||
} else {
|
||||
return Err("Invalid parameters for autofill".to_string());
|
||||
}
|
||||
|
||||
for row in row1..row1 + height1 {
|
||||
let mut index = 0;
|
||||
for column_ref in &column_range {
|
||||
let column = *column_ref;
|
||||
// Save value and style first
|
||||
let old_value = self
|
||||
.model
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(row, column)
|
||||
.cloned();
|
||||
let old_style = self.model.get_style_for_cell(sheet, row, column);
|
||||
|
||||
// compute the new value and set it
|
||||
let source_column = anchor_column + index;
|
||||
let target_value = self
|
||||
.model
|
||||
.extend_to(sheet, row, source_column, row, column)?;
|
||||
self.model
|
||||
.set_user_input(sheet, row, column, target_value.to_string());
|
||||
|
||||
// Compute the new style and set it
|
||||
let new_style = self.model.get_style_for_cell(sheet, row, source_column);
|
||||
self.model.set_cell_style(sheet, row, column, &new_style)?;
|
||||
|
||||
// Add the diffs
|
||||
diff_list.push(Diff::SetCellStyle {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
old_value: Box::new(old_style),
|
||||
new_value: Box::new(new_style),
|
||||
});
|
||||
diff_list.push(Diff::SetCellValue {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
new_value: target_value.to_string(),
|
||||
old_value: Box::new(old_value),
|
||||
});
|
||||
|
||||
index = (index + sign) % width1;
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
self.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns information about the sheets
|
||||
///
|
||||
/// See also:
|
||||
@@ -1156,24 +1082,6 @@ impl UserModel {
|
||||
self.model.get_worksheets_properties()
|
||||
}
|
||||
|
||||
/// Set the gid lines in the worksheet to visible (`true`) or hidden (`false`)
|
||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<(), String> {
|
||||
let old_value = self.model.workbook.worksheet(sheet)?.show_grid_lines;
|
||||
self.model.set_show_grid_lines(sheet, show_grid_lines)?;
|
||||
|
||||
self.push_diff_list(vec![Diff::SetShowGridLines {
|
||||
sheet,
|
||||
new_value: show_grid_lines,
|
||||
old_value,
|
||||
}]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true in the grid lines for
|
||||
pub fn get_show_grid_lines(&self, sheet: u32) -> Result<bool, String> {
|
||||
Ok(self.model.workbook.worksheet(sheet)?.show_grid_lines)
|
||||
}
|
||||
|
||||
// **** Private methods ****** //
|
||||
|
||||
fn push_diff_list(&mut self, diff_list: DiffList) {
|
||||
@@ -1337,12 +1245,93 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, old_value)?;
|
||||
}
|
||||
Diff::SetShowGridLines {
|
||||
Diff::InsertCellsShiftRight {
|
||||
sheet,
|
||||
old_value,
|
||||
new_value: _,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
self.model.set_show_grid_lines(*sheet, *old_value)?;
|
||||
needs_evaluation = true;
|
||||
self.model.delete_cells_and_shift_left(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model.delete_cells_and_shift_up(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
// Sets old data
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for r in *row..*row + *row_delta {
|
||||
for c in *column..*column + *column_delta {
|
||||
if let Some(cell) = &old_data[r as usize][c as usize] {
|
||||
worksheet.update_cell(r, c, cell.clone());
|
||||
} else {
|
||||
worksheet.cell_clear_contents(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.model.insert_cells_and_shift_right(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
// Sets old data
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for r in *row..*row + *row_delta {
|
||||
for c in *column..*column + *column_delta {
|
||||
if let Some(cell) = &old_data[r as usize][c as usize] {
|
||||
worksheet.update_cell(r, c, cell.clone());
|
||||
} else {
|
||||
worksheet.cell_clear_contents(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.model.insert_cells_and_shift_down(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1464,12 +1453,71 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, new_value)?;
|
||||
}
|
||||
Diff::SetShowGridLines {
|
||||
Diff::InsertCellsShiftRight {
|
||||
sheet,
|
||||
old_value: _,
|
||||
new_value,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
self.model.set_show_grid_lines(*sheet, *new_value)?;
|
||||
self.model.insert_cells_and_shift_right(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
self.model.insert_cells_and_shift_down(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_cells_and_shift_left(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_cells_and_shift_up(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1485,7 +1533,7 @@ impl UserModel {
|
||||
mod tests {
|
||||
use crate::{
|
||||
types::{HorizontalAlignment, VerticalAlignment},
|
||||
user_model::common::{horizontal, vertical},
|
||||
user_model::{horizontal, vertical},
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -1,164 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
|
||||
use crate::types::{Cell, Col, Row, Style};
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub(crate) struct RowData {
|
||||
pub(crate) row: Option<Row>,
|
||||
pub(crate) data: HashMap<i32, Cell>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub(crate) struct ColumnData {
|
||||
pub(crate) column: Option<Col>,
|
||||
pub(crate) data: HashMap<i32, Cell>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub(crate) enum Diff {
|
||||
// Cell diffs
|
||||
SetCellValue {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
new_value: String,
|
||||
old_value: Box<Option<Cell>>,
|
||||
},
|
||||
CellClearContents {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Cell>>,
|
||||
},
|
||||
CellClearAll {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Cell>>,
|
||||
old_style: Box<Style>,
|
||||
},
|
||||
SetCellStyle {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Style>,
|
||||
new_value: Box<Style>,
|
||||
},
|
||||
// Column and Row diffs
|
||||
SetColumnWidth {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
new_value: f64,
|
||||
old_value: f64,
|
||||
},
|
||||
SetRowHeight {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
new_value: f64,
|
||||
old_value: f64,
|
||||
},
|
||||
InsertRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
},
|
||||
DeleteRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
old_data: Box<RowData>,
|
||||
},
|
||||
InsertColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
},
|
||||
DeleteColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
SetFrozenRowsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
old_value: i32,
|
||||
},
|
||||
SetFrozenColumnsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
old_value: i32,
|
||||
},
|
||||
DeleteSheet {
|
||||
sheet: u32,
|
||||
},
|
||||
NewSheet {
|
||||
index: u32,
|
||||
name: String,
|
||||
},
|
||||
RenameSheet {
|
||||
index: u32,
|
||||
old_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
SetSheetColor {
|
||||
index: u32,
|
||||
old_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
SetShowGridLines {
|
||||
sheet: u32,
|
||||
old_value: bool,
|
||||
new_value: bool,
|
||||
}, // FIXME: we are missing SetViewDiffs
|
||||
}
|
||||
|
||||
pub(crate) type DiffList = Vec<Diff>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct History {
|
||||
pub(crate) undo_stack: Vec<DiffList>,
|
||||
pub(crate) redo_stack: Vec<DiffList>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn push(&mut self, diff_list: DiffList) {
|
||||
self.undo_stack.push(diff_list);
|
||||
self.redo_stack = vec![];
|
||||
}
|
||||
|
||||
pub fn undo(&mut self) -> Option<Vec<Diff>> {
|
||||
match self.undo_stack.pop() {
|
||||
Some(diff_list) => {
|
||||
self.redo_stack.push(diff_list.clone());
|
||||
Some(diff_list)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) -> Option<Vec<Diff>> {
|
||||
match self.redo_stack.pop() {
|
||||
Some(diff_list) => {
|
||||
self.undo_stack.push(diff_list.clone());
|
||||
Some(diff_list)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.redo_stack = vec![];
|
||||
self.undo_stack = vec![];
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub enum DiffType {
|
||||
Undo,
|
||||
Redo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub struct QueueDiffs {
|
||||
pub r#type: DiffType,
|
||||
pub list: DiffList,
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod common;
|
||||
mod history;
|
||||
mod ui;
|
||||
|
||||
pub use common::UserModel;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use ui::SelectedView;
|
||||
|
||||
pub use common::BorderArea;
|
||||
@@ -1,671 +0,0 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expressions::utils::{is_valid_column_number, is_valid_row};
|
||||
|
||||
use super::common::UserModel;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||
pub struct SelectedView {
|
||||
pub sheet: u32,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub range: [i32; 4],
|
||||
pub top_row: i32,
|
||||
pub left_column: i32,
|
||||
}
|
||||
|
||||
impl UserModel {
|
||||
/// Returns the selected sheet index
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the selected cell
|
||||
pub fn get_selected_cell(&self) -> (u32, i32, i32) {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
return (sheet, view.row, view.column);
|
||||
}
|
||||
}
|
||||
// return a safe default
|
||||
(0, 1, 1)
|
||||
}
|
||||
|
||||
/// Returns selected view
|
||||
pub fn get_selected_view(&self) -> SelectedView {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
return SelectedView {
|
||||
sheet,
|
||||
row: view.row,
|
||||
column: view.column,
|
||||
range: view.range,
|
||||
top_row: view.top_row,
|
||||
left_column: view.left_column,
|
||||
};
|
||||
}
|
||||
}
|
||||
// return a safe default
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the the selected sheet
|
||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
||||
view.sheet = sheet;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the selected cell
|
||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Invalid column: '{column}'"));
|
||||
}
|
||||
if !is_valid_row(row) {
|
||||
return Err(format!("Invalid row: '{row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.row = row;
|
||||
view.column = column;
|
||||
view.range = [row, column, row, column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the selected range
|
||||
pub fn set_selected_range(
|
||||
&mut self,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !is_valid_column_number(start_column) {
|
||||
return Err(format!("Invalid column: '{start_column}'"));
|
||||
}
|
||||
if !is_valid_row(start_row) {
|
||||
return Err(format!("Invalid row: '{start_row}'"));
|
||||
}
|
||||
|
||||
if !is_valid_column_number(end_column) {
|
||||
return Err(format!("Invalid column: '{end_column}'"));
|
||||
}
|
||||
if !is_valid_row(end_row) {
|
||||
return Err(format!("Invalid row: '{end_row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.range = [start_row, start_column, end_row, end_column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The selected range is expanded with the keyboard
|
||||
pub fn on_expand_selected_range(&mut self, key: &str) -> Result<(), String> {
|
||||
let (sheet, window_width, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.sheet,
|
||||
view.window_width as f64,
|
||||
view.window_height as f64,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let (selected_row, selected_column, range, top_row, left_column) =
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.row,
|
||||
view.column,
|
||||
view.range,
|
||||
view.top_row,
|
||||
view.left_column,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let [row_start, column_start, row_end, column_end] = range;
|
||||
|
||||
match key {
|
||||
"ArrowRight" => {
|
||||
if selected_column > column_start {
|
||||
let new_column = column_start + 1;
|
||||
if !(is_valid_column_number(new_column)) {
|
||||
return Ok(());
|
||||
}
|
||||
self.set_selected_range(row_start, new_column, row_end, column_end)?;
|
||||
} else {
|
||||
let new_column = column_end + 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
let mut width = 0.0;
|
||||
let mut c = left_column;
|
||||
while c <= new_column {
|
||||
width += self.model.get_column_width(sheet, c)?;
|
||||
c += 1;
|
||||
}
|
||||
if width > window_width {
|
||||
self.set_top_left_visible_cell(top_row, left_column + 1)?;
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, row_end, column_end + 1)?;
|
||||
}
|
||||
}
|
||||
"ArrowLeft" => {
|
||||
if selected_column < column_end {
|
||||
let new_column = column_end - 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
if new_column < left_column {
|
||||
self.set_top_left_visible_cell(top_row, new_column)?;
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, row_end, new_column)?;
|
||||
} else {
|
||||
let new_column = column_start - 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
if new_column < left_column {
|
||||
self.set_top_left_visible_cell(top_row, new_column)?;
|
||||
}
|
||||
self.set_selected_range(row_start, new_column, row_end, column_end)?;
|
||||
}
|
||||
}
|
||||
"ArrowUp" => {
|
||||
if selected_row < row_end {
|
||||
let new_row = row_end - 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, new_row, column_end)?;
|
||||
} else {
|
||||
let new_row = row_start - 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
if new_row < top_row {
|
||||
self.set_top_left_visible_cell(new_row, left_column)?;
|
||||
}
|
||||
self.set_selected_range(new_row, column_start, row_end, column_end)?;
|
||||
}
|
||||
}
|
||||
"ArrowDown" => {
|
||||
if selected_row > row_start {
|
||||
let new_row = row_start + 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
self.set_selected_range(new_row, column_start, row_end, column_end)?;
|
||||
} else {
|
||||
let new_row = row_end + 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut height = 0.0;
|
||||
let mut r = top_row;
|
||||
while r <= new_row + 1 {
|
||||
height += self.model.get_row_height(sheet, r)?;
|
||||
r += 1;
|
||||
}
|
||||
if height >= window_height {
|
||||
self.set_top_left_visible_cell(top_row + 1, left_column)?;
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, new_row, column_end)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the value of the first visible cell
|
||||
pub fn set_top_left_visible_cell(
|
||||
&mut self,
|
||||
top_row: i32,
|
||||
left_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !is_valid_column_number(left_column) {
|
||||
return Err(format!("Invalid column: '{left_column}'"));
|
||||
}
|
||||
if !is_valid_row(top_row) {
|
||||
return Err(format!("Invalid row: '{top_row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.top_row = top_row;
|
||||
view.left_column = left_column;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the width of the window
|
||||
pub fn set_window_width(&mut self, window_width: f64) {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
view.window_width = window_width as i64;
|
||||
};
|
||||
}
|
||||
|
||||
/// Gets the width of the window
|
||||
pub fn get_window_width(&mut self) -> Result<i64, String> {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
return Ok(view.window_width);
|
||||
};
|
||||
Err("View not found".to_string())
|
||||
}
|
||||
|
||||
/// Sets the height of the window
|
||||
pub fn set_window_height(&mut self, window_height: f64) {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
view.window_height = window_height as i64;
|
||||
};
|
||||
}
|
||||
|
||||
/// Gets the height of the window
|
||||
pub fn get_window_height(&mut self) -> Result<i64, String> {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
return Ok(view.window_height);
|
||||
};
|
||||
Err("View not found".to_string())
|
||||
}
|
||||
|
||||
/// User presses right arrow
|
||||
pub fn on_arrow_right(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_width) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_width)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_column = view.column + 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
let mut width = 0.0;
|
||||
let mut column = view.left_column;
|
||||
while column <= new_column {
|
||||
width += self.model.get_column_width(sheet, column)?;
|
||||
column += 1;
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.column = new_column;
|
||||
view.range = [view.row, new_column, view.row, new_column];
|
||||
if width > window_width as f64 {
|
||||
view.left_column += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User presses left arrow
|
||||
pub fn on_arrow_left(&mut self) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_column = view.column - 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.column = new_column;
|
||||
view.range = [view.row, new_column, view.row, new_column];
|
||||
if new_column < view.left_column {
|
||||
view.left_column = new_column;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User presses up arrow key
|
||||
pub fn on_arrow_up(&mut self) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_row = view.row - 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.row = new_row;
|
||||
view.range = [new_row, view.column, new_row, view.column];
|
||||
if new_row < view.top_row {
|
||||
view.top_row = new_row;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User presses down arrow key
|
||||
pub fn on_arrow_down(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_height)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_row = view.row + 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the row is not fully visible we 'scroll' down until it is
|
||||
let mut height = 0.0;
|
||||
let mut row = view.top_row;
|
||||
while row <= new_row + 1 {
|
||||
height += self.model.get_row_height(sheet, row)?;
|
||||
row += 1;
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.row = new_row;
|
||||
view.range = [new_row, view.column, new_row, view.column];
|
||||
if height > window_height as f64 {
|
||||
view.top_row += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: This function should be memoized
|
||||
/// Returns the x-coordinate of the cell in the top left corner
|
||||
pub fn get_scroll_x(&self) -> Result<f64, String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let mut scroll_x = 0.0;
|
||||
for column in 1..view.left_column {
|
||||
scroll_x += self.model.get_column_width(sheet, column)?;
|
||||
}
|
||||
Ok(scroll_x)
|
||||
}
|
||||
|
||||
// TODO: This function should be memoized
|
||||
/// Returns the y-coordinate of the cell in the top left corner
|
||||
pub fn get_scroll_y(&self) -> Result<f64, String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let mut scroll_y = 0.0;
|
||||
for row in 1..view.top_row {
|
||||
scroll_y += self.model.get_row_height(sheet, row)?;
|
||||
}
|
||||
Ok(scroll_y)
|
||||
}
|
||||
|
||||
/// User presses page down
|
||||
pub fn on_page_down(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_height)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
|
||||
let mut height = 0.0;
|
||||
let mut last_row = view.top_row;
|
||||
while height <= window_height as f64 {
|
||||
height += self.model.get_row_height(sheet, last_row)?;
|
||||
last_row += 1;
|
||||
}
|
||||
if !is_valid_row(last_row) {
|
||||
return Ok(());
|
||||
}
|
||||
let row_delta = view.row - view.top_row;
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.top_row = last_row;
|
||||
view.row = view.top_row + row_delta;
|
||||
view.range = [view.row, view.column, view.row, view.column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// On page up
|
||||
pub fn on_page_up(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_height)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
|
||||
let mut height = 0.0;
|
||||
let mut last_row = view.top_row;
|
||||
while height <= window_height as f64 && last_row > 1 {
|
||||
height += self.model.get_row_height(sheet, last_row)?;
|
||||
last_row -= 1;
|
||||
}
|
||||
let row_delta = view.row - view.top_row;
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.top_row = last_row;
|
||||
view.row = view.top_row + row_delta;
|
||||
view.range = [view.row, view.column, view.row, view.column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// We extend the selection to cell (target_row, target_column)
|
||||
pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<(), String> {
|
||||
let (sheet, window_width, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.sheet,
|
||||
view.window_width as f64,
|
||||
view.window_height as f64,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let (selected_row, selected_column, range, top_row, left_column) =
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.row,
|
||||
view.column,
|
||||
view.range,
|
||||
view.top_row,
|
||||
view.left_column,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let [row_start, column_start, _row_end, _column_end] = range;
|
||||
|
||||
let mut new_left_column = left_column;
|
||||
if target_column >= selected_column {
|
||||
let mut width = 0.0;
|
||||
let mut column = left_column;
|
||||
while column <= target_column {
|
||||
width += self.model.get_column_width(sheet, column)?;
|
||||
column += 1;
|
||||
}
|
||||
|
||||
while width > window_width {
|
||||
width -= self.model.get_column_width(sheet, new_left_column)?;
|
||||
new_left_column += 1;
|
||||
}
|
||||
} else if target_column < new_left_column {
|
||||
new_left_column = target_column;
|
||||
}
|
||||
let mut new_top_row = top_row;
|
||||
if target_row >= selected_row {
|
||||
let mut height = 0.0;
|
||||
let mut row = top_row;
|
||||
while row <= target_row {
|
||||
height += self.model.get_row_height(sheet, row)?;
|
||||
row += 1;
|
||||
}
|
||||
while height > window_height {
|
||||
height -= self.model.get_row_height(sheet, new_top_row)?;
|
||||
new_top_row += 1;
|
||||
}
|
||||
} else if target_row < new_top_row {
|
||||
new_top_row = target_row;
|
||||
}
|
||||
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.range = [row_start, column_start, target_row, target_column];
|
||||
if new_top_row != top_row {
|
||||
view.top_row = new_top_row;
|
||||
}
|
||||
if new_left_column != left_column {
|
||||
view.left_column = new_left_column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -394,7 +394,7 @@ impl Worksheet {
|
||||
}
|
||||
|
||||
/// Calculates dimension of the sheet. This function isn't cheap to calculate.
|
||||
pub fn dimension(&self) -> WorksheetDimension {
|
||||
pub fn get_dimension(&self) -> WorksheetDimension {
|
||||
// FIXME: It's probably better to just track the size as operations happen.
|
||||
if self.sheet_data.is_empty() {
|
||||
return WorksheetDimension {
|
||||
|
||||
@@ -63,97 +63,15 @@ style_types = r"""
|
||||
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
||||
""".strip()
|
||||
|
||||
view = r"""
|
||||
* @returns {any}
|
||||
*/
|
||||
getSelectedView(): any;
|
||||
""".strip()
|
||||
|
||||
view_types = r"""
|
||||
* @returns {CellStyle}
|
||||
*/
|
||||
getSelectedView(): SelectedView;
|
||||
""".strip()
|
||||
|
||||
autofill_rows = r"""
|
||||
/**
|
||||
* @param {any} source_area
|
||||
* @param {number} to_row
|
||||
*/
|
||||
autoFillRows(source_area: any, to_row: number): void;
|
||||
"""
|
||||
|
||||
autofill_rows_types = r"""
|
||||
/**
|
||||
* @param {Area} source_area
|
||||
* @param {number} to_row
|
||||
*/
|
||||
autoFillRows(source_area: Area, to_row: number): void;
|
||||
"""
|
||||
|
||||
autofill_columns = r"""
|
||||
/**
|
||||
* @param {any} source_area
|
||||
* @param {number} to_column
|
||||
*/
|
||||
autoFillColumns(source_area: any, to_column: number): void;
|
||||
"""
|
||||
|
||||
autofill_columns_types = r"""
|
||||
/**
|
||||
* @param {Area} source_area
|
||||
* @param {number} to_column
|
||||
*/
|
||||
autoFillColumns(source_area: Area, to_column: number): void;
|
||||
"""
|
||||
|
||||
set_cell_style = r"""
|
||||
/**
|
||||
* @param {any} styles
|
||||
*/
|
||||
onPasteStyles(styles: any): void;
|
||||
"""
|
||||
|
||||
set_cell_style_types = r"""
|
||||
/**
|
||||
* @param {CellStyle[][]} styles
|
||||
*/
|
||||
onPasteStyles(styles: CellStyle[][]): void;
|
||||
"""
|
||||
|
||||
set_area_border = r"""
|
||||
/**
|
||||
* @param {any} area
|
||||
* @param {any} border_area
|
||||
*/
|
||||
setAreaWithBorder(area: any, border_area: any): void;
|
||||
"""
|
||||
|
||||
set_area_border_types = r"""
|
||||
/**
|
||||
* @param {Area} area
|
||||
* @param {BorderArea} border_area
|
||||
*/
|
||||
setAreaWithBorder(area: Area, border_area: BorderArea): void;
|
||||
"""
|
||||
|
||||
def fix_types(text):
|
||||
text = text.replace(get_tokens_str, get_tokens_str_types)
|
||||
text = text.replace(update_style_str, update_style_str_types)
|
||||
text = text.replace(properties, properties_types)
|
||||
text = text.replace(style, style_types)
|
||||
text = text.replace(view, view_types)
|
||||
text = text.replace(autofill_rows, autofill_rows_types)
|
||||
text = text.replace(autofill_columns, autofill_columns_types)
|
||||
text = text.replace(set_cell_style, set_cell_style_types)
|
||||
text = text.replace(set_area_border, set_area_border_types)
|
||||
with open("types.ts") as f:
|
||||
types_str = f.read()
|
||||
header_types = "{}\n\n{}".format(header, types_str)
|
||||
text = text.replace(header, header_types)
|
||||
if text.find("any") != -1:
|
||||
print("There are 'unfixed' types. Please check.")
|
||||
exit(1)
|
||||
return text
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ use wasm_bindgen::{
|
||||
};
|
||||
|
||||
use ironcalc_base::{
|
||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
||||
types::{CellType, Style},
|
||||
BorderArea, UserModel as BaseModel,
|
||||
expressions::{lexer::marked_token::get_tokens as tokenizer, types::Area},
|
||||
types::CellType,
|
||||
UserModel as BaseModel,
|
||||
};
|
||||
|
||||
fn to_js_error(error: String) -> JsError {
|
||||
@@ -102,13 +102,6 @@ impl Model {
|
||||
self.model.rename_sheet(sheet, name).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSheetColor")]
|
||||
pub fn set_sheet_color(&mut self, sheet: u32, color: &str) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_sheet_color(sheet, color)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "rangeClearAll")]
|
||||
pub fn range_clear_all(
|
||||
&mut self,
|
||||
@@ -271,12 +264,6 @@ impl Model {
|
||||
.map(|x| serde_wasm_bindgen::to_value(&x).unwrap())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onPasteStyles")]
|
||||
pub fn on_paste_styles(&mut self, styles: JsValue) -> Result<(), JsError> {
|
||||
let styles: &Vec<Vec<Style>> = &serde_wasm_bindgen::from_value(styles).unwrap();
|
||||
self.model.on_paste_styles(styles).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getCellType")]
|
||||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32, JsError> {
|
||||
Ok(
|
||||
@@ -299,178 +286,4 @@ impl Model {
|
||||
pub fn get_worksheets_properties(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedSheet")]
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
self.model.get_selected_sheet()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedCell")]
|
||||
pub fn get_selected_cell(&self) -> Vec<i32> {
|
||||
let (sheet, row, column) = self.model.get_selected_cell();
|
||||
vec![sheet as i32, row, column]
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getSelectedView")]
|
||||
pub fn get_selected_view(&self) -> JsValue {
|
||||
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedSheet")]
|
||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
|
||||
self.model.set_selected_sheet(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedCell")]
|
||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_selected_cell(row, column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setSelectedRange")]
|
||||
pub fn set_selected_range(
|
||||
&mut self,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_selected_range(start_row, start_column, end_row, end_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setTopLeftVisibleCell")]
|
||||
pub fn set_top_left_visible_cell(
|
||||
&mut self,
|
||||
top_row: i32,
|
||||
top_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_top_left_visible_cell(top_row, top_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setShowGridLines")]
|
||||
pub fn set_show_grid_lines(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
show_grid_lines: bool,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.set_show_grid_lines(sheet, show_grid_lines)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getShowGridLines")]
|
||||
pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result<bool, JsError> {
|
||||
self.model.get_show_grid_lines(sheet).map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "autoFillRows")]
|
||||
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.auto_fill_rows(&area, to_row)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "autoFillColumns")]
|
||||
pub fn auto_fill_columns(
|
||||
&mut self,
|
||||
source_area: JsValue,
|
||||
to_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
let area: Area =
|
||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.auto_fill_columns(&area, to_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onArrowRight")]
|
||||
pub fn on_arrow_right(&mut self) -> Result<(), JsError> {
|
||||
self.model.on_arrow_right().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onArrowLeft")]
|
||||
pub fn on_arrow_left(&mut self) -> Result<(), JsError> {
|
||||
self.model.on_arrow_left().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onArrowUp")]
|
||||
pub fn on_arrow_up(&mut self) -> Result<(), JsError> {
|
||||
self.model.on_arrow_up().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onArrowDown")]
|
||||
pub fn on_arrow_down(&mut self) -> Result<(), JsError> {
|
||||
self.model.on_arrow_down().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onPageDown")]
|
||||
pub fn on_page_down(&mut self) -> Result<(), JsError> {
|
||||
self.model.on_page_down().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onPageUp")]
|
||||
pub fn on_page_up(&mut self) -> Result<(), JsError> {
|
||||
self.model.on_page_up().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setWindowWidth")]
|
||||
pub fn set_window_width(&mut self, window_width: f64) {
|
||||
self.model.set_window_width(window_width);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setWindowHeight")]
|
||||
pub fn set_window_height(&mut self, window_height: f64) {
|
||||
self.model.set_window_height(window_height);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getScrollX")]
|
||||
pub fn get_scroll_x(&self) -> Result<f64, JsError> {
|
||||
self.model.get_scroll_x().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "getScrollY")]
|
||||
pub fn get_scroll_y(&self) -> Result<f64, JsError> {
|
||||
self.model.get_scroll_y().map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onExpandSelectedRange")]
|
||||
pub fn on_expand_selected_range(&mut self, key: &str) -> Result<(), JsError> {
|
||||
self.model
|
||||
.on_expand_selected_range(key)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "onAreaSelecting")]
|
||||
pub fn on_area_selecting(
|
||||
&mut self,
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) -> Result<(), JsError> {
|
||||
self.model
|
||||
.on_area_selecting(target_row, target_column)
|
||||
.map_err(to_js_error)
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "setAreaWithBorder")]
|
||||
pub fn set_area_with_border(
|
||||
&mut self,
|
||||
area: JsValue,
|
||||
border_area: JsValue,
|
||||
) -> Result<(), JsError> {
|
||||
let range: Area =
|
||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
let border: BorderArea =
|
||||
serde_wasm_bindgen::from_value(border_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||
self.model
|
||||
.set_area_with_border(&range, &border)
|
||||
.map_err(|e| to_js_error(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,14 +119,5 @@ test("floating column numbers get truncated", () => {
|
||||
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
||||
});
|
||||
|
||||
test("autofill", () => {
|
||||
const model = new Model('en', 'UTC');
|
||||
model.setUserInput(0, 1, 1, "23");
|
||||
model.autoFillRows({sheet: 0, row: 1, column: 1, width: 1, height: 1}, 2);
|
||||
|
||||
const result = model.getFormattedCellValue(0, 2, 1);
|
||||
assert.strictEqual(result, "23");
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,24 +6,6 @@ export interface Area {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export enum BorderType {
|
||||
All = "All",
|
||||
Inner = "Inner",
|
||||
Outer = "Outer",
|
||||
Top = "Top",
|
||||
Right = "Right",
|
||||
Bottom = "Bottom",
|
||||
Left = "Left",
|
||||
CenterH = "CenterH",
|
||||
CenterV = "CenterV",
|
||||
None = "None",
|
||||
}
|
||||
|
||||
export interface BorderArea {
|
||||
item: BorderItem;
|
||||
type: BorderType;
|
||||
}
|
||||
|
||||
type ErrorType =
|
||||
| "REF"
|
||||
| "NAME"
|
||||
@@ -133,19 +115,19 @@ interface CellStyleFont {
|
||||
scheme: string;
|
||||
}
|
||||
|
||||
// export enum BorderType {
|
||||
// BorderAll,
|
||||
// BorderInner,
|
||||
// BorderCenterH,
|
||||
// BorderCenterV,
|
||||
// BorderOuter,
|
||||
// BorderNone,
|
||||
// BorderTop,
|
||||
// BorderRight,
|
||||
// BorderBottom,
|
||||
// BorderLeft,
|
||||
// None,
|
||||
// }
|
||||
export enum BorderType {
|
||||
BorderAll,
|
||||
BorderInner,
|
||||
BorderCenterH,
|
||||
BorderCenterV,
|
||||
BorderOuter,
|
||||
BorderNone,
|
||||
BorderTop,
|
||||
BorderRight,
|
||||
BorderBottom,
|
||||
BorderLeft,
|
||||
None,
|
||||
}
|
||||
|
||||
export interface BorderOptions {
|
||||
color: string;
|
||||
@@ -210,12 +192,3 @@ export interface CellStyle {
|
||||
num_fmt: string;
|
||||
alignment?: Alignment;
|
||||
}
|
||||
|
||||
export interface SelectedView {
|
||||
sheet: number;
|
||||
row: number;
|
||||
column: number;
|
||||
range: [number, number, number, number];
|
||||
top_row: number;
|
||||
left_column: number;
|
||||
}
|
||||
|
||||
3
webapp/.gitignore
vendored
3
webapp/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
node_modules/*
|
||||
dist/*
|
||||
example.json
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-mdx-gfm',
|
||||
'@chromatic-com/storybook'
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
import i18n from '../src/i18n';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
const withI18next = (Story: any) => {
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Story />
|
||||
</I18nextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const decorators = [withI18next];
|
||||
export default preview;
|
||||
@@ -1,21 +0,0 @@
|
||||
# IronCalc Web App
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
# Deploy
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Binary file not shown.
@@ -1,16 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- <meta name="theme-color" content="#1bb566"> -->
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#F2994A" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
||||
<title>IronCalc Spreadsheet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { Config } from "jest";
|
||||
// import {defaults} from 'jest-config';
|
||||
|
||||
const config: Config = {
|
||||
// testMatch:["**.jest.mjs"],
|
||||
moduleFileExtensions: ["js", "ts", "mts", "mjs"],
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@ironcalc/wasm$": "<rootDir>/node_modules/@ironcalc/nodejs/"
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
16992
webapp/package-lock.json
generated
16992
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
|
||||
"dev": "vite",
|
||||
"test": "jest",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
|
||||
"@mui/material": "^5.15.15",
|
||||
"@storybook/test": "^8.0.8",
|
||||
"i18next": "^23.11.1",
|
||||
"lucide-react": "^0.375.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^13.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^1.3.2",
|
||||
"@storybook/addon-essentials": "^8.0.8",
|
||||
"@storybook/addon-interactions": "^8.0.8",
|
||||
"@storybook/addon-links": "^8.0.8",
|
||||
"@storybook/addon-mdx-gfm": "^8.0.8",
|
||||
"@storybook/addon-onboarding": "^8.0.8",
|
||||
"@storybook/blocks": "^8.0.8",
|
||||
"@storybook/react": "^8.0.8",
|
||||
"@storybook/react-vite": "^8.0.8",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "^18.2.75",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"jest": "^29.7.0",
|
||||
"storybook": "^8.0.8",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#root {
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
border: 1px solid #AAA;
|
||||
border-radius: 4px;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import "./App.css";
|
||||
import Workbook from "./components/workbook";
|
||||
import "./i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import init, { Model } from "@ironcalc/wasm";
|
||||
import { WorkbookState } from "./components/workbookState";
|
||||
|
||||
function App() {
|
||||
const [model, setModel] = useState<Model | null>(null);
|
||||
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
|
||||
null
|
||||
);
|
||||
useEffect(() => {
|
||||
async function start() {
|
||||
await init();
|
||||
const model_bytes = new Uint8Array(await (await fetch("./example.ic")).arrayBuffer());
|
||||
const _model = Model.from_bytes(model_bytes);
|
||||
// const _model = new Model("en", "UTC");
|
||||
if (!model) setModel(_model);
|
||||
if (!workbookState) setWorkbookState(new WorkbookState());
|
||||
}
|
||||
start();
|
||||
}, []);
|
||||
|
||||
if (!model || !workbookState) {
|
||||
return <div>Loading</div>;
|
||||
}
|
||||
|
||||
// We could use context for model, but the problem is that it should initialized to null.
|
||||
// Passing the property down makes sure it is always defined.
|
||||
return (
|
||||
<Workbook model={model} workbookState={workbookState} />
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Keyboard and mouse events architecture
|
||||
|
||||
This document describes the architecture of the keyboard navigation and mouse events in IronCalc Web
|
||||
|
||||
There are two modes for mouse events:
|
||||
|
||||
* Normal mode: clicking a cell selects it, clicking on a sheet opens it
|
||||
* Browse mode: clicking on a cell updates the formula, etc
|
||||
|
||||
While in browse mode some mouse events might end the browse mode
|
||||
|
||||
We follow Excel's way of navigating a spreadsheet
|
||||
@@ -1,18 +0,0 @@
|
||||
export const headerCornerBackground = '#FFF';
|
||||
export const headerTextColor = '#333';
|
||||
export const headerBackground = '#FFF';
|
||||
export const headerGlobalSelectorColor = '#EAECF4';
|
||||
export const headerSelectedBackground = '#EEEEEE';
|
||||
export const headerFullSelectedBackground = '#D3D6E9';
|
||||
export const headerSelectedColor = '#333';
|
||||
export const headerBorderColor = '#DEE0EF';
|
||||
|
||||
export const gridColor = '#D3D6E9';
|
||||
export const gridSeparatorColor = '#D3D6E9';
|
||||
export const defaultTextColor = '#2E414D';
|
||||
|
||||
export const outlineColor = '#F2994A';
|
||||
export const outlineBackgroundColor = '#F2994A1A';
|
||||
|
||||
export const LAST_COLUMN = 16_384;
|
||||
export const LAST_ROW = 1_048_576;
|
||||
@@ -1,23 +0,0 @@
|
||||
export interface Cell {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface Area {
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
}
|
||||
|
||||
export interface SheetArea extends Area {
|
||||
sheet: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface AreaWithBorderInterface extends Area {
|
||||
border: "left" | "top" | "right" | "bottom";
|
||||
}
|
||||
|
||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
const letters = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
];
|
||||
interface Reference {
|
||||
row: number;
|
||||
column: number;
|
||||
absoluteRow: boolean;
|
||||
absoluteColumn: boolean;
|
||||
}
|
||||
|
||||
export function referenceToString(rf: Reference): string {
|
||||
const absC = rf.absoluteColumn ? '$' : '';
|
||||
const absR = rf.absoluteRow ? '$' : '';
|
||||
return absC + columnNameFromNumber(rf.column) + absR + rf.row;
|
||||
}
|
||||
|
||||
export function columnNameFromNumber(column: number): string {
|
||||
let columnName = '';
|
||||
let index = column;
|
||||
while (index > 0) {
|
||||
columnName = `${letters[(index - 1) % 26]}${columnName}`;
|
||||
index = Math.floor((index - 1) / 26);
|
||||
}
|
||||
return columnName;
|
||||
}
|
||||
|
||||
export function columnNumberFromName(columnName: string): number {
|
||||
let column = 0;
|
||||
for (const character of columnName) {
|
||||
const index = (character.codePointAt(0) ?? 0) - 64;
|
||||
column = column * 26 + index;
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
||||
// EqualTo Color Palette
|
||||
export function getColor(index: number, alpha = 1): string {
|
||||
const colors = [
|
||||
{
|
||||
name: 'Cyan',
|
||||
rgba: [89, 185, 188, 1],
|
||||
hex: '#59B9BC',
|
||||
},
|
||||
{
|
||||
name: 'Flamingo',
|
||||
rgba: [236, 87, 83, 1],
|
||||
hex: '#EC5753',
|
||||
},
|
||||
{
|
||||
hex: '#3358B7',
|
||||
rgba: [51, 88, 183, 1],
|
||||
name: 'Blue',
|
||||
},
|
||||
{
|
||||
hex: '#F8CD3C',
|
||||
rgba: [248, 205, 60, 1],
|
||||
name: 'Yellow',
|
||||
},
|
||||
{
|
||||
hex: '#3BB68A',
|
||||
rgba: [59, 182, 138, 1],
|
||||
name: 'Emerald',
|
||||
},
|
||||
{
|
||||
hex: '#523E93',
|
||||
rgba: [82, 62, 147, 1],
|
||||
name: 'Violet',
|
||||
},
|
||||
{
|
||||
hex: '#A23C52',
|
||||
rgba: [162, 60, 82, 1],
|
||||
name: 'Burgundy',
|
||||
},
|
||||
{
|
||||
hex: '#8CB354',
|
||||
rgba: [162, 60, 82, 1],
|
||||
name: 'Wasabi',
|
||||
},
|
||||
{
|
||||
hex: '#D03627',
|
||||
rgba: [208, 54, 39, 1],
|
||||
name: 'Red',
|
||||
},
|
||||
{
|
||||
hex: '#1B717E',
|
||||
rgba: [27, 113, 126, 1],
|
||||
name: 'Teal',
|
||||
},
|
||||
];
|
||||
if (alpha === 1) {
|
||||
return colors[index % 10].hex;
|
||||
}
|
||||
const { rgba } = colors[index % 10];
|
||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
||||
}
|
||||
|
||||
export function mergedAreas(area1: Area, area2: Area): Area {
|
||||
return {
|
||||
rowStart: Math.min(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
||||
rowEnd: Math.max(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
||||
columnStart: Math.min(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
||||
columnEnd: Math.max(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
||||
};
|
||||
}
|
||||
|
||||
export function getExpandToArea(area: Area, cell: Cell): AreaWithBorder {
|
||||
let { rowStart, rowEnd, columnStart, columnEnd } = area;
|
||||
if (rowStart > rowEnd) {
|
||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||
}
|
||||
if (columnStart > columnEnd) {
|
||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||
}
|
||||
const { row, column } = cell;
|
||||
if (row <= rowEnd && row >= rowStart && column >= columnStart && column <= columnEnd) {
|
||||
return null;
|
||||
}
|
||||
// Two rules:
|
||||
// * The extendTo area must be larger than the selected area
|
||||
// * The extendTo area must be of the same width or the same height as the selected area
|
||||
if (row >= rowEnd && column >= columnStart) {
|
||||
// Normal case: we are expanding down and right
|
||||
if (row - rowEnd > column - columnEnd) {
|
||||
// Expanding by rows (down)
|
||||
return {
|
||||
rowStart: rowEnd + 1,
|
||||
rowEnd: row,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'top',
|
||||
};
|
||||
}
|
||||
// expanding by columns (right)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: columnEnd + 1,
|
||||
columnEnd: column,
|
||||
border: 'left',
|
||||
};
|
||||
}
|
||||
if (row >= rowEnd && column <= columnStart) {
|
||||
// We are expanding down and left
|
||||
if (row - rowEnd > columnStart - column) {
|
||||
// Expanding by rows (down)
|
||||
return {
|
||||
rowStart: rowEnd + 1,
|
||||
rowEnd: row,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'top',
|
||||
};
|
||||
}
|
||||
// Expanding by columns (left)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: column,
|
||||
columnEnd: columnStart - 1,
|
||||
border: 'right',
|
||||
};
|
||||
}
|
||||
if (row <= rowEnd && column >= columnEnd) {
|
||||
// We are expanding up and right
|
||||
if (rowStart - row > column - columnEnd) {
|
||||
// Expanding by rows (up)
|
||||
return {
|
||||
rowStart: row,
|
||||
rowEnd: rowStart - 1,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'bottom',
|
||||
};
|
||||
}
|
||||
// Expanding by columns (right)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: columnEnd + 1,
|
||||
columnEnd: column,
|
||||
border: 'left',
|
||||
};
|
||||
}
|
||||
if (row <= rowEnd && column <= columnStart) {
|
||||
// We are expanding up and left
|
||||
if (rowStart - row > columnStart - column) {
|
||||
// Expanding by rows (up)
|
||||
return {
|
||||
rowStart: row,
|
||||
rowEnd: rowStart - 1,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'bottom',
|
||||
};
|
||||
}
|
||||
// Expanding by columns (left)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: column,
|
||||
columnEnd: columnStart - 1,
|
||||
border: 'right',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the keypress should start editing
|
||||
*/
|
||||
export function isEditingKey(key: string): boolean {
|
||||
if (key.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
const code = key.codePointAt(0) ?? 0;
|
||||
if (code > 0 && code < 255) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// / Common types
|
||||
|
||||
export interface Area {
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
}
|
||||
|
||||
interface AreaWithBorderInterface extends Area {
|
||||
border: 'left' | 'top' | 'right' | 'bottom';
|
||||
}
|
||||
|
||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
||||
|
||||
export interface Cell {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface ScrollPosition {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export interface StateSettings {
|
||||
selectedCell: Cell;
|
||||
selectedArea: Area;
|
||||
scrollPosition: ScrollPosition;
|
||||
extendToArea: AreaWithBorder;
|
||||
}
|
||||
|
||||
export type Dispatch<A> = (value: A) => void;
|
||||
export type SetStateAction<S> = S | ((prevState: S) => S);
|
||||
|
||||
export enum FocusType {
|
||||
Cell = 'cell',
|
||||
FormulaBar = 'formula-bar',
|
||||
}
|
||||
|
||||
/**
|
||||
* In Excel there are two "modes" of editing
|
||||
* * `init`: When you start typing in a cell. In this mode arrow keys will move away from the cell
|
||||
* * `edit`: If you double click on a cell or click in the cell while editing.
|
||||
* In this mode arrow keys will move within the cell.
|
||||
*
|
||||
* In a formula bar mode is always `edit`.
|
||||
*/
|
||||
export type CellEditMode = 'init' | 'edit';
|
||||
export interface CellEditingType {
|
||||
/**
|
||||
* ID of cell editing. Useful when one edit transforms into another and some code needs to run
|
||||
* when target changes.
|
||||
*
|
||||
* Due to problems with focus management (see #339) it's possible to start a new cell editing
|
||||
* without properly cleaning up previous one (lose focus in workbook, regain focus NOT in
|
||||
* the input and then use the keyboard.
|
||||
*/
|
||||
id: number;
|
||||
sheet: number;
|
||||
row: number;
|
||||
column: number;
|
||||
text: string;
|
||||
base: string;
|
||||
mode: CellEditMode;
|
||||
focus: FocusType;
|
||||
}
|
||||
|
||||
export type NavigationKey = 'ArrowRight' | 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';
|
||||
|
||||
export const isNavigationKey = (key: string): key is NavigationKey =>
|
||||
['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key);
|
||||
|
||||
function nameNeedsQuoting(name: string): boolean {
|
||||
const chars = [' ', '(', ')', "'", '$', ',', ';', '-', '+', '{', '}'];
|
||||
const l = chars.length;
|
||||
for (let index = 0; index < l; index += 1) {
|
||||
if (name.includes(chars[index])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME: We should use the function of a similar name in the rust code.
|
||||
export const quoteSheetName = (name: string): string => {
|
||||
if (nameNeedsQuoting(name)) {
|
||||
return `'${name.replace("'", "''")}'`;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
export function cellReprToRowColumn(cellRepr: string): { row: number; column: number } {
|
||||
let row = 0;
|
||||
let column = 0;
|
||||
for (const character of cellRepr) {
|
||||
if (Number.isNaN(Number.parseInt(character, 10))) {
|
||||
column *= 26;
|
||||
const characterCode = character.codePointAt(0);
|
||||
const ACharacterCode = 'A'.codePointAt(0);
|
||||
if (typeof characterCode === 'undefined' || typeof ACharacterCode === 'undefined') {
|
||||
throw new TypeError('Failed to find character code');
|
||||
}
|
||||
const deltaCodes = characterCode - ACharacterCode;
|
||||
if (deltaCodes < 0) {
|
||||
throw new Error('Incorrect character');
|
||||
}
|
||||
column += deltaCodes + 1;
|
||||
} else {
|
||||
row *= 10;
|
||||
row += Number.parseInt(character, 10);
|
||||
}
|
||||
}
|
||||
return { row, column };
|
||||
}
|
||||
|
||||
export const getMessageCellText = (
|
||||
cell: string,
|
||||
getMessageSheetNumber: (sheet: string) => number | undefined,
|
||||
getCellText?: (sheet: number, row: number, column: number) => string | undefined,
|
||||
) => {
|
||||
const messageMatch = /^=?(?<sheet>\w+)!(?<cell>\w+)/.exec(cell);
|
||||
if (messageMatch && messageMatch.groups) {
|
||||
const messageSheet = getMessageSheetNumber(messageMatch.groups.sheet);
|
||||
const dynamicIconCell = cellReprToRowColumn(messageMatch.groups.cell);
|
||||
if (messageSheet !== undefined && getCellText) {
|
||||
return getCellText(messageSheet, dynamicIconCell.row, dynamicIconCell.column) || '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getCellAddress = (selectedArea: Area, selectedCell?: Cell) => {
|
||||
const isSingleCell =
|
||||
selectedArea.rowStart === selectedArea.rowEnd &&
|
||||
selectedArea.columnEnd === selectedArea.columnStart;
|
||||
|
||||
return isSingleCell && selectedCell
|
||||
? `${columnNameFromNumber(selectedCell.column)}${selectedCell.row}`
|
||||
: `${columnNameFromNumber(selectedArea.columnStart)}${
|
||||
selectedArea.rowStart
|
||||
}:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`;
|
||||
};
|
||||
|
||||
export enum Border {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
Right = 'right',
|
||||
Left = 'left',
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,566 +0,0 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BorderBottomIcon,
|
||||
BorderCenterHIcon,
|
||||
BorderCenterVIcon,
|
||||
BorderInnerIcon,
|
||||
BorderLeftIcon,
|
||||
BorderOuterIcon,
|
||||
BorderRightIcon,
|
||||
BorderTopIcon,
|
||||
BorderNoneIcon,
|
||||
BorderStyleIcon,
|
||||
} from "../icons";
|
||||
import ColorPicker from "./colorPicker";
|
||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Grid2X2 as BorderAllIcon,
|
||||
PencilLine,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { theme } from "../theme";
|
||||
import { BorderOptions, BorderStyle, BorderType } from "@ironcalc/wasm";
|
||||
|
||||
type BorderPickerProps = {
|
||||
className?: string;
|
||||
onChange: (border: BorderOptions) => void;
|
||||
anchorEl: React.RefObject<HTMLElement>;
|
||||
anchorOrigin?: PopoverOrigin;
|
||||
transformOrigin?: PopoverOrigin;
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const BorderPicker = (properties: BorderPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [borderSelected, setBorderSelected] = useState(BorderType.None);
|
||||
const [borderColor, setBorderColor] = useState("#000000");
|
||||
const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin);
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
const [stylePickerOpen, setStylePickerOpen] = useState(false);
|
||||
const closePicker = (): void => {
|
||||
properties.onChange({
|
||||
color: borderColor,
|
||||
style: borderStyle,
|
||||
border: borderSelected,
|
||||
});
|
||||
};
|
||||
const borderColorButton = useRef(null);
|
||||
const borderStyleButton = useRef(null);
|
||||
return (
|
||||
<>
|
||||
<StyledPopover
|
||||
open={properties.open}
|
||||
onClose={(): void => closePicker()}
|
||||
anchorEl={properties.anchorEl.current}
|
||||
anchorOrigin={
|
||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
||||
}
|
||||
transformOrigin={
|
||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
||||
}
|
||||
>
|
||||
<BorderPickerDialog>
|
||||
<Borders>
|
||||
<Line>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.All}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.All) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.All);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderAllIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.Inner}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.Inner) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.Inner);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderInnerIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.CenterH}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.CenterH) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.CenterH);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderCenterHIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.CenterV}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.CenterV) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.CenterV);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderCenterVIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.Outer}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.Outer) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.Outer);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderOuterIcon />
|
||||
</Button>
|
||||
</Line>
|
||||
<Line>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.None}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.None) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.None);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderNoneIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.Top}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.Top) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.Top);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderTopIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.Right}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.Right) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.Right);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderRightIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.Bottom}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.Bottom) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.Bottom);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderBottomIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.Left}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.Left) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.Left);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderLeftIcon />
|
||||
</Button>
|
||||
</Line>
|
||||
</Borders>
|
||||
<Divider />
|
||||
<Styles>
|
||||
<ButtonWrapper onClick={() => setColorPickerOpen(true)}>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={false}
|
||||
ref={borderColorButton}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<PencilLine />
|
||||
</Button>
|
||||
<div style={{flexGrow:2}}>Border color</div>
|
||||
<ChevronRightStyled />
|
||||
</ButtonWrapper>
|
||||
<ButtonWrapper onClick={() => setStylePickerOpen(true)} ref={borderStyleButton}>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderStyleIcon />
|
||||
</Button>
|
||||
<div style={{flexGrow:2}}>Border style</div>
|
||||
<ChevronRightStyled />
|
||||
</ButtonWrapper>
|
||||
</Styles>
|
||||
</BorderPickerDialog>
|
||||
<ColorPicker
|
||||
color={borderColor}
|
||||
onChange={(color): void => {
|
||||
setBorderColor(color);
|
||||
setColorPickerOpen(false);
|
||||
}}
|
||||
anchorEl={borderColorButton}
|
||||
open={colorPickerOpen}
|
||||
/>
|
||||
<StyledPopover
|
||||
open={stylePickerOpen}
|
||||
onClose={(): void => {
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
anchorEl={borderStyleButton.current}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: 38, horizontal: -6 }}
|
||||
>
|
||||
<BorderStyleDialog>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dashed);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.None}
|
||||
>
|
||||
<BorderDescription>None</BorderDescription>
|
||||
<NoneLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Thin);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Thin}
|
||||
>
|
||||
<BorderDescription>Thin</BorderDescription>
|
||||
<SolidLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Medium);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Medium}
|
||||
>
|
||||
<BorderDescription>Medium</BorderDescription>
|
||||
<MediumLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Thick);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Thick}
|
||||
>
|
||||
<BorderDescription>Thick</BorderDescription>
|
||||
<ThickLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dotted);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Dotted}
|
||||
>
|
||||
<BorderDescription>Dotted</BorderDescription>
|
||||
<DottedLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dashed);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Dashed}
|
||||
>
|
||||
<BorderDescription>Dashed</BorderDescription>
|
||||
<DashedLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dashed);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Double}
|
||||
>
|
||||
<BorderDescription>Double</BorderDescription>
|
||||
<DoubleLine />
|
||||
</LineWrapper>
|
||||
</BorderStyleDialog>
|
||||
</StyledPopover>
|
||||
</StyledPopover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type LineWrapperProperties = { $checked: boolean };
|
||||
const LineWrapper = styled("div")<LineWrapperProperties>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: ${({ $checked }): string => {
|
||||
if ($checked) {
|
||||
return '#EEEEEE;';
|
||||
} else {
|
||||
return 'inherit;';
|
||||
}
|
||||
}};
|
||||
&:hover {
|
||||
border: 1px solid #EEEEEE;
|
||||
}
|
||||
padding:8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid white;
|
||||
`;
|
||||
|
||||
const CheckIconWrapper = styled("div")`
|
||||
width: 12px;
|
||||
`;
|
||||
|
||||
type CheckIconProperties = { $checked: boolean };
|
||||
const CheckIcon = styled("div")<CheckIconProperties>`
|
||||
width: 2px;
|
||||
background-color: #EEE;
|
||||
height: 28px;
|
||||
visibility: ${({ $checked }): string => {
|
||||
if ($checked) {
|
||||
return "visible";
|
||||
}
|
||||
return "hidden";
|
||||
}};
|
||||
`;
|
||||
const NoneLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px solid #E0E0E0;
|
||||
`;
|
||||
const SolidLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px solid #333333;
|
||||
`;
|
||||
const MediumLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 2px solid #333333;
|
||||
`;
|
||||
const ThickLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 3px solid #333333;
|
||||
`;
|
||||
const DashedLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px dashed #333333;
|
||||
`;
|
||||
const DottedLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px dotted #333333;
|
||||
`;
|
||||
const DoubleLine = styled('div')`
|
||||
width: 68px;
|
||||
border-top: 3px double #333333;
|
||||
`;
|
||||
|
||||
const Divider = styled("div")`
|
||||
display: inline-flex;
|
||||
heigh: 1px;
|
||||
border-bottom: 1px solid #EEE;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
`;
|
||||
|
||||
const Borders = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 4px;
|
||||
`;
|
||||
|
||||
const Styles = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Line = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background-color: #EEE;
|
||||
border-top-color: ${(): string => theme.palette.grey["400"]};
|
||||
}
|
||||
cursor: pointer;
|
||||
padding: 8px
|
||||
`;
|
||||
|
||||
const BorderStyleDialog = styled("div")`
|
||||
background: ${({ theme }): string => theme.palette.background.default};
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledPopover = styled(Popover)`
|
||||
.MuiPopover-paper {
|
||||
border-radius: 10px;
|
||||
border: 0px solid ${({ theme }): string => theme.palette.background.default};
|
||||
box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
||||
}
|
||||
.MuiPopover-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
.MuiList-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
const BorderPickerDialog = styled("div")`
|
||||
background: ${({ theme }): string => theme.palette.background.default};
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const BorderDescription = styled("div")`
|
||||
width: 70px;
|
||||
`;
|
||||
|
||||
// type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
||||
// const Button = styled.button<TypeButtonProperties>`
|
||||
// width: 23px;
|
||||
// height: 23px;
|
||||
// display: inline-flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// font-size: 14px;
|
||||
// border-radius: 2px;
|
||||
// margin-right: 5px;
|
||||
// transition: all 0.2s;
|
||||
|
||||
// ${({ theme, disabled, $pressed, $underlinedColor }): string => {
|
||||
// if (disabled) {
|
||||
// return `
|
||||
// color: ${theme.palette.grey['600']};
|
||||
// cursor: default;
|
||||
// `;
|
||||
// }
|
||||
// return `
|
||||
// border-top: ${$underlinedColor ? '3px solid #FFF' : 'none'};
|
||||
// border-bottom: ${$underlinedColor ? `3px solid ${$underlinedColor}` : 'none'};
|
||||
// color: ${theme.palette.text.primary};
|
||||
// background-color: ${$pressed ? theme.palette.grey['600'] : '#FFF'};
|
||||
// &:hover {
|
||||
// background-color: ${theme.palette.grey['400']};
|
||||
// border-top-color: ${theme.palette.grey['400']};
|
||||
// }
|
||||
// `;
|
||||
// }}
|
||||
// `;
|
||||
|
||||
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
||||
const Button = styled("button")<TypeButtonProperties>(
|
||||
({ disabled, $pressed, $underlinedColor }) => {
|
||||
let result: Record<string, any> = {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// fontSize: "26px",
|
||||
border: "0px solid #fff",
|
||||
borderRadius: "2px",
|
||||
marginRight: "5px",
|
||||
transition: "all 0.2s",
|
||||
cursor: "pointer",
|
||||
padding: "0px",
|
||||
};
|
||||
if (disabled) {
|
||||
result.color = theme.palette.grey["600"];
|
||||
result.cursor = "default";
|
||||
} else {
|
||||
result.borderTop = $underlinedColor ? "3px solid #FFF" : "none";
|
||||
result.borderBottom = $underlinedColor
|
||||
? `3px solid ${$underlinedColor}`
|
||||
: "none";
|
||||
(result.color = "#21243A"),
|
||||
(result.backgroundColor = $pressed
|
||||
? theme.palette.grey["600"]
|
||||
: "inherit");
|
||||
result["&:hover"] = {
|
||||
backgroundColor: "#F1F2F8",
|
||||
borderTopColor: "#F1F2F8",
|
||||
};
|
||||
result["svg"] = {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
const ChevronRightStyled = styled(ChevronRight)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`;
|
||||
|
||||
export default BorderPicker;
|
||||
@@ -1,262 +0,0 @@
|
||||
import styled from "@emotion/styled";
|
||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
import { theme } from "../theme";
|
||||
|
||||
type ColorPickerProps = {
|
||||
className?: string;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
anchorEl: React.RefObject<HTMLElement>;
|
||||
anchorOrigin?: PopoverOrigin;
|
||||
transformOrigin?: PopoverOrigin;
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const colorPickerWidth = 240;
|
||||
const colorfulHeight = 185; // 150 + 15 + 20
|
||||
|
||||
const ColorPicker = (properties: ColorPickerProps) => {
|
||||
const [color, setColor] = useState<string>(properties.color);
|
||||
const recentColors = useRef<string[]>([]);
|
||||
|
||||
const closePicker = (newColor: string): void => {
|
||||
const maxRecentColors = 14;
|
||||
properties.onChange(newColor);
|
||||
const colors = recentColors.current.filter((c) => c !== newColor);
|
||||
recentColors.current = [newColor, ...colors].slice(0, maxRecentColors);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setColor(properties.color);
|
||||
}, [properties.color]);
|
||||
|
||||
const presetColors = [
|
||||
"#FFFFFF",
|
||||
"#1B717E",
|
||||
"#59B9BC",
|
||||
"#3BB68A",
|
||||
"#8CB354",
|
||||
"#F8CD3C",
|
||||
"#EC5753",
|
||||
"#A23C52",
|
||||
"#D03627",
|
||||
"#523E93",
|
||||
"#3358B7",
|
||||
];
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={properties.open}
|
||||
onClose={(): void => closePicker(color)}
|
||||
anchorEl={properties.anchorEl.current}
|
||||
anchorOrigin={
|
||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
||||
}
|
||||
transformOrigin={
|
||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
||||
}
|
||||
>
|
||||
<ColorPickerDialog>
|
||||
<HexColorPicker
|
||||
color={color}
|
||||
onChange={(newColor): void => {
|
||||
setColor(newColor);
|
||||
}}
|
||||
/>
|
||||
<ColorPickerInput>
|
||||
<HexWrapper>
|
||||
<HexLabel>{"Hex"}</HexLabel>
|
||||
<HexColorInputBox>
|
||||
<HashLabel>{"#"}</HashLabel>
|
||||
<HexColorInput
|
||||
color={color}
|
||||
onChange={(newColor): void => {
|
||||
setColor(newColor);
|
||||
}}
|
||||
/>
|
||||
</HexColorInputBox>
|
||||
</HexWrapper>
|
||||
<Swatch $color={color} onClick={(): void => {
|
||||
closePicker(color);
|
||||
}} />
|
||||
</ColorPickerInput>
|
||||
<HorizontalDivider />
|
||||
<ColorList>
|
||||
{presetColors.map((presetColor) => (
|
||||
<Button
|
||||
key={presetColor}
|
||||
$color={presetColor}
|
||||
onClick={(): void => {
|
||||
closePicker(presetColor);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ColorList>
|
||||
<HorizontalDivider />
|
||||
<RecentLabel>{"Recent"}</RecentLabel>
|
||||
<ColorList>
|
||||
{recentColors.current.map((recentColor) => (
|
||||
<Button
|
||||
key={recentColor}
|
||||
$color={recentColor}
|
||||
onClick={(): void => {
|
||||
closePicker(recentColor);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ColorList>
|
||||
</ColorPickerDialog>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const RecentLabel = styled.div`
|
||||
font-size: 12px;
|
||||
color: ${theme.palette.text.secondary};
|
||||
`;
|
||||
|
||||
const ColorList = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const Button = styled.button<{ $color: string }>`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
${({ $color }): string => {
|
||||
if ($color.toUpperCase() === "#FFFFFF") {
|
||||
return `border: 1px solid ${theme.palette.grey["600"]};`;
|
||||
}
|
||||
return `border: 1px solid ${$color};`;
|
||||
}}
|
||||
background-color: ${({ $color }): string => {
|
||||
return $color;
|
||||
}};
|
||||
box-sizing: border-box;
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 2px;
|
||||
`;
|
||||
|
||||
const HorizontalDivider = styled.div`
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
border-top: 1px solid ${theme.palette.grey["400"]};
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
// const StyledPopover = styled(Popover)`
|
||||
// .MuiPopover-paper {
|
||||
// border-radius: 10px;
|
||||
// border: 0px solid ${theme.palette.background.default};
|
||||
// box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
||||
// }
|
||||
// .MuiPopover-padding {
|
||||
// padding: 0px;
|
||||
// }
|
||||
// .MuiList-padding {
|
||||
// padding: 0px;
|
||||
// }
|
||||
// `;
|
||||
|
||||
const ColorPickerDialog = styled.div`
|
||||
background: ${theme.palette.background.default};
|
||||
width: ${colorPickerWidth}px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& .react-colorful {
|
||||
height: ${colorfulHeight}px;
|
||||
width: ${colorPickerWidth}px;
|
||||
}
|
||||
& .react-colorful__saturation {
|
||||
border-bottom: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
& .react-colorful__hue {
|
||||
height: 20px;
|
||||
margin-top: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
& .react-colorful__saturation-pointer {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
& .react-colorful__hue-pointer {
|
||||
width: 7px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HashLabel = styled.div`
|
||||
margin: auto 0px auto 10px;
|
||||
font-size: 13px;
|
||||
color: #7d8ec2;
|
||||
font-family: ${theme.typography.button.fontFamily};
|
||||
`;
|
||||
|
||||
const HexLabel = styled.div`
|
||||
margin: auto 10px auto 0px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
font-family: ${theme.typography.button.fontFamily};
|
||||
`;
|
||||
|
||||
const HexColorInputBox = styled.div`
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
width: 140px;
|
||||
height: 28px;
|
||||
border: 1px solid ${theme.palette.grey["600"]};
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
const HexWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
& input {
|
||||
min-width: 0px;
|
||||
border: 0px;
|
||||
background: ${theme.palette.background.default};
|
||||
outline: none;
|
||||
font-family: ${theme.typography.button.fontFamily};
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
& input:focus {
|
||||
border-color: #4298ef;
|
||||
}
|
||||
`;
|
||||
|
||||
const Swatch = styled.div<{ $color: string }>`
|
||||
display: inline-flex;
|
||||
${({ $color }): string => {
|
||||
if ($color.toUpperCase() === "#FFFFFF") {
|
||||
return `border: 1px solid ${theme.palette.grey["600"]};`;
|
||||
}
|
||||
return `border: 1px solid ${$color};`;
|
||||
}}
|
||||
background-color: ${({ $color }): string => $color};
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
const ColorPickerInput = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
`;
|
||||
|
||||
export default ColorPicker;
|
||||
@@ -1,420 +0,0 @@
|
||||
import {
|
||||
CSSProperties,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
KeyboardEvent,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { useRef } from "react";
|
||||
import EditorContext, { Area } from "./editorContext";
|
||||
import { getStringRange } from "./util";
|
||||
|
||||
/**
|
||||
* This is the Cell Editor for IronCalc
|
||||
* I uses a transparent textarea and a styled mask. What you see is the HTML styled content of the mask
|
||||
* and the caret from the textarea. The alternative would be to have a 'contenteditable' div.
|
||||
* That turns out to be a much more difficult implementation.
|
||||
*
|
||||
* The editor grows horizontally with text if it fits in the screen.
|
||||
* If it doesn't fit, it wraps and grows vertically. If it doesn't fit vertically it scrolls.
|
||||
*
|
||||
* Many keyboard and mouse events are handled gracefully by the textarea in full or in part.
|
||||
* For example letter key strokes like 'q' or '1' are handled full by the textarea.
|
||||
* Some keyboard events like "RightArrow" might need to be handled separately and let them bubble up,
|
||||
* or might be handled by the textarea, depending on the "editor mode".
|
||||
* Some other like "Enter" we need to intercept and change the normal behaviour.
|
||||
*/
|
||||
|
||||
const commonCSS: CSSProperties = {
|
||||
fontWeight: "inherit",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "inherit",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
whiteSpace: "pre",
|
||||
width: "100%",
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
interface Cell {
|
||||
sheet: number;
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
interface EditorOptions {
|
||||
minimalWidth: number;
|
||||
minimalHeight: number;
|
||||
textColor: string;
|
||||
originalText: string;
|
||||
getStyledText: (
|
||||
text: string,
|
||||
insertRangeText: string
|
||||
) => {
|
||||
html: JSX.Element[];
|
||||
isInReferenceMode: boolean;
|
||||
};
|
||||
onEditEnd: (text: string) => void;
|
||||
display: boolean;
|
||||
cell: Cell;
|
||||
sheetNames: string[];
|
||||
}
|
||||
|
||||
// You can either be editing a formula or content.
|
||||
// When editing content (behaviour is common to Excel and Google Sheets):
|
||||
// * If you start editing by typing you are in *accept* mode
|
||||
// * If you start editing by F2 you are in *cruise* mode
|
||||
// * If you start editing by double click you are in *cruise* mode
|
||||
// In Google Sheets "Enter" starts editing and puts you in *cruise* mode. We do not do that
|
||||
// Once you are in cruise mode it is not possible to switch to accept mode
|
||||
// The only way to go from accept mode to cruise mode is clicking in the content somewhere
|
||||
|
||||
// When editing a formula.
|
||||
// In Google Sheets you are either in insert mode or cruise mode.
|
||||
// You can get back to accept mode if you delete the whole formula
|
||||
// In Excel you can be either in insert or accept but if you click in the formula body
|
||||
// you switch to cruise mode. Once in cruise mode you can go to insert mode by selecting a range.
|
||||
// Then you are back in accept/insert modes
|
||||
|
||||
const Editor = (options: EditorOptions) => {
|
||||
const {
|
||||
minimalWidth,
|
||||
minimalHeight,
|
||||
textColor,
|
||||
onEditEnd,
|
||||
originalText,
|
||||
display,
|
||||
cell,
|
||||
sheetNames,
|
||||
} = options;
|
||||
|
||||
const [width, setWidth] = useState(minimalWidth);
|
||||
const [height, setHeight] = useState(minimalHeight);
|
||||
|
||||
const { editorContext, setEditorContext } = useContext(EditorContext);
|
||||
|
||||
const setBaseText = (newText: string) => {
|
||||
console.log('Calling setBaseText');
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
baseText: newText,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const insertRangeText = editorContext.insertRange
|
||||
? getStringRange(editorContext.insertRange, sheetNames)
|
||||
: "";
|
||||
|
||||
const baseText = editorContext.baseText;
|
||||
const text = baseText + insertRangeText;
|
||||
// console.log('baseText', baseText, 'insertRange:', insertRangeText);
|
||||
|
||||
const formulaRef = useRef<HTMLDivElement>(null);
|
||||
const maskRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// setBaseText(originalText);
|
||||
// }, [cell]);
|
||||
|
||||
const { html: styledFormula, isInReferenceMode } = options.getStyledText(
|
||||
baseText,
|
||||
insertRangeText
|
||||
);
|
||||
|
||||
if (display && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (formulaRef.current) {
|
||||
const scrollWidth = formulaRef.current.scrollWidth;
|
||||
if (scrollWidth > width) {
|
||||
setWidth(scrollWidth);
|
||||
} else if (scrollWidth <= minimalWidth) {
|
||||
setWidth(minimalWidth);
|
||||
}
|
||||
const scrollHeight = formulaRef.current.scrollHeight;
|
||||
if (scrollHeight > height) {
|
||||
setHeight(scrollHeight);
|
||||
}
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInReferenceMode) {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
mode: "insert",
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
mode: "cruise",
|
||||
insertRange: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [isInReferenceMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (display && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, [display]);
|
||||
|
||||
// console.log("Ok, this is running", text, editorContext.id);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const { key, shiftKey, altKey } = event;
|
||||
const textarea = textareaRef.current;
|
||||
const mode = editorContext.mode;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case "Enter": {
|
||||
if (altKey) {
|
||||
// new line
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const newText = text.slice(0, start) + "\n" + text.slice(end);
|
||||
setBaseText(newText);
|
||||
setTimeout(() => {
|
||||
textarea.setSelectionRange(start + 1, start + 1);
|
||||
}, 1);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else {
|
||||
// end edit
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
// event bubbles up
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
setBaseText(originalText);
|
||||
textarea.blur();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "ArrowLeft": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
// event bubbles up
|
||||
return;
|
||||
} else if (mode == "insert") {
|
||||
if (shiftKey) {
|
||||
// increase the inserted range to the left
|
||||
if (!editorContext.insertRange) {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
insertRange: {
|
||||
absoluteColumnEnd: false,
|
||||
absoluteColumnStart: false,
|
||||
absoluteRowEnd: false,
|
||||
absoluteRowStart: false,
|
||||
sheet: cell.sheet,
|
||||
rowStart: cell.row,
|
||||
rowEnd: cell.row,
|
||||
columnStart: cell.column,
|
||||
columnEnd: cell.column,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// const r = insertRage;
|
||||
// r.columnStart = Math.max(r.columnStart - 1, 1);
|
||||
// setInsertRange(r);
|
||||
}
|
||||
} else {
|
||||
// move inserted cell to the left
|
||||
if (!editorContext.insertRange) {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
insertRange: {
|
||||
absoluteColumnEnd: false,
|
||||
absoluteColumnStart: false,
|
||||
absoluteRowEnd: false,
|
||||
absoluteRowStart: false,
|
||||
sheet: cell.sheet,
|
||||
rowStart: cell.row,
|
||||
rowEnd: cell.row,
|
||||
columnStart: cell.column,
|
||||
columnEnd: cell.column,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setEditorContext((c) => {
|
||||
const range = c.insertRange as Area;
|
||||
const row = range.rowStart;
|
||||
let column = range.columnStart - 1;
|
||||
if (column < 1) {
|
||||
column = 1;
|
||||
}
|
||||
return {
|
||||
...c,
|
||||
insertRange: {
|
||||
absoluteColumnEnd: false,
|
||||
absoluteColumnStart: false,
|
||||
absoluteRowEnd: false,
|
||||
absoluteRowStart: false,
|
||||
sheet: range.sheet,
|
||||
rowStart: row,
|
||||
rowEnd: row,
|
||||
columnStart: column,
|
||||
columnEnd: column,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
// We don't do anything in "cruise mode" and rely on the textarea default behaviour
|
||||
break;
|
||||
}
|
||||
case "ArrowDown": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowRight": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Tab": {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
// event bubbles up
|
||||
}
|
||||
}
|
||||
if (editorContext.mode === "insert") {
|
||||
setBaseText(text);
|
||||
setEditorContext((context) => {
|
||||
return {
|
||||
...context,
|
||||
mode: "cruise",
|
||||
insertRange: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
[text, editorContext]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
overflow: "hidden",
|
||||
background: "#FFF",
|
||||
display: display ? "block" : "none",
|
||||
}}
|
||||
onClick={(_event) => {
|
||||
console.log("Click on wrapper");
|
||||
}}
|
||||
onPointerDown={() => {
|
||||
console.log("On pointer down wrapper");
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={maskRef}
|
||||
style={{
|
||||
...commonCSS,
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
height,
|
||||
}}
|
||||
onClick={(_event) => {
|
||||
console.log("Click on mask");
|
||||
}}
|
||||
onPointerDown={() => {
|
||||
console.log("On pointer down mask");
|
||||
}}
|
||||
>
|
||||
<div ref={formulaRef}>{styledFormula}</div>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
style={{
|
||||
...commonCSS,
|
||||
color: "transparent",
|
||||
backgroundColor: "transparent",
|
||||
caretColor: textColor,
|
||||
outline: "none",
|
||||
resize: "none",
|
||||
border: "none",
|
||||
height,
|
||||
}}
|
||||
spellCheck="false"
|
||||
value={text}
|
||||
onChange={(event) => {
|
||||
console.log("onChange", event.target.value);
|
||||
setBaseText(event.target.value);
|
||||
}}
|
||||
onScroll={() => {
|
||||
if (maskRef.current && textareaRef.current) {
|
||||
maskRef.current.style.left = `-${textareaRef.current.scrollLeft}px`;
|
||||
maskRef.current.style.top = `-${textareaRef.current.scrollTop}px`;
|
||||
}
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
onClick={(event) => {
|
||||
console.log("Setting mode");
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
mode: "cruise",
|
||||
};
|
||||
});
|
||||
console.log("here");
|
||||
// if (display) {
|
||||
event.stopPropagation();
|
||||
// }
|
||||
}}
|
||||
onBlur={() => {
|
||||
// on blur
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
></textarea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { Dispatch, SetStateAction, createContext } from "react";
|
||||
|
||||
export interface Area {
|
||||
sheet: number | null;
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
absoluteRowStart: boolean;
|
||||
absoluteRowEnd: boolean;
|
||||
absoluteColumnStart: boolean;
|
||||
absoluteColumnEnd: boolean;
|
||||
}
|
||||
|
||||
// Arrow keys behave in different ways depending on the "edit mode":
|
||||
// * In _cruise_ mode arrowy keys navigate within the editor
|
||||
// * In _accept_ mode pressing an arrow key will end editing
|
||||
// * In _insert_ mode arrow keys will change the selected range
|
||||
export type EditorMode = "cruise" | "accept" | "insert";
|
||||
|
||||
export interface EditorState {
|
||||
mode: EditorMode;
|
||||
insertRange: null | Area;
|
||||
baseText: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface EditorContextType {
|
||||
editorContext: EditorState;
|
||||
setEditorContext: Dispatch<
|
||||
SetStateAction<{ mode: EditorMode; insertRange: null | Area }>
|
||||
>;
|
||||
}
|
||||
|
||||
const EditorContext = createContext<EditorContextType>({
|
||||
editorContext: {
|
||||
mode: "accept",
|
||||
insertRange: null,
|
||||
baseText: '',
|
||||
id: Math.floor(Math.random()*1000),
|
||||
},
|
||||
setEditorContext: () => {},
|
||||
});
|
||||
|
||||
export default EditorContext;
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default } from './editor';
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
type ErrorType =
|
||||
| 'REF'
|
||||
| 'NAME'
|
||||
| 'VALUE'
|
||||
| 'DIV'
|
||||
| 'NA'
|
||||
| 'NUM'
|
||||
| 'ERROR'
|
||||
| 'NIMPL'
|
||||
| 'SPILL'
|
||||
| 'CALC'
|
||||
| 'CIRC';
|
||||
|
||||
type OpCompareType =
|
||||
| 'LessThan'
|
||||
| 'GreaterThan'
|
||||
| 'Equal'
|
||||
| 'LessOrEqualThan'
|
||||
| 'GreaterOrEqualThan'
|
||||
| 'NonEqual';
|
||||
|
||||
type OpSumType = 'Add' | 'Minus';
|
||||
|
||||
type OpProductType = 'Times' | 'Divide';
|
||||
|
||||
interface ReferenceType {
|
||||
sheet: string | null;
|
||||
row: number;
|
||||
column: number;
|
||||
absolute_column: boolean;
|
||||
absolute_row: boolean;
|
||||
}
|
||||
|
||||
interface ParsedReferenceType {
|
||||
column: number;
|
||||
row: number;
|
||||
absolute_column: boolean;
|
||||
absolute_row: boolean;
|
||||
}
|
||||
|
||||
interface Reference {
|
||||
Reference: ReferenceType;
|
||||
}
|
||||
|
||||
interface Range {
|
||||
Range: {
|
||||
sheet: string | null;
|
||||
left: ParsedReferenceType;
|
||||
right: ParsedReferenceType;
|
||||
};
|
||||
}
|
||||
|
||||
export type TokenType =
|
||||
| 'Illegal'
|
||||
| 'Eof'
|
||||
| { Ident: string }
|
||||
| { String: string }
|
||||
| { Boolean: boolean }
|
||||
| { Number: number }
|
||||
| { ERROR: ErrorType }
|
||||
| { COMPARE: OpCompareType }
|
||||
| { SUM: OpSumType }
|
||||
| { PRODUCT: OpProductType }
|
||||
| 'POWER'
|
||||
| 'LPAREN'
|
||||
| 'RPAREN'
|
||||
| 'COLON'
|
||||
| 'SEMICOLON'
|
||||
| 'LBRACKET'
|
||||
| 'RBRACKET'
|
||||
| 'LBRACE'
|
||||
| 'RBRACE'
|
||||
| 'COMMA'
|
||||
| 'BANG'
|
||||
| 'PERCENT'
|
||||
| 'AND'
|
||||
| Reference
|
||||
| Range;
|
||||
|
||||
export interface MarkedToken {
|
||||
token: TokenType;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export function tokenIsReferenceType(token: TokenType): token is Reference {
|
||||
return typeof token === 'object' && 'Reference' in token;
|
||||
}
|
||||
|
||||
export function tokenIsRangeType(token: TokenType): token is Range {
|
||||
return typeof token === 'object' && 'Range' in token;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import { useCallback, KeyboardEvent } from "react";
|
||||
import { WorkbookState } from "../workbookState";
|
||||
import { Model } from "@ironcalc/wasm";
|
||||
|
||||
interface Options {
|
||||
// onMoveCaretToStart: () => void;
|
||||
// onMoveCaretToEnd: () => void;
|
||||
// onEditEnd: (delta: { deltaRow: number; deltaColumn: number }) => void;
|
||||
// onEditEscape: () => void;
|
||||
// onReferenceCycle: () => void;
|
||||
// text: string;
|
||||
// setText: (text: string) => void;
|
||||
model: Model;
|
||||
state: WorkbookState;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
const useEditorKeydown = (
|
||||
options: Options
|
||||
): {
|
||||
onKeyDown: (event: KeyboardEvent) => void;
|
||||
} => {
|
||||
const { state, model } = options;
|
||||
const onKeyDown = useCallback((event: KeyboardEvent) => {
|
||||
const { key, shiftKey } = event;
|
||||
const { mode, text } = state.getEditor() ?? { mode: "init", text: "" };
|
||||
switch (key) {
|
||||
// case "Enter":
|
||||
// // options.onEditEnd({ deltaRow: 1, deltaColumn: 0 });
|
||||
// const { row, column } = state.getSelectedCell();
|
||||
// const sheet = state.getSelectedSheet();
|
||||
// model.setUserInput(sheet, row, column, text);
|
||||
// state.selectCell({ row: row + 1, column });
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// options.refresh();
|
||||
// break;
|
||||
// case 'ArrowUp': {
|
||||
// if (mode === 'init') {
|
||||
// options.onEditEnd({ deltaRow: -1, deltaColumn: 0 });
|
||||
// } else {
|
||||
// options.onMoveCaretToStart();
|
||||
// }
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// break;
|
||||
// }
|
||||
// case 'ArrowDown': {
|
||||
// if (mode === 'init') {
|
||||
// options.onEditEnd({ deltaRow: 1, deltaColumn: 0 });
|
||||
// } else {
|
||||
// options.onMoveCaretToEnd();
|
||||
// }
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// break;
|
||||
// }
|
||||
// case 'Tab': {
|
||||
// if (event.shiftKey) {
|
||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: -1 });
|
||||
// } else {
|
||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: 1 });
|
||||
// }
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
|
||||
// break;
|
||||
// }
|
||||
// case 'Escape': {
|
||||
// options.onEditEscape();
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
|
||||
// break;
|
||||
// }
|
||||
// case 'ArrowLeft': {
|
||||
// if (mode === 'init') {
|
||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: -1 });
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// }
|
||||
|
||||
// break;
|
||||
// }
|
||||
// case 'ArrowRight': {
|
||||
// if (mode === 'init') {
|
||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: 1 });
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// }
|
||||
|
||||
// break;
|
||||
// }
|
||||
// case 'F4': {
|
||||
// options.onReferenceCycle();
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
|
||||
// break;
|
||||
// }
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [model, state]);
|
||||
return { onKeyDown };
|
||||
};
|
||||
|
||||
export default useEditorKeydown;
|
||||
@@ -1,334 +0,0 @@
|
||||
import { getTokens } from "@ironcalc/wasm";
|
||||
import { tokenIsRangeType, tokenIsReferenceType } from "./tokenTypes";
|
||||
import { Area } from "./editorContext";
|
||||
|
||||
const letters = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
];
|
||||
interface Reference {
|
||||
row: number;
|
||||
column: number;
|
||||
absoluteRow: boolean;
|
||||
absoluteColumn: boolean;
|
||||
}
|
||||
|
||||
export function referenceToString(rf: Reference): string {
|
||||
const absC = rf.absoluteColumn ? "$" : "";
|
||||
const absR = rf.absoluteRow ? "$" : "";
|
||||
return absC + columnNameFromNumber(rf.column) + absR + rf.row;
|
||||
}
|
||||
|
||||
export function columnNameFromNumber(column: number): string {
|
||||
let columnName = "";
|
||||
let index = column;
|
||||
while (index > 0) {
|
||||
columnName = `${letters[(index - 1) % 26]}${columnName}`;
|
||||
index = Math.floor((index - 1) / 26);
|
||||
}
|
||||
return columnName;
|
||||
}
|
||||
|
||||
export function columnNumberFromName(columnName: string): number {
|
||||
let column = 0;
|
||||
for (const character of columnName) {
|
||||
const index = (character.codePointAt(0) ?? 0) - 64;
|
||||
column = column * 26 + index;
|
||||
}
|
||||
return column;
|
||||
}
|
||||
interface Range {
|
||||
sheet: number | null;
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
absoluteRowStart: boolean;
|
||||
absoluteRowEnd: boolean;
|
||||
absoluteColumnStart: boolean;
|
||||
absoluteColumnEnd: boolean;
|
||||
}
|
||||
|
||||
export function getStringRange(range: Range, sheetNames: string[]) {
|
||||
const name = range.sheet ? `${sheetNames[range.sheet]}!` : "";
|
||||
const left = referenceToString({
|
||||
row: range.rowStart,
|
||||
column: range.columnStart,
|
||||
absoluteRow: range.absoluteRowStart,
|
||||
absoluteColumn: range.absoluteColumnStart,
|
||||
});
|
||||
if (
|
||||
range.rowStart === range.rowEnd &&
|
||||
range.columnStart === range.columnEnd
|
||||
) {
|
||||
return `${name}${left}`;
|
||||
}
|
||||
const right = referenceToString({
|
||||
row: range.rowEnd,
|
||||
column: range.columnEnd,
|
||||
absoluteRow: range.absoluteRowEnd,
|
||||
absoluteColumn: range.absoluteColumnEnd,
|
||||
});
|
||||
return `${name}${left}:${right}`;
|
||||
}
|
||||
|
||||
interface ActiveRange {
|
||||
sheet: number;
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// IronCalc Color Palette
|
||||
export function getColor(index: number, alpha = 1): string {
|
||||
const colors = [
|
||||
{
|
||||
name: "Cyan",
|
||||
rgba: [89, 185, 188, 1],
|
||||
hex: "#59B9BC",
|
||||
},
|
||||
{
|
||||
name: "Flamingo",
|
||||
rgba: [236, 87, 83, 1],
|
||||
hex: "#EC5753",
|
||||
},
|
||||
{
|
||||
hex: "#3358B7",
|
||||
rgba: [51, 88, 183, 1],
|
||||
name: "Blue",
|
||||
},
|
||||
{
|
||||
hex: "#F8CD3C",
|
||||
rgba: [248, 205, 60, 1],
|
||||
name: "Yellow",
|
||||
},
|
||||
{
|
||||
hex: "#3BB68A",
|
||||
rgba: [59, 182, 138, 1],
|
||||
name: "Emerald",
|
||||
},
|
||||
{
|
||||
hex: "#523E93",
|
||||
rgba: [82, 62, 147, 1],
|
||||
name: "Violet",
|
||||
},
|
||||
{
|
||||
hex: "#A23C52",
|
||||
rgba: [162, 60, 82, 1],
|
||||
name: "Burgundy",
|
||||
},
|
||||
{
|
||||
hex: "#8CB354",
|
||||
rgba: [162, 60, 82, 1],
|
||||
name: "Wasabi",
|
||||
},
|
||||
{
|
||||
hex: "#D03627",
|
||||
rgba: [208, 54, 39, 1],
|
||||
name: "Red",
|
||||
},
|
||||
{
|
||||
hex: "#1B717E",
|
||||
rgba: [27, 113, 126, 1],
|
||||
name: "Teal",
|
||||
},
|
||||
];
|
||||
if (alpha === 1) {
|
||||
return colors[index % 10].hex;
|
||||
}
|
||||
const { rgba } = colors[index % 10];
|
||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* This function get a formula like `=A1*SUM(B5:C6)` and transforms it to:
|
||||
*
|
||||
* `<span>=</span><span>A1</span><span>SUM</span><span>(</span><span>B5:C6</span><span>)</span>`
|
||||
*
|
||||
* While also returning the set of ranges [A1, B5:C6] with specific color assignments for each range
|
||||
*/
|
||||
export function getFormulaHTML(
|
||||
text: string,
|
||||
sheet: number,
|
||||
sheetList: string[],
|
||||
insertRage: Area | null,
|
||||
insertRangeText: string
|
||||
): {
|
||||
html: JSX.Element[];
|
||||
activeRanges: ActiveRange[];
|
||||
isInReferenceMode: boolean;
|
||||
} {
|
||||
let html = [];
|
||||
const activeRanges: ActiveRange[] = [];
|
||||
let colorCount = 0;
|
||||
if (text.startsWith("=")) {
|
||||
const formula = text.slice(1);
|
||||
|
||||
const tokens = getTokens(formula);
|
||||
const tokenCount = tokens.length;
|
||||
const usedColors: Record<string, string> = {};
|
||||
for (let index = 0; index < tokenCount; index += 1) {
|
||||
const { token, start, end } = tokens[index];
|
||||
if (tokenIsReferenceType(token)) {
|
||||
const { sheet: refSheet, row, column } = token.Reference;
|
||||
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
||||
const key = `${sheetIndex}-${row}-${column}`;
|
||||
let color = usedColors[key];
|
||||
if (!color) {
|
||||
color = getColor(colorCount);
|
||||
usedColors[key] = color;
|
||||
colorCount += 1;
|
||||
}
|
||||
html.push(
|
||||
<span key={index} style={{ color }}>
|
||||
{formula.slice(start, end)}
|
||||
</span>
|
||||
);
|
||||
activeRanges.push({
|
||||
sheet: sheetIndex,
|
||||
rowStart: row,
|
||||
columnStart: column,
|
||||
rowEnd: row,
|
||||
columnEnd: column,
|
||||
color,
|
||||
});
|
||||
} else if (tokenIsRangeType(token)) {
|
||||
let {
|
||||
sheet: refSheet,
|
||||
left: { row: rowStart, column: columnStart },
|
||||
right: { row: rowEnd, column: columnEnd },
|
||||
} = token.Range;
|
||||
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
||||
|
||||
const key = `${sheetIndex}-${rowStart}-${columnStart}:${rowEnd}-${columnEnd}`;
|
||||
let color = usedColors[key];
|
||||
if (!color) {
|
||||
color = getColor(colorCount);
|
||||
usedColors[key] = color;
|
||||
colorCount += 1;
|
||||
}
|
||||
|
||||
if (rowStart > rowEnd) {
|
||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||
}
|
||||
if (columnStart > columnEnd) {
|
||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||
}
|
||||
html.push(
|
||||
<span key={index} style={{ color }}>
|
||||
{formula.slice(start, end)}
|
||||
</span>
|
||||
);
|
||||
colorCount += 1;
|
||||
|
||||
activeRanges.push({
|
||||
sheet: sheetIndex,
|
||||
rowStart,
|
||||
columnStart,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
color,
|
||||
});
|
||||
} else {
|
||||
html.push(<span key={index}>{formula.slice(start, end)}</span>);
|
||||
}
|
||||
}
|
||||
if (tokenCount > 0) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
if (lastToken.end < text.length - 1) {
|
||||
html.push(
|
||||
<span key="rest">{text.slice(lastToken.end + 1, text.length)}</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
html = [<span key="equals">=</span>].concat(html);
|
||||
} else {
|
||||
html = [<span key="single">{text}</span>];
|
||||
}
|
||||
const isRefMode = isInReferenceMode(text, text.length);
|
||||
if (isRefMode) {
|
||||
if (insertRage) {
|
||||
const color = getColor(colorCount);
|
||||
activeRanges.push({
|
||||
sheet: insertRage.sheet || sheet,
|
||||
rowStart: insertRage.rowStart,
|
||||
rowEnd: insertRage.rowEnd,
|
||||
columnStart: insertRage.columnStart,
|
||||
columnEnd: insertRage.columnEnd,
|
||||
color,
|
||||
});
|
||||
colorCount += 1;
|
||||
html.push(
|
||||
<span key="insert-range" style={{ color, textDecoration: "underline" }}>
|
||||
{insertRangeText}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
html.push(
|
||||
<span
|
||||
key="insert-cue"
|
||||
style={{
|
||||
border: "1px solid #d5d5d5",
|
||||
height: "2px",
|
||||
width: "7px",
|
||||
borderTop: 0,
|
||||
display: "inline-block",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
// We add a clickable element that spans the rest of the available space
|
||||
html.push(<span key="spacer" style={{ flexGrow: 1 }}></span>);
|
||||
return { html, activeRanges, isInReferenceMode: isRefMode };
|
||||
}
|
||||
|
||||
export function isInReferenceMode(text: string, cursor: number): boolean {
|
||||
// FIXME
|
||||
// This is a gross oversimplification
|
||||
// Returns true if both are true:
|
||||
// 1. Cursor is at the end
|
||||
// 2. Last char is one of [',', '(', '+', '*', '-', '/', '<', '>', '=', '&']
|
||||
// This has many false positives like '="1+' and also likely some false negatives
|
||||
// The right way of doing this is to have a partial parse of the formula tree
|
||||
// and check if the next token could be a reference
|
||||
if (!text.startsWith("=")) {
|
||||
return false;
|
||||
}
|
||||
if (text === "=") {
|
||||
return true;
|
||||
}
|
||||
const l = text.length;
|
||||
const chars = [",", "(", "+", "*", "-", "/", "<", ">", "=", "&"];
|
||||
if (cursor === l && chars.includes(text[l - 1])) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useState, useRef, ComponentProps } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NumberFormats } from './formatUtil';
|
||||
import { Menu, MenuItem, styled } from '@mui/material';
|
||||
import FormatPicker from './formatPicker';
|
||||
|
||||
type FormatMenuProps = {
|
||||
children: any; //ReactI18NextChild | Iterable<ReactI18NextChild>;
|
||||
numFmt: string;
|
||||
onChange: (numberFmt: string) => void;
|
||||
onExited?: () => void;
|
||||
anchorOrigin?: ComponentProps<typeof Menu>['anchorOrigin'];
|
||||
};
|
||||
|
||||
const FormatMenu = (properties: FormatMenuProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { onChange } = properties;
|
||||
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||
const [isPickerOpen, setPickerOpen] = useState(false);
|
||||
const anchorElement = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChildrenWrapper onClick={(): void => setMenuOpen(true)} ref={anchorElement}>
|
||||
{properties.children}
|
||||
</ChildrenWrapper>
|
||||
<Menu
|
||||
open={isMenuOpen}
|
||||
onClose={(): void => setMenuOpen(false)}
|
||||
// onExited={properties.onExited}
|
||||
anchorEl={anchorElement.current}
|
||||
anchorOrigin={properties.anchorOrigin}
|
||||
>
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.AUTO)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.auto')}</MenuItemText>
|
||||
</MenuItemWrapper>
|
||||
{/** TODO: Text option that transforms into plain text */}
|
||||
<MenuDivider />
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.NUMBER)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.number')}</MenuItemText>
|
||||
<MenuItemExample>{t('toolbar.format_menu.number_example')}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.PERCENTAGE)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.percentage')}</MenuItemText>
|
||||
<MenuItemExample>{t('toolbar.format_menu.percentage_example')}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
|
||||
<MenuDivider />
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.CURRENCY_EUR)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.currency_eur')}</MenuItemText>
|
||||
<MenuItemExample>{t('toolbar.format_menu.currency_eur_example')}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.CURRENCY_USD)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.currency_usd')}</MenuItemText>
|
||||
<MenuItemExample>{t('toolbar.format_menu.currency_usd_example')}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.CURRENCY_GBP)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.currency_gbp')}</MenuItemText>
|
||||
<MenuItemExample>{t('toolbar.format_menu.currency_gbp_example')}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
|
||||
<MenuDivider />
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.DATE_SHORT)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.date_short')}</MenuItemText>
|
||||
<MenuItemExample>{t('toolbar.format_menu.date_short_example')}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.DATE_LONG)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.date_long')}</MenuItemText>
|
||||
<MenuItemExample>{t('toolbar.format_menu.date_long_example')}</MenuItemExample>
|
||||
</MenuItemWrapper>
|
||||
|
||||
<MenuDivider />
|
||||
<MenuItemWrapper onClick={(): void => setPickerOpen(true)}>
|
||||
<MenuItemText>{t('toolbar.format_menu.custom')}</MenuItemText>
|
||||
</MenuItemWrapper>
|
||||
</Menu>
|
||||
<FormatPicker
|
||||
numFmt={properties.numFmt}
|
||||
onChange={properties.onChange}
|
||||
open={isPickerOpen}
|
||||
onClose={(): void => setPickerOpen(false)}
|
||||
onExited={properties.onExited}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItemWrapper = styled(MenuItem)`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ChildrenWrapper = styled('div')`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const MenuDivider = styled('div')``;
|
||||
|
||||
const MenuItemText = styled('div')`
|
||||
color: #000;
|
||||
`;
|
||||
|
||||
const MenuItemExample = styled('div')`
|
||||
margin-left: 20px;
|
||||
`;
|
||||
|
||||
export default FormatMenu;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type FormatPickerProps = {
|
||||
className?: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onExited?: () => void;
|
||||
numFmt: string;
|
||||
onChange: (numberFmt: string) => void;
|
||||
};
|
||||
|
||||
const FormatPicker = (properties: FormatPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [formatCode, setFormatCode] = useState(properties.numFmt);
|
||||
|
||||
const onSubmit = (format_code: string): void => {
|
||||
properties.onChange(format_code);
|
||||
properties.onClose();
|
||||
};
|
||||
return (
|
||||
<Dialog
|
||||
open={properties.open}
|
||||
onClose={properties.onClose}
|
||||
>
|
||||
<DialogTitle>{t('num_fmt.title')}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextField
|
||||
defaultValue={properties.numFmt}
|
||||
label={t('num_fmt.label')}
|
||||
name="format_code"
|
||||
onChange={(event) => setFormatCode(event.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => onSubmit(formatCode)}>
|
||||
{t('num_fmt.save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default FormatPicker;
|
||||
@@ -1,36 +0,0 @@
|
||||
export function increaseDecimalPlaces(numberFormat: string): string {
|
||||
// FIXME: Should it be done in the Rust? How should it work?
|
||||
// Increase decimal places for existing numbers with decimals
|
||||
const newNumberFormat = numberFormat.replace(/\.0/g, '.00');
|
||||
// If no decimal places declared, add 0.0
|
||||
if (!newNumberFormat.includes('.')) {
|
||||
if (newNumberFormat.includes('0')) {
|
||||
return newNumberFormat.replace(/0/g, '0.0');
|
||||
}
|
||||
if (newNumberFormat.includes('#')) {
|
||||
return newNumberFormat.replace(/#([^#,]|$)/g, '0.0$1');
|
||||
}
|
||||
return '0.0';
|
||||
}
|
||||
return newNumberFormat;
|
||||
}
|
||||
|
||||
export function decreaseDecimalPlaces(numberFormat: string): string {
|
||||
// FIXME: Should it be done in the Rust? How should it work?
|
||||
// Decrease decimal places for existing numbers with decimals
|
||||
let newNumberFormat = numberFormat.replace(/\.0/g, '.');
|
||||
// Fix leftover dots
|
||||
newNumberFormat = newNumberFormat.replace(/0\.([^0]|$)/, '0$1');
|
||||
return newNumberFormat;
|
||||
}
|
||||
|
||||
export enum NumberFormats {
|
||||
AUTO = 'general',
|
||||
CURRENCY_EUR = '"€"#,##0.00',
|
||||
CURRENCY_USD = '"$"#,##0.00',
|
||||
CURRENCY_GBP = '"£"#,##0.00',
|
||||
DATE_SHORT = 'dd"/"mm"/"yyyy',
|
||||
DATE_LONG = 'dddd"," mmmm dd"," yyyy',
|
||||
PERCENTAGE = '0.00%',
|
||||
NUMBER = '#,##0.00',
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
styled,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
interface FormulaDialogProps {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
onFormulaChanged: (name: string) => void;
|
||||
defaultName: string;
|
||||
}
|
||||
|
||||
export const FormulaDialog = (properties: FormulaDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState(properties.defaultName);
|
||||
return (
|
||||
<Dialog open={properties.isOpen} onClose={properties.close}>
|
||||
<DialogTitle>{t("sheet_rename.title")}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextField
|
||||
defaultValue={name}
|
||||
label={t("sheet_rename.label")}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value);
|
||||
}}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
properties.onFormulaChanged(name);
|
||||
}}
|
||||
>
|
||||
{t("sheet_rename.rename")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
import { Button, styled } from "@mui/material";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { Fx } from "../icons";
|
||||
import { useState } from "react";
|
||||
import { FormulaDialog } from "./formulaDialog";
|
||||
|
||||
type FormulaBarProps = {
|
||||
cellAddress: string;
|
||||
formulaValue: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const formulaBarHeight = 30;
|
||||
const headerColumnWidth = 30;
|
||||
|
||||
function FormulaBar(properties: FormulaBarProps) {
|
||||
const [formulaDialogOpen, setFormulaDialogOpen] = useState(false);
|
||||
const handleCloseFormulaDialog = () => {
|
||||
setFormulaDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AddressContainer>
|
||||
<CellBarAddress>{properties.cellAddress}</CellBarAddress>
|
||||
<StyledButton>
|
||||
<ChevronDown />
|
||||
</StyledButton>
|
||||
</AddressContainer>
|
||||
<Divider />
|
||||
<FormulaContainer>
|
||||
<FormulaSymbolButton>
|
||||
<Fx />
|
||||
</FormulaSymbolButton>
|
||||
<Editor
|
||||
onClick={() => {
|
||||
setFormulaDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
{properties.formulaValue}
|
||||
</Editor>
|
||||
</FormulaContainer>
|
||||
<FormulaDialog
|
||||
isOpen={formulaDialogOpen}
|
||||
close={handleCloseFormulaDialog}
|
||||
defaultName={properties.formulaValue}
|
||||
onFormulaChanged={(newName) => {
|
||||
properties.onChange(newName);
|
||||
setFormulaDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
width: 15px;
|
||||
min-width: 0px;
|
||||
padding: 0px;
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormulaSymbolButton = styled(StyledButton)`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const Divider = styled("div")`
|
||||
background-color: #e0e0e0;
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
`;
|
||||
|
||||
const FormulaContainer = styled("div")`
|
||||
margin-left: 10px;
|
||||
line-height: 22px;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Container = styled("div")`
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: ${(properties): string =>
|
||||
properties.theme.palette.background.default};
|
||||
height: ${formulaBarHeight}px;
|
||||
`;
|
||||
|
||||
const AddressContainer = styled("div")`
|
||||
padding-left: 16px;
|
||||
color: #333;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
flex-grow: row;
|
||||
min-width: ${headerColumnWidth}px;
|
||||
`;
|
||||
|
||||
const CellBarAddress = styled("div")`
|
||||
width: 100%;
|
||||
text-align: "center";
|
||||
`;
|
||||
|
||||
const Editor = styled("div")`
|
||||
position: relative;
|
||||
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;
|
||||
}
|
||||
`;
|
||||
|
||||
export default FormulaBar;
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default } from './navigation';
|
||||
export type { NavigationProps } from './navigation';
|
||||
@@ -1,122 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
styled,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { SheetOptions } from "./types";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
interface SheetRenameDialogProps {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
onNameChanged: (name: string) => void;
|
||||
defaultName: string;
|
||||
}
|
||||
|
||||
export const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState(properties.defaultName);
|
||||
return (
|
||||
<Dialog open={properties.isOpen} onClose={properties.close}>
|
||||
<DialogTitle>{t("sheet_rename.title")}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<TextField
|
||||
defaultValue={name}
|
||||
label={t("sheet_rename.label")}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value);
|
||||
}}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
properties.onNameChanged(name);
|
||||
}}
|
||||
>
|
||||
{t("sheet_rename.rename")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
interface SheetListMenuProps {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
anchorEl: HTMLButtonElement | null;
|
||||
onSheetSelected: (index: number) => void;
|
||||
sheetOptionsList: SheetOptions[];
|
||||
}
|
||||
|
||||
const SheetListMenu = (properties: SheetListMenuProps) => {
|
||||
const { isOpen, close, anchorEl, onSheetSelected, sheetOptionsList } =
|
||||
properties;
|
||||
|
||||
return (
|
||||
<StyledMenu
|
||||
open={isOpen}
|
||||
onClose={close}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: 6,
|
||||
}}
|
||||
>
|
||||
{sheetOptionsList.map((tab, index) => (
|
||||
<StyledMenuItem
|
||||
key={tab.sheetId}
|
||||
onClick={(): void => onSheetSelected(index)}
|
||||
>
|
||||
<ItemColor style={{ backgroundColor: tab.color }} />
|
||||
<ItemName>{tab.name}</ItemName>
|
||||
</StyledMenuItem>
|
||||
))}
|
||||
</StyledMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledMenu = styled(Menu)({
|
||||
"& .MuiPaper-root": {
|
||||
borderRadius: 8,
|
||||
padding: 4,
|
||||
},
|
||||
"& .MuiList-padding": {
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const StyledMenuItem = styled(MenuItem)({
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
});
|
||||
|
||||
const ItemColor = styled("div")`
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const ItemName = styled("div")`
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
`;
|
||||
|
||||
export default SheetListMenu;
|
||||
@@ -1,141 +0,0 @@
|
||||
import { styled } from "@mui/material";
|
||||
import { ChevronLeft, ChevronRight, Menu, Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SheetOptions } from "./types";
|
||||
import SheetListMenu, { SheetRenameDialog } from "./menus";
|
||||
import Sheet from "./sheet";
|
||||
import { StyledButton } from "../toolbar";
|
||||
|
||||
export interface NavigationProps {
|
||||
sheets: SheetOptions[];
|
||||
selectedIndex: number;
|
||||
onSheetSelected: (index: number) => void;
|
||||
onAddBlankSheet: () => void;
|
||||
onSheetColorChanged: (hex: string) => void;
|
||||
onSheetRenamed: (name: string) => void;
|
||||
onSheetDeleted: () => void;
|
||||
}
|
||||
|
||||
function Navigation(props: NavigationProps) {
|
||||
const { t } = useTranslation();
|
||||
const { onSheetSelected, sheets, selectedIndex } = props;
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<StyledButton title={t("navigation.add_sheet")} $pressed={false}>
|
||||
<Plus />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
onClick={handleClick}
|
||||
title={t("navigation.sheet_list")}
|
||||
$pressed={false}
|
||||
>
|
||||
<Menu />
|
||||
</StyledButton>
|
||||
<Sheets>
|
||||
<SheetInner>
|
||||
{sheets.map((tab, index) => (
|
||||
<Sheet
|
||||
key={tab.sheetId}
|
||||
name={tab.name}
|
||||
color={tab.color}
|
||||
selected={index === selectedIndex}
|
||||
onSelected={() => onSheetSelected(index)}
|
||||
onColorChanged={function (hex: string): void {
|
||||
props.onSheetColorChanged(hex);
|
||||
}}
|
||||
onRenamed={function (name: string): void {
|
||||
props.onSheetRenamed(name);
|
||||
}}
|
||||
onDeleted={function (): void {
|
||||
props.onSheetDeleted();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</SheetInner>
|
||||
</Sheets>
|
||||
<LeftDivider />
|
||||
<ChevronLeftStyled />
|
||||
<ChevronRightStyled />
|
||||
<RightDivider />
|
||||
<Advert>ironcalc.com</Advert>
|
||||
<SheetListMenu
|
||||
anchorEl={anchorEl}
|
||||
isOpen={open}
|
||||
close={handleClose}
|
||||
sheetOptionsList={sheets}
|
||||
onSheetSelected={onSheetSelected}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const ChevronLeftStyled = styled(ChevronLeft)`
|
||||
color: #333333;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const ChevronRightStyled = styled(ChevronRight)`
|
||||
color: #333333;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
// Note I have to specify the font-family in every component that can be considered stand-alone
|
||||
const Container = styled("div")`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
padding-left: 12px;
|
||||
font-family: Inter;
|
||||
background-color: #fff;
|
||||
`;
|
||||
|
||||
const Sheets = styled("div")`
|
||||
flex-grow: 2;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const SheetInner = styled("div")`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const LeftDivider = styled("div")`
|
||||
height: 10px;
|
||||
width: 1px;
|
||||
background-color: #eee;
|
||||
margin: 0px 10px 0px 0px;
|
||||
`;
|
||||
|
||||
const RightDivider = styled("div")`
|
||||
height: 10px;
|
||||
width: 1px;
|
||||
background-color: #eee;
|
||||
margin: 0px 20px 0px 10px;
|
||||
`;
|
||||
|
||||
const Advert = styled("div")`
|
||||
color: #f2994a;
|
||||
margin-right: 12px;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
export default Navigation;
|
||||
@@ -1,127 +0,0 @@
|
||||
import { Button, Menu, MenuItem, styled } from "@mui/material";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { SheetRenameDialog } from "./menus";
|
||||
import ColorPicker from "../colorPicker";
|
||||
interface SheetProps {
|
||||
name: string;
|
||||
color: string;
|
||||
selected: boolean;
|
||||
onSelected: () => void;
|
||||
onColorChanged: (hex: string) => void;
|
||||
onRenamed: (name: string) => void;
|
||||
onDeleted: () => void;
|
||||
}
|
||||
function Sheet(props: SheetProps) {
|
||||
const { name, color, selected, onSelected } = props;
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
const colorButton = useRef(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
||||
const handleCloseRenameDialog = () => {
|
||||
setRenameDialogOpen(false);
|
||||
};
|
||||
const handleOpenRenameDialog = () => {
|
||||
setRenameDialogOpen(true);
|
||||
};
|
||||
return (
|
||||
<Wrapper
|
||||
style={{ borderBottomColor: color, fontWeight: selected ? 600 : 400 }}
|
||||
onClick={onSelected}
|
||||
ref={colorButton}
|
||||
>
|
||||
<Name>{name}</Name>
|
||||
<StyledButton onClick={handleOpen}>
|
||||
<ChevronDown />
|
||||
</StyledButton>
|
||||
<StyledMenu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: 6,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleOpenRenameDialog();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setColorPickerOpen(true);
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
Change Color
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => props.onDeleted()}> Delete</MenuItem>
|
||||
</StyledMenu>
|
||||
<SheetRenameDialog
|
||||
isOpen={renameDialogOpen}
|
||||
close={handleCloseRenameDialog}
|
||||
defaultName={name}
|
||||
onNameChanged={(newName) => {
|
||||
props.onRenamed(newName);
|
||||
setRenameDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={color}
|
||||
onChange={(color): void => {
|
||||
props.onColorChanged(color);
|
||||
setColorPickerOpen(false);
|
||||
}}
|
||||
anchorEl={colorButton}
|
||||
open={colorPickerOpen}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledMenu = styled(Menu)``;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
width: 15px;
|
||||
height: 24px;
|
||||
min-width: 0px;
|
||||
padding: 0px;
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Wrapper = styled("div")`
|
||||
display: flex;
|
||||
margin-left: 20px;
|
||||
border-bottom: 3px solid;
|
||||
border-top: 3px solid white;
|
||||
line-height: 34px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Name = styled("div")`
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
text-wrap: nowrap;
|
||||
`;
|
||||
|
||||
export default Sheet;
|
||||
@@ -1,5 +0,0 @@
|
||||
export interface SheetOptions {
|
||||
name: string;
|
||||
color: string;
|
||||
sheetId: number;
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
import {
|
||||
AlignCenter,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Bold,
|
||||
ChevronDown,
|
||||
Euro,
|
||||
Italic,
|
||||
PaintBucket,
|
||||
Paintbrush2,
|
||||
Percent,
|
||||
Redo2,
|
||||
Strikethrough,
|
||||
Underline,
|
||||
Undo2,
|
||||
Grid2X2,
|
||||
Type,
|
||||
ArrowDownToLine,
|
||||
ArrowUpToLine,
|
||||
Grid2x2Check,
|
||||
Grid2x2X,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import ColorPicker from "./colorPicker";
|
||||
import BorderPicker from "./borderPicker";
|
||||
import {
|
||||
ArrowMiddleFromLine,
|
||||
DecimalPlacesDecreaseIcon,
|
||||
DecimalPlacesIncreaseIcon,
|
||||
} from "../icons";
|
||||
import {
|
||||
NumberFormats,
|
||||
decreaseDecimalPlaces,
|
||||
increaseDecimalPlaces,
|
||||
} from "./formatUtil";
|
||||
import FormatMenu from "./formatMenu";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { theme } from "../theme";
|
||||
import {
|
||||
BorderOptions,
|
||||
HorizontalAlignment,
|
||||
VerticalAlignment,
|
||||
} from "@ironcalc/wasm";
|
||||
|
||||
type ToolbarProperties = {
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
onRedo: () => void;
|
||||
onUndo: () => void;
|
||||
onToggleUnderline: (u: boolean) => void;
|
||||
onToggleBold: (v: boolean) => void;
|
||||
onToggleItalic: (v: boolean) => void;
|
||||
onToggleStrike: (v: boolean) => void;
|
||||
onToggleHorizontalAlign: (v: string) => void;
|
||||
onToggleVerticalAlign: (v: string) => void;
|
||||
onCopyStyles: () => void;
|
||||
onTextColorPicked: (hex: string) => void;
|
||||
onFillColorPicked: (hex: string) => void;
|
||||
onNumberFormatPicked: (numberFmt: string) => void;
|
||||
onBorderChanged: (border: BorderOptions) => void;
|
||||
fillColor: string;
|
||||
fontColor: string;
|
||||
bold: boolean;
|
||||
underline: boolean;
|
||||
italic: boolean;
|
||||
strike: boolean;
|
||||
horizontalAlign: HorizontalAlignment;
|
||||
verticalAlign: VerticalAlignment;
|
||||
canEdit: boolean;
|
||||
numFmt: string;
|
||||
showGridLines: boolean;
|
||||
onToggleShowGridLines: (show: boolean) => void;
|
||||
};
|
||||
|
||||
function Toolbar(properties: ToolbarProperties) {
|
||||
const [fontColorPickerOpen, setFontColorPickerOpen] = useState(false);
|
||||
const [fillColorPickerOpen, setFillColorPickerOpen] = useState(false);
|
||||
const [borderPickerOpen, setBorderPickerOpen] = useState(false);
|
||||
|
||||
const fontColorButton = useRef(null);
|
||||
const fillColorButton = useRef(null);
|
||||
const borderButton = useRef(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { canEdit } = properties;
|
||||
|
||||
return (
|
||||
<ToolbarContainer>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={properties.onUndo}
|
||||
disabled={!properties.canUndo}
|
||||
title={t("toolbar.undo")}
|
||||
>
|
||||
<Undo2 />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={properties.onRedo}
|
||||
disabled={!properties.canRedo}
|
||||
title={t("toolbar.redo")}
|
||||
>
|
||||
<Redo2 />
|
||||
</StyledButton>
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={properties.onCopyStyles}
|
||||
title={t("toolbar.copy_styles")}
|
||||
>
|
||||
<Paintbrush2 />
|
||||
</StyledButton>
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={(): void => {
|
||||
properties.onNumberFormatPicked(NumberFormats.CURRENCY_EUR);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.euro")}
|
||||
>
|
||||
<Euro />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={(): void => {
|
||||
properties.onNumberFormatPicked(NumberFormats.PERCENTAGE);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.percentage")}
|
||||
>
|
||||
<Percent />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={(): void => {
|
||||
properties.onNumberFormatPicked(
|
||||
decreaseDecimalPlaces(properties.numFmt)
|
||||
);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.decimal_places_decrease")}
|
||||
>
|
||||
<DecimalPlacesDecreaseIcon />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={(): void => {
|
||||
properties.onNumberFormatPicked(
|
||||
increaseDecimalPlaces(properties.numFmt)
|
||||
);
|
||||
}}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.decimal_places_increase")}
|
||||
>
|
||||
<DecimalPlacesIncreaseIcon />
|
||||
</StyledButton>
|
||||
<FormatMenu
|
||||
numFmt={properties.numFmt}
|
||||
onChange={(numberFmt): void => {
|
||||
properties.onNumberFormatPicked(numberFmt);
|
||||
}}
|
||||
onExited={(): void => {}}
|
||||
anchorOrigin={{
|
||||
horizontal: 20, // Aligning the menu to the middle of FormatButton
|
||||
vertical: "bottom",
|
||||
}}
|
||||
>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.format_number")}
|
||||
sx={{
|
||||
width: "40px", // Keep in sync with anchorOrigin in FormatMenu above
|
||||
fontSize: "13px",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{"123"}
|
||||
<ChevronDown />
|
||||
</StyledButton>
|
||||
</FormatMenu>
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.bold}
|
||||
onClick={() => properties.onToggleBold(!properties.bold)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.bold")}
|
||||
>
|
||||
<Bold />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.italic}
|
||||
onClick={() => properties.onToggleItalic(!properties.italic)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.italic")}
|
||||
>
|
||||
<Italic />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.underline}
|
||||
onClick={() => properties.onToggleUnderline(!properties.underline)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.underline")}
|
||||
>
|
||||
<Underline />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.strike}
|
||||
onClick={() => properties.onToggleStrike(!properties.strike)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.strike_trough")}
|
||||
>
|
||||
<Strikethrough />
|
||||
</StyledButton>
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.font_color")}
|
||||
ref={fontColorButton}
|
||||
$underlinedColor={properties.fontColor}
|
||||
onClick={() => setFontColorPickerOpen(true)}
|
||||
>
|
||||
<Type />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.fill_color")}
|
||||
ref={fillColorButton}
|
||||
$underlinedColor={properties.fillColor}
|
||||
onClick={() => setFillColorPickerOpen(true)}
|
||||
>
|
||||
<PaintBucket />
|
||||
</StyledButton>
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.horizontalAlign === "left"}
|
||||
onClick={() =>
|
||||
properties.onToggleHorizontalAlign(
|
||||
properties.horizontalAlign === "left" ? "general" : "left"
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.align_left")}
|
||||
>
|
||||
<AlignLeft />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.horizontalAlign === "center"}
|
||||
onClick={() =>
|
||||
properties.onToggleHorizontalAlign(
|
||||
properties.horizontalAlign === "center" ? "general" : "center"
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.align_center")}
|
||||
>
|
||||
<AlignCenter />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.horizontalAlign === "right"}
|
||||
onClick={() =>
|
||||
properties.onToggleHorizontalAlign(
|
||||
properties.horizontalAlign === "right" ? "general" : "right"
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.align_right")}
|
||||
>
|
||||
<AlignRight />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.verticalAlign === "top"}
|
||||
onClick={() =>
|
||||
properties.onToggleVerticalAlign(
|
||||
properties.verticalAlign === "top" ? "bottom" : "top"
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.vertical_align_top")}
|
||||
>
|
||||
<ArrowUpToLine />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.verticalAlign === "center"}
|
||||
onClick={() =>
|
||||
properties.onToggleVerticalAlign(
|
||||
properties.verticalAlign === "center" ? "bottom" : "center"
|
||||
)
|
||||
}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.vertical_align_center")}
|
||||
>
|
||||
<ArrowMiddleFromLine />
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={properties.verticalAlign === "bottom"}
|
||||
onClick={() => properties.onToggleVerticalAlign("bottom")}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.vertical_align_bottom")}
|
||||
>
|
||||
<ArrowDownToLine />
|
||||
</StyledButton>
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={() => setBorderPickerOpen(true)}
|
||||
ref={borderButton}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.borders")}
|
||||
>
|
||||
<Grid2X2 />
|
||||
</StyledButton>
|
||||
<Divider />
|
||||
<StyledButton
|
||||
type="button"
|
||||
$pressed={false}
|
||||
onClick={() => properties.onToggleShowGridLines(!properties.showGridLines)}
|
||||
disabled={!canEdit}
|
||||
title={t("toolbar.show_hide_grid_lines")}
|
||||
>
|
||||
{properties.showGridLines ? <Grid2x2Check /> : <Grid2x2X />}
|
||||
</StyledButton>
|
||||
|
||||
<ColorPicker
|
||||
color={properties.fontColor}
|
||||
onChange={(color): void => {
|
||||
properties.onTextColorPicked(color);
|
||||
setFontColorPickerOpen(false);
|
||||
}}
|
||||
anchorEl={fontColorButton}
|
||||
open={fontColorPickerOpen}
|
||||
/>
|
||||
<ColorPicker
|
||||
color={properties.fillColor}
|
||||
onChange={(color): void => {
|
||||
properties.onFillColorPicked(color);
|
||||
setFillColorPickerOpen(false);
|
||||
}}
|
||||
anchorEl={fillColorButton}
|
||||
open={fillColorPickerOpen}
|
||||
/>
|
||||
<BorderPicker
|
||||
onChange={(border): void => {
|
||||
properties.onBorderChanged(border);
|
||||
setBorderPickerOpen(false);
|
||||
}}
|
||||
anchorEl={borderButton}
|
||||
open={borderPickerOpen}
|
||||
/>
|
||||
</ToolbarContainer>
|
||||
);
|
||||
}
|
||||
const toolbarHeight = 40;
|
||||
|
||||
const ToolbarContainer = styled("div")`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
background: ${({ }) => theme.palette.background.paper};
|
||||
height: ${toolbarHeight}px;
|
||||
line-height: ${toolbarHeight}px;
|
||||
border-bottom: 1px solid ${({}) => theme.palette.grey["600"]};
|
||||
font-family: Inter;
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
overflow-x: auto;
|
||||
`;
|
||||
|
||||
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
||||
export const StyledButton = styled("button")<TypeButtonProperties>(({
|
||||
disabled,
|
||||
$pressed,
|
||||
$underlinedColor,
|
||||
}) => {
|
||||
let result: Record<string, any> = {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "26px",
|
||||
border: "0px solid #fff",
|
||||
borderRadius: "2px",
|
||||
marginRight: "5px",
|
||||
transition: "all 0.2s",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "white",
|
||||
padding: "0px",
|
||||
};
|
||||
if (disabled) {
|
||||
result.color = theme.palette.grey["600"];
|
||||
result.cursor = "default";
|
||||
} else {
|
||||
result.borderTop = $underlinedColor ? "3px solid #FFF" : "none";
|
||||
result.borderBottom = $underlinedColor
|
||||
? `3px solid ${$underlinedColor}`
|
||||
: "none";
|
||||
(result.color = "#21243A"), //theme.palette.text.primary;
|
||||
(result.backgroundColor = $pressed ? "#EEE" : "#FFF");
|
||||
result["&:hover"] = {
|
||||
backgroundColor: "#F1F2F8",
|
||||
borderTopColor: "#F1F2F8",
|
||||
};
|
||||
}
|
||||
result["svg"] = {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
};
|
||||
return result;
|
||||
});
|
||||
|
||||
const Divider = styled("div")({
|
||||
width: "0px",
|
||||
height: "10px",
|
||||
borderLeft: "1px solid #D3D6E9",
|
||||
marginLeft: "5px",
|
||||
marginRight: "10px",
|
||||
});
|
||||
|
||||
export default Toolbar;
|
||||
@@ -1,229 +0,0 @@
|
||||
import { useCallback, KeyboardEvent, RefObject } from "react";
|
||||
import {
|
||||
isEditingKey,
|
||||
isNavigationKey,
|
||||
NavigationKey,
|
||||
} from "./WorksheetCanvas/util";
|
||||
|
||||
export enum Border {
|
||||
Top = "top",
|
||||
Bottom = "bottom",
|
||||
Right = "right",
|
||||
Left = "left",
|
||||
}
|
||||
|
||||
interface Options {
|
||||
onCellsDeleted: () => void;
|
||||
onExpandAreaSelectedKeyboard: (
|
||||
key: "ArrowRight" | "ArrowLeft" | "ArrowUp" | "ArrowDown"
|
||||
) => void;
|
||||
onEditKeyPressStart: (initText: string) => void;
|
||||
onCellEditStart: () => void;
|
||||
onBold: () => void;
|
||||
onItalic: () => void;
|
||||
onUnderline: () => void;
|
||||
onNavigationToEdge: (direction: NavigationKey) => void;
|
||||
onPageDown: () => void;
|
||||
onPageUp: () => void;
|
||||
onArrowDown: () => void;
|
||||
onArrowUp: () => void;
|
||||
onArrowLeft: () => void;
|
||||
onArrowRight: () => void;
|
||||
onKeyHome: () => void;
|
||||
onKeyEnd: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onNextSheet: () => void;
|
||||
onPreviousSheet: () => void;
|
||||
root: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
// # IronCalc Keyboard accessibility:
|
||||
// * ArrowKeys: navigation
|
||||
// * Enter: ArrowDown (Excel behaviour not g-sheets)
|
||||
// * Tab: arrow right
|
||||
// * Shift+Tab: arrow left
|
||||
// * Home/End: First/last column
|
||||
// * Shift+Arrows: selection
|
||||
// * Ctrl+Arrows: navigating to edge
|
||||
// * Ctrl+Home/End: navigation to end
|
||||
// * PagDown/Up scroll Down/Up
|
||||
// * Alt+ArrowDown/Up: next/previous sheet
|
||||
// (NB: Excel uses Ctrl+PageUp/Down for this but that highjacks a browser behaviour,
|
||||
// go to next/previous tab)
|
||||
// * Ctrl+u/i/b: style
|
||||
// * Ctrl+z/y: undo/redo
|
||||
// * F2: start editing
|
||||
|
||||
// References:
|
||||
// In Google Sheets: Ctrl+/ shows the list of keyboard shortcuts
|
||||
// https://support.google.com/docs/answer/181110
|
||||
// https://support.microsoft.com/en-us/office/keyboard-shortcuts-in-excel-1798d9d5-842a-42b8-9c99-9b7213f0040f
|
||||
|
||||
const useKeyboardNavigation = (
|
||||
options: Options
|
||||
): { onKeyDown: (event: KeyboardEvent) => void } => {
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const { key } = event;
|
||||
const { root } = options;
|
||||
console.log(key);
|
||||
// Silence the linter
|
||||
if (!root.current) {
|
||||
return;
|
||||
}
|
||||
if (event.target !== root.current) {
|
||||
return;
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
switch (key) {
|
||||
case "z": {
|
||||
options.onUndo();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case "y": {
|
||||
options.onRedo();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case "b": {
|
||||
options.onBold();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case "i": {
|
||||
options.onItalic();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case "u": {
|
||||
options.onUnderline();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
if (isNavigationKey(key)) {
|
||||
// Ctrl+Arrows, Ctrl+Home/End
|
||||
options.onNavigationToEdge(key);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
} else if (event.altKey) {
|
||||
switch (key) {
|
||||
case "ArrowDown": {
|
||||
// select next sheet
|
||||
options.onNextSheet();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
// select previous sheet
|
||||
options.onPreviousSheet();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key === "F2") {
|
||||
options.onCellEditStart();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (isEditingKey(key) || key === "Backspace") {
|
||||
const initText = key === "Backspace" ? "" : key;
|
||||
options.onEditKeyPressStart(initText);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
// Worksheet Navigation
|
||||
if (event.shiftKey) {
|
||||
if (
|
||||
key === "ArrowRight" ||
|
||||
key === "ArrowLeft" ||
|
||||
key === "ArrowUp" ||
|
||||
key === "ArrowDown"
|
||||
) {
|
||||
options.onExpandAreaSelectedKeyboard(key);
|
||||
} else if (key === "Tab") {
|
||||
options.onArrowLeft();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case "ArrowRight":
|
||||
case "Tab": {
|
||||
options.onArrowRight();
|
||||
|
||||
break;
|
||||
}
|
||||
case "ArrowLeft": {
|
||||
options.onArrowLeft();
|
||||
|
||||
break;
|
||||
}
|
||||
case "ArrowDown":
|
||||
case "Enter": {
|
||||
options.onArrowDown();
|
||||
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
options.onArrowUp();
|
||||
|
||||
break;
|
||||
}
|
||||
case "End": {
|
||||
options.onKeyEnd();
|
||||
|
||||
break;
|
||||
}
|
||||
case "Home": {
|
||||
options.onKeyHome();
|
||||
|
||||
break;
|
||||
}
|
||||
case "Delete": {
|
||||
options.onCellsDeleted();
|
||||
|
||||
break;
|
||||
}
|
||||
case "PageDown": {
|
||||
options.onPageDown();
|
||||
|
||||
break;
|
||||
}
|
||||
case "PageUp": {
|
||||
options.onPageUp();
|
||||
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
[options]
|
||||
);
|
||||
return { onKeyDown };
|
||||
};
|
||||
|
||||
export default useKeyboardNavigation;
|
||||
@@ -1,227 +0,0 @@
|
||||
import { useCallback, RefObject, PointerEvent, useRef } from 'react';
|
||||
import WorksheetCanvas, { headerColumnWidth, headerRowHeight } from './WorksheetCanvas/worksheetCanvas';
|
||||
import { Cell } from './WorksheetCanvas/util';
|
||||
|
||||
interface PointerSettings {
|
||||
canvasElement: RefObject<HTMLCanvasElement>;
|
||||
worksheetCanvas: RefObject<WorksheetCanvas | null>;
|
||||
worksheetElement: RefObject<HTMLDivElement>;
|
||||
// rowContextMenuAnchorElement: RefObject<HTMLDivElement>;
|
||||
// columnContextMenuAnchorElement: RefObject<HTMLDivElement>;
|
||||
onCellSelected: (cell: Cell, event: React.MouseEvent) => void;
|
||||
onAreaSelecting: (cell: Cell) => void;
|
||||
onAreaSelected: () => void;
|
||||
onExtendToCell: (cell: Cell) => void;
|
||||
onExtendToEnd: () => void;
|
||||
// onRowContextMenu: (row: number) => void;
|
||||
// onColumnContextMenu: (column: number) => void;
|
||||
}
|
||||
|
||||
interface PointerEvents {
|
||||
onPointerDown: (event: PointerEvent) => void;
|
||||
onPointerMove: (event: PointerEvent) => void;
|
||||
onPointerUp: (event: PointerEvent) => void;
|
||||
onPointerHandleDown: (event: PointerEvent) => void;
|
||||
// onContextMenu: (event: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const usePointer = (options: PointerSettings): PointerEvents => {
|
||||
const isSelecting = useRef(false);
|
||||
const isExtending = useRef(false);
|
||||
|
||||
// const onContextMenu = useCallback(
|
||||
// (event: React.MouseEvent): void => {
|
||||
// let x = event.clientX;
|
||||
// let y = event.clientY;
|
||||
// const {
|
||||
// canvasElement,
|
||||
// worksheetElement,
|
||||
// worksheetCanvas,
|
||||
// onRowContextMenu,
|
||||
// rowContextMenuAnchorElement,
|
||||
// onColumnContextMenu,
|
||||
// columnContextMenuAnchorElement,
|
||||
// } = options;
|
||||
// const worksheet = worksheetCanvas.current;
|
||||
// const canvas = canvasElement.current;
|
||||
// const worksheetWrapper = worksheetElement.current;
|
||||
// // Silence the linter
|
||||
// if (!canvas || !worksheet || !worksheetWrapper) {
|
||||
// return;
|
||||
// }
|
||||
// const canvasRect = canvas.getBoundingClientRect();
|
||||
// x -= canvasRect.x;
|
||||
// y -= canvasRect.y;
|
||||
// const menuAnchorOffsetY = 10;
|
||||
// if (x > 0 && x < headerColumnWidth && y > headerRowHeight && y < canvasRect.height) {
|
||||
// // Click on a row number
|
||||
// const cell = worksheet.getCellByCoordinates(headerColumnWidth, y);
|
||||
// if (cell) {
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// if (rowContextMenuAnchorElement.current) {
|
||||
// const scrollPosition = worksheet.getScrollPosition();
|
||||
// rowContextMenuAnchorElement.current.style.left = `${x + scrollPosition.left}px`;
|
||||
// rowContextMenuAnchorElement.current.style.top = `${
|
||||
// y + scrollPosition.top + menuAnchorOffsetY
|
||||
// }px`;
|
||||
// }
|
||||
// options.onPointerDownAtCell(cell, event);
|
||||
// onRowContextMenu(cell.row);
|
||||
// }
|
||||
// }
|
||||
// if (x > headerColumnWidth && x < canvas.width && y > 0 && y < headerRowHeight) {
|
||||
// // Click on a column number
|
||||
// const cell = worksheet.getCellByCoordinates(x, headerRowHeight);
|
||||
// if (cell) {
|
||||
// event.preventDefault();
|
||||
// event.stopPropagation();
|
||||
// if (columnContextMenuAnchorElement.current) {
|
||||
// const scrollPosition = worksheet.getScrollPosition();
|
||||
// columnContextMenuAnchorElement.current.style.left = `${x + scrollPosition.left}px`;
|
||||
// columnContextMenuAnchorElement.current.style.top = `${
|
||||
// y + scrollPosition.top + menuAnchorOffsetY
|
||||
// }px`;
|
||||
// }
|
||||
// options.onPointerDownAtCell(cell, event);
|
||||
// onColumnContextMenu(cell.column);
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// [options],
|
||||
// );
|
||||
|
||||
const onPointerMove = useCallback(
|
||||
(event: PointerEvent): void => {
|
||||
// Range selections are disabled on non-mouse devices. Use touch move only
|
||||
// to scroll for now.
|
||||
if (event.pointerType !== 'mouse') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelecting.current) {
|
||||
const { canvasElement, worksheetCanvas } = options;
|
||||
const canvas = canvasElement.current;
|
||||
const worksheet = worksheetCanvas.current;
|
||||
// Silence the linter
|
||||
if (!worksheet || !canvas) {
|
||||
return;
|
||||
}
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
x -= canvasRect.x;
|
||||
y -= canvasRect.y;
|
||||
const cell = worksheet.getCellByCoordinates(x, y);
|
||||
if (cell) {
|
||||
options.onAreaSelecting(cell);
|
||||
} else {
|
||||
console.log('Failed');
|
||||
}
|
||||
} else if (isExtending.current) {
|
||||
const { canvasElement, worksheetCanvas } = options;
|
||||
const canvas = canvasElement.current;
|
||||
const worksheet = worksheetCanvas.current;
|
||||
// Silence the linter
|
||||
if (!worksheet || !canvas) {
|
||||
return;
|
||||
}
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
x -= canvasRect.x;
|
||||
y -= canvasRect.y;
|
||||
const cell = worksheet.getCellByCoordinates(x, y);
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
options.onExtendToCell(cell);
|
||||
}
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
const onPointerUp = useCallback(
|
||||
(event: PointerEvent): void => {
|
||||
if (isSelecting.current) {
|
||||
const { worksheetElement } = options;
|
||||
isSelecting.current = false;
|
||||
worksheetElement.current?.releasePointerCapture(event.pointerId);
|
||||
options.onAreaSelected();
|
||||
} else if (isExtending.current) {
|
||||
const { worksheetElement } = options;
|
||||
isExtending.current = false;
|
||||
worksheetElement.current?.releasePointerCapture(event.pointerId);
|
||||
options.onExtendToEnd();
|
||||
}
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
const onPointerDown = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
const { canvasElement, worksheetElement, worksheetCanvas } = options;
|
||||
const worksheet = worksheetCanvas.current;
|
||||
const canvas = canvasElement.current;
|
||||
const worksheetWrapper = worksheetElement.current;
|
||||
// Silence the linter
|
||||
if (!canvas || !worksheet || !worksheetWrapper) {
|
||||
return;
|
||||
}
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
x -= canvasRect.x;
|
||||
y -= canvasRect.y;
|
||||
// Makes sure is in the sheet area
|
||||
if (
|
||||
x > canvasRect.width ||
|
||||
x < headerColumnWidth ||
|
||||
y < headerRowHeight ||
|
||||
y > canvasRect.height
|
||||
) {
|
||||
if (x > 0 && x < headerColumnWidth && y > headerRowHeight && y < canvasRect.height) {
|
||||
// Click on a row number
|
||||
const cell = worksheet.getCellByCoordinates(headerColumnWidth, y);
|
||||
if (cell) {
|
||||
// TODO
|
||||
// Row selected
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const cell = worksheet.getCellByCoordinates(x, y);
|
||||
if (cell) {
|
||||
options.onCellSelected(cell, event);
|
||||
isSelecting.current = true;
|
||||
worksheetWrapper.setPointerCapture(event.pointerId);
|
||||
}
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
const onPointerHandleDown = useCallback(
|
||||
(event: PointerEvent) => {
|
||||
const worksheetWrapper = options.worksheetElement.current;
|
||||
// Silence the linter
|
||||
if (!worksheetWrapper) {
|
||||
return;
|
||||
}
|
||||
isExtending.current = true;
|
||||
worksheetWrapper.setPointerCapture(event.pointerId);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
return {
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
onPointerHandleDown,
|
||||
// onContextMenu,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePointer;
|
||||
@@ -1,377 +0,0 @@
|
||||
import Toolbar from "./toolbar";
|
||||
import FormulaBar from "./formulabar";
|
||||
import Navigation from "./navigation/navigation";
|
||||
import Worksheet from "./worksheet";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useKeyboardNavigation from "./useKeyboardNavigation";
|
||||
import { NavigationKey, getCellAddress } from "./WorksheetCanvas/util";
|
||||
import { LAST_COLUMN, LAST_ROW } from "./WorksheetCanvas/constants";
|
||||
import { WorkbookState } from "./workbookState";
|
||||
import { BorderOptions, Model, WorksheetProperties } from "@ironcalc/wasm";
|
||||
|
||||
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
const { model, workbookState } = props;
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [_redrawId, setRedrawId] = useState(0);
|
||||
const info = model
|
||||
.getWorksheetsProperties()
|
||||
.map(({ name, color, sheet_id }: WorksheetProperties) => {
|
||||
return { name, color: color ? color : "#FFF", sheetId: sheet_id };
|
||||
});
|
||||
|
||||
const onRedo = () => {
|
||||
model.redo();
|
||||
setRedrawId((id) => id + 1);
|
||||
};
|
||||
|
||||
const onUndo = () => {
|
||||
model.undo();
|
||||
setRedrawId((id) => id + 1);
|
||||
};
|
||||
|
||||
const updateRangeStyle = (stylePath: string, value: string) => {
|
||||
const {
|
||||
sheet,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
const row = Math.min(rowStart, rowEnd);
|
||||
const column = Math.min(columnStart, columnEnd);
|
||||
const range = {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
width: Math.abs(columnEnd - columnStart) + 1,
|
||||
height: Math.abs(rowEnd - rowStart) + 1,
|
||||
};
|
||||
model.updateRangeStyle(range, stylePath, value);
|
||||
setRedrawId((id) => id + 1);
|
||||
};
|
||||
|
||||
const onToggleUnderline = (value: boolean) => {
|
||||
updateRangeStyle("font.u", `${value}`);
|
||||
};
|
||||
|
||||
const onToggleItalic = (value: boolean) => {
|
||||
updateRangeStyle("font.i", `${value}`);
|
||||
};
|
||||
|
||||
const onToggleBold = (value: boolean) => {
|
||||
updateRangeStyle("font.b", `${value}`);
|
||||
};
|
||||
|
||||
const onToggleStrike = (value: boolean) => {
|
||||
updateRangeStyle("font.strike", `${value}`);
|
||||
};
|
||||
|
||||
const onToggleHorizontalAlign = (value: string) => {
|
||||
updateRangeStyle("alignment.horizontal", value);
|
||||
};
|
||||
|
||||
const onToggleVerticalAlign = (value: string) => {
|
||||
updateRangeStyle("alignment.vertical", value);
|
||||
};
|
||||
|
||||
const onTextColorPicked = (hex: string) => {
|
||||
updateRangeStyle("font.color", hex);
|
||||
};
|
||||
|
||||
const onFillColorPicked = (hex: string) => {
|
||||
updateRangeStyle("fill.fg_color", hex);
|
||||
};
|
||||
|
||||
const onNumberFormatPicked = (numberFmt: string) => {
|
||||
updateRangeStyle("num_fmt", numberFmt);
|
||||
};
|
||||
|
||||
const onCopyStyles = () => {
|
||||
const {
|
||||
sheet,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
const row1 = Math.min(rowStart, rowEnd);
|
||||
const column1 = Math.min(columnStart, columnEnd);
|
||||
const row2 = Math.max(rowStart, rowEnd);
|
||||
const column2 = Math.max(columnStart, columnEnd);
|
||||
|
||||
const styles = [];
|
||||
for (let row = row1; row <= row2; row++) {
|
||||
const styleRow = [];
|
||||
for (let column = column1; column <= column2; column++) {
|
||||
styleRow.push(model.getCellStyle(sheet, row, column));
|
||||
}
|
||||
styles.push(styleRow);
|
||||
}
|
||||
console.log("set styles", styles);
|
||||
workbookState.setCopyStyles(styles);
|
||||
const el = rootRef.current?.getElementsByClassName("sheet-container")[0];
|
||||
if (el) {
|
||||
(el as HTMLElement).style.cursor =
|
||||
`url('data:image/svg+xml;utf8,<svg data-v-56bd7dfc="" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-paintbrush-vertical"><path d="M10 2v2"></path><path d="M14 2v4"></path><path d="M17 2a1 1 0 0 1 1 1v9H6V3a1 1 0 0 1 1-1z"></path><path d="M6 12a1 1 0 0 0-1 1v1a2 2 0 0 0 2 2h2a1 1 0 0 1 1 1v2.9a2 2 0 1 0 4 0V17a1 1 0 0 1 1-1h2a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1"></path></svg>'), auto`;
|
||||
}
|
||||
};
|
||||
|
||||
// FIXME: My gut tells me that we should have only one on onKeyPressed function that goes to
|
||||
// the Rust end
|
||||
const { onKeyDown } = useKeyboardNavigation({
|
||||
onCellsDeleted: function (): void {
|
||||
const {
|
||||
sheet,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
const row = Math.min(rowStart, rowEnd);
|
||||
const column = Math.min(columnStart, columnEnd);
|
||||
|
||||
const width = Math.abs(columnEnd - columnStart) + 1;
|
||||
const height = Math.abs(rowEnd - rowStart) + 1;
|
||||
model.rangeClearContents(
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row + height,
|
||||
column + width
|
||||
);
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onExpandAreaSelectedKeyboard: function (
|
||||
key: "ArrowRight" | "ArrowLeft" | "ArrowUp" | "ArrowDown"
|
||||
): void {
|
||||
model.onExpandSelectedRange(key);
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onEditKeyPressStart: function (initText: string): void {
|
||||
console.log(initText);
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
onCellEditStart: function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
onBold: () => {
|
||||
let { sheet, row, column } = model.getSelectedView();
|
||||
let value = !model.getCellStyle(sheet, row, column).font.b;
|
||||
onToggleBold(!value);
|
||||
},
|
||||
onItalic: () => {
|
||||
let { sheet, row, column } = model.getSelectedView();
|
||||
let value = !model.getCellStyle(sheet, row, column).font.i;
|
||||
onToggleItalic(!value);
|
||||
},
|
||||
onUnderline: () => {
|
||||
let { sheet, row, column } = model.getSelectedView();
|
||||
let value = !model.getCellStyle(sheet, row, column).font.u;
|
||||
onToggleUnderline(!value);
|
||||
},
|
||||
onNavigationToEdge: function (direction: NavigationKey): void {
|
||||
console.log(direction);
|
||||
// const newSelectedCell = model.getNavigationEdge(
|
||||
// key,
|
||||
// selectedSheet,
|
||||
// selectedCell.row,
|
||||
// selectedCell.column,
|
||||
// canvas.lastRow,
|
||||
// canvas.lastColumn,
|
||||
// );
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onPageDown: function (): void {
|
||||
model.onPageDown();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onPageUp: function (): void {
|
||||
model.onPageUp();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowDown: function (): void {
|
||||
model.onArrowDown();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowUp: function (): void {
|
||||
model.onArrowUp();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowLeft: function (): void {
|
||||
model.onArrowLeft();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowRight: function (): void {
|
||||
model.onArrowRight();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onKeyHome: function (): void {
|
||||
const view = model.getSelectedView();
|
||||
const cell = model.getSelectedCell();
|
||||
model.setSelectedCell(cell[1], 1);
|
||||
model.setTopLeftVisibleCell(view.top_row, 1);
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onKeyEnd: function (): void {
|
||||
const view = model.getSelectedView();
|
||||
const cell = model.getSelectedCell();
|
||||
model.setSelectedCell(cell[1], LAST_COLUMN);
|
||||
model.setTopLeftVisibleCell(view.top_row, LAST_COLUMN - 5);
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onUndo: function (): void {
|
||||
model.undo();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onRedo: function (): void {
|
||||
model.redo();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onNextSheet: function (): void {
|
||||
const nextSheet = model.getSelectedSheet() + 1;
|
||||
if (nextSheet >= model.getWorksheetsProperties().length) {
|
||||
model.setSelectedSheet(0);
|
||||
} else {
|
||||
model.setSelectedSheet(nextSheet);
|
||||
}
|
||||
},
|
||||
onPreviousSheet: function (): void {
|
||||
const nextSheet = model.getSelectedSheet() - 1;
|
||||
if (nextSheet < 0) {
|
||||
model.setSelectedSheet(model.getWorksheetsProperties().length - 1);
|
||||
} else {
|
||||
model.setSelectedSheet(nextSheet);
|
||||
}
|
||||
},
|
||||
root: rootRef,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) {
|
||||
return;
|
||||
}
|
||||
rootRef.current.focus();
|
||||
});
|
||||
|
||||
const {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
|
||||
const cellAddress = getCellAddress(
|
||||
{ rowStart, rowEnd, columnStart, columnEnd },
|
||||
{ row, column }
|
||||
);
|
||||
const formulaValue = model.getCellContent(sheet, row, column);
|
||||
|
||||
const style = model.getCellStyle(sheet, row, column);
|
||||
|
||||
return (
|
||||
<Container ref={rootRef} onKeyDown={onKeyDown} tabIndex={0}>
|
||||
<Toolbar
|
||||
canUndo={model.canUndo()}
|
||||
canRedo={model.canRedo()}
|
||||
onRedo={onRedo}
|
||||
onUndo={onUndo}
|
||||
onToggleUnderline={onToggleUnderline}
|
||||
onToggleBold={onToggleBold}
|
||||
onToggleItalic={onToggleItalic}
|
||||
onToggleStrike={onToggleStrike}
|
||||
onToggleHorizontalAlign={onToggleHorizontalAlign}
|
||||
onToggleVerticalAlign={onToggleVerticalAlign}
|
||||
onCopyStyles={onCopyStyles}
|
||||
onTextColorPicked={onTextColorPicked}
|
||||
onFillColorPicked={onFillColorPicked}
|
||||
onNumberFormatPicked={onNumberFormatPicked}
|
||||
onBorderChanged={function (border: BorderOptions): void {
|
||||
const {
|
||||
sheet,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
const row = Math.min(rowStart, rowEnd);
|
||||
const column = Math.min(columnStart, columnEnd);
|
||||
|
||||
const width = Math.abs(columnEnd - columnStart) + 1;
|
||||
const height = Math.abs(rowEnd - rowStart) + 1;
|
||||
const borderArea = {
|
||||
type: border.border,
|
||||
item: border,
|
||||
};
|
||||
model.setAreaWithBorder(
|
||||
{ sheet, row, column, width, height },
|
||||
borderArea
|
||||
);
|
||||
setRedrawId((id) => id + 1);
|
||||
}}
|
||||
fillColor={style.fill.fg_color || "#FFF"}
|
||||
fontColor={style.font.color}
|
||||
bold={style.font.b}
|
||||
underline={style.font.u}
|
||||
italic={style.font.i}
|
||||
strike={style.font.strike}
|
||||
horizontalAlign={
|
||||
style.alignment ? style.alignment.horizontal : "general"
|
||||
}
|
||||
verticalAlign={style.alignment ? style.alignment.vertical : "center"}
|
||||
canEdit={true}
|
||||
numFmt={""}
|
||||
showGridLines={model.getShowGridLines(sheet)}
|
||||
onToggleShowGridLines={(show) => {
|
||||
model.setShowGridLines(sheet, show);
|
||||
setRedrawId((id) => id + 1);
|
||||
}}
|
||||
/>
|
||||
<FormulaBar
|
||||
cellAddress={cellAddress}
|
||||
formulaValue={formulaValue}
|
||||
onChange={(value) => {
|
||||
console.log('set', sheet, row, column, value);
|
||||
model.setUserInput(sheet, row, column, value);
|
||||
setRedrawId((id) => id + 1);
|
||||
}}
|
||||
/>
|
||||
<Worksheet
|
||||
model={model}
|
||||
workbookState={workbookState}
|
||||
refresh={(): void => {
|
||||
setRedrawId((id) => id + 1);
|
||||
}}
|
||||
/>
|
||||
<Navigation
|
||||
sheets={info}
|
||||
selectedIndex={model.getSelectedSheet()}
|
||||
onSheetSelected={function (sheet: number): void {
|
||||
model.setSelectedSheet(sheet);
|
||||
setRedrawId((value) => value + 1);
|
||||
}}
|
||||
onAddBlankSheet={function (): void {
|
||||
model.newSheet();
|
||||
}}
|
||||
onSheetColorChanged={function (hex: string): void {
|
||||
try {
|
||||
model.setSheetColor(model.getSelectedSheet(), hex);
|
||||
} catch (e) {
|
||||
alert(`${e}`);
|
||||
}
|
||||
}}
|
||||
onSheetRenamed={function (name: string): void {
|
||||
try {
|
||||
model.renameSheet(model.getSelectedSheet(), name);
|
||||
} catch (e) {
|
||||
alert(`${e}`);
|
||||
}
|
||||
}}
|
||||
onSheetDeleted={function (): void {
|
||||
model.deleteSheet(model.getSelectedSheet());
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
const Container = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Workbook;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user