Compare commits
16 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d83cc87c9 | ||
|
|
0ba80035d2 | ||
|
|
55a043366a | ||
|
|
864a37f1e6 | ||
|
|
72c7c94f3d | ||
|
|
c3a9b006d2 | ||
|
|
b37397acb8 | ||
|
|
49c3b14bf0 | ||
|
|
d2cba48f8e | ||
|
|
f752c90058 | ||
|
|
a78d5593f2 | ||
|
|
079208a1bd | ||
|
|
4721582dfe | ||
|
|
1746eec5da | ||
|
|
f9cf86a17c | ||
|
|
49ef846ebd |
2
.github/workflows/test-coverage.yaml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Coverage
|
||||
|
||||
on: [pull_request, push]
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
|
||||
3
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
target/*
|
||||
target/*
|
||||
.DS_Store
|
||||
12
Cargo.lock
generated
@@ -370,7 +370,6 @@ dependencies = [
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -679,17 +678,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
|
||||
12
Makefile
@@ -1,13 +1,6 @@
|
||||
all:
|
||||
cargo build --release
|
||||
cd bindings/wasm/ && make
|
||||
cd webapp && npm install && npm run build
|
||||
|
||||
lint:
|
||||
cargo fmt -- --check
|
||||
# TODO: See issue #33
|
||||
# cargo clippy --all-targets --all-features -- -D warnings -D clippy::expect_used -D clippy::unwrap_used -D clippy::panic
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo clippy --all-targets --all-features
|
||||
|
||||
format:
|
||||
cargo fmt
|
||||
@@ -17,7 +10,7 @@ tests: lint
|
||||
./target/debug/documentation
|
||||
cmp functions.md wiki/functions.md || exit 1
|
||||
make remove-artifacts
|
||||
cd bindings/wasm/ && make tests
|
||||
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs
|
||||
|
||||
remove-artifacts:
|
||||
rm -f xlsx/hello-calc.xlsx
|
||||
@@ -32,7 +25,6 @@ clean: remove-artifacts
|
||||
rm -f cargo-test-*
|
||||
rm -f base/cargo-test-*
|
||||
rm -f xlsx/cargo-test-*
|
||||
rm -r -f webapp/node_modules
|
||||
|
||||
|
||||
coverage:
|
||||
|
||||
BIN
assets/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
assets/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 441 B |
BIN
assets/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 729 B |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 15 KiB |
BIN
assets/icon/ironcalc_icon.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
8
assets/icon/ironcalc_icon.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="600" height="600" rx="20" fill="#F2994A"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/logo.png
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 33 KiB |
BIN
assets/logo/png/black.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/logo/png/orange+black.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/logo/png/orange+white.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
assets/logo/png/white.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
8
assets/logo/svg/black.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
8
assets/logo/svg/orange+black.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
8
assets/logo/svg/orange+white.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
8
assets/logo/svg/white.svg
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
@@ -12,8 +12,6 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
ryu = "1.0"
|
||||
chrono = "0.4"
|
||||
chrono-tz = "0.9"
|
||||
@@ -21,6 +19,9 @@ regex = "1.0"
|
||||
once_cell = "1.16.0"
|
||||
bitcode = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.69" }
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use crate::{
|
||||
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
/// A CellValue is the representation of the cell content.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum CellValue {
|
||||
None,
|
||||
String(String),
|
||||
@@ -14,17 +11,6 @@ pub enum CellValue {
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
impl CellValue {
|
||||
pub fn to_json_str(&self) -> String {
|
||||
match &self {
|
||||
CellValue::None => "null".to_string(),
|
||||
CellValue::String(s) => json!(s).to_string(),
|
||||
CellValue::Number(f) => json!(f).to_string(),
|
||||
CellValue::Boolean(b) => json!(b).to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for CellValue {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Number(value)
|
||||
|
||||
@@ -222,7 +222,7 @@ impl Parser {
|
||||
|
||||
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
|
||||
self.lexer.set_formula(formula);
|
||||
self.context = context.clone();
|
||||
self.context.clone_from(context);
|
||||
self.parse_expr()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::fmt;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
use crate::language::Language;
|
||||
|
||||
@@ -81,8 +80,7 @@ impl fmt::Display for OpProduct {
|
||||
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
|
||||
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
|
||||
/// Note that they are serialized/deserialized by index
|
||||
#[derive(Serialize_repr, Deserialize_repr, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u8)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum Error {
|
||||
REF,
|
||||
NAME,
|
||||
|
||||
1
base/src/language/language.bin
Normal file
@@ -0,0 +1 @@
|
||||
PfrendeesD<>VRAITRUEWAHRVERDADEROTVFAUXFALSEFALSCHFALSOUw#REF!#REF!#BEZUG!#¡REF!e<>#NOM?#NAME?#NAME?#¿NOMBRE?x<>#VALEUR!#VALUE!#WERT!#¡VALOR!w<>#DIV/0!#DIV/0!#DIV/0!#¡DIV/0!<04>#N/A#N/A#NV#N/AXv#NOMBRE!#NUM!#ZAHL!#¡NUM!<02><>#N/IMPL!#N/IMPL!#N/IMPL!#N/IMPL!w{#SPILL!#SPILL!#ÜBERLAUF!#SPILL!ff#CALC!#CALC!#CALC!#CALC!ff#CIRC!#CIRC!#CIRC!#CIRC!ww#ERROR!#ERROR!#ERROR!#ERROR!ff#NULL!#NULL!#NULL!#NULL!
|
||||
@@ -1,15 +1,15 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Booleans {
|
||||
pub r#true: String,
|
||||
pub r#false: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Errors {
|
||||
pub r#ref: String,
|
||||
pub name: String,
|
||||
@@ -25,14 +25,14 @@ pub struct Errors {
|
||||
pub null: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Language {
|
||||
pub booleans: Booleans,
|
||||
pub errors: Errors,
|
||||
}
|
||||
|
||||
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
|
||||
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file")
|
||||
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
|
||||
});
|
||||
|
||||
pub fn get_language(id: &str) -> Result<&Language, String> {
|
||||
|
||||
@@ -58,3 +58,4 @@ 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;
|
||||
|
||||
BIN
base/src/locale/locales.bin
Normal file
@@ -1,32 +1,29 @@
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Locale {
|
||||
pub dates: Dates,
|
||||
pub numbers: NumbersProperties,
|
||||
pub currency: Currency,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Currency {
|
||||
pub iso: String,
|
||||
pub symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct NumbersProperties {
|
||||
#[serde(rename = "symbols-numberSystem-latn")]
|
||||
pub symbols: NumbersSymbols,
|
||||
#[serde(rename = "decimalFormats-numberSystem-latn")]
|
||||
pub decimal_formats: DecimalFormats,
|
||||
#[serde(rename = "currencyFormats-numberSystem-latn")]
|
||||
pub currency_formats: CurrencyFormats,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct Dates {
|
||||
pub day_names: Vec<String>,
|
||||
pub day_names_short: Vec<String>,
|
||||
@@ -35,8 +32,7 @@ pub struct Dates {
|
||||
pub months_letter: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct NumbersSymbols {
|
||||
pub decimal: String,
|
||||
pub group: String,
|
||||
@@ -54,31 +50,23 @@ pub struct NumbersSymbols {
|
||||
}
|
||||
|
||||
// See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct CurrencyFormats {
|
||||
pub standard: String,
|
||||
#[serde(rename = "standard-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub standard_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "standard-noCurrency")]
|
||||
pub standard_no_currency: String,
|
||||
pub accounting: String,
|
||||
#[serde(rename = "accounting-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub accounting_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "accounting-noCurrency")]
|
||||
pub accounting_no_currency: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
pub struct DecimalFormats {
|
||||
pub standard: String,
|
||||
}
|
||||
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| {
|
||||
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
|
||||
});
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> =
|
||||
Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
|
||||
|
||||
pub fn get_locale(id: &str) -> Result<&Locale, String> {
|
||||
// TODO: pass the locale once we implement locales in Rust
|
||||
|
||||
@@ -118,6 +118,8 @@ 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
|
||||
@@ -681,6 +683,13 @@ 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 {
|
||||
@@ -886,6 +895,7 @@ impl Model {
|
||||
language,
|
||||
locale,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
|
||||
model.parse_formulas();
|
||||
|
||||
@@ -6,14 +6,18 @@ use crate::{
|
||||
calc_result::Range,
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::stringify::{rename_sheet_in_node, to_rc_format},
|
||||
parser::Parser,
|
||||
parser::{
|
||||
stringify::{rename_sheet_in_node, to_rc_format},
|
||||
Parser,
|
||||
},
|
||||
types::CellReferenceRC,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet},
|
||||
types::{
|
||||
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
|
||||
},
|
||||
utils::ParsedReference,
|
||||
};
|
||||
|
||||
@@ -33,7 +37,20 @@ 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) -> Worksheet {
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
Worksheet {
|
||||
cols: vec![],
|
||||
rows: vec![],
|
||||
@@ -48,6 +65,8 @@ impl Model {
|
||||
color: Default::default(),
|
||||
frozen_columns: 0,
|
||||
frozen_rows: 0,
|
||||
show_grid_lines: true,
|
||||
views,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +141,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());
|
||||
@@ -153,7 +172,8 @@ impl Model {
|
||||
let sheet_name = format!("{}{}", base_name, index);
|
||||
// Now we need a sheet_id
|
||||
let sheet_id = self.get_new_sheet_id();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
|
||||
self.workbook.worksheets.push(worksheet);
|
||||
self.reset_parsed_structures();
|
||||
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
|
||||
@@ -184,7 +204,8 @@ impl Model {
|
||||
Some(id) => id,
|
||||
None => self.get_new_sheet_id(),
|
||||
};
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
|
||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
|
||||
if sheet_index as usize > self.workbook.worksheets.len() {
|
||||
return Err("Sheet index out of range".to_string());
|
||||
}
|
||||
@@ -331,11 +352,21 @@ 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)],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])],
|
||||
styles: Default::default(),
|
||||
name: name.to_string(),
|
||||
settings: WorkbookSettings {
|
||||
@@ -351,6 +382,7 @@ impl Model {
|
||||
last_modified: now,
|
||||
},
|
||||
tables: HashMap::new(),
|
||||
views,
|
||||
};
|
||||
let parsed_formulas = Vec::new();
|
||||
let worksheets = &workbook.worksheets;
|
||||
@@ -371,6 +403,7 @@ impl Model {
|
||||
locale,
|
||||
language,
|
||||
tz,
|
||||
view_id: 0,
|
||||
};
|
||||
model.parse_formulas();
|
||||
Ok(model)
|
||||
|
||||
@@ -76,10 +76,16 @@ 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("A1"), "-6.58066304055116+7.58155274274654i");
|
||||
assert_eq!(model._get_text("A3"), "TRUE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -53,4 +53,5 @@ mod test_frozen_rows_and_columns;
|
||||
mod test_get_cell_content;
|
||||
mod test_percentage;
|
||||
mod test_today;
|
||||
mod test_types;
|
||||
mod user_model;
|
||||
|
||||
24
base/src/test/test_types.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::types::{Alignment, HorizontalAlignment, VerticalAlignment};
|
||||
|
||||
#[test]
|
||||
fn alignment_default() {
|
||||
let alignment = Alignment::default();
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Bottom,
|
||||
wrap_text: false
|
||||
}
|
||||
);
|
||||
|
||||
let s = serde_json::to_string(&alignment).unwrap();
|
||||
// defaults stringifies as an empty object
|
||||
assert_eq!(s, "{}");
|
||||
|
||||
let a: Alignment = serde_json::from_str("{}").unwrap();
|
||||
|
||||
assert_eq!(a, alignment)
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
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_styles;
|
||||
mod test_to_from_bytes;
|
||||
mod test_undo_redo;
|
||||
mod test_view;
|
||||
|
||||
404
base/src/test/user_model/test_autofill_columns.rs
Normal file
@@ -0,0 +1,404 @@
|
||||
#![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())
|
||||
);
|
||||
}
|
||||
399
base/src/test/user_model/test_autofill_rows.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
#![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())
|
||||
);
|
||||
}
|
||||
42
base/src/test/user_model/test_grid_lines.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![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));
|
||||
}
|
||||
@@ -144,13 +144,18 @@ fn basic_fill() {
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
assert_eq!(style.fill.fg_color, None);
|
||||
|
||||
// bg_color
|
||||
model
|
||||
.update_range_style(&range, "fill.bg_color", "#F2F2F2")
|
||||
.unwrap();
|
||||
model
|
||||
.update_range_style(&range, "fill.fg_color", "#F3F4F5")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
||||
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
@@ -159,6 +164,7 @@ fn basic_fill() {
|
||||
|
||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
||||
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -171,9 +177,15 @@ fn fill_errors() {
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
assert!(model
|
||||
.update_range_style(&range, "fill.bg_color", "#FFF")
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "fill.bg_color", "#FFF"),
|
||||
Err("Invalid color: '#FFF'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "fill.fg_color", "#FFF"),
|
||||
Err("Invalid color: '#FFF'.".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
216
base/src/test/user_model/test_view.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
#![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));
|
||||
}
|
||||
@@ -4,37 +4,15 @@ use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
|
||||
// Useful for `#[serde(default = "default_as_true")]`
|
||||
fn default_as_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_as_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// Useful for `#[serde(skip_serializing_if = "is_true")]`
|
||||
fn is_true(b: &bool) -> bool {
|
||||
*b
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!*b
|
||||
}
|
||||
|
||||
fn is_zero(num: &i32) -> bool {
|
||||
*num == 0
|
||||
}
|
||||
|
||||
fn is_default_alignment(o: &Option<Alignment>) -> bool {
|
||||
o.is_none() || *o == Some(Alignment::default())
|
||||
}
|
||||
|
||||
fn hashmap_is_empty(h: &HashMap<String, Table>) -> bool {
|
||||
h.values().len() == 0
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Metadata {
|
||||
pub application: String,
|
||||
pub app_version: String,
|
||||
@@ -44,14 +22,25 @@ pub struct Metadata {
|
||||
pub last_modified: String, //"2020-11-20T16:24:35"
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
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(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Workbook {
|
||||
pub shared_strings: Vec<String>,
|
||||
pub defined_names: Vec<DefinedName>,
|
||||
@@ -60,28 +49,22 @@ pub struct Workbook {
|
||||
pub name: String,
|
||||
pub settings: WorkbookSettings,
|
||||
pub metadata: Metadata,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "hashmap_is_empty")]
|
||||
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
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct DefinedName {
|
||||
pub name: String,
|
||||
pub formula: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum SheetState {
|
||||
Visible,
|
||||
Hidden,
|
||||
@@ -98,8 +81,25 @@ 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 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
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Worksheet {
|
||||
pub dimension: String,
|
||||
pub cols: Vec<Col>,
|
||||
@@ -109,16 +109,14 @@ pub struct Worksheet {
|
||||
pub shared_formulas: Vec<String>,
|
||||
pub sheet_id: u32,
|
||||
pub state: SheetState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
pub merge_cells: Vec<String>,
|
||||
pub comments: Vec<Comment>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub frozen_rows: i32,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
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,
|
||||
}
|
||||
|
||||
/// Internal representation of Excel's sheet_data
|
||||
@@ -126,7 +124,7 @@ pub struct Worksheet {
|
||||
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
|
||||
|
||||
// ECMA-376-1:2016 section 18.3.1.73
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Row {
|
||||
/// Row index
|
||||
pub r: i32,
|
||||
@@ -134,23 +132,19 @@ pub struct Row {
|
||||
pub custom_format: bool,
|
||||
pub custom_height: bool,
|
||||
pub s: i32,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
// ECMA-376-1:2016 section 18.3.1.13
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Col {
|
||||
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
|
||||
/// First column affected by this record. Settings apply to column in \[min, max\] range.
|
||||
pub min: i32,
|
||||
/// Last column affected by this record. Settings apply to column in \[min, max\] range.
|
||||
pub max: i32,
|
||||
|
||||
pub width: f64,
|
||||
pub custom_width: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub style: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -165,32 +159,55 @@ pub enum CellType {
|
||||
CompoundData = 128,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq)]
|
||||
#[serde(tag = "t", deny_unknown_fields)]
|
||||
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
|
||||
pub enum Cell {
|
||||
#[serde(rename = "empty")]
|
||||
EmptyCell { s: i32 },
|
||||
#[serde(rename = "b")]
|
||||
BooleanCell { v: bool, s: i32 },
|
||||
#[serde(rename = "n")]
|
||||
NumberCell { v: f64, s: i32 },
|
||||
EmptyCell {
|
||||
s: i32,
|
||||
},
|
||||
|
||||
BooleanCell {
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
NumberCell {
|
||||
v: f64,
|
||||
s: i32,
|
||||
},
|
||||
// Maybe we should not have this type. In Excel this is just a string
|
||||
#[serde(rename = "e")]
|
||||
ErrorCell { ei: Error, s: i32 },
|
||||
ErrorCell {
|
||||
ei: Error,
|
||||
s: i32,
|
||||
},
|
||||
// Always a shared string
|
||||
#[serde(rename = "s")]
|
||||
SharedString { si: i32, s: i32 },
|
||||
SharedString {
|
||||
si: i32,
|
||||
s: i32,
|
||||
},
|
||||
// Non evaluated Formula
|
||||
#[serde(rename = "u")]
|
||||
CellFormula { f: i32, s: i32 },
|
||||
#[serde(rename = "fb")]
|
||||
CellFormulaBoolean { f: i32, v: bool, s: i32 },
|
||||
#[serde(rename = "fn")]
|
||||
CellFormulaNumber { f: i32, v: f64, s: i32 },
|
||||
CellFormula {
|
||||
f: i32,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaBoolean {
|
||||
f: i32,
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaNumber {
|
||||
f: i32,
|
||||
v: f64,
|
||||
s: i32,
|
||||
},
|
||||
// always inline string
|
||||
#[serde(rename = "str")]
|
||||
CellFormulaString { f: i32, v: String, s: i32 },
|
||||
#[serde(rename = "fe")]
|
||||
CellFormulaString {
|
||||
f: i32,
|
||||
v: String,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaError {
|
||||
f: i32,
|
||||
ei: Error,
|
||||
@@ -209,17 +226,16 @@ impl Default for Cell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Comment {
|
||||
pub text: String,
|
||||
pub author_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub author_id: Option<String>,
|
||||
pub cell_ref: String,
|
||||
}
|
||||
|
||||
// ECMA-376-1:2016 section 18.5.1.2
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Table {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
@@ -227,34 +243,24 @@ pub struct Table {
|
||||
pub reference: String,
|
||||
pub totals_row_count: u32,
|
||||
pub header_row_count: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_dxf_id: Option<u32>,
|
||||
pub columns: Vec<TableColumn>,
|
||||
pub style_info: TableStyleInfo,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub has_filters: bool,
|
||||
}
|
||||
|
||||
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
|
||||
// the totals_row_function is an enum not String methinks
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct TableColumn {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_label: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_function: Option<String>,
|
||||
}
|
||||
|
||||
@@ -272,25 +278,16 @@ impl Default for TableColumn {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct TableStyleInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_first_column: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_last_column: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_row_stripes: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_column_stripes: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Styles {
|
||||
pub num_fmts: Vec<NumFmt>,
|
||||
pub fonts: Vec<Font>,
|
||||
@@ -326,7 +323,7 @@ pub struct Style {
|
||||
pub quote_prefix: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct NumFmt {
|
||||
pub num_fmt_id: i32,
|
||||
pub format_code: String,
|
||||
@@ -516,29 +513,17 @@ pub struct Alignment {
|
||||
pub wrap_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CellStyleXfs {
|
||||
pub num_fmt_id: i32,
|
||||
pub font_id: i32,
|
||||
pub fill_id: i32,
|
||||
pub border_id: i32,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_number_format: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_border: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_alignment: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_protection: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_font: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_fill: bool,
|
||||
}
|
||||
|
||||
@@ -559,39 +544,24 @@ impl Default for CellStyleXfs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct CellXfs {
|
||||
pub xf_id: i32,
|
||||
pub num_fmt_id: i32,
|
||||
pub font_id: i32,
|
||||
pub fill_id: i32,
|
||||
pub border_id: i32,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_number_format: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_border: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_alignment: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_protection: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_font: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_fill: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub quote_prefix: bool,
|
||||
#[serde(skip_serializing_if = "is_default_alignment")]
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CellStyles {
|
||||
pub name: String,
|
||||
pub xf_id: i32,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::{collections::HashMap, fmt::Debug};
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
constants,
|
||||
@@ -12,164 +12,35 @@ use crate::{
|
||||
},
|
||||
model::Model,
|
||||
types::{
|
||||
Alignment, BorderItem, BorderStyle, Cell, CellType, Col, HorizontalAlignment, Row,
|
||||
SheetProperties, Style, VerticalAlignment,
|
||||
Alignment, BorderItem, BorderStyle, CellType, Col, HorizontalAlignment, SheetProperties,
|
||||
Style, VerticalAlignment,
|
||||
},
|
||||
utils::is_valid_hex_color,
|
||||
};
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
struct RowData {
|
||||
row: Option<Row>,
|
||||
data: HashMap<i32, Cell>,
|
||||
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 ColumnData {
|
||||
column: Option<Col>,
|
||||
data: HashMap<i32, Cell>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
enum Diff {
|
||||
// Cell diffs
|
||||
SetCellValue {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
new_value: String,
|
||||
old_value: Box<Option<Cell>>,
|
||||
},
|
||||
CellClearContents {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Cell>>,
|
||||
},
|
||||
CellClearAll {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Option<Cell>>,
|
||||
old_style: Box<Style>,
|
||||
},
|
||||
SetCellStyle {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
old_value: Box<Style>,
|
||||
new_value: Box<Style>,
|
||||
},
|
||||
// Column and Row diffs
|
||||
SetColumnWidth {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
new_value: f64,
|
||||
old_value: f64,
|
||||
},
|
||||
SetRowHeight {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
new_value: f64,
|
||||
old_value: f64,
|
||||
},
|
||||
InsertRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
},
|
||||
DeleteRow {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
old_data: Box<RowData>,
|
||||
},
|
||||
InsertColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
},
|
||||
DeleteColumn {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
SetFrozenRowsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
old_value: i32,
|
||||
},
|
||||
SetFrozenColumnsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
old_value: i32,
|
||||
},
|
||||
DeleteSheet {
|
||||
sheet: u32,
|
||||
},
|
||||
NewSheet {
|
||||
index: u32,
|
||||
name: String,
|
||||
},
|
||||
RenameSheet {
|
||||
index: u32,
|
||||
old_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
SetSheetColor {
|
||||
index: u32,
|
||||
old_value: String,
|
||||
new_value: String,
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
/// This is the struct for a border area
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BorderArea {
|
||||
item: BorderItem,
|
||||
r#type: BorderType,
|
||||
}
|
||||
|
||||
fn boolean(value: &str) -> Result<bool, String> {
|
||||
@@ -249,7 +120,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_ and automatic evaluation.
|
||||
/// UserModel is a wrapper around Model with undo/redo history, _diffs_, automatic evaluation and view management.
|
||||
///
|
||||
/// A diff in this context (or more correctly a _user diff_) is a change created by a user.
|
||||
///
|
||||
@@ -275,7 +146,7 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct UserModel {
|
||||
model: Model,
|
||||
pub(crate) model: Model,
|
||||
history: History,
|
||||
send_queue: Vec<QueueDiffs>,
|
||||
pause_evaluation: bool,
|
||||
@@ -811,6 +682,154 @@ 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]
|
||||
@@ -849,7 +868,7 @@ impl UserModel {
|
||||
style.fill.fg_color = color(value)?;
|
||||
}
|
||||
"num_fmt" => {
|
||||
style.num_fmt = value.to_owned();
|
||||
value.clone_into(&mut style.num_fmt);
|
||||
}
|
||||
"border.left" => {
|
||||
style.border.left = border(value)?;
|
||||
@@ -910,7 +929,7 @@ impl UserModel {
|
||||
column,
|
||||
old_value: Box::new(old_value),
|
||||
new_value: Box::new(style),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
self.push_diff_list(diff_list);
|
||||
@@ -926,6 +945,208 @@ 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:
|
||||
@@ -935,6 +1156,24 @@ 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) {
|
||||
@@ -1098,6 +1337,13 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, old_value)?;
|
||||
}
|
||||
Diff::SetShowGridLines {
|
||||
sheet,
|
||||
old_value,
|
||||
new_value: _,
|
||||
} => {
|
||||
self.model.set_show_grid_lines(*sheet, *old_value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if needs_evaluation {
|
||||
@@ -1218,6 +1464,13 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, new_value)?;
|
||||
}
|
||||
Diff::SetShowGridLines {
|
||||
sheet,
|
||||
old_value: _,
|
||||
new_value,
|
||||
} => {
|
||||
self.model.set_show_grid_lines(*sheet, *new_value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1232,7 +1485,7 @@ impl UserModel {
|
||||
mod tests {
|
||||
use crate::{
|
||||
types::{HorizontalAlignment, VerticalAlignment},
|
||||
user_model::{horizontal, vertical},
|
||||
user_model::common::{horizontal, vertical},
|
||||
};
|
||||
|
||||
#[test]
|
||||
164
base/src/user_model/history.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
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,
|
||||
}
|
||||
12
base/src/user_model/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
mod common;
|
||||
mod history;
|
||||
mod ui;
|
||||
|
||||
pub use common::UserModel;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use ui::SelectedView;
|
||||
|
||||
pub use common::BorderArea;
|
||||
671
base/src/user_model/ui.rs
Normal file
@@ -0,0 +1,671 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expressions::utils::{is_valid_column_number, is_valid_row};
|
||||
|
||||
use super::common::UserModel;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Debug))]
|
||||
pub struct SelectedView {
|
||||
pub sheet: u32,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub range: [i32; 4],
|
||||
pub top_row: i32,
|
||||
pub left_column: i32,
|
||||
}
|
||||
|
||||
impl UserModel {
|
||||
/// Returns the selected sheet index
|
||||
pub fn get_selected_sheet(&self) -> u32 {
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the selected cell
|
||||
pub fn get_selected_cell(&self) -> (u32, i32, i32) {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
return (sheet, view.row, view.column);
|
||||
}
|
||||
}
|
||||
// return a safe default
|
||||
(0, 1, 1)
|
||||
}
|
||||
|
||||
/// Returns selected view
|
||||
pub fn get_selected_view(&self) -> SelectedView {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
return SelectedView {
|
||||
sheet,
|
||||
row: view.row,
|
||||
column: view.column,
|
||||
range: view.range,
|
||||
top_row: view.top_row,
|
||||
left_column: view.left_column,
|
||||
};
|
||||
}
|
||||
}
|
||||
// return a safe default
|
||||
SelectedView {
|
||||
sheet: 0,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
top_row: 1,
|
||||
left_column: 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the the selected sheet
|
||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
||||
view.sheet = sheet;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the selected cell
|
||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if !is_valid_column_number(column) {
|
||||
return Err(format!("Invalid column: '{column}'"));
|
||||
}
|
||||
if !is_valid_row(row) {
|
||||
return Err(format!("Invalid row: '{row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.row = row;
|
||||
view.column = column;
|
||||
view.range = [row, column, row, column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the selected range
|
||||
pub fn set_selected_range(
|
||||
&mut self,
|
||||
start_row: i32,
|
||||
start_column: i32,
|
||||
end_row: i32,
|
||||
end_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !is_valid_column_number(start_column) {
|
||||
return Err(format!("Invalid column: '{start_column}'"));
|
||||
}
|
||||
if !is_valid_row(start_row) {
|
||||
return Err(format!("Invalid row: '{start_row}'"));
|
||||
}
|
||||
|
||||
if !is_valid_column_number(end_column) {
|
||||
return Err(format!("Invalid column: '{end_column}'"));
|
||||
}
|
||||
if !is_valid_row(end_row) {
|
||||
return Err(format!("Invalid row: '{end_row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.range = [start_row, start_column, end_row, end_column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The selected range is expanded with the keyboard
|
||||
pub fn on_expand_selected_range(&mut self, key: &str) -> Result<(), String> {
|
||||
let (sheet, window_width, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.sheet,
|
||||
view.window_width as f64,
|
||||
view.window_height as f64,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let (selected_row, selected_column, range, top_row, left_column) =
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.row,
|
||||
view.column,
|
||||
view.range,
|
||||
view.top_row,
|
||||
view.left_column,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let [row_start, column_start, row_end, column_end] = range;
|
||||
|
||||
match key {
|
||||
"ArrowRight" => {
|
||||
if selected_column > column_start {
|
||||
let new_column = column_start + 1;
|
||||
if !(is_valid_column_number(new_column)) {
|
||||
return Ok(());
|
||||
}
|
||||
self.set_selected_range(row_start, new_column, row_end, column_end)?;
|
||||
} else {
|
||||
let new_column = column_end + 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
let mut width = 0.0;
|
||||
let mut c = left_column;
|
||||
while c <= new_column {
|
||||
width += self.model.get_column_width(sheet, c)?;
|
||||
c += 1;
|
||||
}
|
||||
if width > window_width {
|
||||
self.set_top_left_visible_cell(top_row, left_column + 1)?;
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, row_end, column_end + 1)?;
|
||||
}
|
||||
}
|
||||
"ArrowLeft" => {
|
||||
if selected_column < column_end {
|
||||
let new_column = column_end - 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
if new_column < left_column {
|
||||
self.set_top_left_visible_cell(top_row, new_column)?;
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, row_end, new_column)?;
|
||||
} else {
|
||||
let new_column = column_start - 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
if new_column < left_column {
|
||||
self.set_top_left_visible_cell(top_row, new_column)?;
|
||||
}
|
||||
self.set_selected_range(row_start, new_column, row_end, column_end)?;
|
||||
}
|
||||
}
|
||||
"ArrowUp" => {
|
||||
if selected_row < row_end {
|
||||
let new_row = row_end - 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, new_row, column_end)?;
|
||||
} else {
|
||||
let new_row = row_start - 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
if new_row < top_row {
|
||||
self.set_top_left_visible_cell(new_row, left_column)?;
|
||||
}
|
||||
self.set_selected_range(new_row, column_start, row_end, column_end)?;
|
||||
}
|
||||
}
|
||||
"ArrowDown" => {
|
||||
if selected_row > row_start {
|
||||
let new_row = row_start + 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
self.set_selected_range(new_row, column_start, row_end, column_end)?;
|
||||
} else {
|
||||
let new_row = row_end + 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
let mut height = 0.0;
|
||||
let mut r = top_row;
|
||||
while r <= new_row + 1 {
|
||||
height += self.model.get_row_height(sheet, r)?;
|
||||
r += 1;
|
||||
}
|
||||
if height >= window_height {
|
||||
self.set_top_left_visible_cell(top_row + 1, left_column)?;
|
||||
}
|
||||
self.set_selected_range(row_start, column_start, new_row, column_end)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the value of the first visible cell
|
||||
pub fn set_top_left_visible_cell(
|
||||
&mut self,
|
||||
top_row: i32,
|
||||
left_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !is_valid_column_number(left_column) {
|
||||
return Err(format!("Invalid column: '{left_column}'"));
|
||||
}
|
||||
if !is_valid_row(top_row) {
|
||||
return Err(format!("Invalid row: '{top_row}'"));
|
||||
}
|
||||
if self.model.workbook.worksheet(sheet).is_err() {
|
||||
return Err(format!("Invalid worksheet index {}", sheet));
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||
view.top_row = top_row;
|
||||
view.left_column = left_column;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the width of the window
|
||||
pub fn set_window_width(&mut self, window_width: f64) {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
view.window_width = window_width as i64;
|
||||
};
|
||||
}
|
||||
|
||||
/// Gets the width of the window
|
||||
pub fn get_window_width(&mut self) -> Result<i64, String> {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
return Ok(view.window_width);
|
||||
};
|
||||
Err("View not found".to_string())
|
||||
}
|
||||
|
||||
/// Sets the height of the window
|
||||
pub fn set_window_height(&mut self, window_height: f64) {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
view.window_height = window_height as i64;
|
||||
};
|
||||
}
|
||||
|
||||
/// Gets the height of the window
|
||||
pub fn get_window_height(&mut self) -> Result<i64, String> {
|
||||
if let Some(view) = self.model.workbook.views.get_mut(&self.model.view_id) {
|
||||
return Ok(view.window_height);
|
||||
};
|
||||
Err("View not found".to_string())
|
||||
}
|
||||
|
||||
/// User presses right arrow
|
||||
pub fn on_arrow_right(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_width) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_width)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_column = view.column + 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
let mut width = 0.0;
|
||||
let mut column = view.left_column;
|
||||
while column <= new_column {
|
||||
width += self.model.get_column_width(sheet, column)?;
|
||||
column += 1;
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.column = new_column;
|
||||
view.range = [view.row, new_column, view.row, new_column];
|
||||
if width > window_width as f64 {
|
||||
view.left_column += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User presses left arrow
|
||||
pub fn on_arrow_left(&mut self) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_column = view.column - 1;
|
||||
if !is_valid_column_number(new_column) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.column = new_column;
|
||||
view.range = [view.row, new_column, view.row, new_column];
|
||||
if new_column < view.left_column {
|
||||
view.left_column = new_column;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User presses up arrow key
|
||||
pub fn on_arrow_up(&mut self) -> Result<(), String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_row = view.row - 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the column is not fully visible we 'scroll' right until it is
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.row = new_row;
|
||||
view.range = [new_row, view.column, new_row, view.column];
|
||||
if new_row < view.top_row {
|
||||
view.top_row = new_row;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// User presses down arrow key
|
||||
pub fn on_arrow_down(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_height)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let new_row = view.row + 1;
|
||||
if !is_valid_row(new_row) {
|
||||
return Ok(());
|
||||
}
|
||||
// if the row is not fully visible we 'scroll' down until it is
|
||||
let mut height = 0.0;
|
||||
let mut row = view.top_row;
|
||||
while row <= new_row + 1 {
|
||||
height += self.model.get_row_height(sheet, row)?;
|
||||
row += 1;
|
||||
}
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.row = new_row;
|
||||
view.range = [new_row, view.column, new_row, view.column];
|
||||
if height > window_height as f64 {
|
||||
view.top_row += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: This function should be memoized
|
||||
/// Returns the x-coordinate of the cell in the top left corner
|
||||
pub fn get_scroll_x(&self) -> Result<f64, String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let mut scroll_x = 0.0;
|
||||
for column in 1..view.left_column {
|
||||
scroll_x += self.model.get_column_width(sheet, column)?;
|
||||
}
|
||||
Ok(scroll_x)
|
||||
}
|
||||
|
||||
// TODO: This function should be memoized
|
||||
/// Returns the y-coordinate of the cell in the top left corner
|
||||
pub fn get_scroll_y(&self) -> Result<f64, String> {
|
||||
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
view.sheet
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
let mut scroll_y = 0.0;
|
||||
for row in 1..view.top_row {
|
||||
scroll_y += self.model.get_row_height(sheet, row)?;
|
||||
}
|
||||
Ok(scroll_y)
|
||||
}
|
||||
|
||||
/// User presses page down
|
||||
pub fn on_page_down(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_height)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
|
||||
let mut height = 0.0;
|
||||
let mut last_row = view.top_row;
|
||||
while height <= window_height as f64 {
|
||||
height += self.model.get_row_height(sheet, last_row)?;
|
||||
last_row += 1;
|
||||
}
|
||||
if !is_valid_row(last_row) {
|
||||
return Ok(());
|
||||
}
|
||||
let row_delta = view.row - view.top_row;
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.top_row = last_row;
|
||||
view.row = view.top_row + row_delta;
|
||||
view.range = [view.row, view.column, view.row, view.column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// On page up
|
||||
pub fn on_page_up(&mut self) -> Result<(), String> {
|
||||
let (sheet, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(view.sheet, view.window_height)
|
||||
} else {
|
||||
return Err("View not found".to_string());
|
||||
};
|
||||
let worksheet = match self.model.workbook.worksheet(sheet) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err("Worksheet not found".to_string()),
|
||||
};
|
||||
let view = match worksheet.views.get(&self.model.view_id) {
|
||||
Some(s) => s,
|
||||
None => return Err("View not found".to_string()),
|
||||
};
|
||||
|
||||
let mut height = 0.0;
|
||||
let mut last_row = view.top_row;
|
||||
while height <= window_height as f64 && last_row > 1 {
|
||||
height += self.model.get_row_height(sheet, last_row)?;
|
||||
last_row -= 1;
|
||||
}
|
||||
let row_delta = view.row - view.top_row;
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.top_row = last_row;
|
||||
view.row = view.top_row + row_delta;
|
||||
view.range = [view.row, view.column, view.row, view.column];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// We extend the selection to cell (target_row, target_column)
|
||||
pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<(), String> {
|
||||
let (sheet, window_width, window_height) =
|
||||
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.sheet,
|
||||
view.window_width as f64,
|
||||
view.window_height as f64,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let (selected_row, selected_column, range, top_row, left_column) =
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
|
||||
if let Some(view) = worksheet.views.get(&self.model.view_id) {
|
||||
(
|
||||
view.row,
|
||||
view.column,
|
||||
view.range,
|
||||
view.top_row,
|
||||
view.left_column,
|
||||
)
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Ok(());
|
||||
};
|
||||
let [row_start, column_start, _row_end, _column_end] = range;
|
||||
|
||||
let mut new_left_column = left_column;
|
||||
if target_column >= selected_column {
|
||||
let mut width = 0.0;
|
||||
let mut column = left_column;
|
||||
while column <= target_column {
|
||||
width += self.model.get_column_width(sheet, column)?;
|
||||
column += 1;
|
||||
}
|
||||
|
||||
while width > window_width {
|
||||
width -= self.model.get_column_width(sheet, new_left_column)?;
|
||||
new_left_column += 1;
|
||||
}
|
||||
} else if target_column < new_left_column {
|
||||
new_left_column = target_column;
|
||||
}
|
||||
let mut new_top_row = top_row;
|
||||
if target_row >= selected_row {
|
||||
let mut height = 0.0;
|
||||
let mut row = top_row;
|
||||
while row <= target_row {
|
||||
height += self.model.get_row_height(sheet, row)?;
|
||||
row += 1;
|
||||
}
|
||||
while height > window_height {
|
||||
height -= self.model.get_row_height(sheet, new_top_row)?;
|
||||
new_top_row += 1;
|
||||
}
|
||||
} else if target_row < new_top_row {
|
||||
new_top_row = target_row;
|
||||
}
|
||||
|
||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||
if let Some(view) = worksheet.views.get_mut(&self.model.view_id) {
|
||||
view.range = [row_start, column_start, target_row, target_column];
|
||||
if new_top_row != top_row {
|
||||
view.top_row = new_top_row;
|
||||
}
|
||||
if new_left_column != left_column {
|
||||
view.left_column = new_left_column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -63,15 +63,97 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ use wasm_bindgen::{
|
||||
|
||||
use ironcalc_base::{
|
||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
||||
types::CellType,
|
||||
UserModel as BaseModel,
|
||||
types::{CellType, Style},
|
||||
BorderArea, UserModel as BaseModel,
|
||||
};
|
||||
|
||||
fn to_js_error(error: String) -> JsError {
|
||||
@@ -102,6 +102,13 @@ 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,
|
||||
@@ -264,6 +271,12 @@ 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(
|
||||
@@ -286,4 +299,178 @@ 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,5 +119,14 @@ 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,6 +6,24 @@ 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"
|
||||
@@ -115,19 +133,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;
|
||||
@@ -192,3 +210,12 @@ 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;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ const config: StorybookConfig = {
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-mdx-gfm',
|
||||
'@chromatic-com/storybook'
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
|
||||
BIN
webapp/example.ic
Normal file
@@ -7,7 +7,7 @@
|
||||
<!-- <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>Spreadsheet</title>
|
||||
<title>IronCalc Spreadsheet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
4889
webapp/package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
|
||||
@@ -18,22 +18,24 @@
|
||||
"@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.292.0",
|
||||
"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": {
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-interactions": "^7.6.17",
|
||||
"@storybook/addon-links": "^7.6.17",
|
||||
"@storybook/addon-onboarding": "^1.0.11",
|
||||
"@storybook/blocks": "^7.5.3",
|
||||
"@storybook/react": "^7.5.3",
|
||||
"@storybook/react-vite": "^7.6.17",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@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",
|
||||
@@ -45,7 +47,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"jest": "^29.7.0",
|
||||
"storybook": "^7.6.17",
|
||||
"storybook": "^8.0.8",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "./App.css";
|
||||
import Workbook from "./components/workbook";
|
||||
import "./i18n";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import init, { Model } from "@ironcalc/wasm";
|
||||
import { WorkbookState } from "./components/workbookState";
|
||||
import WorkbookContext from "./components/workbookContext";
|
||||
|
||||
function App() {
|
||||
const [model, setModel] = useState<Model | null>(null);
|
||||
@@ -30,9 +29,7 @@ function App() {
|
||||
// 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 (
|
||||
// <WorkbookContext.Provider value={{}}>
|
||||
<Workbook model={model} workbookState={workbookState} />
|
||||
// </WorkbookContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,3 @@ export const outlineBackgroundColor = '#F2994A1A';
|
||||
|
||||
export const LAST_COLUMN = 16_384;
|
||||
export const LAST_ROW = 1_048_576;
|
||||
|
||||
// FIXME: Browsers cannot have a height that big
|
||||
// For now we will go A-IZ and 10_000 rows
|
||||
export const lastColumn = 260; // TODO: Excel supports up to 16_384
|
||||
// I know of a world with one million moons.
|
||||
// Carl Sagan in The cosmic connection Chapter 7 "Space Exploration as a Human Enterprise"
|
||||
export const lastRow = 10_000; // TODO: Excel supports up to 1_048_576
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
headerSelectedBackground,
|
||||
headerSelectedColor,
|
||||
headerTextColor,
|
||||
lastColumn,
|
||||
lastRow,
|
||||
LAST_COLUMN,
|
||||
LAST_ROW,
|
||||
outlineColor,
|
||||
} from "./constants";
|
||||
import { columnNameFromNumber } from "./util";
|
||||
@@ -46,7 +46,7 @@ export const fonts = {
|
||||
|
||||
export const headerRowHeight = 24;
|
||||
export const headerColumnWidth = 30;
|
||||
export const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
export const devicePixelRatio = 1;//window.devicePixelRatio || 1;
|
||||
|
||||
export const defaultCellFontFamily = fonts.regular;
|
||||
export const headerFontFamily = fonts.regular;
|
||||
@@ -111,15 +111,15 @@ export default class WorksheetCanvas {
|
||||
this.resetHeaders();
|
||||
}
|
||||
|
||||
setScrollPosition(scrollPosition: {left: number; top: number;}): void {
|
||||
setScrollPosition(scrollPosition: { left: number; top: number }): void {
|
||||
// We ony scroll whole rows and whole columns
|
||||
// left, top are maximized with constraints:
|
||||
// 1. left <= scrollPosition.left
|
||||
// 2. top <= scrollPosition.top
|
||||
// 3. (left, top) are the absolute coordinates of a cell
|
||||
const { left } = this.getBoundedColumn(scrollPosition.left);
|
||||
const { top } = this.getBoundedRow(scrollPosition.top);
|
||||
this.workbookState.setScroll({ left, top });
|
||||
const { column } = this.getBoundedColumn(scrollPosition.left);
|
||||
const { row } = this.getBoundedRow(scrollPosition.top);
|
||||
this.model.setTopLeftVisibleCell(row, column);
|
||||
}
|
||||
|
||||
resetHeaders(): void {
|
||||
@@ -167,17 +167,14 @@ export default class WorksheetCanvas {
|
||||
*/
|
||||
getFrozenRowsHeight(): number {
|
||||
const frozenRows = this.model.getFrozenRowsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
this.model.getSelectedSheet()
|
||||
);
|
||||
if (frozenRows === 0) {
|
||||
return 0;
|
||||
}
|
||||
let frozenRowsHeight = 0;
|
||||
for (let row = 1; row <= frozenRows; row += 1) {
|
||||
frozenRowsHeight += this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
frozenRowsHeight += this.getRowHeight(this.model.getSelectedSheet(), row);
|
||||
}
|
||||
return frozenRowsHeight + frozenSeparatorWidth;
|
||||
}
|
||||
@@ -188,15 +185,15 @@ export default class WorksheetCanvas {
|
||||
*/
|
||||
getFrozenColumnsWidth(): number {
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
this.model.getSelectedSheet()
|
||||
);
|
||||
if (frozenColumns === 0) {
|
||||
return 0;
|
||||
}
|
||||
let frozenColumnsWidth = 0;
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
frozenColumnsWidth += this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
frozenColumnsWidth += this.getColumnWidth(
|
||||
this.model.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
}
|
||||
@@ -208,57 +205,46 @@ export default class WorksheetCanvas {
|
||||
topLeftCell: CellCoordinates;
|
||||
bottomRightCell: CellCoordinates;
|
||||
} {
|
||||
const frozenRows = this.model.getFrozenRowsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const scroll = this.workbookState.getScroll();
|
||||
let rowTop = frozenRows + 1;
|
||||
let rowBottom = frozenRows + 1;
|
||||
let columnLeft = frozenColumns + 1;
|
||||
let columnRight = frozenColumns + 1;
|
||||
const view = this.model.getSelectedView();
|
||||
let selectedSheet = view.sheet;
|
||||
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
let rowTop = Math.max(frozenRows + 1, view.top_row);
|
||||
let rowBottom = rowTop;
|
||||
let columnLeft = Math.max(frozenColumns + 1, view.left_column);
|
||||
let columnRight = columnLeft;
|
||||
const frozenColumnsWidth = this.getFrozenColumnsWidth();
|
||||
const frozenRowsHeight = this.getFrozenRowsHeight();
|
||||
let y = headerRowHeight + frozenRowsHeight - scroll.top;
|
||||
for (let row = frozenRows + 1; row <= lastRow; row += 1) {
|
||||
const rowHeight = this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
if (y >= this.height - rowHeight || row === lastRow) {
|
||||
let y = headerRowHeight + frozenRowsHeight;
|
||||
for (let row = rowTop; row <= LAST_ROW; row += 1) {
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
if (y >= this.height - rowHeight || row === LAST_ROW) {
|
||||
rowBottom = row;
|
||||
break;
|
||||
} else if (y < headerRowHeight + frozenRowsHeight) {
|
||||
y += rowHeight;
|
||||
rowTop = row + 1;
|
||||
} else {
|
||||
y += rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
let x =
|
||||
headerColumnWidth + frozenColumnsWidth - scroll.left;
|
||||
for (let column = frozenColumns + 1; column <= lastColumn; column += 1) {
|
||||
const columnWidth = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
if (x >= this.width - columnWidth || column === lastColumn) {
|
||||
let x = headerColumnWidth + frozenColumnsWidth;
|
||||
for (let column = columnLeft; column <= LAST_COLUMN; column += 1) {
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
if (x >= this.width - columnWidth || column === LAST_COLUMN) {
|
||||
columnRight = column;
|
||||
break;
|
||||
} else if (x < headerColumnWidth + frozenColumnsWidth) {
|
||||
x += columnWidth;
|
||||
columnLeft = column + 1;
|
||||
} else {
|
||||
x += columnWidth;
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
||||
let cells = {
|
||||
topLeftCell: { row: rowTop, column: columnLeft },
|
||||
bottomRightCell: { row: rowBottom, column: columnRight },
|
||||
};
|
||||
}
|
||||
|
||||
this.workbookState.setVisibleCells(cells)
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,13 +252,11 @@ export default class WorksheetCanvas {
|
||||
* Both top and maxTop are absolute coordinates
|
||||
*/
|
||||
getBoundedRow(maxTop: number): { row: number; top: number } {
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
let top = 0;
|
||||
let row = 1 + this.model.getFrozenRowsCount(this.workbookState.getSelectedSheet());
|
||||
while (row <= lastRow && top <= maxTop) {
|
||||
const height = this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
let row = 1 + this.model.getFrozenRowsCount(selectedSheet);
|
||||
while (row <= LAST_ROW && top <= maxTop) {
|
||||
const height = this.getRowHeight(selectedSheet, row);
|
||||
if (top + height <= maxTop) {
|
||||
top += height;
|
||||
} else {
|
||||
@@ -285,13 +269,10 @@ export default class WorksheetCanvas {
|
||||
|
||||
private getBoundedColumn(maxLeft: number): { column: number; left: number } {
|
||||
let left = 0;
|
||||
let column =
|
||||
1 + this.model.getFrozenColumnsCount(this.workbookState.getSelectedSheet());
|
||||
while (left <= maxLeft && column <= lastColumn) {
|
||||
const width = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
let column = 1 + this.model.getFrozenColumnsCount(selectedSheet);
|
||||
while (left <= maxLeft && column <= LAST_COLUMN) {
|
||||
const width = this.getColumnWidth(selectedSheet, column);
|
||||
if (width + left <= maxLeft) {
|
||||
left += width;
|
||||
} else {
|
||||
@@ -309,14 +290,11 @@ export default class WorksheetCanvas {
|
||||
*/
|
||||
getMinScrollLeft(targetColumn: number): number {
|
||||
const columnStart =
|
||||
1 + this.model.getFrozenColumnsCount(this.workbookState.getSelectedSheet());
|
||||
1 + this.model.getFrozenColumnsCount(this.model.getSelectedSheet());
|
||||
/** Distance from the first non frozen cell to the right border of column*/
|
||||
let distance = 0;
|
||||
for (let column = columnStart; column <= targetColumn; column += 1) {
|
||||
const width = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
const width = this.getColumnWidth(this.model.getSelectedSheet(), column);
|
||||
distance += width;
|
||||
}
|
||||
/** Minimum we need to scroll so that `column` is visible */
|
||||
@@ -326,11 +304,8 @@ export default class WorksheetCanvas {
|
||||
// Because scrolling is quantified, we only scroll whole columns,
|
||||
// we need to find the minimum quantum that is larger than minLeft
|
||||
let left = 0;
|
||||
for (let column = columnStart; column <= lastColumn; column += 1) {
|
||||
const width = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
for (let column = columnStart; column <= LAST_COLUMN; column += 1) {
|
||||
const width = this.getColumnWidth(this.model.getSelectedSheet(), column);
|
||||
if (left < minLeft) {
|
||||
left += width;
|
||||
} else {
|
||||
@@ -348,16 +323,16 @@ export default class WorksheetCanvas {
|
||||
width: number,
|
||||
height: number
|
||||
): void {
|
||||
const style = this.model.getCellStyle(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row,
|
||||
column
|
||||
);
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
const style = this.model.getCellStyle(selectedSheet, row, column);
|
||||
|
||||
let backgroundColor = "#FFFFFF";
|
||||
if (style.fill.fg_color) {
|
||||
backgroundColor = style.fill.fg_color;
|
||||
}
|
||||
const cellGridColor = this.model.getShowGridLines(selectedSheet)
|
||||
? gridColor
|
||||
: backgroundColor;
|
||||
|
||||
const fontSize = 13;
|
||||
let font = `${fontSize}px ${defaultCellFontFamily}`;
|
||||
@@ -382,10 +357,96 @@ export default class WorksheetCanvas {
|
||||
context.font = font;
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(x, y, width, height);
|
||||
context.strokeStyle = gridColor;
|
||||
context.strokeRect(x, y, width, height);
|
||||
context.fillStyle = textColor;
|
||||
|
||||
// Let's do the border
|
||||
// Algorithm:
|
||||
// * we use the border if present
|
||||
// * otherwise we use the border of the adjacent cell
|
||||
// * otherwise we use the color of the background
|
||||
// * otherwise we use the background color of the adjacent cell
|
||||
// * if everything else fails we use the default grid color
|
||||
// We only set the left and top borders (right and bottom are set later)
|
||||
const border = style.border;
|
||||
|
||||
let borderLeftColor = cellGridColor;
|
||||
let borderLeftWidth = 1;
|
||||
if (border.left) {
|
||||
borderLeftColor = border.left.color;
|
||||
switch (border.left.style) {
|
||||
case "thin":
|
||||
break;
|
||||
case "medium":
|
||||
borderLeftWidth = 2;
|
||||
break;
|
||||
case "thick":
|
||||
borderLeftWidth = 3;
|
||||
}
|
||||
} else {
|
||||
let leftStyle = this.model.getCellStyle(selectedSheet, row, column - 1);
|
||||
if (leftStyle.border.right) {
|
||||
borderLeftColor = leftStyle.border.right.color;
|
||||
switch (leftStyle.border.right.style) {
|
||||
case "thin":
|
||||
break;
|
||||
case "medium":
|
||||
borderLeftWidth = 2;
|
||||
break;
|
||||
case "thick":
|
||||
borderLeftWidth = 3;
|
||||
}
|
||||
} else if (style.fill.fg_color) {
|
||||
borderLeftColor = style.fill.fg_color;
|
||||
} else if (leftStyle.fill.fg_color) {
|
||||
borderLeftColor = leftStyle.fill.fg_color;
|
||||
}
|
||||
}
|
||||
context.beginPath();
|
||||
context.strokeStyle = borderLeftColor;
|
||||
context.lineWidth = borderLeftWidth;
|
||||
context.moveTo(x, y);
|
||||
context.lineTo(x, y + height);
|
||||
context.stroke();
|
||||
|
||||
let borderTopColor = cellGridColor;
|
||||
let borderTopWidth = 1;
|
||||
if (border.top) {
|
||||
borderTopColor = border.top.color;
|
||||
switch (border.top.style) {
|
||||
case "thin":
|
||||
break;
|
||||
case "medium":
|
||||
borderTopWidth = 2;
|
||||
break;
|
||||
case "thick":
|
||||
borderTopWidth = 3;
|
||||
}
|
||||
} else {
|
||||
let topStyle = this.model.getCellStyle(selectedSheet, row - 1, column);
|
||||
if (topStyle.border.bottom) {
|
||||
borderTopColor = topStyle.border.bottom.color;
|
||||
switch (topStyle.border.bottom.style) {
|
||||
case "thin":
|
||||
break;
|
||||
case "medium":
|
||||
borderTopWidth = 2;
|
||||
break;
|
||||
case "thick":
|
||||
borderTopWidth = 3;
|
||||
}
|
||||
} else if (style.fill.fg_color) {
|
||||
borderTopColor = style.fill.fg_color;
|
||||
} else if (topStyle.fill.fg_color) {
|
||||
borderTopColor = topStyle.fill.fg_color;
|
||||
}
|
||||
}
|
||||
context.beginPath();
|
||||
context.strokeStyle = borderTopColor;
|
||||
context.lineWidth = borderTopWidth;
|
||||
context.moveTo(x, y);
|
||||
context.lineTo(x + width, y);
|
||||
context.stroke();
|
||||
|
||||
// Number = 1,
|
||||
// Text = 2,
|
||||
// LogicalValue = 4,
|
||||
@@ -393,13 +454,9 @@ export default class WorksheetCanvas {
|
||||
// Array = 64,
|
||||
// CompoundData = 128,
|
||||
|
||||
const cellType = this.model.getCellType(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row,
|
||||
column
|
||||
);
|
||||
const cellType = this.model.getCellType(selectedSheet, row, column);
|
||||
const fullText = this.model.getFormattedCellValue(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
selectedSheet,
|
||||
row,
|
||||
column
|
||||
);
|
||||
@@ -432,7 +489,6 @@ export default class WorksheetCanvas {
|
||||
let verticalPadding = 4;
|
||||
if (horizontalAlign === "right") {
|
||||
textX = width - padding + x - textWidth / 2;
|
||||
|
||||
} else if (horizontalAlign === "center") {
|
||||
textX = x + width / 2;
|
||||
} else {
|
||||
@@ -440,12 +496,12 @@ export default class WorksheetCanvas {
|
||||
textX = padding + x + textWidth / 2;
|
||||
}
|
||||
if (verticalAlign === "bottom") {
|
||||
textY = y + height - fontSize/2 - verticalPadding;
|
||||
textY = y + height - fontSize / 2 - verticalPadding;
|
||||
} else if (verticalAlign === "center") {
|
||||
textY = y + height / 2;
|
||||
} else {
|
||||
// aligned top
|
||||
textY = y + fontSize/2 + verticalPadding;
|
||||
textY = y + fontSize / 2 + verticalPadding;
|
||||
}
|
||||
textY += line * lineHeight;
|
||||
context.fillText(text, textX, textY);
|
||||
@@ -504,7 +560,7 @@ export default class WorksheetCanvas {
|
||||
document.removeEventListener("mouseup", resizeHandleUp);
|
||||
const newColumnWidth = columnWidth + event.pageX - initPageX;
|
||||
this.onColumnWidthChanges(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
this.model.getSelectedSheet(),
|
||||
column,
|
||||
newColumnWidth
|
||||
);
|
||||
@@ -525,7 +581,7 @@ export default class WorksheetCanvas {
|
||||
div.className = "row-resize-handle";
|
||||
div.style.top = `${y - 1}px`;
|
||||
div.style.width = `${headerColumnWidth}px`;
|
||||
const sheet = this.workbookState.getSelectedSheet();
|
||||
const sheet = this.model.getSelectedSheet();
|
||||
this.canvas.parentElement?.insertBefore(div, null);
|
||||
let initPageY = 0;
|
||||
/* istanbul ignore next */
|
||||
@@ -568,7 +624,8 @@ export default class WorksheetCanvas {
|
||||
: headerBackground;
|
||||
div.style.color = selected ? headerSelectedColor : headerTextColor;
|
||||
div.style.fontWeight = "bold";
|
||||
div.style.border = `1px solid ${headerBorderColor}`;
|
||||
div.style.borderLeft = `1px solid ${headerBorderColor}`;
|
||||
div.style.borderTop = `1px solid ${headerBorderColor}`;
|
||||
if (selected) {
|
||||
div.style.borderBottom = `1px solid ${outlineColor}`;
|
||||
div.classList.add("selected");
|
||||
@@ -590,7 +647,9 @@ export default class WorksheetCanvas {
|
||||
topLeftCell: CellCoordinates,
|
||||
bottomRightCell: CellCoordinates
|
||||
): void {
|
||||
let { rowStart, rowEnd } = this.workbookState.getSelectedArea();
|
||||
let { sheet: selectedSheet, range } = this.model.getSelectedView();
|
||||
let rowStart = range[0];
|
||||
let rowEnd = range[2];
|
||||
if (rowStart > rowEnd) {
|
||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||
}
|
||||
@@ -600,10 +659,7 @@ export default class WorksheetCanvas {
|
||||
const firstRow = frozenRows === 0 ? topLeftCell.row : 1;
|
||||
|
||||
for (let row = firstRow; row <= bottomRightCell.row; row += 1) {
|
||||
const rowHeight = this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
const selected = row >= rowStart && row <= rowEnd;
|
||||
context.fillStyle = headerBorderColor;
|
||||
context.fillRect(0, topLeftCornerY, headerColumnWidth, rowHeight);
|
||||
@@ -644,7 +700,9 @@ export default class WorksheetCanvas {
|
||||
): void {
|
||||
const { columnHeaders } = this;
|
||||
let deltaX = 0;
|
||||
let { columnStart, columnEnd } = this.workbookState.getSelectedArea();
|
||||
let { range } = this.model.getSelectedView();
|
||||
let columnStart = range[1];
|
||||
let columnEnd = range[3];
|
||||
if (columnStart > columnEnd) {
|
||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||
}
|
||||
@@ -694,8 +752,8 @@ export default class WorksheetCanvas {
|
||||
column: number,
|
||||
selected: boolean
|
||||
): number {
|
||||
const columnWidth = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
const columnWidth = this.getColumnWidth(
|
||||
this.model.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
const div = document.createElement("div");
|
||||
@@ -710,18 +768,18 @@ export default class WorksheetCanvas {
|
||||
|
||||
getSheetDimensions(): [number, number] {
|
||||
let x = headerColumnWidth;
|
||||
for (let column = 1; column < lastColumn + 1; column += 1) {
|
||||
x += this.model.getColumnWidth(this.workbookState.getSelectedSheet(), column);
|
||||
for (let column = 1; column < LAST_COLUMN + 1; column += 1) {
|
||||
x += this.getColumnWidth(this.model.getSelectedSheet(), column);
|
||||
}
|
||||
let y = headerRowHeight;
|
||||
for (let row = 1; row < lastRow + 1; row += 1) {
|
||||
y += this.model.getRowHeight(this.workbookState.getSelectedSheet(), row);
|
||||
for (let row = 1; row < LAST_ROW + 1; row += 1) {
|
||||
y += this.getRowHeight(this.model.getSelectedSheet(), row);
|
||||
}
|
||||
this.sheetWidth = Math.floor(
|
||||
x + this.model.getColumnWidth(this.workbookState.getSelectedSheet(), lastColumn)
|
||||
x + this.getColumnWidth(this.model.getSelectedSheet(), LAST_COLUMN)
|
||||
);
|
||||
this.sheetHeight = Math.floor(
|
||||
y + 2 * this.model.getRowHeight(this.workbookState.getSelectedSheet(), lastRow)
|
||||
y + 2 * this.getRowHeight(this.model.getSelectedSheet(), LAST_ROW)
|
||||
);
|
||||
return [this.sheetWidth, this.sheetHeight];
|
||||
}
|
||||
@@ -775,25 +833,25 @@ export default class WorksheetCanvas {
|
||||
): [number, number] {
|
||||
const [xStart, yStart] = this.getCoordinatesByCell(startRow, startColumn);
|
||||
let [xEnd, yEnd] = this.getCoordinatesByCell(endRow, endColumn);
|
||||
xEnd += this.model.getColumnWidth(this.workbookState.getSelectedSheet(), endColumn);
|
||||
yEnd += this.model.getRowHeight(this.workbookState.getSelectedSheet(), endRow);
|
||||
xEnd += this.getColumnWidth(this.model.getSelectedSheet(), endColumn);
|
||||
yEnd += this.getRowHeight(this.model.getSelectedSheet(), endRow);
|
||||
const frozenRows = this.model.getFrozenRowsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
this.model.getSelectedSheet()
|
||||
);
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
this.model.getSelectedSheet()
|
||||
);
|
||||
if (frozenRows !== 0 || frozenColumns !== 0) {
|
||||
let [xFrozenEnd, yFrozenEnd] = this.getCoordinatesByCell(
|
||||
frozenRows,
|
||||
frozenColumns
|
||||
);
|
||||
xFrozenEnd += this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
xFrozenEnd += this.getColumnWidth(
|
||||
this.model.getSelectedSheet(),
|
||||
frozenColumns
|
||||
);
|
||||
yFrozenEnd += this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
yFrozenEnd += this.getRowHeight(
|
||||
this.model.getSelectedSheet(),
|
||||
frozenRows
|
||||
);
|
||||
if (startRow <= frozenRows && endRow > frozenRows) {
|
||||
@@ -812,13 +870,10 @@ export default class WorksheetCanvas {
|
||||
* for the top left corner of the first visible cell
|
||||
*/
|
||||
getCoordinatesByCell(row: number, column: number): [number, number] {
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
const frozenColumnsWidth = this.getFrozenColumnsWidth();
|
||||
const frozenRows = this.model.getFrozenRowsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
|
||||
const frozenRowsHeight = this.getFrozenRowsHeight();
|
||||
const { topLeftCell } = this.getVisibleCells();
|
||||
let x, y;
|
||||
@@ -826,38 +881,38 @@ export default class WorksheetCanvas {
|
||||
// row is one of the frozen rows
|
||||
y = headerRowHeight;
|
||||
for (let r = 1; r < row; r += 1) {
|
||||
y += this.model.getRowHeight(this.workbookState.getSelectedSheet(), r);
|
||||
y += this.getRowHeight(selectedSheet, r);
|
||||
}
|
||||
} else if (row >= topLeftCell.row) {
|
||||
// row is bellow the frozen rows
|
||||
y = headerRowHeight + frozenRowsHeight;
|
||||
for (let r = topLeftCell.row; r < row; r += 1) {
|
||||
y += this.model.getRowHeight(this.workbookState.getSelectedSheet(), r);
|
||||
y += this.getRowHeight(selectedSheet, r);
|
||||
}
|
||||
} else {
|
||||
// row is _above_ the frozen rows
|
||||
y = headerRowHeight + frozenRowsHeight;
|
||||
for (let r = topLeftCell.row; r > row; r -= 1) {
|
||||
y -= this.model.getRowHeight(this.workbookState.getSelectedSheet(), r - 1);
|
||||
y -= this.getRowHeight(selectedSheet, r - 1);
|
||||
}
|
||||
}
|
||||
if (column <= frozenColumns) {
|
||||
// It is one of the frozen columns
|
||||
x = headerColumnWidth;
|
||||
for (let c = 1; c < column; c += 1) {
|
||||
x += this.model.getColumnWidth(this.workbookState.getSelectedSheet(), c);
|
||||
x += this.getColumnWidth(selectedSheet, c);
|
||||
}
|
||||
} else if (column >= topLeftCell.column) {
|
||||
// column is to the right of the frozen columns
|
||||
x = headerColumnWidth + frozenColumnsWidth;
|
||||
for (let c = topLeftCell.column; c < column; c += 1) {
|
||||
x += this.model.getColumnWidth(this.workbookState.getSelectedSheet(), c);
|
||||
x += this.getColumnWidth(selectedSheet, c);
|
||||
}
|
||||
} else {
|
||||
// column is to the left of the frozen columns
|
||||
x = headerColumnWidth + frozenColumnsWidth;
|
||||
for (let c = topLeftCell.column; c > column; c -= 1) {
|
||||
x -= this.model.getColumnWidth(this.workbookState.getSelectedSheet(), c - 1);
|
||||
x -= this.getColumnWidth(selectedSheet, c - 1);
|
||||
}
|
||||
}
|
||||
return [Math.floor(x), Math.floor(y)];
|
||||
@@ -874,23 +929,30 @@ export default class WorksheetCanvas {
|
||||
y: number
|
||||
): { row: number; column: number } | null {
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
this.model.getSelectedSheet()
|
||||
);
|
||||
const frozenColumnsWidth = this.getFrozenColumnsWidth();
|
||||
const frozenRows = this.model.getFrozenRowsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
this.model.getSelectedSheet()
|
||||
);
|
||||
const frozenRowsHeight = this.getFrozenRowsHeight();
|
||||
let column = 0;
|
||||
let cellX = headerColumnWidth;
|
||||
const { topLeftCell } = this.getVisibleCells();
|
||||
if (x < headerColumnWidth + frozenColumnsWidth) {
|
||||
if (x < headerColumnWidth) {
|
||||
column = topLeftCell.column;
|
||||
while (cellX >= x) {
|
||||
column -= 1;
|
||||
if (column < 1) {
|
||||
column = 1;
|
||||
break;
|
||||
}
|
||||
cellX -= this.getColumnWidth(this.model.getSelectedSheet(), column);
|
||||
}
|
||||
} else if (x < headerColumnWidth + frozenColumnsWidth) {
|
||||
while (cellX <= x) {
|
||||
column += 1;
|
||||
cellX += this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
cellX += this.getColumnWidth(this.model.getSelectedSheet(), column);
|
||||
// This cannot happen (would mean cellX > headerColumnWidth + frozenColumnsWidth)
|
||||
if (column > frozenColumns) {
|
||||
/* istanbul ignore next */
|
||||
@@ -902,21 +964,29 @@ export default class WorksheetCanvas {
|
||||
column = topLeftCell.column - 1;
|
||||
while (cellX <= x) {
|
||||
column += 1;
|
||||
cellX += this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
if (column > lastColumn) {
|
||||
if (column > LAST_COLUMN) {
|
||||
return null;
|
||||
}
|
||||
cellX += this.getColumnWidth(this.model.getSelectedSheet(), column);
|
||||
}
|
||||
}
|
||||
let cellY = headerRowHeight;
|
||||
let row = 0;
|
||||
if (y < headerRowHeight + frozenRowsHeight) {
|
||||
if (y < headerRowHeight) {
|
||||
row = topLeftCell.row;
|
||||
while (cellY >= y) {
|
||||
row -= 1;
|
||||
if (row < 1) {
|
||||
row = 1;
|
||||
break;
|
||||
}
|
||||
cellY -= this.getRowHeight(this.model.getSelectedSheet(), row);
|
||||
}
|
||||
|
||||
} else if (y < headerRowHeight + frozenRowsHeight) {
|
||||
while (cellY <= y) {
|
||||
row += 1;
|
||||
cellY += this.model.getRowHeight(this.workbookState.getSelectedSheet(), row);
|
||||
cellY += this.getRowHeight(this.model.getSelectedSheet(), row);
|
||||
// This cannot happen (would mean cellY > headerRowHeight + frozenRowsHeight)
|
||||
if (row > frozenRows) {
|
||||
/* istanbul ignore next */
|
||||
@@ -928,26 +998,26 @@ export default class WorksheetCanvas {
|
||||
row = topLeftCell.row - 1;
|
||||
while (cellY <= y) {
|
||||
row += 1;
|
||||
cellY += this.model.getRowHeight(this.workbookState.getSelectedSheet(), row);
|
||||
if (row > lastRow) {
|
||||
return null;
|
||||
if (row > LAST_ROW) {
|
||||
row = LAST_ROW;
|
||||
break;
|
||||
}
|
||||
cellY += this.getRowHeight(this.model.getSelectedSheet(), row);
|
||||
}
|
||||
}
|
||||
if (row === 0 || column === 0) {
|
||||
return null;
|
||||
}
|
||||
if (row < 1) row = 1;
|
||||
if (column < 1) column = 1;
|
||||
return { row, column };
|
||||
}
|
||||
|
||||
private drawExtendToArea(): void {
|
||||
const { extendToOutline } = this;
|
||||
const extendToArea = this.workbookState.getExtendToArea();
|
||||
const extendToArea = this.workbookState.getExtendToArea();
|
||||
if (extendToArea === null) {
|
||||
extendToOutline.style.visibility = 'hidden';
|
||||
extendToOutline.style.visibility = "hidden";
|
||||
return;
|
||||
}
|
||||
extendToOutline.style.visibility = 'visible';
|
||||
extendToOutline.style.visibility = "visible";
|
||||
|
||||
let { rowStart, rowEnd, columnStart, columnEnd } = extendToArea;
|
||||
if (rowStart > rowEnd) {
|
||||
@@ -962,11 +1032,11 @@ export default class WorksheetCanvas {
|
||||
rowStart,
|
||||
columnStart,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
columnEnd
|
||||
);
|
||||
// const { border } = extendToArea;
|
||||
extendToOutline.style.border = `1px dashed ${outlineColor}`;
|
||||
extendToOutline.style.borderRadius = '3px';
|
||||
extendToOutline.style.borderRadius = "3px";
|
||||
// switch (border) {
|
||||
// case 'left': {
|
||||
// extendToOutline.style.borderLeft = 'none';
|
||||
@@ -1006,36 +1076,34 @@ export default class WorksheetCanvas {
|
||||
extendToOutline.style.height = `${areaHeight + 2 * padding}px`;
|
||||
}
|
||||
|
||||
private getColumnWidth(sheet: number, column: number): number {
|
||||
return Math.round(this.model.getColumnWidth(sheet, column));
|
||||
}
|
||||
|
||||
private getRowHeight(sheet: number, row: number): number {
|
||||
return Math.round(this.model.getRowHeight(sheet, row));
|
||||
}
|
||||
|
||||
private drawCellOutline(): void {
|
||||
const { row: selectedRow, column: selectedColumn } =
|
||||
this.workbookState.getSelectedCell();
|
||||
const [selectedSheet, selectedRow, selectedColumn] =
|
||||
this.model.getSelectedCell();
|
||||
const { topLeftCell } = this.getVisibleCells();
|
||||
const frozenRows = this.model.getFrozenRowsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
const [x, y] = this.getCoordinatesByCell(selectedRow, selectedColumn);
|
||||
const style = this.model.getCellStyle(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
selectedSheet,
|
||||
selectedRow,
|
||||
selectedColumn
|
||||
);
|
||||
const padding = -1;
|
||||
const width =
|
||||
this.model.getColumnWidth(this.workbookState.getSelectedSheet(), selectedColumn) +
|
||||
2 * padding;
|
||||
const height =
|
||||
this.model.getRowHeight(this.workbookState.getSelectedSheet(), selectedRow) +
|
||||
2 * padding;
|
||||
this.getColumnWidth(selectedSheet, selectedColumn) + 2 * padding;
|
||||
const height = this.getRowHeight(selectedSheet, selectedRow) + 2 * padding;
|
||||
|
||||
const { cellOutline, areaOutline, cellOutlineHandle } = this;
|
||||
const cellEditing = null;
|
||||
|
||||
// If we are editing a cell we want the editor to be a bit larger than a single cell
|
||||
// TODO [MVP]: Set initial size of the editor
|
||||
|
||||
cellOutline.style.visibility = "visible";
|
||||
cellOutlineHandle.style.visibility = "visible";
|
||||
if (
|
||||
@@ -1070,7 +1138,9 @@ export default class WorksheetCanvas {
|
||||
}
|
||||
// border is 2px so line-height must be height - 4
|
||||
cellOutline.style.lineHeight = `${height - 4}px`;
|
||||
let { rowStart, rowEnd, columnStart, columnEnd } = this.workbookState.getSelectedArea();
|
||||
let {
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = this.model.getSelectedView();
|
||||
if (rowStart > rowEnd) {
|
||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||
}
|
||||
@@ -1083,14 +1153,8 @@ export default class WorksheetCanvas {
|
||||
if (columnStart === columnEnd && rowStart === rowEnd) {
|
||||
areaOutline.style.visibility = "hidden";
|
||||
[handleX, handleY] = this.getCoordinatesByCell(rowStart, columnStart);
|
||||
handleX += this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
columnStart
|
||||
);
|
||||
handleY += this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
rowStart
|
||||
);
|
||||
handleX += this.getColumnWidth(selectedSheet, columnStart);
|
||||
handleY += this.getRowHeight(selectedSheet, rowStart);
|
||||
} else {
|
||||
areaOutline.style.visibility = "visible";
|
||||
cellOutlineHandle.style.visibility = "visible";
|
||||
@@ -1160,14 +1224,16 @@ export default class WorksheetCanvas {
|
||||
}
|
||||
|
||||
renderSheet(): void {
|
||||
// console.time("render");
|
||||
console.time('renderSheet');
|
||||
this._renderSheet();
|
||||
// console.timeEnd("render");
|
||||
console.timeEnd('renderSheet');
|
||||
|
||||
}
|
||||
|
||||
private _renderSheet(): void {
|
||||
const context = this.ctx;
|
||||
const { canvas } = this;
|
||||
const selectedSheet = this.model.getSelectedSheet();
|
||||
context.lineWidth = 1;
|
||||
context.textAlign = "center";
|
||||
context.textBaseline = "middle";
|
||||
@@ -1179,27 +1245,17 @@ export default class WorksheetCanvas {
|
||||
|
||||
const { topLeftCell, bottomRightCell } = this.getVisibleCells();
|
||||
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const frozenRows = this.model.getFrozenRowsCount(
|
||||
this.workbookState.getSelectedSheet()
|
||||
);
|
||||
const frozenColumns = this.model.getFrozenColumnsCount(selectedSheet);
|
||||
const frozenRows = this.model.getFrozenRowsCount(selectedSheet);
|
||||
|
||||
// Draw frozen rows and columns (top-left-pane)
|
||||
let x = headerColumnWidth + 0.5;
|
||||
let y = headerRowHeight + 0.5;
|
||||
for (let row = 1; row <= frozenRows; row += 1) {
|
||||
const rowHeight = this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
x = headerColumnWidth;
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
const columnWidth = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
x += columnWidth;
|
||||
}
|
||||
@@ -1208,7 +1264,7 @@ export default class WorksheetCanvas {
|
||||
if (frozenRows === 0 && frozenColumns !== 0) {
|
||||
x = headerColumnWidth;
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
x += this.model.getColumnWidth(this.workbookState.getSelectedSheet(), column);
|
||||
x += this.getColumnWidth(selectedSheet, column);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,19 +1299,13 @@ export default class WorksheetCanvas {
|
||||
y = headerRowHeight;
|
||||
for (let row = 1; row <= frozenRows; row += 1) {
|
||||
x = frozenX;
|
||||
const rowHeight = this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
for (
|
||||
let { column } = topLeftCell;
|
||||
column <= bottomRightCell.column;
|
||||
column += 1
|
||||
) {
|
||||
const columnWidth = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
x += columnWidth;
|
||||
}
|
||||
@@ -1266,16 +1316,10 @@ export default class WorksheetCanvas {
|
||||
y = frozenY;
|
||||
for (let { row } = topLeftCell; row <= bottomRightCell.row; row += 1) {
|
||||
x = headerColumnWidth;
|
||||
const rowHeight = this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
|
||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||
const columnWidth = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
@@ -1287,20 +1331,14 @@ export default class WorksheetCanvas {
|
||||
y = frozenY;
|
||||
for (let { row } = topLeftCell; row <= bottomRightCell.row; row += 1) {
|
||||
x = frozenX;
|
||||
const rowHeight = this.model.getRowHeight(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
row
|
||||
);
|
||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||
|
||||
for (
|
||||
let { column } = topLeftCell;
|
||||
column <= bottomRightCell.column;
|
||||
column += 1
|
||||
) {
|
||||
const columnWidth = this.model.getColumnWidth(
|
||||
this.workbookState.getSelectedSheet(),
|
||||
column
|
||||
);
|
||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||
|
||||
x += columnWidth;
|
||||
|
||||
@@ -69,12 +69,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
<Line>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderAll}
|
||||
$pressed={borderSelected === BorderType.All}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderAll) {
|
||||
if (borderSelected === BorderType.All) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderAll);
|
||||
setBorderSelected(BorderType.All);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -84,12 +84,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderInner}
|
||||
$pressed={borderSelected === BorderType.Inner}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderInner) {
|
||||
if (borderSelected === BorderType.Inner) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderInner);
|
||||
setBorderSelected(BorderType.Inner);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -99,12 +99,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderCenterH}
|
||||
$pressed={borderSelected === BorderType.CenterH}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderCenterH) {
|
||||
if (borderSelected === BorderType.CenterH) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderCenterH);
|
||||
setBorderSelected(BorderType.CenterH);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -114,12 +114,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderCenterV}
|
||||
$pressed={borderSelected === BorderType.CenterV}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderCenterV) {
|
||||
if (borderSelected === BorderType.CenterV) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderCenterV);
|
||||
setBorderSelected(BorderType.CenterV);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -129,12 +129,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderOuter}
|
||||
$pressed={borderSelected === BorderType.Outer}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderOuter) {
|
||||
if (borderSelected === BorderType.Outer) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderOuter);
|
||||
setBorderSelected(BorderType.Outer);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -146,12 +146,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
<Line>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderNone}
|
||||
$pressed={borderSelected === BorderType.None}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderNone) {
|
||||
if (borderSelected === BorderType.None) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderNone);
|
||||
setBorderSelected(BorderType.None);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -161,12 +161,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderTop}
|
||||
$pressed={borderSelected === BorderType.Top}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderTop) {
|
||||
if (borderSelected === BorderType.Top) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderTop);
|
||||
setBorderSelected(BorderType.Top);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -176,12 +176,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderRight}
|
||||
$pressed={borderSelected === BorderType.Right}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderRight) {
|
||||
if (borderSelected === BorderType.Right) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderRight);
|
||||
setBorderSelected(BorderType.Right);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -191,12 +191,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderBottom}
|
||||
$pressed={borderSelected === BorderType.Bottom}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderBottom) {
|
||||
if (borderSelected === BorderType.Bottom) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderBottom);
|
||||
setBorderSelected(BorderType.Bottom);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
@@ -206,12 +206,12 @@ const BorderPicker = (properties: BorderPickerProps) => {
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderLeft}
|
||||
$pressed={borderSelected === BorderType.Left}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderLeft) {
|
||||
if (borderSelected === BorderType.Left) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderLeft);
|
||||
setBorderSelected(BorderType.Left);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
|
||||
@@ -15,7 +15,6 @@ type ColorPickerProps = {
|
||||
};
|
||||
|
||||
const colorPickerWidth = 240;
|
||||
const colorPickerPadding = 15;
|
||||
const colorfulHeight = 185; // 150 + 15 + 20
|
||||
|
||||
const ColorPicker = (properties: ColorPickerProps) => {
|
||||
@@ -79,7 +78,9 @@ const ColorPicker = (properties: ColorPickerProps) => {
|
||||
/>
|
||||
</HexColorInputBox>
|
||||
</HexWrapper>
|
||||
<Swatch $color={color} />
|
||||
<Swatch $color={color} onClick={(): void => {
|
||||
closePicker(color);
|
||||
}} />
|
||||
</ColorPickerInput>
|
||||
<HorizontalDivider />
|
||||
<ColorList>
|
||||
@@ -127,7 +128,7 @@ const Button = styled.button<{ $color: string }>`
|
||||
height: 20px;
|
||||
${({ $color }): string => {
|
||||
if ($color.toUpperCase() === "#FFFFFF") {
|
||||
return `border: 1px solid ${theme.palette.grey['600']};`;
|
||||
return `border: 1px solid ${theme.palette.grey["600"]};`;
|
||||
}
|
||||
return `border: 1px solid ${$color};`;
|
||||
}}
|
||||
@@ -143,7 +144,7 @@ const Button = styled.button<{ $color: string }>`
|
||||
const HorizontalDivider = styled.div`
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
border-top: 1px solid ${theme.palette.grey['400']};
|
||||
border-top: 1px solid ${theme.palette.grey["400"]};
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
@@ -171,7 +172,7 @@ const ColorPickerDialog = styled.div`
|
||||
|
||||
& .react-colorful {
|
||||
height: ${colorfulHeight}px;
|
||||
width: ${colorPickerWidth - colorPickerPadding * 2}px;
|
||||
width: ${colorPickerWidth}px;
|
||||
}
|
||||
& .react-colorful__saturation {
|
||||
border-bottom: none;
|
||||
@@ -212,7 +213,7 @@ const HexColorInputBox = styled.div`
|
||||
margin-right: 10px;
|
||||
width: 140px;
|
||||
height: 28px;
|
||||
border: 1px solid ${theme.palette.grey['600']};
|
||||
border: 1px solid ${theme.palette.grey["600"]};
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
@@ -241,7 +242,7 @@ 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 ${theme.palette.grey["600"]};`;
|
||||
}
|
||||
return `border: 1px solid ${$color};`;
|
||||
}}
|
||||
|
||||
@@ -111,7 +111,7 @@ const Editor = (options: EditorOptions) => {
|
||||
|
||||
const baseText = editorContext.baseText;
|
||||
const text = baseText + insertRangeText;
|
||||
console.log('baseText', baseText, 'insertRange:', insertRangeText);
|
||||
// console.log('baseText', baseText, 'insertRange:', insertRangeText);
|
||||
|
||||
const formulaRef = useRef<HTMLDivElement>(null);
|
||||
const maskRef = useRef<HTMLDivElement>(null);
|
||||
@@ -170,7 +170,7 @@ const Editor = (options: EditorOptions) => {
|
||||
}
|
||||
}, [display]);
|
||||
|
||||
console.log("Ok, this is running", text, editorContext.id);
|
||||
// console.log("Ok, this is running", text, editorContext.id);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
|
||||
@@ -25,16 +25,16 @@ const useEditorKeydown = (
|
||||
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 "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 });
|
||||
|
||||
51
webapp/src/components/formulaDialog.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
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,15 +1,24 @@
|
||||
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>
|
||||
@@ -20,15 +29,30 @@ function FormulaBar(properties: FormulaBarProps) {
|
||||
</AddressContainer>
|
||||
<Divider />
|
||||
<FormulaContainer>
|
||||
<FormulaSymbolButton><Fx /></FormulaSymbolButton>
|
||||
<Editor contentEditable="true" spellCheck="false" />
|
||||
<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;
|
||||
|
||||
@@ -1,7 +1,57 @@
|
||||
import { styled } from "@mui/material";
|
||||
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;
|
||||
@@ -45,17 +95,17 @@ const SheetListMenu = (properties: SheetListMenuProps) => {
|
||||
const StyledMenu = styled(Menu)({
|
||||
"& .MuiPaper-root": {
|
||||
borderRadius: 8,
|
||||
padding: 4
|
||||
padding: 4,
|
||||
},
|
||||
"& .MuiList-padding": {
|
||||
padding: 0,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const StyledMenuItem = styled(MenuItem)({
|
||||
padding: 8,
|
||||
borderRadius: 4,
|
||||
})
|
||||
});
|
||||
|
||||
const ItemColor = styled("div")`
|
||||
width: 12px;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChevronLeft, ChevronRight, Menu, Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SheetOptions } from "./types";
|
||||
import SheetListMenu from "./menus";
|
||||
import SheetListMenu, { SheetRenameDialog } from "./menus";
|
||||
import Sheet from "./sheet";
|
||||
import { StyledButton } from "../toolbar";
|
||||
|
||||
@@ -28,12 +28,17 @@ function Navigation(props: NavigationProps) {
|
||||
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}>
|
||||
<StyledButton
|
||||
onClick={handleClick}
|
||||
title={t("navigation.sheet_list")}
|
||||
$pressed={false}
|
||||
>
|
||||
<Menu />
|
||||
</StyledButton>
|
||||
<Sheets>
|
||||
@@ -46,15 +51,13 @@ function Navigation(props: NavigationProps) {
|
||||
selected={index === selectedIndex}
|
||||
onSelected={() => onSheetSelected(index)}
|
||||
onColorChanged={function (hex: string): void {
|
||||
console.log("Picked:", hex);
|
||||
throw new Error("Function not implemented.");
|
||||
props.onSheetColorChanged(hex);
|
||||
}}
|
||||
onRenamed={function (name: string): void {
|
||||
console.log("Renamed:", name);
|
||||
throw new Error("Function not implemented.");
|
||||
props.onSheetRenamed(name);
|
||||
}}
|
||||
onDeleted={function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
props.onSheetDeleted();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
@@ -76,7 +79,6 @@ function Navigation(props: NavigationProps) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const ChevronLeftStyled = styled(ChevronLeft)`
|
||||
color: #333333;
|
||||
width: 16px;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Button, Menu, MenuItem, styled } from "@mui/material";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { SheetRenameDialog } from "./menus";
|
||||
import ColorPicker from "../colorPicker";
|
||||
interface SheetProps {
|
||||
name: string;
|
||||
color: string;
|
||||
@@ -13,6 +15,8 @@ interface SheetProps {
|
||||
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);
|
||||
@@ -20,10 +24,18 @@ function Sheet(props: SheetProps) {
|
||||
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}>
|
||||
@@ -42,10 +54,42 @@ function Sheet(props: SheetProps) {
|
||||
horizontal: 6,
|
||||
}}
|
||||
>
|
||||
<MenuItem>Rename</MenuItem>
|
||||
<MenuItem>Change Color</MenuItem>
|
||||
<MenuItem>Delete</MenuItem>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
Type,
|
||||
ArrowDownToLine,
|
||||
ArrowUpToLine,
|
||||
Grid2x2Check,
|
||||
Grid2x2X,
|
||||
} from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRef, useState } from "react";
|
||||
@@ -67,6 +69,8 @@ type ToolbarProperties = {
|
||||
verticalAlign: VerticalAlignment;
|
||||
canEdit: boolean;
|
||||
numFmt: string;
|
||||
showGridLines: boolean;
|
||||
onToggleShowGridLines: (show: boolean) => void;
|
||||
};
|
||||
|
||||
function Toolbar(properties: ToolbarProperties) {
|
||||
@@ -332,6 +336,17 @@ function Toolbar(properties: ToolbarProperties) {
|
||||
>
|
||||
<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 => {
|
||||
@@ -367,7 +382,7 @@ const ToolbarContainer = styled("div")`
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.palette.background.paper};
|
||||
background: ${({ }) => theme.palette.background.paper};
|
||||
height: ${toolbarHeight}px;
|
||||
line-height: ${toolbarHeight}px;
|
||||
border-bottom: 1px solid ${({}) => theme.palette.grey["600"]};
|
||||
@@ -377,47 +392,47 @@ const ToolbarContainer = styled("div")`
|
||||
`;
|
||||
|
||||
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",
|
||||
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",
|
||||
};
|
||||
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
|
||||
? theme.palette.grey["600"]
|
||||
: "#FFF");
|
||||
result["&:hover"] = {
|
||||
backgroundColor: "#F1F2F8",
|
||||
borderTopColor: "#F1F2F8",
|
||||
};
|
||||
}
|
||||
result["svg"] = {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
};
|
||||
return result;
|
||||
}
|
||||
);
|
||||
result["svg"] = {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
};
|
||||
return result;
|
||||
});
|
||||
|
||||
const Divider = styled("div")({
|
||||
width: "0px",
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { useCallback, KeyboardEvent, RefObject } from 'react';
|
||||
import { isEditingKey, isNavigationKey, NavigationKey } from './WorksheetCanvas/util';
|
||||
import { useCallback, KeyboardEvent, RefObject } from "react";
|
||||
import {
|
||||
isEditingKey,
|
||||
isNavigationKey,
|
||||
NavigationKey,
|
||||
} from "./WorksheetCanvas/util";
|
||||
|
||||
export enum Border {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
Right = 'right',
|
||||
Left = 'left',
|
||||
Top = "top",
|
||||
Bottom = "bottom",
|
||||
Right = "right",
|
||||
Left = "left",
|
||||
}
|
||||
|
||||
interface Options {
|
||||
onCellsDeleted: () => void;
|
||||
onExpandAreaSelectedKeyboard: (key: 'ArrowRight' | 'ArrowLeft' | 'ArrowUp' | 'ArrowDown') => void;
|
||||
onExpandAreaSelectedKeyboard: (
|
||||
key: "ArrowRight" | "ArrowLeft" | "ArrowUp" | "ArrowDown"
|
||||
) => void;
|
||||
onEditKeyPressStart: (initText: string) => void;
|
||||
onCellEditStart: () => void;
|
||||
onBold: () => void;
|
||||
@@ -27,14 +33,41 @@ interface Options {
|
||||
onKeyEnd: () => void;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
onNextSheet: () => void;
|
||||
onPreviousSheet: () => void;
|
||||
root: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const useKeyboardNavigation = (options: Options): { onKeyDown: (event: KeyboardEvent) => void } => {
|
||||
// # 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;
|
||||
@@ -44,35 +77,35 @@ const useKeyboardNavigation = (options: Options): { onKeyDown: (event: KeyboardE
|
||||
}
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
switch (key) {
|
||||
case 'z': {
|
||||
case "z": {
|
||||
options.onUndo();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'y': {
|
||||
case "y": {
|
||||
options.onRedo();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'b': {
|
||||
case "b": {
|
||||
options.onBold();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'i': {
|
||||
case "i": {
|
||||
options.onItalic();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'u': {
|
||||
case "u": {
|
||||
options.onUnderline();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -82,20 +115,38 @@ const useKeyboardNavigation = (options: Options): { onKeyDown: (event: KeyboardE
|
||||
// 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') {
|
||||
if (key === "F2") {
|
||||
options.onCellEditStart();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (isEditingKey(key) || key === 'Backspace') {
|
||||
const initText = key === 'Backspace' ? '' : key;
|
||||
if (isEditingKey(key) || key === "Backspace") {
|
||||
const initText = key === "Backspace" ? "" : key;
|
||||
options.onEditKeyPressStart(initText);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -104,13 +155,13 @@ const useKeyboardNavigation = (options: Options): { onKeyDown: (event: KeyboardE
|
||||
// Worksheet Navigation
|
||||
if (event.shiftKey) {
|
||||
if (
|
||||
key === 'ArrowRight' ||
|
||||
key === 'ArrowLeft' ||
|
||||
key === 'ArrowUp' ||
|
||||
key === 'ArrowDown'
|
||||
key === "ArrowRight" ||
|
||||
key === "ArrowLeft" ||
|
||||
key === "ArrowUp" ||
|
||||
key === "ArrowDown"
|
||||
) {
|
||||
options.onExpandAreaSelectedKeyboard(key);
|
||||
} else if (key === 'Tab') {
|
||||
} else if (key === "Tab") {
|
||||
options.onArrowLeft();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -118,49 +169,49 @@ const useKeyboardNavigation = (options: Options): { onKeyDown: (event: KeyboardE
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case 'ArrowRight':
|
||||
case 'Tab': {
|
||||
case "ArrowRight":
|
||||
case "Tab": {
|
||||
options.onArrowRight();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
case "ArrowLeft": {
|
||||
options.onArrowLeft();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowDown':
|
||||
case 'Enter': {
|
||||
case "ArrowDown":
|
||||
case "Enter": {
|
||||
options.onArrowDown();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'ArrowUp': {
|
||||
case "ArrowUp": {
|
||||
options.onArrowUp();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'End': {
|
||||
case "End": {
|
||||
options.onKeyEnd();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Home': {
|
||||
case "Home": {
|
||||
options.onKeyHome();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'Delete': {
|
||||
case "Delete": {
|
||||
options.onCellsDeleted();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'PageDown': {
|
||||
case "PageDown": {
|
||||
options.onPageDown();
|
||||
|
||||
break;
|
||||
}
|
||||
case 'PageUp': {
|
||||
case "PageUp": {
|
||||
options.onPageUp();
|
||||
|
||||
break;
|
||||
@@ -170,7 +221,7 @@ const useKeyboardNavigation = (options: Options): { onKeyDown: (event: KeyboardE
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
},
|
||||
[options],
|
||||
[options]
|
||||
);
|
||||
return { onKeyDown };
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ interface PointerSettings {
|
||||
// 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;
|
||||
@@ -114,6 +115,8 @@ const usePointer = (options: PointerSettings): PointerEvents => {
|
||||
const cell = worksheet.getCellByCoordinates(x, y);
|
||||
if (cell) {
|
||||
options.onAreaSelecting(cell);
|
||||
} else {
|
||||
console.log('Failed');
|
||||
}
|
||||
} else if (isExtending.current) {
|
||||
const { canvasElement, worksheetCanvas } = options;
|
||||
@@ -144,6 +147,7 @@ const usePointer = (options: PointerSettings): PointerEvents => {
|
||||
const { worksheetElement } = options;
|
||||
isSelecting.current = false;
|
||||
worksheetElement.current?.releasePointerCapture(event.pointerId);
|
||||
options.onAreaSelected();
|
||||
} else if (isExtending.current) {
|
||||
const { worksheetElement } = options;
|
||||
isExtending.current = false;
|
||||
|
||||
@@ -31,16 +31,18 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
};
|
||||
|
||||
const updateRangeStyle = (stylePath: string, value: string) => {
|
||||
const area = {
|
||||
sheet: workbookState.getSelectedSheet(),
|
||||
...workbookState.getSelectedArea(),
|
||||
};
|
||||
const {
|
||||
sheet,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
const row = Math.min(rowStart, rowEnd);
|
||||
const column = Math.min(columnStart, columnEnd);
|
||||
const range = {
|
||||
sheet: area.sheet,
|
||||
row: area.rowStart,
|
||||
column: area.columnStart,
|
||||
width: area.columnEnd - area.columnStart + 1,
|
||||
height: area.rowEnd - area.rowStart + 1,
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
width: Math.abs(columnEnd - columnStart) + 1,
|
||||
height: Math.abs(rowEnd - rowStart) + 1,
|
||||
};
|
||||
model.updateRangeStyle(range, stylePath, value);
|
||||
setRedrawId((id) => id + 1);
|
||||
@@ -83,29 +85,59 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
};
|
||||
|
||||
const onCopyStyles = () => {
|
||||
const area = {
|
||||
sheet: workbookState.getSelectedSheet(),
|
||||
...workbookState.getSelectedArea(),
|
||||
};
|
||||
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 = area.rowStart; row < area.rowEnd; row++) {
|
||||
for (let row = row1; row <= row2; row++) {
|
||||
const styleRow = [];
|
||||
for (let column = area.columnStart; column < area.columnEnd; column++) {
|
||||
styleRow.push(model.getCellStyle(area.sheet, row, column));
|
||||
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 {
|
||||
throw new Error("Function not implemented.");
|
||||
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 {
|
||||
console.log(key);
|
||||
throw new Error("Function not implemented.");
|
||||
model.onExpandSelectedRange(key);
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onEditKeyPressStart: function (initText: string): void {
|
||||
console.log(initText);
|
||||
@@ -115,74 +147,69 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
onBold: () => {
|
||||
let sheet = workbookState.getSelectedSheet();
|
||||
let { row, column } = workbookState.getSelectedCell();
|
||||
let { sheet, row, column } = model.getSelectedView();
|
||||
let value = !model.getCellStyle(sheet, row, column).font.b;
|
||||
onToggleBold(!value);
|
||||
},
|
||||
onItalic: () => {
|
||||
let sheet = workbookState.getSelectedSheet();
|
||||
let { row, column } = workbookState.getSelectedCell();
|
||||
let { sheet, row, column } = model.getSelectedView();
|
||||
let value = !model.getCellStyle(sheet, row, column).font.i;
|
||||
onToggleItalic(!value);
|
||||
},
|
||||
onUnderline: () => {
|
||||
let sheet = workbookState.getSelectedSheet();
|
||||
let { row, column } = workbookState.getSelectedCell();
|
||||
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);
|
||||
throw new Error("Function not implemented.");
|
||||
// const newSelectedCell = model.getNavigationEdge(
|
||||
// key,
|
||||
// selectedSheet,
|
||||
// selectedCell.row,
|
||||
// selectedCell.column,
|
||||
// canvas.lastRow,
|
||||
// canvas.lastColumn,
|
||||
// );
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onPageDown: function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
model.onPageDown();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onPageUp: function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
model.onPageUp();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowDown: function (): void {
|
||||
const cell = workbookState.getSelectedCell();
|
||||
const row = cell.row + 1;
|
||||
if (row > LAST_ROW) {
|
||||
return;
|
||||
}
|
||||
workbookState.selectCell({ row, column: cell.column });
|
||||
model.onArrowDown();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowUp: function (): void {
|
||||
const cell = workbookState.getSelectedCell();
|
||||
const row = cell.row - 1;
|
||||
if (row < 1) {
|
||||
return;
|
||||
}
|
||||
workbookState.selectCell({ row, column: cell.column });
|
||||
model.onArrowUp();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowLeft: function (): void {
|
||||
const cell = workbookState.getSelectedCell();
|
||||
const column = cell.column - 1;
|
||||
if (column < 1) {
|
||||
return;
|
||||
}
|
||||
workbookState.selectCell({ row: cell.row, column });
|
||||
model.onArrowLeft();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onArrowRight: function (): void {
|
||||
const cell = workbookState.getSelectedCell();
|
||||
const column = cell.column + 1;
|
||||
if (column > LAST_COLUMN) {
|
||||
return;
|
||||
}
|
||||
workbookState.selectCell({ row: cell.row, column });
|
||||
model.onArrowRight();
|
||||
setRedrawId((id) => id + 1);
|
||||
},
|
||||
onKeyHome: function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
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 {
|
||||
throw new Error("Function not implemented.");
|
||||
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();
|
||||
@@ -192,6 +219,22 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -202,16 +245,20 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
rootRef.current.focus();
|
||||
});
|
||||
|
||||
const cellAddress = getCellAddress(
|
||||
workbookState.getSelectedArea(),
|
||||
workbookState.getSelectedCell()
|
||||
);
|
||||
const {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
|
||||
const sheet = workbookState.getSelectedSheet();
|
||||
const { row, column } = workbookState.getSelectedCell();
|
||||
const cellAddress = getCellAddress(
|
||||
{ rowStart, rowEnd, columnStart, columnEnd },
|
||||
{ row, column }
|
||||
);
|
||||
const formulaValue = model.getCellContent(sheet, row, column);
|
||||
|
||||
const style = model.getCellStyle(sheet, row, column);
|
||||
console.log("data", sheet, row, column, style);
|
||||
|
||||
return (
|
||||
<Container ref={rootRef} onKeyDown={onKeyDown} tabIndex={0}>
|
||||
@@ -230,8 +277,25 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
onTextColorPicked={onTextColorPicked}
|
||||
onFillColorPicked={onFillColorPicked}
|
||||
onNumberFormatPicked={onNumberFormatPicked}
|
||||
onBorderChanged={function (_border: BorderOptions): void {
|
||||
throw new Error("Function not implemented.");
|
||||
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}
|
||||
@@ -245,8 +309,21 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
<FormulaBar cellAddress={cellAddress} />
|
||||
<Worksheet
|
||||
model={model}
|
||||
workbookState={workbookState}
|
||||
@@ -256,24 +333,30 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||
/>
|
||||
<Navigation
|
||||
sheets={info}
|
||||
selectedIndex={workbookState.getSelectedSheet()}
|
||||
selectedIndex={model.getSelectedSheet()}
|
||||
onSheetSelected={function (sheet: number): void {
|
||||
workbookState.setSelectedSheet(sheet);
|
||||
model.setSelectedSheet(sheet);
|
||||
setRedrawId((value) => value + 1);
|
||||
}}
|
||||
onAddBlankSheet={function (): void {
|
||||
model.newSheet();
|
||||
}}
|
||||
onSheetColorChanged={function (hex: string): void {
|
||||
console.log(hex);
|
||||
throw new Error("Function not implemented.");
|
||||
try {
|
||||
model.setSheetColor(model.getSelectedSheet(), hex);
|
||||
} catch (e) {
|
||||
alert(`${e}`);
|
||||
}
|
||||
}}
|
||||
onSheetRenamed={function (name: string): void {
|
||||
console.log(name);
|
||||
throw new Error("Function not implemented.");
|
||||
try {
|
||||
model.renameSheet(model.getSelectedSheet(), name);
|
||||
} catch (e) {
|
||||
alert(`${e}`);
|
||||
}
|
||||
}}
|
||||
onSheetDeleted={function (): void {
|
||||
throw new Error("Function not implemented.");
|
||||
model.deleteSheet(model.getSelectedSheet());
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import { CellStyle } from "@ironcalc/wasm";
|
||||
|
||||
export interface Cell {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export enum AreaType {
|
||||
rowsDown,
|
||||
columnsRight,
|
||||
rowsUp,
|
||||
columnsLeft,
|
||||
}
|
||||
|
||||
export interface Area {
|
||||
type: AreaType;
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
}
|
||||
|
||||
interface Scroll {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
type FocusType = 'cell' | 'formula-bar';
|
||||
|
||||
type FocusType = "cell" | "formula-bar";
|
||||
|
||||
/**
|
||||
* In Excel there are two "modes" of editing
|
||||
@@ -26,7 +30,7 @@ type FocusType = 'cell' | 'formula-bar';
|
||||
*
|
||||
* In a formula bar mode is always `edit`.
|
||||
*/
|
||||
type CellEditMode = 'init' | 'edit';
|
||||
type CellEditMode = "init" | "edit";
|
||||
|
||||
interface Editor {
|
||||
id: number;
|
||||
@@ -39,48 +43,40 @@ interface Editor {
|
||||
focus: FocusType;
|
||||
}
|
||||
|
||||
interface Cells {
|
||||
topLeftCell: { row: number; column: number };
|
||||
bottomRightCell: { row: number; column: number };
|
||||
}
|
||||
|
||||
type AreaStyles = CellStyle[][];
|
||||
|
||||
export class WorkbookState {
|
||||
private selectedSheet: number;
|
||||
private selectedCell: Cell;
|
||||
private selectedArea: Area;
|
||||
private scroll: Scroll;
|
||||
private extendToArea: Area | null;
|
||||
private editor: Editor | null;
|
||||
private visibleCells: Cells | null;
|
||||
private id;
|
||||
private copyStyles: AreaStyles | null;
|
||||
|
||||
constructor() {
|
||||
const row = 1;
|
||||
const column = 1;
|
||||
const sheet = 0;
|
||||
this.selectedSheet = sheet;
|
||||
this.selectedCell = { row, column };
|
||||
this.selectedArea = {
|
||||
rowStart: row,
|
||||
rowEnd: row,
|
||||
columnStart: column,
|
||||
columnEnd: column,
|
||||
};
|
||||
this.extendToArea = null;
|
||||
this.scroll = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
this.visibleCells = null;
|
||||
this.editor = null;
|
||||
this.id = Math.floor(Math.random()*1000);
|
||||
this.id = Math.floor(Math.random() * 1000);
|
||||
this.copyStyles = null;
|
||||
}
|
||||
|
||||
startEditing(focus: FocusType, text: string) {
|
||||
const {row, column} = this.selectedCell;
|
||||
this.editor = {
|
||||
id: 0,
|
||||
sheet: this.selectedSheet,
|
||||
row,
|
||||
column,
|
||||
base: '',
|
||||
text,
|
||||
mode: 'init',
|
||||
focus
|
||||
}
|
||||
startEditing(_focus: FocusType, _text: string) {
|
||||
// const {row, column} = this.selectedCell;
|
||||
// this.editor = {
|
||||
// id: 0,
|
||||
// sheet: this.selectedSheet,
|
||||
// row,
|
||||
// column,
|
||||
// base: '',
|
||||
// text,
|
||||
// mode: 'init',
|
||||
// focus
|
||||
// }
|
||||
}
|
||||
|
||||
setEditorText(text: string) {
|
||||
@@ -90,64 +86,27 @@ export class WorkbookState {
|
||||
this.editor.text = text;
|
||||
}
|
||||
|
||||
setVisibleCells(cells: Cells) {
|
||||
this.visibleCells = cells;
|
||||
}
|
||||
|
||||
getVisibleCells(): Cells | null {
|
||||
return this.visibleCells;
|
||||
}
|
||||
|
||||
endEditing() {
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
getEditor(): Editor | null {
|
||||
console.log('getEditor', this.id);
|
||||
console.log("getEditor", this.id);
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
getSelectedSheet(): number {
|
||||
return this.selectedSheet;
|
||||
}
|
||||
|
||||
setSelectedSheet(sheet: number): void {
|
||||
this.selectedSheet = sheet;
|
||||
}
|
||||
|
||||
getSelectedCell(): Cell {
|
||||
return this.selectedCell;
|
||||
}
|
||||
|
||||
setSelectedCell(cell: Cell): void {
|
||||
this.selectedCell = cell;
|
||||
}
|
||||
|
||||
getSelectedArea(): Area {
|
||||
return this.selectedArea;
|
||||
}
|
||||
|
||||
setSelectedArea(area: Area): void {
|
||||
this.selectedArea = area;
|
||||
}
|
||||
|
||||
selectCell(cell: { row: number; column: number }): void {
|
||||
console.log('selectCell: ', this.id)
|
||||
const { row, column } = cell;
|
||||
this.selectedArea = {
|
||||
rowStart: row,
|
||||
rowEnd: row,
|
||||
columnStart: column,
|
||||
columnEnd: column,
|
||||
};
|
||||
this.selectedCell = { row, column };
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
getScroll(): Scroll {
|
||||
return this.scroll;
|
||||
}
|
||||
|
||||
setScroll(scroll: Scroll): void {
|
||||
this.scroll = scroll;
|
||||
}
|
||||
|
||||
getExtendToArea(): Area | null {
|
||||
return this.extendToArea;
|
||||
}
|
||||
|
||||
|
||||
clearExtendToArea(): void {
|
||||
this.extendToArea = null;
|
||||
}
|
||||
@@ -155,4 +114,12 @@ export class WorkbookState {
|
||||
setExtendToArea(area: Area): void {
|
||||
this.extendToArea = area;
|
||||
}
|
||||
|
||||
setCopyStyles(styles: AreaStyles | null): void {
|
||||
this.copyStyles = styles;
|
||||
}
|
||||
|
||||
getCopyStyles(): AreaStyles | null {
|
||||
return this.copyStyles;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
outlineColor,
|
||||
} from "./WorksheetCanvas/constants";
|
||||
import usePointer from "./usePointer";
|
||||
import { WorkbookState } from "./workbookState";
|
||||
import { AreaType, WorkbookState } from "./workbookState";
|
||||
import { Cell } from "./WorksheetCanvas/types";
|
||||
import Editor from "./editor";
|
||||
import EditorContext, { EditorState } from "./editor/editorContext";
|
||||
@@ -35,16 +35,15 @@ function Worksheet(props: {
|
||||
const worksheetCanvas = useRef<WorksheetCanvas | null>(null);
|
||||
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
const ignoreScrollEventRef = useRef(false);
|
||||
|
||||
const [editorContext, setEditorContext] = useState<EditorState>({
|
||||
mode: "accept",
|
||||
insertRange: null,
|
||||
baseText: '',
|
||||
id: Math.floor(Math.random()*1000),
|
||||
baseText: "",
|
||||
id: Math.floor(Math.random() * 1000),
|
||||
});
|
||||
|
||||
console.log('worksheet', editorContext.id);
|
||||
|
||||
const { model, workbookState, refresh } = props;
|
||||
useEffect(() => {
|
||||
const canvasRef = canvasElement.current;
|
||||
@@ -67,9 +66,12 @@ function Worksheet(props: {
|
||||
!outline ||
|
||||
!handle ||
|
||||
!area ||
|
||||
!extendTo
|
||||
!extendTo ||
|
||||
!scrollElement.current
|
||||
)
|
||||
return;
|
||||
model.setWindowWidth(worksheetRef.clientWidth);
|
||||
model.setWindowHeight(worksheetRef.clientHeight);
|
||||
const canvas = new WorksheetCanvas({
|
||||
width: worksheetRef.clientWidth,
|
||||
height: worksheetRef.clientHeight,
|
||||
@@ -94,16 +96,38 @@ function Worksheet(props: {
|
||||
worksheetCanvas.current?.renderSheet();
|
||||
},
|
||||
});
|
||||
const [sheetWidth, sheetHeight] = canvas.getSheetDimensions();
|
||||
const scrollX = model.getScrollX();
|
||||
const scrollY = model.getScrollY();
|
||||
const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000]; //canvas.getSheetDimensions();
|
||||
if (spacerElement.current) {
|
||||
spacerElement.current.style.height = `${sheetHeight}px`;
|
||||
spacerElement.current.style.width = `${sheetWidth}px`;
|
||||
}
|
||||
const left = scrollElement.current.scrollLeft;
|
||||
const top = scrollElement.current.scrollTop;
|
||||
if (scrollX !== left) {
|
||||
ignoreScrollEventRef.current = true;
|
||||
scrollElement.current.scrollLeft = scrollX;
|
||||
setTimeout(() => {
|
||||
ignoreScrollEventRef.current = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (scrollY !== top) {
|
||||
ignoreScrollEventRef.current = true;
|
||||
scrollElement.current.scrollTop = scrollY;
|
||||
setTimeout(() => {
|
||||
ignoreScrollEventRef.current = false;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
canvas.renderSheet();
|
||||
worksheetCanvas.current = canvas;
|
||||
});
|
||||
|
||||
const sheetNames = model.getWorksheetsProperties().map((s: { name: string; }) => s.name);
|
||||
const sheetNames = model
|
||||
.getWorksheetsProperties()
|
||||
.map((s: { name: string }) => s.name);
|
||||
|
||||
const {
|
||||
onPointerMove,
|
||||
@@ -115,8 +139,7 @@ function Worksheet(props: {
|
||||
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
workbookState.selectCell(cell);
|
||||
// worksheetCanvas.current?.renderSheet();
|
||||
model.setSelectedCell(cell.row, cell.column);
|
||||
refresh();
|
||||
},
|
||||
onAreaSelecting: (cell: Cell) => {
|
||||
@@ -125,74 +148,141 @@ function Worksheet(props: {
|
||||
return;
|
||||
}
|
||||
const { row, column } = cell;
|
||||
// const { width, height } = worksheet.getBoundingClientRect();
|
||||
// const [x, y] = canvas.getCoordinatesByCell(row, column);
|
||||
// const [x1, y1] = canvas.getCoordinatesByCell(row + 1, column + 1);
|
||||
// const { left: canvasLeft, top: canvasTop } = canvas.getScrollPosition();
|
||||
// // let border = Border.Right;
|
||||
// // let { left, top } = state.scrollPosition;
|
||||
// // if (x < headerColumnWidth) {
|
||||
// // border = Border.Left;
|
||||
// // left = canvasLeft - headerColumnWidth + x;
|
||||
// // } else if (x1 > width - 20) {
|
||||
// // border = Border.Right;
|
||||
// // }
|
||||
// // if (y < headerRowHeight) {
|
||||
// // border = Border.Top;
|
||||
// // top = canvasTop - headerRowHeight + y;
|
||||
// // } else if (y1 > height - 20) {
|
||||
// // border = Border.Bottom;
|
||||
// // }
|
||||
const selectedCell = workbookState.getSelectedCell();
|
||||
const area = {
|
||||
rowStart: Math.min(selectedCell.row, row),
|
||||
rowEnd: Math.max(selectedCell.row, row),
|
||||
columnStart: Math.min(selectedCell.column, column),
|
||||
columnEnd: Math.max(selectedCell.column, column),
|
||||
};
|
||||
workbookState.setSelectedArea(area);
|
||||
model.onAreaSelecting(row, column);
|
||||
canvas.renderSheet();
|
||||
// // If there are frozen rows or columns snap to origin if we cross boundaries
|
||||
// const frozenRows = canvas.workbook.getFrozenRowsCount();
|
||||
// const frozenColumns = canvas.workbook.getFrozenColumnsCount();
|
||||
// if (area.rowStart <= frozenRows && area.rowEnd > frozenRows) {
|
||||
// top = 0;
|
||||
// }
|
||||
// if (area.columnStart <= frozenColumns && area.columnEnd > frozenColumns) {
|
||||
// left = 0;
|
||||
// }
|
||||
}, // editorActions.onPointerMoveToCell,
|
||||
},
|
||||
onAreaSelected: () => {
|
||||
let styles = workbookState.getCopyStyles();
|
||||
if (styles && styles.length) {
|
||||
model.onPasteStyles(styles);
|
||||
const canvas = worksheetCanvas.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
canvas.renderSheet();
|
||||
}
|
||||
workbookState.setCopyStyles(null);
|
||||
if (worksheetElement.current) {
|
||||
worksheetElement.current.style.cursor = "auto";
|
||||
}
|
||||
},
|
||||
onExtendToCell: (cell) => {
|
||||
const canvas = worksheetCanvas.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
const { row, column } = cell;
|
||||
const selectedCell = workbookState.getSelectedCell();
|
||||
const area = {
|
||||
rowStart: Math.min(selectedCell.row, row),
|
||||
rowEnd: Math.max(selectedCell.row, row),
|
||||
columnStart: Math.min(selectedCell.column, column),
|
||||
columnEnd: Math.max(selectedCell.column, column),
|
||||
};
|
||||
workbookState.setExtendToArea(area);
|
||||
canvas.renderSheet();
|
||||
}, // editorActions.onExtendToCell,
|
||||
const {
|
||||
sheet,
|
||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||
} = model.getSelectedView();
|
||||
// We are either extending by rows or by columns
|
||||
// And we could be doing it in the positive direction (downwards or right)
|
||||
// or the negative direction (upwards or left)
|
||||
|
||||
if (
|
||||
row > rowEnd &&
|
||||
((column <= columnEnd && column >= columnStart) ||
|
||||
(column < columnStart && columnStart - column < row - rowEnd) ||
|
||||
(column > columnEnd && column - columnEnd < row - rowEnd))
|
||||
) {
|
||||
// rows downwards
|
||||
const area = {
|
||||
type: AreaType.rowsDown,
|
||||
rowStart: rowEnd + 1,
|
||||
rowEnd: row,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
};
|
||||
workbookState.setExtendToArea(area);
|
||||
canvas.renderSheet();
|
||||
} else if (
|
||||
row < rowStart &&
|
||||
((column <= columnEnd && column >= columnStart) ||
|
||||
(column < columnStart && columnStart - column < rowStart - row) ||
|
||||
(column > columnEnd && column - columnEnd < rowStart - row))
|
||||
) {
|
||||
// rows upwards
|
||||
const area = {
|
||||
type: AreaType.rowsUp,
|
||||
rowStart: row,
|
||||
rowEnd: rowStart,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
};
|
||||
workbookState.setExtendToArea(area);
|
||||
canvas.renderSheet();
|
||||
} else if (
|
||||
column > columnEnd &&
|
||||
((row <= rowEnd && row >= rowStart) ||
|
||||
(row < rowStart && rowStart - row < column - columnEnd) ||
|
||||
(row > rowEnd && row - rowEnd < column - columnEnd))
|
||||
) {
|
||||
// columns right
|
||||
const area = {
|
||||
type: AreaType.columnsRight,
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: columnEnd + 1,
|
||||
columnEnd: column,
|
||||
};
|
||||
workbookState.setExtendToArea(area);
|
||||
canvas.renderSheet();
|
||||
} else if (
|
||||
column < columnStart &&
|
||||
((row <= rowEnd && row >= rowStart) ||
|
||||
(row < rowStart && rowStart - row < columnStart - column) ||
|
||||
(row > rowEnd && row - rowEnd < columnStart - column))
|
||||
) {
|
||||
// columns left
|
||||
const area = {
|
||||
type: AreaType.columnsLeft,
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: column,
|
||||
columnEnd: columnStart,
|
||||
};
|
||||
workbookState.setExtendToArea(area);
|
||||
canvas.renderSheet();
|
||||
}
|
||||
},
|
||||
onExtendToEnd: () => {
|
||||
const canvas = worksheetCanvas.current;
|
||||
if (!canvas) {
|
||||
return;
|
||||
}
|
||||
const sheet = workbookState.getSelectedSheet();
|
||||
const initialArea = workbookState.getSelectedArea();
|
||||
const { sheet, range } = model.getSelectedView();
|
||||
const extendedArea = workbookState.getExtendToArea();
|
||||
if (!extendedArea) {
|
||||
return;
|
||||
}
|
||||
// model.extendTo(sheet, initialArea, extendedArea);
|
||||
const rowStart = Math.min(range[0], range[2]);
|
||||
const height = Math.abs(range[2] - range[0]) + 1;
|
||||
const width = Math.abs(range[3] - range[1]) + 1;
|
||||
const columnStart = Math.min(range[1], range[3]);
|
||||
|
||||
const area = { sheet, row: rowStart, column: columnStart, width, height };
|
||||
|
||||
switch (extendedArea.type) {
|
||||
case AreaType.rowsDown:
|
||||
model.autoFillRows(area, extendedArea.rowEnd);
|
||||
break;
|
||||
case AreaType.rowsUp: {
|
||||
model.autoFillRows(area, extendedArea.rowStart);
|
||||
break;
|
||||
}
|
||||
case AreaType.columnsRight: {
|
||||
model.autoFillColumns(area, extendedArea.columnEnd);
|
||||
break;
|
||||
}
|
||||
case AreaType.columnsLeft: {
|
||||
model.autoFillColumns(area, extendedArea.columnStart);
|
||||
break;
|
||||
}
|
||||
}
|
||||
workbookState.clearExtendToArea();
|
||||
canvas.renderSheet();
|
||||
}, // editorActions.onExtendToEnd,
|
||||
},
|
||||
canvasElement,
|
||||
worksheetElement,
|
||||
worksheetCanvas,
|
||||
@@ -202,10 +292,14 @@ function Worksheet(props: {
|
||||
// onColumnContextMenu,
|
||||
});
|
||||
|
||||
const onScroll = (): void => {
|
||||
const onScroll = (_event: any): void => {
|
||||
if (!scrollElement.current || !worksheetCanvas.current) {
|
||||
return;
|
||||
}
|
||||
if (ignoreScrollEventRef.current) {
|
||||
// Programmatic scroll ignored
|
||||
return;
|
||||
}
|
||||
const left = scrollElement.current.scrollLeft;
|
||||
const top = scrollElement.current.scrollTop;
|
||||
|
||||
@@ -213,33 +307,37 @@ function Worksheet(props: {
|
||||
worksheetCanvas.current.renderSheet();
|
||||
};
|
||||
|
||||
const {row, column} = workbookState.getSelectedCell();
|
||||
const selectedSheet = workbookState.getSelectedSheet();
|
||||
const { row, column, sheet: selectedSheet } = model.getSelectedView();
|
||||
|
||||
return (
|
||||
// <EditorContext.Provider value={{editorContext}}>
|
||||
<Wrapper ref={scrollElement} onScroll={onScroll}>
|
||||
<Wrapper ref={scrollElement} onScroll={onScroll} className="scroll">
|
||||
<Spacer ref={spacerElement} />
|
||||
<SheetContainer
|
||||
className="sheet-container"
|
||||
ref={worksheetElement}
|
||||
onPointerDown={(event) => {
|
||||
if (isEditing === true && editorContext.mode !== 'insert') {
|
||||
if (isEditing === true && editorContext.mode !== "insert") {
|
||||
setEditing(false);
|
||||
model.setUserInput(selectedSheet, row, column, editorContext.baseText);
|
||||
model.setUserInput(
|
||||
selectedSheet,
|
||||
row,
|
||||
column,
|
||||
editorContext.baseText
|
||||
);
|
||||
}
|
||||
onPointerDown(event);
|
||||
}}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
onDoubleClick={(event) => {
|
||||
const sheet = workbookState.getSelectedSheet();
|
||||
const {row, column} = workbookState.getSelectedCell();
|
||||
const text = model.getCellContent(sheet, row, column) || '';
|
||||
console.log('dbclick', text);
|
||||
const { sheet, row, column } = model.getSelectedView();
|
||||
const text = model.getCellContent(sheet, row, column) || "";
|
||||
console.log("dbclick", text);
|
||||
|
||||
workbookState.startEditing("cell", `${text}`);
|
||||
setEditorContext ((c: EditorState) => {
|
||||
console.log('text', text, c.id);
|
||||
setEditorContext((c: EditorState) => {
|
||||
console.log("text", text, c.id);
|
||||
return {
|
||||
mode: c.mode,
|
||||
insertRange: c.insertRange,
|
||||
@@ -248,7 +346,7 @@ function Worksheet(props: {
|
||||
id: c.id,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
setEditing(true);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
@@ -270,30 +368,19 @@ function Worksheet(props: {
|
||||
editorContext.insertRange,
|
||||
insertRangeText
|
||||
);
|
||||
} }
|
||||
}}
|
||||
onEditEnd={(text: string) => {
|
||||
console.log(text);
|
||||
setEditing(false);
|
||||
model.setUserInput(selectedSheet, row, column, text);
|
||||
} }
|
||||
originalText={model.getCellContent(selectedSheet, row, column) || ''}
|
||||
}}
|
||||
originalText={
|
||||
model.getCellContent(selectedSheet, row, column) || ""
|
||||
}
|
||||
display={isEditing}
|
||||
cell={{ sheet: selectedSheet, row, column }}
|
||||
sheetNames={sheetNames}
|
||||
/>
|
||||
/* <Editor
|
||||
data-testid={WorkbookTestId.WorkbookCellEditor}
|
||||
onEditChange={onEditChange}
|
||||
onEditEnd={onEditEnd}
|
||||
onEditEscape={onEditEscape}
|
||||
onReferenceCycle={onReferenceCycle}
|
||||
display={!!cellEditing}
|
||||
focus={cellEditing?.focus === FocusType.Cell}
|
||||
html={cellEditing?.html ?? ''}
|
||||
cursorStart={cellEditing?.cursorStart ?? 0}
|
||||
cursorEnd={cellEditing?.cursorEnd ?? 0}
|
||||
mode={cellEditing?.mode ?? 'init'}
|
||||
/> */
|
||||
}
|
||||
</CellOutline>
|
||||
<AreaOutline ref={areaOutline} />
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('fonts/inter-v13-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
/* inter-600 - latin */
|
||||
@font-face {
|
||||
}
|
||||
/* inter-600 - latin */
|
||||
@font-face {
|
||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('fonts/inter-v13-latin-600.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh; */
|
||||
}
|
||||
|
||||
@@ -41,5 +41,10 @@
|
||||
"title": "Custom number format",
|
||||
"label": "Number format",
|
||||
"save": "Save"
|
||||
},
|
||||
"sheet_rename": {
|
||||
"rename": "Save",
|
||||
"label": "New name",
|
||||
"title": "Rename Sheet"
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Button } from './Button';
|
||||
|
||||
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
|
||||
const meta = {
|
||||
title: 'Example/Button',
|
||||
component: Button,
|
||||
parameters: {
|
||||
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
|
||||
layout: 'centered',
|
||||
},
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
|
||||
argTypes: {
|
||||
backgroundColor: { control: 'color' },
|
||||
},
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
primary: true,
|
||||
label: 'Button',
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
label: 'Button',
|
||||
},
|
||||
};
|
||||
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
size: 'large',
|
||||
label: 'Button',
|
||||
},
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
size: 'small',
|
||||
label: 'Button',
|
||||
},
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import './button.css';
|
||||
|
||||
interface ButtonProps {
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
primary?: boolean;
|
||||
/**
|
||||
* What background color to use
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
/**
|
||||
* How large should the button be?
|
||||
*/
|
||||
size?: 'small' | 'medium' | 'large';
|
||||
/**
|
||||
* Button contents
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Optional click handler
|
||||
*/
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const Button = ({
|
||||
primary = false,
|
||||
size = 'medium',
|
||||
backgroundColor,
|
||||
label,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
|
||||
style={{ backgroundColor }}
|
||||
{...props}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -1,364 +0,0 @@
|
||||
import { Meta } from "@storybook/blocks";
|
||||
|
||||
import Github from "./assets/github.svg";
|
||||
import Discord from "./assets/discord.svg";
|
||||
import Youtube from "./assets/youtube.svg";
|
||||
import Tutorials from "./assets/tutorials.svg";
|
||||
import Styling from "./assets/styling.png";
|
||||
import Context from "./assets/context.png";
|
||||
import Assets from "./assets/assets.png";
|
||||
import Docs from "./assets/docs.png";
|
||||
import Share from "./assets/share.png";
|
||||
import FigmaPlugin from "./assets/figma-plugin.png";
|
||||
import Testing from "./assets/testing.png";
|
||||
import Accessibility from "./assets/accessibility.png";
|
||||
import Theming from "./assets/theming.png";
|
||||
import AddonLibrary from "./assets/addon-library.png";
|
||||
|
||||
export const RightArrow = () => <svg
|
||||
viewBox="0 0 14 14"
|
||||
width="8px"
|
||||
height="14px"
|
||||
style={{
|
||||
marginLeft: '4px',
|
||||
display: 'inline-block',
|
||||
shapeRendering: 'inherit',
|
||||
verticalAlign: 'middle',
|
||||
fill: 'currentColor',
|
||||
'path fill': 'currentColor'
|
||||
}}
|
||||
>
|
||||
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
|
||||
</svg>
|
||||
|
||||
<Meta title="Configure your project" />
|
||||
|
||||
<div className="sb-container">
|
||||
<div className='sb-section-title'>
|
||||
# Configure your project
|
||||
|
||||
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
|
||||
</div>
|
||||
<div className="sb-section">
|
||||
<div className="sb-section-item">
|
||||
<img
|
||||
src={Styling}
|
||||
alt="A wall of logos representing different styling technologies"
|
||||
/>
|
||||
<h4 className="sb-section-item-heading">Add styling and CSS</h4>
|
||||
<p className="sb-section-item-paragraph">Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/configure/styling-and-css"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-section-item">
|
||||
<img
|
||||
src={Context}
|
||||
alt="An abstraction representing the composition of data for a component"
|
||||
/>
|
||||
<h4 className="sb-section-item-heading">Provide context and mocking</h4>
|
||||
<p className="sb-section-item-paragraph">Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/writing-stories/decorators#context-for-mocking"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-section-item">
|
||||
<img src={Assets} alt="A representation of typography and image assets" />
|
||||
<div>
|
||||
<h4 className="sb-section-item-heading">Load assets and resources</h4>
|
||||
<p className="sb-section-item-paragraph">To link static files (like fonts) to your projects and stories, use the
|
||||
`staticDirs` configuration option to specify folders to load when
|
||||
starting Storybook.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/configure/images-and-assets"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sb-container">
|
||||
<div className='sb-section-title'>
|
||||
# Do more with Storybook
|
||||
|
||||
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
|
||||
</div>
|
||||
|
||||
<div className="sb-section">
|
||||
<div className="sb-features-grid">
|
||||
<div className="sb-grid-item">
|
||||
<img src={Docs} alt="A screenshot showing the autodocs tag being set, pointing a docs page being generated" />
|
||||
<h4 className="sb-section-item-heading">Autodocs</h4>
|
||||
<p className="sb-section-item-paragraph">Auto-generate living,
|
||||
interactive reference documentation from your components and stories.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/writing-docs/autodocs"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-grid-item">
|
||||
<img src={Share} alt="A browser window showing a Storybook being published to a chromatic.com URL" />
|
||||
<h4 className="sb-section-item-heading">Publish to Chromatic</h4>
|
||||
<p className="sb-section-item-paragraph">Publish your Storybook to review and collaborate with your entire team.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/sharing/publish-storybook#publish-storybook-with-chromatic"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-grid-item">
|
||||
<img src={FigmaPlugin} alt="Windows showing the Storybook plugin in Figma" />
|
||||
<h4 className="sb-section-item-heading">Figma Plugin</h4>
|
||||
<p className="sb-section-item-paragraph">Embed your stories into Figma to cross-reference the design and live
|
||||
implementation in one place.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/sharing/design-integrations#embed-storybook-in-figma-with-the-plugin"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-grid-item">
|
||||
<img src={Testing} alt="Screenshot of tests passing and failing" />
|
||||
<h4 className="sb-section-item-heading">Testing</h4>
|
||||
<p className="sb-section-item-paragraph">Use stories to test a component in all its variations, no matter how
|
||||
complex.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/writing-tests/introduction"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-grid-item">
|
||||
<img src={Accessibility} alt="Screenshot of accessibility tests passing and failing" />
|
||||
<h4 className="sb-section-item-heading">Accessibility</h4>
|
||||
<p className="sb-section-item-paragraph">Automatically test your components for a11y issues as you develop.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/writing-tests/accessibility-testing"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-grid-item">
|
||||
<img src={Theming} alt="Screenshot of Storybook in light and dark mode" />
|
||||
<h4 className="sb-section-item-heading">Theming</h4>
|
||||
<p className="sb-section-item-paragraph">Theme Storybook's UI to personalize it to your project.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/docs/react/configure/theming"
|
||||
target="_blank"
|
||||
>Learn more<RightArrow /></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='sb-addon'>
|
||||
<div className='sb-addon-text'>
|
||||
<h4>Addons</h4>
|
||||
<p className="sb-section-item-paragraph">Integrate your tools with Storybook to connect workflows.</p>
|
||||
<a
|
||||
href="https://storybook.js.org/integrations/"
|
||||
target="_blank"
|
||||
>Discover all addons<RightArrow /></a>
|
||||
</div>
|
||||
<div className='sb-addon-img'>
|
||||
<img src={AddonLibrary} alt="Integrate your tools with Storybook to connect workflows." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sb-section sb-socials">
|
||||
<div className="sb-section-item">
|
||||
<img src={Github} alt="Github logo" className="sb-explore-image"/>
|
||||
Join our contributors building the future of UI development.
|
||||
|
||||
<a
|
||||
href="https://github.com/storybookjs/storybook"
|
||||
target="_blank"
|
||||
>Star on GitHub<RightArrow /></a>
|
||||
</div>
|
||||
<div className="sb-section-item">
|
||||
<img src={Discord} alt="Discord logo" className="sb-explore-image"/>
|
||||
<div>
|
||||
Get support and chat with frontend developers.
|
||||
|
||||
<a
|
||||
href="https://discord.gg/storybook"
|
||||
target="_blank"
|
||||
>Join Discord server<RightArrow /></a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sb-section-item">
|
||||
<img src={Youtube} alt="Youtube logo" className="sb-explore-image"/>
|
||||
<div>
|
||||
Watch tutorials, feature previews and interviews.
|
||||
|
||||
<a
|
||||
href="https://www.youtube.com/@chromaticui"
|
||||
target="_blank"
|
||||
>Watch on YouTube<RightArrow /></a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sb-section-item">
|
||||
<img src={Tutorials} alt="A book" className="sb-explore-image"/>
|
||||
<p>Follow guided walkthroughs on for key workflows.</p>
|
||||
|
||||
<a
|
||||
href="https://storybook.js.org/tutorials/"
|
||||
target="_blank"
|
||||
>Discover tutorials<RightArrow /></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
{`
|
||||
.sb-container {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.sb-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sb-section-title {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.sb-section a:not(h1 a, h2 a, h3 a) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sb-section-item, .sb-grid-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sb-section-item-heading {
|
||||
padding-top: 20px !important;
|
||||
padding-bottom: 5px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.sb-section-item-paragraph {
|
||||
margin: 0;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.sb-chevron {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.sb-features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: 32px 20px;
|
||||
}
|
||||
|
||||
.sb-socials {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.sb-socials p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.sb-explore-image {
|
||||
max-height: 32px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.sb-addon {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background-color: #EEF3F8;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
background: #EEF3F8;
|
||||
height: 180px;
|
||||
margin-bottom: 48px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sb-addon-text {
|
||||
padding-left: 48px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.sb-addon-text h4 {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.sb-addon-img {
|
||||
position: absolute;
|
||||
left: 345px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 200%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sb-addon-img img {
|
||||
width: 650px;
|
||||
transform: rotate(-15deg);
|
||||
margin-left: 40px;
|
||||
margin-top: -72px;
|
||||
box-shadow: 0 0 1px rgba(255, 255, 255, 0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.sb-addon-img {
|
||||
left: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.sb-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sb-features-grid {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
.sb-socials {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.sb-addon {
|
||||
height: 280px;
|
||||
align-items: flex-start;
|
||||
padding-top: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sb-addon-text {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.sb-addon-img {
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 130px;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
width: 124%;
|
||||
}
|
||||
|
||||
.sb-addon-img img {
|
||||
width: 1200px;
|
||||
transform: rotate(-12deg);
|
||||
margin-left: 0;
|
||||
margin-top: 48px;
|
||||
margin-bottom: -40px;
|
||||
margin-left: -24px;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
@@ -1,27 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { Header } from './Header';
|
||||
|
||||
const meta = {
|
||||
title: 'Example/Header',
|
||||
component: Header,
|
||||
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies Meta<typeof Header>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const LoggedIn: Story = {
|
||||
args: {
|
||||
user: {
|
||||
name: 'Jane Doe',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const LoggedOut: Story = {};
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Button } from './Button';
|
||||
import './header.css';
|
||||
|
||||
type User = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
interface HeaderProps {
|
||||
user?: User;
|
||||
onLogin: () => void;
|
||||
onLogout: () => void;
|
||||
onCreateAccount: () => void;
|
||||
}
|
||||
|
||||
export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
|
||||
<header>
|
||||
<div className="storybook-header">
|
||||
<div>
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
|
||||
fill="#FFF"
|
||||
/>
|
||||
<path
|
||||
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
|
||||
fill="#555AB9"
|
||||
/>
|
||||
<path
|
||||
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
|
||||
fill="#91BAF8"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Acme</h1>
|
||||
</div>
|
||||
<div>
|
||||
{user ? (
|
||||
<>
|
||||
<span className="welcome">
|
||||
Welcome, <b>{user.name}</b>!
|
||||
</span>
|
||||
<Button size="small" onClick={onLogout} label="Log out" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button size="small" onClick={onLogin} label="Log in" />
|
||||
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within, userEvent } from '@storybook/testing-library';
|
||||
|
||||
import { Page } from './Page';
|
||||
|
||||
const meta = {
|
||||
title: 'Example/Page',
|
||||
component: Page,
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
} satisfies Meta<typeof Page>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const LoggedOut: Story = {};
|
||||
|
||||
// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing
|
||||
export const LoggedIn: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const loginButton = await canvas.getByRole('button', {
|
||||
name: /Log in/i,
|
||||
});
|
||||
await userEvent.click(loginButton);
|
||||
},
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Header } from './Header';
|
||||
import './page.css';
|
||||
|
||||
type User = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const Page: React.FC = () => {
|
||||
const [user, setUser] = React.useState<User>();
|
||||
|
||||
return (
|
||||
<article>
|
||||
<Header
|
||||
user={user}
|
||||
onLogin={() => setUser({ name: 'Jane Doe' })}
|
||||
onLogout={() => setUser(undefined)}
|
||||
onCreateAccount={() => setUser({ name: 'Jane Doe' })}
|
||||
/>
|
||||
|
||||
<section className="storybook-page">
|
||||
<h2>Pages in Storybook</h2>
|
||||
<p>
|
||||
We recommend building UIs with a{' '}
|
||||
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
|
||||
<strong>component-driven</strong>
|
||||
</a>{' '}
|
||||
process starting with atomic components and ending with pages.
|
||||
</p>
|
||||
<p>
|
||||
Render pages with mock data. This makes it easy to build and review page states without
|
||||
needing to navigate to them in your app. Here are some handy patterns for managing page
|
||||
data in Storybook:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Use a higher-level connected component. Storybook helps you compose such data from the
|
||||
"args" of child component stories
|
||||
</li>
|
||||
<li>
|
||||
Assemble data in the page component from your services. You can mock these services out
|
||||
using Storybook.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Get a guided tutorial on component-driven development at{' '}
|
||||
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
|
||||
Storybook tutorials
|
||||
</a>
|
||||
. Read more in the{' '}
|
||||
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="tip-wrapper">
|
||||
<span className="tip">Tip</span> Adjust the width of the canvas with the{' '}
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
|
||||
id="a"
|
||||
fill="#999"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
Viewports addon in the toolbar
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 41 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Accessibility</title>
|
||||
<circle cx="24.334" cy="24" r="24" fill="#A849FF" fill-opacity="0.3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.8609 11.585C27.8609 9.59506 26.2497 7.99023 24.2519 7.99023C22.254 7.99023 20.6429 9.65925 20.6429 11.585C20.6429 13.575 22.254 15.1799 24.2519 15.1799C26.2497 15.1799 27.8609 13.575 27.8609 11.585ZM21.8922 22.6473C21.8467 23.9096 21.7901 25.4788 21.5897 26.2771C20.9853 29.0462 17.7348 36.3314 17.3325 37.2275C17.1891 37.4923 17.1077 37.7955 17.1077 38.1178C17.1077 39.1519 17.946 39.9902 18.9802 39.9902C19.6587 39.9902 20.253 39.6293 20.5814 39.0889L20.6429 38.9874L24.2841 31.22C24.2841 31.22 27.5529 37.9214 27.9238 38.6591C28.2948 39.3967 28.8709 39.9902 29.7168 39.9902C30.751 39.9902 31.5893 39.1519 31.5893 38.1178C31.5893 37.7951 31.3639 37.2265 31.3639 37.2265C30.9581 36.3258 27.698 29.0452 27.0938 26.2771C26.8975 25.4948 26.847 23.9722 26.8056 22.7236C26.7927 22.333 26.7806 21.9693 26.7653 21.6634C26.7008 21.214 27.0231 20.8289 27.4097 20.7005L35.3366 18.3253C36.3033 18.0685 36.8834 16.9773 36.6256 16.0144C36.3678 15.0515 35.2722 14.4737 34.3055 14.7305C34.3055 14.7305 26.8619 17.1057 24.2841 17.1057C21.7062 17.1057 14.456 14.7947 14.456 14.7947C13.4893 14.5379 12.3937 14.9873 12.0715 15.9502C11.7493 16.9131 12.3293 18.0044 13.3604 18.3253L21.2873 20.7005C21.674 20.8289 21.9318 21.214 21.9318 21.6634C21.9174 21.9493 21.9053 22.2857 21.8922 22.6473Z" fill="#A470D5"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -1,15 +0,0 @@
|
||||
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10031_177575)">
|
||||
<mask id="mask0_10031_177575" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="4" width="33" height="25">
|
||||
<path d="M32.5034 4.00195H0.503906V28.7758H32.5034V4.00195Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_10031_177575)">
|
||||
<path d="M27.5928 6.20817C25.5533 5.27289 23.3662 4.58382 21.0794 4.18916C21.0378 4.18154 20.9962 4.20057 20.9747 4.23864C20.6935 4.73863 20.3819 5.3909 20.1637 5.90358C17.7042 5.53558 15.2573 5.53558 12.8481 5.90358C12.6299 5.37951 12.307 4.73863 12.0245 4.23864C12.003 4.20184 11.9614 4.18281 11.9198 4.18916C9.63431 4.58255 7.44721 5.27163 5.40641 6.20817C5.38874 6.21578 5.3736 6.22848 5.36355 6.24497C1.21508 12.439 0.078646 18.4809 0.636144 24.4478C0.638667 24.477 0.655064 24.5049 0.677768 24.5227C3.41481 26.5315 6.06609 27.7511 8.66815 28.5594C8.70979 28.5721 8.75392 28.5569 8.78042 28.5226C9.39594 27.6826 9.94461 26.7968 10.4151 25.8653C10.4428 25.8107 10.4163 25.746 10.3596 25.7244C9.48927 25.3945 8.66058 24.9922 7.86343 24.5354C7.80038 24.4986 7.79533 24.4084 7.85333 24.3653C8.02108 24.2397 8.18888 24.109 8.34906 23.977C8.37804 23.9529 8.41842 23.9478 8.45249 23.963C13.6894 26.3526 19.359 26.3526 24.5341 23.963C24.5682 23.9465 24.6086 23.9516 24.6388 23.9757C24.799 24.1077 24.9668 24.2397 25.1358 24.3653C25.1938 24.4084 25.19 24.4986 25.127 24.5354C24.3298 25.0011 23.5011 25.3945 22.6296 25.7232C22.5728 25.7447 22.5476 25.8107 22.5754 25.8653C23.0559 26.7955 23.6046 27.6812 24.2087 28.5213C24.234 28.5569 24.2794 28.5721 24.321 28.5594C26.9357 27.7511 29.5869 26.5315 32.324 24.5227C32.348 24.5049 32.3631 24.4783 32.3656 24.4491C33.0328 17.5506 31.2481 11.5584 27.6344 6.24623C27.6256 6.22848 27.6105 6.21578 27.5928 6.20817ZM11.1971 20.8146C9.62043 20.8146 8.32129 19.3679 8.32129 17.5913C8.32129 15.8146 9.59523 14.368 11.1971 14.368C12.8115 14.368 14.0981 15.8273 14.0729 17.5913C14.0729 19.3679 12.7989 20.8146 11.1971 20.8146ZM21.8299 20.8146C20.2533 20.8146 18.9541 19.3679 18.9541 17.5913C18.9541 15.8146 20.228 14.368 21.8299 14.368C23.4444 14.368 24.7309 15.8273 24.7057 17.5913C24.7057 19.3679 23.4444 20.8146 21.8299 20.8146Z" fill="#5865F2"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10031_177575">
|
||||
<rect width="31.9995" height="32" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 43 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.0001 0C7.16466 0 0 7.17472 0 16.0256C0 23.1061 4.58452 29.1131 10.9419 31.2322C11.7415 31.3805 12.0351 30.8845 12.0351 30.4613C12.0351 30.0791 12.0202 28.8167 12.0133 27.4776C7.56209 28.447 6.62283 25.5868 6.62283 25.5868C5.89499 23.7345 4.8463 23.2419 4.8463 23.2419C3.39461 22.2473 4.95573 22.2678 4.95573 22.2678C6.56242 22.3808 7.40842 23.9192 7.40842 23.9192C8.83547 26.3691 11.1514 25.6609 12.0645 25.2514C12.2081 24.2156 12.6227 23.5087 13.0803 23.1085C9.52648 22.7032 5.7906 21.3291 5.7906 15.1886C5.7906 13.4389 6.41563 12.0094 7.43916 10.8871C7.27303 10.4834 6.72537 8.85349 7.59415 6.64609C7.59415 6.64609 8.93774 6.21539 11.9953 8.28877C13.2716 7.9337 14.6404 7.75563 16.0001 7.74953C17.3599 7.75563 18.7297 7.9337 20.0084 8.28877C23.0623 6.21539 24.404 6.64609 24.404 6.64609C25.2749 8.85349 24.727 10.4834 24.5608 10.8871C25.5868 12.0094 26.2075 13.4389 26.2075 15.1886C26.2075 21.3437 22.4645 22.699 18.9017 23.0957C19.4756 23.593 19.9869 24.5683 19.9869 26.0634C19.9869 28.2077 19.9684 29.9334 19.9684 30.4613C19.9684 30.8877 20.2564 31.3874 21.0674 31.2301C27.4213 29.1086 32 23.1037 32 16.0256C32 7.17472 24.8364 0 16.0001 0ZM5.99257 22.8288C5.95733 22.9084 5.83227 22.9322 5.71834 22.8776C5.60229 22.8253 5.53711 22.7168 5.57474 22.6369C5.60918 22.5549 5.7345 22.5321 5.85029 22.587C5.9666 22.6393 6.03284 22.7489 5.99257 22.8288ZM6.7796 23.5321C6.70329 23.603 6.55412 23.5701 6.45291 23.4581C6.34825 23.3464 6.32864 23.197 6.40601 23.125C6.4847 23.0542 6.62937 23.0874 6.73429 23.1991C6.83895 23.3121 6.85935 23.4605 6.7796 23.5321ZM7.31953 24.4321C7.2215 24.5003 7.0612 24.4363 6.96211 24.2938C6.86407 24.1513 6.86407 23.9804 6.96422 23.9119C7.06358 23.8435 7.2215 23.905 7.32191 24.0465C7.41968 24.1914 7.41968 24.3623 7.31953 24.4321ZM8.23267 25.4743C8.14497 25.5712 7.95818 25.5452 7.82146 25.413C7.68156 25.2838 7.64261 25.1004 7.73058 25.0035C7.81934 24.9064 8.00719 24.9337 8.14497 25.0648C8.28381 25.1938 8.3262 25.3785 8.23267 25.4743ZM9.41281 25.8262C9.37413 25.9517 9.19423 26.0088 9.013 25.9554C8.83203 25.9005 8.7136 25.7535 8.75016 25.6266C8.78778 25.5003 8.96848 25.4408 9.15104 25.4979C9.33174 25.5526 9.45044 25.6985 9.41281 25.8262ZM10.7559 25.9754C10.7604 26.1076 10.6067 26.2172 10.4165 26.2196C10.2252 26.2238 10.0704 26.1169 10.0683 25.9868C10.0683 25.8534 10.2185 25.7448 10.4098 25.7416C10.6001 25.7379 10.7559 25.8441 10.7559 25.9754ZM12.0753 25.9248C12.0981 26.0537 11.9658 26.1862 11.7769 26.2215C11.5912 26.2554 11.4192 26.1758 11.3957 26.0479C11.3726 25.9157 11.5072 25.7833 11.6927 25.7491C11.8819 25.7162 12.0512 25.7937 12.0753 25.9248Z" fill="#161614"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 43 KiB |
@@ -1,12 +0,0 @@
|
||||
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_10031_177597)">
|
||||
<path opacity="0.7" fill-rule="evenodd" clip-rule="evenodd" d="M17 7.87059C17 6.48214 17.9812 5.28722 19.3431 5.01709L29.5249 2.99755C31.3238 2.64076 33 4.01717 33 5.85105V22.1344C33 23.5229 32.0188 24.7178 30.6569 24.9879L20.4751 27.0074C18.6762 27.3642 17 25.9878 17 24.1539L17 7.87059Z" fill="#B7F0EF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 5.85245C1 4.01857 2.67623 2.64215 4.47507 2.99895L14.6569 5.01848C16.0188 5.28861 17 6.48354 17 7.87198V24.1553C17 25.9892 15.3238 27.3656 13.5249 27.0088L3.34311 24.9893C1.98119 24.7192 1 23.5242 1 22.1358V5.85245Z" fill="#87E6E5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.543 5.71289C15.543 5.71289 16.8157 5.96289 17.4002 6.57653C17.9847 7.19016 18.4521 9.03107 18.4521 9.03107C18.4521 9.03107 18.4521 25.1106 18.4521 26.9629C18.4521 28.8152 19.3775 31.4174 19.3775 31.4174L17.4002 28.8947L16.2575 31.4174C16.2575 31.4174 15.543 29.0765 15.543 27.122C15.543 25.1674 15.543 5.71289 15.543 5.71289Z" fill="#61C1FD"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_10031_177597">
|
||||
<rect width="32" height="32" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.3313 8.44657C30.9633 7.08998 29.8791 6.02172 28.5022 5.65916C26.0067 5.00026 16 5.00026 16 5.00026C16 5.00026 5.99333 5.00026 3.4978 5.65916C2.12102 6.02172 1.03665 7.08998 0.668678 8.44657C0 10.9053 0 16.0353 0 16.0353C0 16.0353 0 21.1652 0.668678 23.6242C1.03665 24.9806 2.12102 26.0489 3.4978 26.4116C5.99333 27.0703 16 27.0703 16 27.0703C16 27.0703 26.0067 27.0703 28.5022 26.4116C29.8791 26.0489 30.9633 24.9806 31.3313 23.6242C32 21.1652 32 16.0353 32 16.0353C32 16.0353 32 10.9053 31.3313 8.44657Z" fill="#ED1D24"/>
|
||||
<path d="M12.7266 20.6934L21.0902 16.036L12.7266 11.3781V20.6934Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 721 B |
@@ -1,30 +0,0 @@
|
||||
.storybook-button {
|
||||
font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
font-weight: 700;
|
||||
border: 0;
|
||||
border-radius: 3em;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
.storybook-button--primary {
|
||||
color: white;
|
||||
background-color: #1ea7fd;
|
||||
}
|
||||
.storybook-button--secondary {
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
|
||||
}
|
||||
.storybook-button--small {
|
||||
font-size: 12px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
.storybook-button--medium {
|
||||
font-size: 14px;
|
||||
padding: 11px 20px;
|
||||
}
|
||||
.storybook-button--large {
|
||||
font-size: 16px;
|
||||
padding: 12px 24px;
|
||||
}
|
||||