Compare commits
53 Commits
v0.3.0
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3263522f13 | ||
|
|
409b77c210 | ||
|
|
eecf6f3c3b | ||
|
|
ce7318840d | ||
|
|
7bc563ef29 | ||
|
|
8ed88e1445 | ||
|
|
a1353e0817 | ||
|
|
c0fa55c5f7 | ||
|
|
1ff0c38aa5 | ||
|
|
e5a2db4d8c | ||
|
|
fc7335707a | ||
|
|
4095b7db6e | ||
|
|
dd9ca4224d | ||
|
|
5aa7617e97 | ||
|
|
a10d1f4615 | ||
|
|
1e8441a674 | ||
|
|
b2c5027f56 | ||
|
|
91984dc920 | ||
|
|
74be62823d | ||
|
|
edd00096b6 | ||
|
|
d764752f16 | ||
|
|
ce6c908dc7 | ||
|
|
6ee450709a | ||
|
|
23ab5dfef2 | ||
|
|
7e54cb6aa2 | ||
|
|
857ebabf16 | ||
|
|
f0af3048b7 | ||
|
|
99125f1fea | ||
|
|
f96481feb8 | ||
|
|
dc8bb6da21 | ||
|
|
d866e283e9 | ||
|
|
8a54f45d75 | ||
|
|
42d557d485 | ||
|
|
293f7c6de6 | ||
|
|
38325b0bb9 | ||
|
|
282ed16f0d | ||
|
|
fd744d28a3 | ||
|
|
9a717daf04 | ||
|
|
84bf859c2c | ||
|
|
e57101f279 | ||
|
|
264fcac63c | ||
|
|
7777f8e5d6 | ||
|
|
6aa73171c7 | ||
|
|
8051913b2d | ||
|
|
cfa38548d5 | ||
|
|
9787721c5a | ||
|
|
610b899f66 | ||
|
|
24fb87721f | ||
|
|
d3bc8b135c | ||
|
|
0f6d311de2 | ||
|
|
0c15ae194d | ||
|
|
20c4a596bf | ||
|
|
f07a69260f |
11
.github/workflows/npm.yml
vendored
11
.github/workflows/npm.yml
vendored
@@ -12,11 +12,7 @@ permissions:
|
|||||||
publish:
|
publish:
|
||||||
description: "Publish to npm"
|
description: "Publish to npm"
|
||||||
required: true
|
required: true
|
||||||
type: choice
|
type: boolean
|
||||||
options:
|
|
||||||
- yes
|
|
||||||
- no
|
|
||||||
default: "no"
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./bindings/nodejs
|
working-directory: ./bindings/nodejs
|
||||||
@@ -437,10 +433,11 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
- name: Publish
|
- name: Publish
|
||||||
run: |
|
run: |
|
||||||
npm config set provenance true
|
echo "${{ github.event.inputs.publish }}"
|
||||||
if [ "${{ github.event.inputs.publish }}" = "yes" ]; then
|
if [ "${{ github.event.inputs.publish }}" = "true" ]; then
|
||||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
|
||||||
npm publish --access public
|
npm publish --access public
|
||||||
|
echo "Published to npm"
|
||||||
else
|
else
|
||||||
echo "Not a release, skipping publish"
|
echo "Not a release, skipping publish"
|
||||||
fi
|
fi
|
||||||
|
|||||||
143
.github/workflows/pypi.yml
vendored
Normal file
143
.github/workflows/pypi.yml
vendored
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
name: Upload component to Python Package Index
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
release:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
description: "Release? If false, publish to test.pypi.org, if true, publish to pypi.org"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Build wheels
|
||||||
|
uses: PyO3/maturin-action@v1
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --release --out dist --find-interpreter
|
||||||
|
sccache: 'true'
|
||||||
|
manylinux: auto
|
||||||
|
working-directory: bindings/python
|
||||||
|
- name: Upload wheels
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wheels
|
||||||
|
path: bindings/python/dist
|
||||||
|
|
||||||
|
windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target: [x64, x86]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
architecture: ${{ matrix.target }}
|
||||||
|
- name: Build wheels
|
||||||
|
uses: PyO3/maturin-action@v1
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --release --out dist --find-interpreter
|
||||||
|
sccache: 'true'
|
||||||
|
working-directory: bindings/python
|
||||||
|
- name: Upload wheels
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wheels
|
||||||
|
path: bindings/python/dist
|
||||||
|
|
||||||
|
macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
target: [x86_64, aarch64]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Build wheels
|
||||||
|
uses: PyO3/maturin-action@v1
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
args: --release --out dist --find-interpreter
|
||||||
|
sccache: 'true'
|
||||||
|
working-directory: bindings/python
|
||||||
|
- name: Upload wheels
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wheels
|
||||||
|
path: bindings/python/dist
|
||||||
|
|
||||||
|
sdist:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Build sdist
|
||||||
|
uses: PyO3/maturin-action@v1
|
||||||
|
with:
|
||||||
|
command: sdist
|
||||||
|
args: --out dist
|
||||||
|
working-directory: bindings/python
|
||||||
|
- name: Upload sdist
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wheels
|
||||||
|
path: bindings/python/dist
|
||||||
|
|
||||||
|
publish-to-test-pypi:
|
||||||
|
if: ${{ github.event.inputs.release != 'true' }}
|
||||||
|
name: >-
|
||||||
|
Publish Python 🐍 distribution 📦 to Test PyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [linux, windows, macos, sdist]
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wheels
|
||||||
|
path: bindings/python/
|
||||||
|
- name: Publish distribution 📦 to Test PyPI
|
||||||
|
uses: PyO3/maturin-action@v1
|
||||||
|
env:
|
||||||
|
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_TEST_API_TOKEN }}
|
||||||
|
MATURIN_REPOSITORY_URL: "https://test.pypi.org/legacy/"
|
||||||
|
with:
|
||||||
|
command: upload
|
||||||
|
args: --skip-existing *
|
||||||
|
working-directory: bindings/python
|
||||||
|
|
||||||
|
publish-pypi:
|
||||||
|
if: ${{ github.event.inputs.release == 'true' }}
|
||||||
|
name: >-
|
||||||
|
Publish Python 🐍 distribution 📦 to PyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [linux, windows, macos, sdist]
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: wheels
|
||||||
|
path: bindings/python/
|
||||||
|
- name: Publish distribution 📦 to PyPI
|
||||||
|
uses: PyO3/maturin-action@v1
|
||||||
|
env:
|
||||||
|
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
|
||||||
|
MATURIN_REPOSITORY_URL: "https://upload.pypi.org/legacy/"
|
||||||
|
with:
|
||||||
|
command: upload
|
||||||
|
args: --skip-existing *
|
||||||
|
working-directory: bindings/python
|
||||||
@@ -8,12 +8,20 @@
|
|||||||
- New document server (Thanks Dani!)
|
- New document server (Thanks Dani!)
|
||||||
- New function FORMULATEXT
|
- New function FORMULATEXT
|
||||||
- Name Manager ([#212](https://github.com/ironcalc/IronCalc/pull/212) [#220](https://github.com/ironcalc/IronCalc/pull/220))
|
- Name Manager ([#212](https://github.com/ironcalc/IronCalc/pull/212) [#220](https://github.com/ironcalc/IronCalc/pull/220))
|
||||||
|
- Add context menu. We can now insert rows and columns. Freeze and unfreeze rows and columns. Delete rows and columns [#271]
|
||||||
|
- Add nodejs bindings [#254]
|
||||||
|
- Add python bindings for all platforms
|
||||||
|
- Add is split into the product and widget
|
||||||
|
- Add Python documentation [#260]
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed several issues with pasting content
|
- Fixed several issues with pasting content
|
||||||
- Fixed several issues with borders
|
- Fixed several issues with borders
|
||||||
- Fixed bug where columns and rows could be resized to negative width and height, respectively
|
- Fixed bug where columns and rows could be resized to negative width and height, respectively
|
||||||
|
- Undo/redo when add/delete sheet now works [#270]
|
||||||
|
- Numerous small fixes
|
||||||
|
- Multiple fixes to the documentation
|
||||||
|
|
||||||
## [0.2.0] - 2024-11-06 (The HN release)
|
## [0.2.0] - 2024-11-06 (The HN release)
|
||||||
|
|
||||||
|
|||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -448,7 +448,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironcalc_nodejs"
|
name = "ironcalc_nodejs"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironcalc",
|
"ironcalc",
|
||||||
"napi",
|
"napi",
|
||||||
@@ -1070,7 +1070,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm"
|
name = "wasm"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ironcalc_base",
|
"ironcalc_base",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -136,6 +136,33 @@ impl Model {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// In the list of columns:
|
||||||
|
// * Keep all the columns to the left
|
||||||
|
// * Displace all the columns to the right
|
||||||
|
|
||||||
|
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||||
|
|
||||||
|
let mut new_columns = Vec::new();
|
||||||
|
for col in worksheet.cols.iter_mut() {
|
||||||
|
// range under study
|
||||||
|
let min = col.min;
|
||||||
|
let max = col.max;
|
||||||
|
if column > max {
|
||||||
|
// If the range under study is to our left, this is a noop
|
||||||
|
} else if column <= min {
|
||||||
|
// If the range under study is to our right, we displace it
|
||||||
|
col.min = min + column_count;
|
||||||
|
col.max = max + column_count;
|
||||||
|
} else {
|
||||||
|
// If the range under study is in the middle we augment it
|
||||||
|
col.max = max + column_count;
|
||||||
|
}
|
||||||
|
new_columns.push(col.clone());
|
||||||
|
}
|
||||||
|
// TODO: If in a row the cell to the right and left have the same style we should copy it
|
||||||
|
|
||||||
|
worksheet.cols = new_columns;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1872,12 +1872,29 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the style for cell (`sheet`, `row`, `column`)
|
/// Returns the style for cell (`sheet`, `row`, `column`)
|
||||||
|
/// If the cell does not have a style defined we check the row, otherwise the column and finally a default
|
||||||
pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
|
pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Result<Style, String> {
|
||||||
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
let style_index = self.get_cell_style_index(sheet, row, column)?;
|
||||||
let style = self.workbook.styles.get_style(style_index)?;
|
let style = self.workbook.styles.get_style(style_index)?;
|
||||||
Ok(style)
|
Ok(style)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the style defined in a cell if any.
|
||||||
|
pub fn get_cell_style_or_none(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<Option<Style>, String> {
|
||||||
|
let style = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.cell(row, column)
|
||||||
|
.map(|c| self.workbook.styles.get_style(c.get_style()))
|
||||||
|
.transpose();
|
||||||
|
style
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns an internal binary representation of the workbook
|
/// Returns an internal binary representation of the workbook
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@@ -2161,6 +2178,73 @@ impl Model {
|
|||||||
Err("Defined name not found".to_string())
|
Err("Defined name not found".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Returns the style object of a column, if any
|
||||||
|
pub fn get_column_style(&self, sheet: u32, column: i32) -> Result<Option<Style>, String> {
|
||||||
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||||
|
let cols = &worksheet.cols;
|
||||||
|
for col in cols {
|
||||||
|
if column >= col.min && column <= col.max {
|
||||||
|
if let Some(style_index) = col.style {
|
||||||
|
let style = self.workbook.styles.get_style(style_index)?;
|
||||||
|
return Ok(Some(style));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the style object of a row, if any
|
||||||
|
pub fn get_row_style(&self, sheet: u32, row: i32) -> Result<Option<Style>, String> {
|
||||||
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||||
|
let rows = &worksheet.rows;
|
||||||
|
for r in rows {
|
||||||
|
if row == r.r {
|
||||||
|
let style = self.workbook.styles.get_style(r.s)?;
|
||||||
|
return Ok(Some(style));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Err("Invalid sheet".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a column with style
|
||||||
|
pub fn set_column_style(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
style: &Style,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let style_index = self.workbook.styles.get_style_index_or_create(style);
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.set_column_style(column, style_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a row with style
|
||||||
|
pub fn set_row_style(&mut self, sheet: u32, row: i32, style: &Style) -> Result<(), String> {
|
||||||
|
let style_index = self.workbook.styles.get_style_index_or_create(style);
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.set_row_style(row, style_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes the style of a column if the is any
|
||||||
|
pub fn delete_column_style(&mut self, sheet: u32, column: i32) -> Result<(), String> {
|
||||||
|
self.workbook
|
||||||
|
.worksheet_mut(sheet)?
|
||||||
|
.delete_column_style(column)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes the style of a row if there is any
|
||||||
|
pub fn delete_row_style(&mut self, sheet: u32, row: i32) -> Result<(), String> {
|
||||||
|
self.workbook.worksheet_mut(sheet)?.delete_row_style(row)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ impl Model {
|
|||||||
};
|
};
|
||||||
if sheet_index >= sheet_count {
|
if sheet_index >= sheet_count {
|
||||||
return Err("Sheet index too large".to_string());
|
return Err("Sheet index too large".to_string());
|
||||||
}
|
};
|
||||||
self.workbook.worksheets.remove(sheet_index as usize);
|
self.workbook.worksheets.remove(sheet_index as usize);
|
||||||
self.reset_parsed_structures();
|
self.reset_parsed_structures();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use crate::{
|
|||||||
types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Style, Styles},
|
types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Style, Styles},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Move Styles and all related types from crate::types here
|
|
||||||
// Not doing it right now to not have conflicts with exporter branch
|
|
||||||
impl Styles {
|
impl Styles {
|
||||||
fn get_font_index(&self, font: &Font) -> Option<i32> {
|
fn get_font_index(&self, font: &Font) -> Option<i32> {
|
||||||
for (font_index, item) in self.fonts.iter().enumerate() {
|
for (font_index, item) in self.fonts.iter().enumerate() {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ mod test_model_cell_clear_all;
|
|||||||
mod test_model_is_empty_cell;
|
mod test_model_is_empty_cell;
|
||||||
mod test_move_formula;
|
mod test_move_formula;
|
||||||
mod test_quote_prefix;
|
mod test_quote_prefix;
|
||||||
|
mod test_row_column_styles;
|
||||||
mod test_set_user_input;
|
mod test_set_user_input;
|
||||||
mod test_sheet_markup;
|
mod test_sheet_markup;
|
||||||
mod test_sheets;
|
mod test_sheets;
|
||||||
|
|||||||
32
base/src/test/test_row_column_styles.rs
Normal file
32
base/src/test/test_row_column_styles.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{constants::DEFAULT_COLUMN_WIDTH, test::util::new_empty_model};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_model_set_cells_with_values_styles() {
|
||||||
|
let mut model = new_empty_model();
|
||||||
|
|
||||||
|
let style_base = model.get_style_for_cell(0, 1, 1).unwrap();
|
||||||
|
let mut style = style_base.clone();
|
||||||
|
style.font.b = true;
|
||||||
|
|
||||||
|
model.set_column_style(0, 10, &style).unwrap();
|
||||||
|
|
||||||
|
assert!(model.get_style_for_cell(0, 21, 10).unwrap().font.b);
|
||||||
|
|
||||||
|
model.delete_column_style(0, 10).unwrap();
|
||||||
|
|
||||||
|
// There are no styles in the column
|
||||||
|
assert!(model.workbook.worksheets[0].cols.is_empty());
|
||||||
|
|
||||||
|
// lets change the column width and check it does not affect the style
|
||||||
|
model
|
||||||
|
.set_column_width(0, 10, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
model.set_column_style(0, 10, &style).unwrap();
|
||||||
|
|
||||||
|
model.delete_column_style(0, 10).unwrap();
|
||||||
|
|
||||||
|
// There are no styles in the column
|
||||||
|
assert!(model.workbook.worksheets[0].cols.len() == 1);
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ mod test_autofill_columns;
|
|||||||
mod test_autofill_rows;
|
mod test_autofill_rows;
|
||||||
mod test_border;
|
mod test_border;
|
||||||
mod test_clear_cells;
|
mod test_clear_cells;
|
||||||
|
mod test_column_style;
|
||||||
mod test_defined_names;
|
mod test_defined_names;
|
||||||
|
mod test_delete_row_column_formatting;
|
||||||
mod test_diff_queue;
|
mod test_diff_queue;
|
||||||
mod test_evaluation;
|
mod test_evaluation;
|
||||||
mod test_general;
|
mod test_general;
|
||||||
@@ -13,9 +15,11 @@ mod test_on_area_selection;
|
|||||||
mod test_on_expand_selected_range;
|
mod test_on_expand_selected_range;
|
||||||
mod test_on_paste_styles;
|
mod test_on_paste_styles;
|
||||||
mod test_paste_csv;
|
mod test_paste_csv;
|
||||||
|
mod test_recursive;
|
||||||
mod test_rename_sheet;
|
mod test_rename_sheet;
|
||||||
mod test_row_column;
|
mod test_row_column;
|
||||||
mod test_sheet_state;
|
mod test_sheet_state;
|
||||||
|
mod test_sheets_undo_redo;
|
||||||
mod test_styles;
|
mod test_styles;
|
||||||
mod test_to_from_bytes;
|
mod test_to_from_bytes;
|
||||||
mod test_undo_redo;
|
mod test_undo_redo;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ fn add_undo_redo() {
|
|||||||
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
|
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
|
||||||
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
|
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
|
||||||
model
|
model
|
||||||
.set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
.set_columns_width(1, 5, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
model.new_sheet().unwrap();
|
model.new_sheet().unwrap();
|
||||||
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
|
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
|
||||||
@@ -25,9 +25,6 @@ fn add_undo_redo() {
|
|||||||
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
|
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
|
||||||
|
|
||||||
model.delete_sheet(1).unwrap();
|
model.delete_sheet(1).unwrap();
|
||||||
|
|
||||||
assert!(!model.can_undo());
|
|
||||||
assert!(!model.can_redo());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -520,14 +520,19 @@ fn borders_top() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
model.set_area_with_border(range, &border_area).unwrap();
|
model.set_area_with_border(range, &border_area).unwrap();
|
||||||
check_borders(&model);
|
check_borders(&model);
|
||||||
for row in 5..9 {
|
for row in 4..9 {
|
||||||
for column in 6..9 {
|
for column in 6..9 {
|
||||||
let style = model.get_cell_style(0, row, column).unwrap();
|
let style = model.get_cell_style(0, row, column).unwrap();
|
||||||
let border_item = BorderItem {
|
let border_item = BorderItem {
|
||||||
style: BorderStyle::Thin,
|
style: BorderStyle::Thin,
|
||||||
color: Some("#FF5566".to_string()),
|
color: Some("#FF5566".to_string()),
|
||||||
};
|
};
|
||||||
let bottom = if row == 8 {
|
let bottom = if row != 4 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(border_item.clone())
|
||||||
|
};
|
||||||
|
let top = if row != 5 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(border_item.clone())
|
Some(border_item.clone())
|
||||||
@@ -537,7 +542,7 @@ fn borders_top() {
|
|||||||
diagonal_down: false,
|
diagonal_down: false,
|
||||||
left: None,
|
left: None,
|
||||||
right: None,
|
right: None,
|
||||||
top: Some(border_item.clone()),
|
top,
|
||||||
bottom,
|
bottom,
|
||||||
diagonal: None,
|
diagonal: None,
|
||||||
};
|
};
|
||||||
@@ -647,12 +652,12 @@ fn borders_right() {
|
|||||||
style: BorderStyle::Thin,
|
style: BorderStyle::Thin,
|
||||||
color: Some("#FF5566".to_string()),
|
color: Some("#FF5566".to_string()),
|
||||||
};
|
};
|
||||||
let left = if column == 6 {
|
let left = if column != 9 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(border_item.clone())
|
Some(border_item.clone())
|
||||||
};
|
};
|
||||||
let right = if column == 9 {
|
let right = if column != 8 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(border_item.clone())
|
Some(border_item.clone())
|
||||||
@@ -705,7 +710,7 @@ fn borders_bottom() {
|
|||||||
color: Some("#FF5566".to_string()),
|
color: Some("#FF5566".to_string()),
|
||||||
};
|
};
|
||||||
// The top will also have a value for all but the first one
|
// The top will also have a value for all but the first one
|
||||||
let top = if row == 5 {
|
let bottom = if row != 8 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(border_item.clone())
|
Some(border_item.clone())
|
||||||
@@ -715,8 +720,8 @@ fn borders_bottom() {
|
|||||||
diagonal_down: false,
|
diagonal_down: false,
|
||||||
left: None,
|
left: None,
|
||||||
right: None,
|
right: None,
|
||||||
top,
|
top: None,
|
||||||
bottom: Some(border_item.clone()),
|
bottom,
|
||||||
diagonal: None,
|
diagonal: None,
|
||||||
};
|
};
|
||||||
assert_eq!(style.border, expected_border);
|
assert_eq!(style.border, expected_border);
|
||||||
@@ -751,18 +756,13 @@ fn borders_left() {
|
|||||||
model.set_area_with_border(range, &border_area).unwrap();
|
model.set_area_with_border(range, &border_area).unwrap();
|
||||||
|
|
||||||
for row in 5..9 {
|
for row in 5..9 {
|
||||||
for column in 5..9 {
|
for column in 6..9 {
|
||||||
let style = model.get_cell_style(0, row, column).unwrap();
|
let style = model.get_cell_style(0, row, column).unwrap();
|
||||||
let border_item = BorderItem {
|
let border_item = BorderItem {
|
||||||
style: BorderStyle::Thin,
|
style: BorderStyle::Thin,
|
||||||
color: Some("#FF5566".to_string()),
|
color: Some("#FF5566".to_string()),
|
||||||
};
|
};
|
||||||
let left = if column == 5 {
|
let left = if column != 6 {
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(border_item.clone())
|
|
||||||
};
|
|
||||||
let right = if column == 8 {
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(border_item.clone())
|
Some(border_item.clone())
|
||||||
@@ -771,13 +771,29 @@ fn borders_left() {
|
|||||||
diagonal_up: false,
|
diagonal_up: false,
|
||||||
diagonal_down: false,
|
diagonal_down: false,
|
||||||
left,
|
left,
|
||||||
right,
|
right: None,
|
||||||
top: None,
|
top: None,
|
||||||
bottom: None,
|
bottom: None,
|
||||||
diagonal: None,
|
diagonal: None,
|
||||||
};
|
};
|
||||||
assert_eq!(style.border, expected_border);
|
assert_eq!(style.border, expected_border);
|
||||||
}
|
}
|
||||||
|
// Column 5 has a border to the right, of course:
|
||||||
|
let style = model.get_cell_style(0, row, 5).unwrap();
|
||||||
|
let border_item = BorderItem {
|
||||||
|
style: BorderStyle::Thin,
|
||||||
|
color: Some("#FF5566".to_string()),
|
||||||
|
};
|
||||||
|
let expected_border = Border {
|
||||||
|
diagonal_up: false,
|
||||||
|
diagonal_down: false,
|
||||||
|
left: None,
|
||||||
|
right: Some(border_item.clone()),
|
||||||
|
top: None,
|
||||||
|
bottom: None,
|
||||||
|
diagonal: None,
|
||||||
|
};
|
||||||
|
assert_eq!(style.border, expected_border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,10 +1034,7 @@ fn border_top() {
|
|||||||
style: BorderStyle::Thin,
|
style: BorderStyle::Thin,
|
||||||
color: Some("#F2F2F2".to_string()),
|
color: Some("#F2F2F2".to_string()),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(model._get_cell_actual_border("C4").top, Some(border_item));
|
||||||
model._get_cell_actual_border("C4").bottom,
|
|
||||||
Some(border_item)
|
|
||||||
);
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
|||||||
504
base/src/test/user_model/test_column_style.rs
Normal file
504
base/src/test/user_model/test_column_style.rs
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW};
|
||||||
|
use crate::expressions::types::Area;
|
||||||
|
use crate::UserModel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_width() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||||
|
assert!(!style.font.i);
|
||||||
|
assert!(!style.font.b);
|
||||||
|
assert!(!style.font.u);
|
||||||
|
assert!(!style.font.strike);
|
||||||
|
assert_eq!(style.font.color, Some("#000000".to_owned()));
|
||||||
|
|
||||||
|
// Set the whole column style and check it works
|
||||||
|
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||||
|
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||||
|
assert!(style.font.b);
|
||||||
|
|
||||||
|
// undo and check it works
|
||||||
|
model.undo().unwrap();
|
||||||
|
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||||
|
assert!(!style.font.b);
|
||||||
|
|
||||||
|
// redo and check it works
|
||||||
|
model.redo().unwrap();
|
||||||
|
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||||
|
assert!(style.font.b);
|
||||||
|
|
||||||
|
// change the column width and check it does not affect the style
|
||||||
|
model
|
||||||
|
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
let style = model.get_cell_style(0, 109, 7).unwrap();
|
||||||
|
assert!(style.font.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn existing_style() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
|
||||||
|
let cell_g123 = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 123,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set G123 background to red
|
||||||
|
model
|
||||||
|
.update_range_style(&cell_g123, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Now set the style of the whole column
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Get the style of G123
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// Check the style of G123 is now what it was before
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||||
|
|
||||||
|
model.redo().unwrap();
|
||||||
|
|
||||||
|
// Check G123 has the column style now
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn row_column() {
|
||||||
|
// We set the row style, then a column style
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_3_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// update the row style
|
||||||
|
model
|
||||||
|
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// update the column style
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check G3 has the column style
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||||
|
|
||||||
|
// undo twice. Color must be default
|
||||||
|
model.undo().unwrap();
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||||
|
model.undo().unwrap();
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_row() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
|
||||||
|
let default_style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_3_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// update the column style
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// update the row style
|
||||||
|
model
|
||||||
|
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check G3 has the row style
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// Check G3 has the column style
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// Check G3 has the default_style
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, default_style.fill.bg_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn row_column_column() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
|
||||||
|
let column_c_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 3,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_e_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 5,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_5_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// update the row style
|
||||||
|
model
|
||||||
|
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// update the column style
|
||||||
|
model
|
||||||
|
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
model.undo().unwrap();
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// Test E5 has the default style
|
||||||
|
let style = model.get_cell_style(0, 5, 5).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn width_column_undo() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#CCC111")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.get_column_width(0, 7).unwrap(),
|
||||||
|
DEFAULT_COLUMN_WIDTH * 2.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn height_row_undo() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model
|
||||||
|
.set_rows_height(0, 10, 10, DEFAULT_ROW_HEIGHT * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let row_10_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 10,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&row_10_range, "fill.bg_color", "#CCC111")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.get_row_height(0, 10).unwrap(),
|
||||||
|
2.0 * DEFAULT_ROW_HEIGHT
|
||||||
|
);
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_row_height(0, 10).unwrap(),
|
||||||
|
2.0 * DEFAULT_ROW_HEIGHT
|
||||||
|
);
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_row_height(0, 10).unwrap(), DEFAULT_ROW_HEIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_row_undo() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let cell_g12 = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 12,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_12_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 12,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set G12 background to red
|
||||||
|
model
|
||||||
|
.update_range_style(&cell_g12, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&row_12_range, "fill.bg_color", "#CCC111")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_column_style_then_cell() {
|
||||||
|
// We check that if we set a cell style in a column that already has a style
|
||||||
|
// the styles compound
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let cell_g12 = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 12,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set G12 background to red
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&cell_g12, "alignment.horizontal", "center")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
model.undo().unwrap();
|
||||||
|
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_row_style_then_cell() {
|
||||||
|
// We check that if we set a cell style in a column that already has a style
|
||||||
|
// the styles compound
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let cell_g12 = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 12,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_12_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 12,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set G12 background to red
|
||||||
|
model
|
||||||
|
.update_range_style(&row_12_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&cell_g12, "alignment.horizontal", "center")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 12, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_style_then_row_alignment() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
let row_3_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
model
|
||||||
|
.update_range_style(&row_3_range, "alignment.horizontal", "center")
|
||||||
|
.unwrap();
|
||||||
|
// check the row alignment does not affect the column style
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_style_then_width() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
model
|
||||||
|
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check column width worked:
|
||||||
|
assert_eq!(
|
||||||
|
model.get_column_width(0, 7).unwrap(),
|
||||||
|
DEFAULT_COLUMN_WIDTH * 2.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_row_column_column() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
|
||||||
|
let column_c_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 3,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_e_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 5,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_5_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 5,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// update the row style
|
||||||
|
model
|
||||||
|
.update_range_style(&row_5_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// update the column style
|
||||||
|
model
|
||||||
|
.update_range_style(&column_c_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&column_e_range, "fill.bg_color", "#CCC111")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// test E5 has the column style
|
||||||
|
let style = model.get_cell_style(0, 5, 5).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#CCC111".to_string()));
|
||||||
|
}
|
||||||
@@ -396,3 +396,30 @@ fn undo_redo() {
|
|||||||
Ok("Hola!".to_string())
|
Ok("Hola!".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn change_scope_to_first_sheet() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model.new_sheet().unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "Hello").unwrap();
|
||||||
|
model
|
||||||
|
.set_user_input(1, 2, 1, r#"=CONCATENATE(MyName, " world!")"#)
|
||||||
|
.unwrap();
|
||||||
|
model
|
||||||
|
.new_defined_name("myName", None, "Sheet1!$A$1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(1, 2, 1),
|
||||||
|
Ok("Hello world!".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_defined_name("myName", None, "myName", Some(0), "Sheet1!$A$1")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
model.get_formatted_cell_value(1, 2, 1),
|
||||||
|
Ok("#NAME?".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
243
base/src/test/user_model/test_delete_row_column_formatting.rs
Normal file
243
base/src/test/user_model/test_delete_row_column_formatting.rs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN, LAST_ROW},
|
||||||
|
expressions::types::Area,
|
||||||
|
UserModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_column_formatting() {
|
||||||
|
// We are going to delete formatting in column G (7)
|
||||||
|
// There are cells with their own styles
|
||||||
|
// There are rows with their own styles
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let cell_g123 = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 123,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_3_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the style of the whole column
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Set G123 background to red
|
||||||
|
model
|
||||||
|
.update_range_style(&cell_g123, "fill.bg_color", "#FF5533")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Set the style of the whole row
|
||||||
|
model
|
||||||
|
.update_range_style(&row_3_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Delete the column formatting
|
||||||
|
model.range_clear_formatting(&column_g_range).unwrap();
|
||||||
|
|
||||||
|
// Check the style of G123 is now what it was before
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
|
||||||
|
// Check the style of the whole row is still there
|
||||||
|
let style = model.get_cell_style(0, 3, 1).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||||
|
|
||||||
|
// Check the style of the whole column is now gone
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// Check the style of G123 is now what it was before
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#FF5533".to_owned()));
|
||||||
|
|
||||||
|
// Check G3 is the row style
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||||
|
|
||||||
|
// Check G40 is the column style
|
||||||
|
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#555666".to_owned()));
|
||||||
|
|
||||||
|
model.redo().unwrap();
|
||||||
|
|
||||||
|
// Check the style of G123 is now what it was before
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
|
||||||
|
// Check the style of the whole row is still there
|
||||||
|
let style = model.get_cell_style(0, 3, 1).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#333444".to_owned()));
|
||||||
|
|
||||||
|
// Check the style of the whole column is now gone
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 40, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_width() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model
|
||||||
|
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the style of the whole column
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Delete the column formatting
|
||||||
|
model.range_clear_formatting(&column_g_range).unwrap();
|
||||||
|
// This does not change the column width
|
||||||
|
assert_eq!(
|
||||||
|
model.get_column_width(0, 7).unwrap(),
|
||||||
|
2.0 * DEFAULT_COLUMN_WIDTH
|
||||||
|
);
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_column_width(0, 7).unwrap(),
|
||||||
|
2.0 * DEFAULT_COLUMN_WIDTH
|
||||||
|
);
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
model.get_column_width(0, 7).unwrap(),
|
||||||
|
2.0 * DEFAULT_COLUMN_WIDTH
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_row_style_undo() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
model
|
||||||
|
.set_columns_width(0, 7, 7, DEFAULT_COLUMN_WIDTH * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_123_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 123,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let delete_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 120,
|
||||||
|
column: 5,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the style of the whole column
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&row_123_range, "fill.bg_color", "#111222")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model.range_clear_formatting(&delete_range).unwrap();
|
||||||
|
|
||||||
|
// check G123 is empty
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
|
||||||
|
// uno clear formatting
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// G123 has the row style
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#111222".to_owned()));
|
||||||
|
|
||||||
|
// undo twice
|
||||||
|
model.undo().unwrap();
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// check G123 is empty
|
||||||
|
let style = model.get_cell_style(0, 123, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_row_row_height_undo() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let row_3_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 3,
|
||||||
|
column: 1,
|
||||||
|
width: LAST_COLUMN,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#555666")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.set_rows_height(0, 3, 3, DEFAULT_ROW_HEIGHT * 2.0)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&row_3_range, "fill.bg_color", "#111222")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
// check G3 has the column style
|
||||||
|
let style = model.get_cell_style(0, 3, 7).unwrap();
|
||||||
|
assert_eq!(style.fill.bg_color, Some("#555666".to_string()));
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use crate::{
|
|||||||
fn send_queue() {
|
fn send_queue() {
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
let mut model1 = UserModel::from_model(new_empty_model());
|
||||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||||
model1.set_column_width(0, 3, width).unwrap();
|
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||||
let send_queue = model1.flush_send_queue();
|
let send_queue = model1.flush_send_queue();
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ fn apply_external_diffs_wrong_str() {
|
|||||||
fn queue_undo_redo() {
|
fn queue_undo_redo() {
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
let mut model1 = UserModel::from_model(new_empty_model());
|
||||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||||
model1.set_column_width(0, 3, width).unwrap();
|
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||||
assert!(model1.undo().is_ok());
|
assert!(model1.undo().is_ok());
|
||||||
assert!(model1.redo().is_ok());
|
assert!(model1.redo().is_ok());
|
||||||
@@ -57,8 +57,8 @@ fn queue_undo_redo_multiple() {
|
|||||||
// do a bunch of things
|
// do a bunch of things
|
||||||
model1.set_frozen_columns_count(0, 5).unwrap();
|
model1.set_frozen_columns_count(0, 5).unwrap();
|
||||||
model1.set_frozen_rows_count(0, 6).unwrap();
|
model1.set_frozen_rows_count(0, 6).unwrap();
|
||||||
model1.set_column_width(0, 7, 300.0).unwrap();
|
model1.set_columns_width(0, 7, 7, 300.0).unwrap();
|
||||||
model1.set_row_height(0, 23, 123.0).unwrap();
|
model1.set_rows_height(0, 23, 23, 123.0).unwrap();
|
||||||
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
|
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
|
||||||
|
|
||||||
for row in 1..5 {
|
for row in 1..5 {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ fn insert_remove_rows() {
|
|||||||
// Insert some data in row 5 (and change the style)
|
// Insert some data in row 5 (and change the style)
|
||||||
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
|
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
|
||||||
// Change the height of the column
|
// Change the height of the column
|
||||||
assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
|
assert!(model.set_rows_height(0, 5, 5, 3.0 * height).is_ok());
|
||||||
|
|
||||||
// remove the row
|
// remove the row
|
||||||
assert!(model.delete_row(0, 5).is_ok());
|
assert!(model.delete_row(0, 5).is_ok());
|
||||||
@@ -95,7 +95,7 @@ fn insert_remove_columns() {
|
|||||||
// Insert some data in row 5 (and change the style) in E1
|
// Insert some data in row 5 (and change the style) in E1
|
||||||
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
|
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
|
||||||
// Change the width of the column
|
// Change the width of the column
|
||||||
assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
|
assert!(model.set_columns_width(0, 5, 5, 3.0 * column_width).is_ok());
|
||||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
||||||
|
|
||||||
// remove the column
|
// remove the column
|
||||||
|
|||||||
42
base/src/test/user_model/test_recursive.rs
Normal file
42
base/src/test/user_model/test_recursive.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
constants::LAST_ROW, expressions::types::Area, test::util::new_empty_model, UserModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn two_columns() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
|
||||||
|
// Set style in column C (column 3)
|
||||||
|
let column_c_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 3,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
model
|
||||||
|
.update_range_style(&column_c_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
model.set_user_input(0, 5, 3, "2").unwrap();
|
||||||
|
|
||||||
|
// Set Style in column G (column 7)
|
||||||
|
let column_g_range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 7,
|
||||||
|
width: 1,
|
||||||
|
height: LAST_ROW,
|
||||||
|
};
|
||||||
|
|
||||||
|
model
|
||||||
|
.update_range_style(&column_g_range, "fill.bg_color", "#333444")
|
||||||
|
.unwrap();
|
||||||
|
model.set_user_input(0, 5, 6, "42").unwrap();
|
||||||
|
// Set formula in G5: =F5*C5
|
||||||
|
model.set_user_input(0, 5, 7, "=F5*C5").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 5, 7).unwrap(), "84");
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ fn simple_delete_column() {
|
|||||||
model.set_user_input(0, 1, 5, "3").unwrap();
|
model.set_user_input(0, 1, 5, "3").unwrap();
|
||||||
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
|
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
|
||||||
model
|
model
|
||||||
.set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
.set_columns_width(0, 5, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
model.delete_column(0, 5).unwrap();
|
model.delete_column(0, 5).unwrap();
|
||||||
@@ -116,7 +116,7 @@ fn simple_delete_row() {
|
|||||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
||||||
|
|
||||||
model
|
model
|
||||||
.set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
.set_rows_height(0, 15, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
model.delete_row(0, 15).unwrap();
|
model.delete_row(0, 15).unwrap();
|
||||||
@@ -172,3 +172,42 @@ fn row_heigh_increases_automatically() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT));
|
assert_eq!(model.get_row_height(0, 1), Ok(2.0 * DEFAULT_ROW_HEIGHT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_row_evaluates() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||||
|
model.set_user_input(0, 1, 2, "=A1*2").unwrap();
|
||||||
|
|
||||||
|
assert!(model.insert_row(0, 1).is_ok());
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 2, 2).unwrap(), "84");
|
||||||
|
|
||||||
|
model.delete_row(0, 1).unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 1, 2).unwrap(), "84");
|
||||||
|
assert_eq!(model.get_cell_content(0, 1, 2).unwrap(), "=A1*2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_column_evaluates() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||||
|
model.set_user_input(0, 10, 1, "=A1*2").unwrap();
|
||||||
|
|
||||||
|
assert!(model.insert_column(0, 1).is_ok());
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 2).unwrap(), "84");
|
||||||
|
|
||||||
|
model.delete_column(0, 1).unwrap();
|
||||||
|
assert_eq!(model.get_formatted_cell_value(0, 10, 1).unwrap(), "84");
|
||||||
|
assert_eq!(model.get_cell_content(0, 10, 1).unwrap(), "=A1*2");
|
||||||
|
}
|
||||||
|
|||||||
52
base/src/test/user_model/test_sheets_undo_redo.rs
Normal file
52
base/src/test/user_model/test_sheets_undo_redo.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use crate::test::util::new_empty_model;
|
||||||
|
use crate::UserModel;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_undo_redo() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
assert_eq!(model.get_selected_sheet(), 0);
|
||||||
|
|
||||||
|
model.new_sheet().unwrap();
|
||||||
|
assert_eq!(model.get_selected_sheet(), 1);
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_selected_sheet(), 0);
|
||||||
|
{
|
||||||
|
let props = model.get_worksheets_properties();
|
||||||
|
assert_eq!(props.len(), 1);
|
||||||
|
let view = model.get_selected_view();
|
||||||
|
assert_eq!(view.sheet, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_selected_sheet(), 1);
|
||||||
|
{
|
||||||
|
let props = model.get_worksheets_properties();
|
||||||
|
assert_eq!(props.len(), 2);
|
||||||
|
let view = model.get_selected_view();
|
||||||
|
|
||||||
|
assert_eq!(view.sheet, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_undo() {
|
||||||
|
let model = new_empty_model();
|
||||||
|
let mut model = UserModel::from_model(model);
|
||||||
|
assert_eq!(model.get_selected_sheet(), 0);
|
||||||
|
|
||||||
|
model.new_sheet().unwrap();
|
||||||
|
assert_eq!(model.get_selected_sheet(), 1);
|
||||||
|
model.set_user_input(1, 1, 1, "42").unwrap();
|
||||||
|
model.set_user_input(1, 1, 2, "=A1*2").unwrap();
|
||||||
|
model.delete_sheet(1).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(model.get_selected_sheet(), 0);
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
assert_eq!(model.get_selected_sheet(), 1);
|
||||||
|
model.redo().unwrap();
|
||||||
|
assert_eq!(model.get_selected_sheet(), 0);
|
||||||
|
}
|
||||||
@@ -436,3 +436,47 @@ fn false_removes_value() {
|
|||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||||
assert!(!style.font.b);
|
assert!(!style.font.b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_clear_formatting() {
|
||||||
|
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
|
let range = Area {
|
||||||
|
sheet: 0,
|
||||||
|
row: 1,
|
||||||
|
column: 1,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// bold
|
||||||
|
model.update_range_style(&range, "font.b", "true").unwrap();
|
||||||
|
model
|
||||||
|
.update_range_style(&range, "alignment.horizontal", "centerContinuous")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||||
|
assert!(style.font.b);
|
||||||
|
assert_eq!(
|
||||||
|
style.alignment.unwrap().horizontal,
|
||||||
|
HorizontalAlignment::CenterContinuous
|
||||||
|
);
|
||||||
|
|
||||||
|
model.range_clear_all(&range).unwrap();
|
||||||
|
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||||
|
assert!(!style.font.b);
|
||||||
|
assert_eq!(style.alignment, None);
|
||||||
|
|
||||||
|
model.undo().unwrap();
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||||
|
assert!(style.font.b);
|
||||||
|
assert_eq!(
|
||||||
|
style.alignment.unwrap().horizontal,
|
||||||
|
HorizontalAlignment::CenterContinuous
|
||||||
|
);
|
||||||
|
model.redo().unwrap();
|
||||||
|
|
||||||
|
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||||
|
assert!(!style.font.b);
|
||||||
|
assert_eq!(style.alignment, None);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use crate::{test::util::new_empty_model, UserModel};
|
|||||||
fn basic() {
|
fn basic() {
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
let mut model1 = UserModel::from_model(new_empty_model());
|
||||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
||||||
model1.set_column_width(0, 3, width).unwrap();
|
model1.set_columns_width(0, 3, 3, width).unwrap();
|
||||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
||||||
|
|
||||||
let model_bytes = model1.to_bytes();
|
let model_bytes = model1.to_bytes();
|
||||||
|
|||||||
@@ -323,6 +323,19 @@ pub struct Style {
|
|||||||
pub quote_prefix: bool,
|
pub quote_prefix: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Style {
|
||||||
|
fn default() -> Self {
|
||||||
|
Style {
|
||||||
|
alignment: None,
|
||||||
|
num_fmt: "general".to_string(),
|
||||||
|
fill: Fill::default(),
|
||||||
|
font: Font::default(),
|
||||||
|
border: Border::default(),
|
||||||
|
quote_prefix: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct NumFmt {
|
pub struct NumFmt {
|
||||||
pub num_fmt_id: i32,
|
pub num_fmt_id: i32,
|
||||||
@@ -394,7 +407,7 @@ impl Default for Font {
|
|||||||
u: false,
|
u: false,
|
||||||
b: false,
|
b: false,
|
||||||
i: false,
|
i: false,
|
||||||
sz: 11,
|
sz: 13,
|
||||||
color: Some("#000000".to_string()),
|
color: Some("#000000".to_string()),
|
||||||
name: "Calibri".to_string(),
|
name: "Calibri".to_string(),
|
||||||
family: 2,
|
family: 2,
|
||||||
|
|||||||
@@ -50,8 +50,9 @@ impl Units {
|
|||||||
fn get_units_from_format_string(num_fmt: &str) -> Option<Units> {
|
fn get_units_from_format_string(num_fmt: &str) -> Option<Units> {
|
||||||
let mut parser = Parser::new(num_fmt);
|
let mut parser = Parser::new(num_fmt);
|
||||||
parser.parse();
|
parser.parse();
|
||||||
|
let parts = parser.parts.first()?;
|
||||||
// We only care about the first part (positive number)
|
// We only care about the first part (positive number)
|
||||||
match &parser.parts[0] {
|
match parts {
|
||||||
ParsePart::Number(part) => {
|
ParsePart::Number(part) => {
|
||||||
if part.percent > 0 {
|
if part.percent > 0 {
|
||||||
Some(Units::Percentage {
|
Some(Units::Percentage {
|
||||||
|
|||||||
507
base/src/user_model/border.rs
Normal file
507
base/src/user_model/border.rs
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
use crate::{
|
||||||
|
constants::{LAST_COLUMN, LAST_ROW},
|
||||||
|
expressions::types::Area,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
border_utils::is_max_border, common::BorderType, history::Diff, BorderArea, UserModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl UserModel {
|
||||||
|
fn update_single_cell_border(
|
||||||
|
&mut self,
|
||||||
|
border_area: &BorderArea,
|
||||||
|
cell: (u32, i32, i32),
|
||||||
|
range: (i32, i32, i32, i32),
|
||||||
|
diff_list: &mut Vec<Diff>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let (sheet, row, column) = cell;
|
||||||
|
let (first_row, first_column, last_row, last_column) = range;
|
||||||
|
|
||||||
|
let old_value = self.model.get_cell_style_or_none(sheet, row, column)?;
|
||||||
|
let mut new_value = match &old_value {
|
||||||
|
Some(value) => value.clone(),
|
||||||
|
None => Default::default(),
|
||||||
|
};
|
||||||
|
match border_area.r#type {
|
||||||
|
BorderType::All => {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
BorderType::Inner => {
|
||||||
|
if row != first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if row != last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column != first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column != last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Outer => {
|
||||||
|
if row == first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if row == last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column == first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column == last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Top => {
|
||||||
|
if row == first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Right => {
|
||||||
|
if column == last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Bottom => {
|
||||||
|
if row == last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Left => {
|
||||||
|
if column == first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::CenterH => {
|
||||||
|
if row != first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if row != last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::CenterV => {
|
||||||
|
if column != first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column != last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::None => {
|
||||||
|
new_value.border.top = None;
|
||||||
|
new_value.border.right = None;
|
||||||
|
new_value.border.bottom = None;
|
||||||
|
new_value.border.left = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.model.set_cell_style(sheet, row, column, &new_value)?;
|
||||||
|
diff_list.push(Diff::SetCellStyle {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
old_value: Box::new(old_value),
|
||||||
|
new_value: Box::new(new_value),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_rows_with_border(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
first_row: i32,
|
||||||
|
last_row: i32,
|
||||||
|
border_area: &BorderArea,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut diff_list = Vec::new();
|
||||||
|
for row in first_row..=last_row {
|
||||||
|
let old_value = self.model.get_row_style(sheet, row)?;
|
||||||
|
let mut new_value = match &old_value {
|
||||||
|
Some(value) => value.clone(),
|
||||||
|
None => Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match border_area.r#type {
|
||||||
|
BorderType::All => {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
BorderType::Inner => {
|
||||||
|
if row != first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if row != last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Outer => {
|
||||||
|
if row == first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if row == last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Top => {
|
||||||
|
if row == first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Right => {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
BorderType::Bottom => {
|
||||||
|
if row == last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Left => {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
BorderType::CenterH => {
|
||||||
|
if row != first_row {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if row != last_row {
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::CenterV => {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
BorderType::None => {
|
||||||
|
new_value.border.top = None;
|
||||||
|
new_value.border.right = None;
|
||||||
|
new_value.border.bottom = None;
|
||||||
|
new_value.border.left = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to go throw each non-empty cell in the row
|
||||||
|
let columns: Vec<i32> = self
|
||||||
|
.model
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.sheet_data
|
||||||
|
.get(&row)
|
||||||
|
.map(|row_data| row_data.keys().copied().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
for column in columns {
|
||||||
|
self.update_single_cell_border(
|
||||||
|
border_area,
|
||||||
|
(sheet, row, column),
|
||||||
|
(first_row, 1, last_row, LAST_COLUMN),
|
||||||
|
&mut diff_list,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.model.set_row_style(sheet, row, &new_value)?;
|
||||||
|
diff_list.push(Diff::SetRowStyle {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
old_value: Box::new(old_value),
|
||||||
|
new_value: Box::new(new_value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO: We need to check the rows above and below. also any non empty cell in the rows above and below.
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_columns_with_border(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
first_column: i32,
|
||||||
|
last_column: i32,
|
||||||
|
border_area: &BorderArea,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut diff_list = Vec::new();
|
||||||
|
// We need all the rows in the column to update the style
|
||||||
|
// NB: This is too much, this is all the rows that have values
|
||||||
|
let data_rows: Vec<i32> = self
|
||||||
|
.model
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)?
|
||||||
|
.sheet_data
|
||||||
|
.keys()
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
let styled_rows = &self.model.workbook.worksheet(sheet)?.rows.clone();
|
||||||
|
for column in first_column..=last_column {
|
||||||
|
let old_value = self.model.get_column_style(sheet, column)?;
|
||||||
|
let mut new_value = match &old_value {
|
||||||
|
Some(value) => value.clone(),
|
||||||
|
None => Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match border_area.r#type {
|
||||||
|
BorderType::All => {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
BorderType::Inner => {
|
||||||
|
if column != first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column != last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Outer => {
|
||||||
|
if column == first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column == last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Top => {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
BorderType::Right => {
|
||||||
|
if column == last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::Bottom => {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
BorderType::Left => {
|
||||||
|
if column == first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::CenterH => {
|
||||||
|
new_value.border.top = Some(border_area.item.clone());
|
||||||
|
new_value.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
BorderType::CenterV => {
|
||||||
|
if column != first_column {
|
||||||
|
new_value.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
if column != last_column {
|
||||||
|
new_value.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BorderType::None => {
|
||||||
|
new_value.border.top = None;
|
||||||
|
new_value.border.right = None;
|
||||||
|
new_value.border.bottom = None;
|
||||||
|
new_value.border.left = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We need to go through each non empty cell in the column
|
||||||
|
for &row in &data_rows {
|
||||||
|
if let Some(data_row) = self.model.workbook.worksheet(sheet)?.sheet_data.get(&row) {
|
||||||
|
if data_row.get(&column).is_some() {
|
||||||
|
self.update_single_cell_border(
|
||||||
|
border_area,
|
||||||
|
(sheet, row, column),
|
||||||
|
(1, first_column, LAST_ROW, last_column),
|
||||||
|
&mut diff_list,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We also need to overwrite those that have a row style
|
||||||
|
for row_s in styled_rows.iter() {
|
||||||
|
let row = row_s.r;
|
||||||
|
self.update_single_cell_border(
|
||||||
|
border_area,
|
||||||
|
(sheet, row, column),
|
||||||
|
(1, first_column, LAST_ROW, last_column),
|
||||||
|
&mut diff_list,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.model.set_column_style(sheet, column, &new_value)?;
|
||||||
|
diff_list.push(Diff::SetColumnStyle {
|
||||||
|
sheet,
|
||||||
|
column,
|
||||||
|
old_value: Box::new(old_value),
|
||||||
|
new_value: Box::new(new_value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// We need to check the borders of the column to the left and the column to the right
|
||||||
|
// We also need to check every non-empty cell in the columns to the left and right
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the border in an area of cells.
|
||||||
|
/// When setting the border we need to check if the adjacent cells have a "heavier" border
|
||||||
|
/// If that is the case we need to change it
|
||||||
|
pub fn set_area_with_border(
|
||||||
|
&mut self,
|
||||||
|
range: &Area,
|
||||||
|
border_area: &BorderArea,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let sheet = range.sheet;
|
||||||
|
let first_row = range.row;
|
||||||
|
let first_column = range.column;
|
||||||
|
let last_row = first_row + range.height - 1;
|
||||||
|
let last_column = first_column + range.width - 1;
|
||||||
|
if first_row == 1 && last_row == LAST_ROW {
|
||||||
|
// full columns
|
||||||
|
self.set_columns_with_border(sheet, first_column, last_column, border_area)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if first_column == 1 && last_column == LAST_COLUMN {
|
||||||
|
// full rows
|
||||||
|
self.set_rows_with_border(sheet, first_row, last_row, border_area)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut diff_list = Vec::new();
|
||||||
|
for row in first_row..=last_row {
|
||||||
|
for column in first_column..=last_column {
|
||||||
|
self.update_single_cell_border(
|
||||||
|
border_area,
|
||||||
|
(sheet, row, column),
|
||||||
|
(first_row, first_column, last_row, last_column),
|
||||||
|
&mut diff_list,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom of the cells above the first
|
||||||
|
if first_row > 1
|
||||||
|
&& [
|
||||||
|
BorderType::All,
|
||||||
|
BorderType::None,
|
||||||
|
BorderType::Outer,
|
||||||
|
BorderType::Top,
|
||||||
|
]
|
||||||
|
.contains(&border_area.r#type)
|
||||||
|
{
|
||||||
|
let row = first_row - 1;
|
||||||
|
for column in first_column..=last_column {
|
||||||
|
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||||
|
if is_max_border(Some(&border_area.item), old_value.border.bottom.as_ref()) {
|
||||||
|
let mut style = old_value.clone();
|
||||||
|
if border_area.r#type == BorderType::None {
|
||||||
|
style.border.bottom = None;
|
||||||
|
} else {
|
||||||
|
style.border.bottom = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||||
|
diff_list.push(Diff::SetCellStyle {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
old_value: Box::new(Some(old_value)),
|
||||||
|
new_value: Box::new(style),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cells to the right
|
||||||
|
if last_column < LAST_COLUMN
|
||||||
|
&& [
|
||||||
|
BorderType::All,
|
||||||
|
BorderType::None,
|
||||||
|
BorderType::Outer,
|
||||||
|
BorderType::Right,
|
||||||
|
]
|
||||||
|
.contains(&border_area.r#type)
|
||||||
|
{
|
||||||
|
let column = last_column + 1;
|
||||||
|
for row in first_row..=last_row {
|
||||||
|
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||||
|
// If the border in the adjacent cell is "heavier" we change it
|
||||||
|
if is_max_border(Some(&border_area.item), old_value.border.left.as_ref()) {
|
||||||
|
let mut style = old_value.clone();
|
||||||
|
if border_area.r#type == BorderType::None {
|
||||||
|
style.border.left = None;
|
||||||
|
} else {
|
||||||
|
style.border.left = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||||
|
diff_list.push(Diff::SetCellStyle {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
old_value: Box::new(Some(old_value)),
|
||||||
|
new_value: Box::new(style),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cells bellow
|
||||||
|
if last_row < LAST_ROW
|
||||||
|
&& [
|
||||||
|
BorderType::All,
|
||||||
|
BorderType::None,
|
||||||
|
BorderType::Outer,
|
||||||
|
BorderType::Bottom,
|
||||||
|
]
|
||||||
|
.contains(&border_area.r#type)
|
||||||
|
{
|
||||||
|
let row = last_row + 1;
|
||||||
|
for column in first_column..=last_column {
|
||||||
|
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||||
|
if is_max_border(Some(&border_area.item), old_value.border.top.as_ref()) {
|
||||||
|
let mut style = old_value.clone();
|
||||||
|
if border_area.r#type == BorderType::None {
|
||||||
|
style.border.top = None;
|
||||||
|
} else {
|
||||||
|
style.border.top = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||||
|
diff_list.push(Diff::SetCellStyle {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
old_value: Box::new(Some(old_value)),
|
||||||
|
new_value: Box::new(style),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cells to the left
|
||||||
|
if first_column > 1
|
||||||
|
&& [
|
||||||
|
BorderType::All,
|
||||||
|
BorderType::None,
|
||||||
|
BorderType::Outer,
|
||||||
|
BorderType::Left,
|
||||||
|
]
|
||||||
|
.contains(&border_area.r#type)
|
||||||
|
{
|
||||||
|
let column = first_column - 1;
|
||||||
|
for row in first_row..=last_row {
|
||||||
|
let old_value = self.model.get_style_for_cell(sheet, row, column)?;
|
||||||
|
if is_max_border(Some(&border_area.item), old_value.border.right.as_ref()) {
|
||||||
|
let mut style = old_value.clone();
|
||||||
|
if border_area.r#type == BorderType::None {
|
||||||
|
style.border.right = None;
|
||||||
|
} else {
|
||||||
|
style.border.right = Some(border_area.item.clone());
|
||||||
|
}
|
||||||
|
self.model.set_cell_style(sheet, row, column, &style)?;
|
||||||
|
diff_list.push(Diff::SetCellStyle {
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
old_value: Box::new(Some(old_value)),
|
||||||
|
new_value: Box::new(style),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.push_diff_list(diff_list);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use bitcode::{Decode, Encode};
|
use bitcode::{Decode, Encode};
|
||||||
|
|
||||||
use crate::types::{Cell, Col, Row, SheetState, Style};
|
use crate::types::{Cell, Col, Row, SheetState, Style, Worksheet};
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
#[derive(Clone, Encode, Decode)]
|
||||||
pub(crate) struct RowData {
|
pub(crate) struct RowData {
|
||||||
@@ -39,11 +39,17 @@ pub(crate) enum Diff {
|
|||||||
old_value: Box<Option<Cell>>,
|
old_value: Box<Option<Cell>>,
|
||||||
old_style: Box<Style>,
|
old_style: Box<Style>,
|
||||||
},
|
},
|
||||||
|
CellClearFormatting {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
old_style: Box<Option<Style>>,
|
||||||
|
},
|
||||||
SetCellStyle {
|
SetCellStyle {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
row: i32,
|
row: i32,
|
||||||
column: i32,
|
column: i32,
|
||||||
old_value: Box<Style>,
|
old_value: Box<Option<Style>>,
|
||||||
new_value: Box<Style>,
|
new_value: Box<Style>,
|
||||||
},
|
},
|
||||||
// Column and Row diffs
|
// Column and Row diffs
|
||||||
@@ -59,6 +65,28 @@ pub(crate) enum Diff {
|
|||||||
new_value: f64,
|
new_value: f64,
|
||||||
old_value: f64,
|
old_value: f64,
|
||||||
},
|
},
|
||||||
|
SetColumnStyle {
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
old_value: Box<Option<Style>>,
|
||||||
|
new_value: Box<Style>,
|
||||||
|
},
|
||||||
|
SetRowStyle {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
old_value: Box<Option<Style>>,
|
||||||
|
new_value: Box<Style>,
|
||||||
|
},
|
||||||
|
DeleteColumnStyle {
|
||||||
|
sheet: u32,
|
||||||
|
column: i32,
|
||||||
|
old_value: Box<Option<Style>>,
|
||||||
|
},
|
||||||
|
DeleteRowStyle {
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
old_value: Box<Option<Style>>,
|
||||||
|
},
|
||||||
InsertRow {
|
InsertRow {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
row: i32,
|
row: i32,
|
||||||
@@ -77,6 +105,10 @@ pub(crate) enum Diff {
|
|||||||
column: i32,
|
column: i32,
|
||||||
old_data: Box<ColumnData>,
|
old_data: Box<ColumnData>,
|
||||||
},
|
},
|
||||||
|
DeleteSheet {
|
||||||
|
sheet: u32,
|
||||||
|
old_data: Box<Worksheet>,
|
||||||
|
},
|
||||||
SetFrozenRowsCount {
|
SetFrozenRowsCount {
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
new_value: i32,
|
new_value: i32,
|
||||||
@@ -87,9 +119,6 @@ pub(crate) enum Diff {
|
|||||||
new_value: i32,
|
new_value: i32,
|
||||||
old_value: i32,
|
old_value: i32,
|
||||||
},
|
},
|
||||||
DeleteSheet {
|
|
||||||
sheet: u32,
|
|
||||||
},
|
|
||||||
NewSheet {
|
NewSheet {
|
||||||
index: u32,
|
index: u32,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -168,11 +197,6 @@ impl History {
|
|||||||
None => None,
|
None => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
|
||||||
self.redo_stack = vec![];
|
|
||||||
self.undo_stack = vec![];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Encode, Decode)]
|
#[derive(Clone, Encode, Decode)]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
mod border;
|
||||||
mod border_utils;
|
mod border_utils;
|
||||||
mod common;
|
mod common;
|
||||||
mod history;
|
mod history;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ impl Workbook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the a list of defined names in the workbook with their scope
|
/// Returns the a list of defined names in the workbook with their scope
|
||||||
pub(crate) fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> {
|
pub fn get_defined_names_with_scope(&self) -> Vec<(String, Option<u32>, String)> {
|
||||||
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
|
let sheet_id_index: Vec<u32> = self.worksheets.iter().map(|s| s.sheet_id).collect();
|
||||||
|
|
||||||
let defined_names = self
|
let defined_names = self
|
||||||
|
|||||||
@@ -108,37 +108,120 @@ impl Worksheet {
|
|||||||
self.cols = vec![Col {
|
self.cols = vec![Col {
|
||||||
min: 1,
|
min: 1,
|
||||||
max: constants::LAST_COLUMN,
|
max: constants::LAST_COLUMN,
|
||||||
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
|
width: constants::DEFAULT_COLUMN_WIDTH,
|
||||||
custom_width: true,
|
custom_width: false,
|
||||||
style: Some(style_index),
|
style: Some(style_index),
|
||||||
}];
|
}];
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> {
|
pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> {
|
||||||
let width = constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR;
|
let width = self
|
||||||
|
.get_column_width(column)
|
||||||
|
.unwrap_or(constants::DEFAULT_COLUMN_WIDTH);
|
||||||
self.set_column_width_and_style(column, width, Some(style_index))
|
self.set_column_width_and_style(column, width, Some(style_index))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> {
|
pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> {
|
||||||
|
// FIXME: This is a HACK
|
||||||
|
let custom_format = style_index != 0;
|
||||||
for r in self.rows.iter_mut() {
|
for r in self.rows.iter_mut() {
|
||||||
if r.r == row {
|
if r.r == row {
|
||||||
r.s = style_index;
|
r.s = style_index;
|
||||||
r.custom_format = true;
|
r.custom_format = custom_format;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.rows.push(Row {
|
self.rows.push(Row {
|
||||||
height: constants::DEFAULT_ROW_HEIGHT / constants::ROW_HEIGHT_FACTOR,
|
height: constants::DEFAULT_ROW_HEIGHT / constants::ROW_HEIGHT_FACTOR,
|
||||||
r: row,
|
r: row,
|
||||||
custom_format: true,
|
custom_format,
|
||||||
custom_height: true,
|
custom_height: false,
|
||||||
s: style_index,
|
s: style_index,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete_row_style(&mut self, row: i32) -> Result<(), String> {
|
||||||
|
let mut index = None;
|
||||||
|
for (i, r) in self.rows.iter().enumerate() {
|
||||||
|
if r.r == row {
|
||||||
|
index = Some(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(i) = index {
|
||||||
|
if let Some(r) = self.rows.get_mut(i) {
|
||||||
|
r.s = 0;
|
||||||
|
r.custom_format = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_column_style(&mut self, column: i32) -> Result<(), String> {
|
||||||
|
if !is_valid_column_number(column) {
|
||||||
|
return Err(format!("Column number '{column}' is not valid."));
|
||||||
|
}
|
||||||
|
let cols = &mut self.cols;
|
||||||
|
|
||||||
|
let mut index = 0;
|
||||||
|
let mut split = false;
|
||||||
|
for c in cols.iter_mut() {
|
||||||
|
let min = c.min;
|
||||||
|
let max = c.max;
|
||||||
|
if min <= column && column <= max {
|
||||||
|
//
|
||||||
|
split = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if column < min {
|
||||||
|
// We passed, there is nothing to delete
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
if split {
|
||||||
|
let min = cols[index].min;
|
||||||
|
let max = cols[index].max;
|
||||||
|
let custom_width = cols[index].custom_width;
|
||||||
|
let width = cols[index].width;
|
||||||
|
let pre = Col {
|
||||||
|
min,
|
||||||
|
max: column - 1,
|
||||||
|
width,
|
||||||
|
custom_width,
|
||||||
|
style: cols[index].style,
|
||||||
|
};
|
||||||
|
let col = Col {
|
||||||
|
min: column,
|
||||||
|
max: column,
|
||||||
|
width,
|
||||||
|
custom_width,
|
||||||
|
style: None,
|
||||||
|
};
|
||||||
|
let post = Col {
|
||||||
|
min: column + 1,
|
||||||
|
max,
|
||||||
|
width,
|
||||||
|
custom_width,
|
||||||
|
style: cols[index].style,
|
||||||
|
};
|
||||||
|
cols.remove(index);
|
||||||
|
if column != max {
|
||||||
|
cols.insert(index, post);
|
||||||
|
}
|
||||||
|
if custom_width {
|
||||||
|
cols.insert(index, col);
|
||||||
|
}
|
||||||
|
if column != min {
|
||||||
|
cols.insert(index, pre);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_cell_style(
|
pub fn set_cell_style(
|
||||||
&mut self,
|
&mut self,
|
||||||
row: i32,
|
row: i32,
|
||||||
@@ -285,11 +368,12 @@ impl Worksheet {
|
|||||||
|
|
||||||
/// Changes the width of a column.
|
/// Changes the width of a column.
|
||||||
/// * If the column does not a have a width we simply add it
|
/// * If the column does not a have a width we simply add it
|
||||||
/// * If it has, it might be part of a range and we ned to split the range.
|
/// * If it has, it might be part of a range and we need to split the range.
|
||||||
///
|
///
|
||||||
/// Fails if column index is outside allowed range or width is negative.
|
/// Fails if column index is outside allowed range or width is negative.
|
||||||
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
|
pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> {
|
||||||
self.set_column_width_and_style(column, width, None)
|
let style = self.get_column_style(column)?;
|
||||||
|
self.set_column_width_and_style(column, width, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_column_width_and_style(
|
pub(crate) fn set_column_width_and_style(
|
||||||
@@ -309,7 +393,7 @@ impl Worksheet {
|
|||||||
min: column,
|
min: column,
|
||||||
max: column,
|
max: column,
|
||||||
width: width / constants::COLUMN_WIDTH_FACTOR,
|
width: width / constants::COLUMN_WIDTH_FACTOR,
|
||||||
custom_width: true,
|
custom_width: width != constants::DEFAULT_COLUMN_WIDTH,
|
||||||
style,
|
style,
|
||||||
};
|
};
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
@@ -319,7 +403,9 @@ impl Worksheet {
|
|||||||
let max = c.max;
|
let max = c.max;
|
||||||
if min <= column && column <= max {
|
if min <= column && column <= max {
|
||||||
if min == column && max == column {
|
if min == column && max == column {
|
||||||
|
c.style = style;
|
||||||
c.width = width / constants::COLUMN_WIDTH_FACTOR;
|
c.width = width / constants::COLUMN_WIDTH_FACTOR;
|
||||||
|
c.custom_width = width != constants::DEFAULT_COLUMN_WIDTH;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
split = true;
|
split = true;
|
||||||
@@ -383,6 +469,23 @@ impl Worksheet {
|
|||||||
Ok(constants::DEFAULT_COLUMN_WIDTH)
|
Ok(constants::DEFAULT_COLUMN_WIDTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the column style index if present
|
||||||
|
pub fn get_column_style(&self, column: i32) -> Result<Option<i32>, String> {
|
||||||
|
if !is_valid_column_number(column) {
|
||||||
|
return Err(format!("Column number '{column}' is not valid."));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cols = &self.cols;
|
||||||
|
for col in cols {
|
||||||
|
let min = col.min;
|
||||||
|
let max = col.max;
|
||||||
|
if column >= min && column <= max {
|
||||||
|
return Ok(col.style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
// Returns non empty cells in a column
|
// Returns non empty cells in a column
|
||||||
pub fn column_cell_references(&self, column: i32) -> Result<Vec<CellReferenceIndex>, String> {
|
pub fn column_cell_references(&self, column: i32) -> Result<Vec<CellReferenceIndex>, String> {
|
||||||
let mut column_cell_references: Vec<CellReferenceIndex> = Vec::new();
|
let mut column_cell_references: Vec<CellReferenceIndex> = Vec::new();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "ironcalc_nodejs"
|
name = "ironcalc_nodejs"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import test from 'ava'
|
import test from 'ava'
|
||||||
|
|
||||||
import { Model } from '../index.js';
|
import { UserModel, Model } from '../index.js';
|
||||||
|
|
||||||
test('sum from native', (t) => {
|
test('User Model smoke test', (t) => {
|
||||||
const model = new Model("Workbook1", "en", "UTC");
|
const model = new UserModel("Workbook1", "en", "UTC");
|
||||||
|
|
||||||
model.setUserInput(0, 1, 1, "=1+1");
|
model.setUserInput(0, 1, 1, "=1+1");
|
||||||
t.is(model.getFormattedCellValue(0, 1, 1), '2');
|
t.is(model.getFormattedCellValue(0, 1, 1), '2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('Raw API smoke test', (t) => {
|
||||||
|
const model = new Model("Workbook1", "en", "UTC");
|
||||||
|
|
||||||
|
model.setUserInput(0, 1, 1, "=1+1");
|
||||||
|
model.evaluate();
|
||||||
|
t.is(model.getFormattedCellValue(0, 1, 1), '2');
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
BIN
bindings/nodejs/example.xlsx
Normal file
BIN
bindings/nodejs/example.xlsx
Normal file
Binary file not shown.
40
bindings/nodejs/index.d.ts
vendored
40
bindings/nodejs/index.d.ts
vendored
@@ -5,7 +5,45 @@
|
|||||||
|
|
||||||
export declare class Model {
|
export declare class Model {
|
||||||
constructor(name: string, locale: string, timezone: string)
|
constructor(name: string, locale: string, timezone: string)
|
||||||
static fromBytes(bytes: Uint8Array): Model
|
static fromXlsx(filePath: string, locale: string, tz: string): Model
|
||||||
|
static fromIcalc(fileName: string): Model
|
||||||
|
saveToXlsx(file: string): void
|
||||||
|
saveToIcalc(file: string): void
|
||||||
|
evaluate(): void
|
||||||
|
setUserInput(sheet: number, row: number, column: number, value: string): void
|
||||||
|
clearCellContents(sheet: number, row: number, column: number): void
|
||||||
|
getCellContent(sheet: number, row: number, column: number): string
|
||||||
|
getCellType(sheet: number, row: number, column: number): number
|
||||||
|
getFormattedCellValue(sheet: number, row: number, column: number): string
|
||||||
|
setCellStyle(sheet: number, row: number, column: number, style: unknown): void
|
||||||
|
getCellStyle(sheet: number, row: number, column: number): unknown
|
||||||
|
insertRows(sheet: number, row: number, rowCount: number): void
|
||||||
|
insertColumns(sheet: number, column: number, columnCount: number): void
|
||||||
|
deleteRows(sheet: number, row: number, rowCount: number): void
|
||||||
|
deleteColumns(sheet: number, column: number, columnCount: number): void
|
||||||
|
getColumnWidth(sheet: number, column: number): number
|
||||||
|
getRowHeight(sheet: number, row: number): number
|
||||||
|
setColumnWidth(sheet: number, column: number, width: number): void
|
||||||
|
setRowHeight(sheet: number, row: number, height: number): void
|
||||||
|
getFrozenColumnsCount(sheet: number): number
|
||||||
|
getFrozenRowsCount(sheet: number): number
|
||||||
|
setFrozenColumnsCount(sheet: number, columnCount: number): void
|
||||||
|
setFrozenRowsCount(sheet: number, rowCount: number): void
|
||||||
|
getWorksheetsProperties(): unknown
|
||||||
|
setSheetColor(sheet: number, color: string): void
|
||||||
|
addSheet(sheetName: string): void
|
||||||
|
newSheet(): void
|
||||||
|
deleteSheet(sheet: number): void
|
||||||
|
renameSheet(sheet: number, newName: string): void
|
||||||
|
getDefinedNameList(): unknown
|
||||||
|
newDefinedName(name: string, scope: number | undefined | null, formula: string): void
|
||||||
|
updateDefinedName(name: string, scope: number | undefined | null, newName: string, newScope: number | undefined | null, newFormula: string): void
|
||||||
|
deleteDefinedName(name: string, scope?: number | undefined | null): void
|
||||||
|
testPanic(): void
|
||||||
|
}
|
||||||
|
export declare class UserModel {
|
||||||
|
constructor(name: string, locale: string, timezone: string)
|
||||||
|
static fromBytes(bytes: Uint8Array): UserModel
|
||||||
canUndo(): boolean
|
canUndo(): boolean
|
||||||
canRedo(): boolean
|
canRedo(): boolean
|
||||||
pauseEvaluation(): void
|
pauseEvaluation(): void
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ if (!nativeBinding) {
|
|||||||
throw new Error(`Failed to load native binding`)
|
throw new Error(`Failed to load native binding`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Model } = nativeBinding
|
const { Model, UserModel } = nativeBinding
|
||||||
|
|
||||||
module.exports.Model = Model
|
module.exports.Model = Model
|
||||||
|
module.exports.UserModel = UserModel
|
||||||
|
|||||||
@@ -1,11 +1,28 @@
|
|||||||
import { Model } from './index.js'
|
import { UserModel, Model } from './index.js'
|
||||||
|
|
||||||
const model = new Model("Workbook1", "en", "UTC");
|
|
||||||
|
|
||||||
model.setUserInput(0, 1, 1, "=1+1");
|
|
||||||
let t = model.getFormattedCellValue(0, 1, 1);
|
|
||||||
|
|
||||||
console.log('From native', t);
|
function testUserModel() {
|
||||||
|
const model = new UserModel("Workbook1", "en", "UTC");
|
||||||
|
|
||||||
let t2 = model.getCellStyle(0, 1, 1);
|
model.setUserInput(0, 1, 1, "=1+1");
|
||||||
console.log('From native', t2);
|
let t = model.getFormattedCellValue(0, 1, 1);
|
||||||
|
|
||||||
|
console.log('From native', t);
|
||||||
|
|
||||||
|
let t2 = model.getCellStyle(0, 1, 1);
|
||||||
|
console.log('From native', t2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testModel() {
|
||||||
|
const model = Model.fromXlsx("example.xlsx", "en", "UTC");
|
||||||
|
const style = model.getCellStyle(0, 1, 6);
|
||||||
|
console.log(style);
|
||||||
|
|
||||||
|
const quantum = model.getFormattedCellValue(0, 14, 4);
|
||||||
|
console.log(quantum);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
testUserModel();
|
||||||
|
testModel();
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ironcalc/nodejs",
|
"name": "@ironcalc/nodejs",
|
||||||
"version": "0.3.0",
|
"version": "0.3.1",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"napi": {
|
"napi": {
|
||||||
|
|||||||
@@ -1,623 +1,8 @@
|
|||||||
#![deny(clippy::all)]
|
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate napi_derive;
|
extern crate napi_derive;
|
||||||
|
|
||||||
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
|
mod model;
|
||||||
|
mod user_model;
|
||||||
|
|
||||||
use ironcalc::base::{
|
pub use model::Model;
|
||||||
expressions::types::Area,
|
pub use user_model::UserModel;
|
||||||
types::{CellType, Style},
|
|
||||||
BorderArea, ClipboardData, UserModel as BaseModel,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct DefinedName {
|
|
||||||
name: String,
|
|
||||||
scope: Option<u32>,
|
|
||||||
formula: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_js_error(error: String) -> Error {
|
|
||||||
Error::new(Status::Unknown, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub struct Model {
|
|
||||||
model: BaseModel,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
impl Model {
|
|
||||||
#[napi(constructor)]
|
|
||||||
pub fn new(name: String, locale: String, timezone: String) -> Result<Self> {
|
|
||||||
let model = BaseModel::new_empty(&name, &locale, &timezone).map_err(to_js_error)?;
|
|
||||||
Ok(Self { model })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(factory)]
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<Model> {
|
|
||||||
let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?;
|
|
||||||
Ok(Model { model })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn undo(&mut self) -> Result<()> {
|
|
||||||
self.model.undo().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn redo(&mut self) -> Result<()> {
|
|
||||||
self.model.redo().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "canUndo")]
|
|
||||||
pub fn can_undo(&self) -> bool {
|
|
||||||
self.model.can_undo()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "canRedo")]
|
|
||||||
pub fn can_redo(&self) -> bool {
|
|
||||||
self.model.can_redo()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "pauseEvaluation")]
|
|
||||||
pub fn pause_evaluation(&mut self) {
|
|
||||||
self.model.pause_evaluation()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "resumeEvaluation")]
|
|
||||||
pub fn resume_evaluation(&mut self) {
|
|
||||||
self.model.resume_evaluation()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn evaluate(&mut self) {
|
|
||||||
self.model.evaluate();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "flushSendQueue")]
|
|
||||||
pub fn flush_send_queue(&mut self) -> Vec<u8> {
|
|
||||||
self.model.flush_send_queue()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "applyExternalDiffs")]
|
|
||||||
pub fn apply_external_diffs(&mut self, diffs: &[u8]) -> Result<()> {
|
|
||||||
self.model.apply_external_diffs(diffs).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getCellContent")]
|
|
||||||
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.get_cell_content(sheet, row, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "newSheet")]
|
|
||||||
pub fn new_sheet(&mut self) -> Result<()> {
|
|
||||||
self.model.new_sheet().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "deleteSheet")]
|
|
||||||
pub fn delete_sheet(&mut self, sheet: u32) -> Result<()> {
|
|
||||||
self.model.delete_sheet(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "hideSheet")]
|
|
||||||
pub fn hide_sheet(&mut self, sheet: u32) -> Result<()> {
|
|
||||||
self.model.hide_sheet(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "unhideSheet")]
|
|
||||||
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<()> {
|
|
||||||
self.model.unhide_sheet(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "renameSheet")]
|
|
||||||
pub fn rename_sheet(&mut self, sheet: u32, name: String) -> Result<()> {
|
|
||||||
self.model.rename_sheet(sheet, &name).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setSheetColor")]
|
|
||||||
pub fn set_sheet_color(&mut self, sheet: u32, color: String) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_sheet_color(sheet, &color)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "rangeClearAll")]
|
|
||||||
pub fn range_clear_all(
|
|
||||||
&mut self,
|
|
||||||
sheet: u32,
|
|
||||||
start_row: i32,
|
|
||||||
start_column: i32,
|
|
||||||
end_row: i32,
|
|
||||||
end_column: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
let range = Area {
|
|
||||||
sheet,
|
|
||||||
row: start_row,
|
|
||||||
column: start_column,
|
|
||||||
width: end_column - start_column + 1,
|
|
||||||
height: end_row - start_row + 1,
|
|
||||||
};
|
|
||||||
self.model.range_clear_all(&range).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "rangeClearContents")]
|
|
||||||
pub fn range_clear_contents(
|
|
||||||
&mut self,
|
|
||||||
sheet: u32,
|
|
||||||
start_row: i32,
|
|
||||||
start_column: i32,
|
|
||||||
end_row: i32,
|
|
||||||
end_column: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
let range = Area {
|
|
||||||
sheet,
|
|
||||||
row: start_row,
|
|
||||||
column: start_column,
|
|
||||||
width: end_column - start_column + 1,
|
|
||||||
height: end_row - start_row + 1,
|
|
||||||
};
|
|
||||||
self.model.range_clear_contents(&range).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "insertRow")]
|
|
||||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
|
||||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "insertColumn")]
|
|
||||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
|
||||||
self.model.insert_column(sheet, column).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "deleteRow")]
|
|
||||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
|
||||||
self.model.delete_row(sheet, row).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "deleteColumn")]
|
|
||||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
|
||||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setRowHeight")]
|
|
||||||
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_row_height(sheet, row, height)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setColumnWidth")]
|
|
||||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_column_width(sheet, column, width)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getRowHeight")]
|
|
||||||
pub fn get_row_height(&mut self, sheet: u32, row: i32) -> Result<f64> {
|
|
||||||
self.model.get_row_height(sheet, row).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getColumnWidth")]
|
|
||||||
pub fn get_column_width(&mut self, sheet: u32, column: i32) -> Result<f64> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.get_column_width(sheet, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setUserInput")]
|
|
||||||
pub fn set_user_input(&mut self, sheet: u32, row: i32, column: i32, input: String) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_user_input(sheet, row, column, &input)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getFormattedCellValue")]
|
|
||||||
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.get_formatted_cell_value(sheet, row, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getFrozenRowsCount")]
|
|
||||||
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32> {
|
|
||||||
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getFrozenColumnsCount")]
|
|
||||||
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.get_frozen_columns_count(sheet)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setFrozenRowsCount")]
|
|
||||||
pub fn set_frozen_rows_count(&mut self, sheet: u32, count: i32) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_frozen_rows_count(sheet, count)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setFrozenColumnsCount")]
|
|
||||||
pub fn set_frozen_columns_count(&mut self, sheet: u32, count: i32) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_frozen_columns_count(sheet, count)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "updateRangeStyle")]
|
|
||||||
pub fn update_range_style(
|
|
||||||
&mut self,
|
|
||||||
env: Env,
|
|
||||||
range: JsUnknown,
|
|
||||||
style_path: String,
|
|
||||||
value: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
let range: Area = env
|
|
||||||
.from_js_value(range)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.update_range_style(&range, &style_path, &value)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getCellStyle")]
|
|
||||||
pub fn get_cell_style(
|
|
||||||
&mut self,
|
|
||||||
env: Env,
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
) -> Result<JsUnknown> {
|
|
||||||
let style = self
|
|
||||||
.model
|
|
||||||
.get_cell_style(sheet, row, column)
|
|
||||||
.map_err(to_js_error)?;
|
|
||||||
|
|
||||||
env
|
|
||||||
.to_js_value(&style)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onPasteStyles")]
|
|
||||||
pub fn on_paste_styles(&mut self, env: Env, styles: JsUnknown) -> Result<()> {
|
|
||||||
let styles: &Vec<Vec<Style>> = &env
|
|
||||||
.from_js_value(styles)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
self.model.on_paste_styles(styles).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getCellType")]
|
|
||||||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32> {
|
|
||||||
Ok(
|
|
||||||
match self
|
|
||||||
.model
|
|
||||||
.get_cell_type(sheet, row, column)
|
|
||||||
.map_err(to_js_error)?
|
|
||||||
{
|
|
||||||
CellType::Number => 1,
|
|
||||||
CellType::Text => 2,
|
|
||||||
CellType::LogicalValue => 4,
|
|
||||||
CellType::ErrorValue => 16,
|
|
||||||
CellType::Array => 64,
|
|
||||||
CellType::CompoundData => 128,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// I don't _think_ serializing to JsUnknown can't fail
|
|
||||||
// FIXME: Remove this clippy directive
|
|
||||||
#[napi(js_name = "getWorksheetsProperties")]
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
|
|
||||||
env
|
|
||||||
.to_js_value(&self.model.get_worksheets_properties())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getSelectedSheet")]
|
|
||||||
pub fn get_selected_sheet(&self) -> u32 {
|
|
||||||
self.model.get_selected_sheet()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(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]
|
|
||||||
}
|
|
||||||
|
|
||||||
// I don't _think_ serializing to JsUnknown can't fail
|
|
||||||
// FIXME: Remove this clippy directive
|
|
||||||
#[napi(js_name = "getSelectedView")]
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
pub fn get_selected_view(&self, env: Env) -> JsUnknown {
|
|
||||||
env.to_js_value(&self.model.get_selected_view()).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setSelectedSheet")]
|
|
||||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<()> {
|
|
||||||
self.model.set_selected_sheet(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setSelectedCell")]
|
|
||||||
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_selected_cell(row, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setSelectedRange")]
|
|
||||||
pub fn set_selected_range(
|
|
||||||
&mut self,
|
|
||||||
start_row: i32,
|
|
||||||
start_column: i32,
|
|
||||||
end_row: i32,
|
|
||||||
end_column: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_selected_range(start_row, start_column, end_row, end_column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setTopLeftVisibleCell")]
|
|
||||||
pub fn set_top_left_visible_cell(&mut self, top_row: i32, top_column: i32) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_top_left_visible_cell(top_row, top_column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setShowGridLines")]
|
|
||||||
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.set_show_grid_lines(sheet, show_grid_lines)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getShowGridLines")]
|
|
||||||
pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result<bool> {
|
|
||||||
self.model.get_show_grid_lines(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "autoFillRows")]
|
|
||||||
pub fn auto_fill_rows(&mut self, env: Env, source_area: JsUnknown, to_row: i32) -> Result<()> {
|
|
||||||
let area: Area = env
|
|
||||||
.from_js_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)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "autoFillColumns")]
|
|
||||||
pub fn auto_fill_columns(
|
|
||||||
&mut self,
|
|
||||||
env: Env,
|
|
||||||
source_area: JsUnknown,
|
|
||||||
to_column: i32,
|
|
||||||
) -> Result<()> {
|
|
||||||
let area: Area = env
|
|
||||||
.from_js_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)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onArrowRight")]
|
|
||||||
pub fn on_arrow_right(&mut self) -> Result<()> {
|
|
||||||
self.model.on_arrow_right().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onArrowLeft")]
|
|
||||||
pub fn on_arrow_left(&mut self) -> Result<()> {
|
|
||||||
self.model.on_arrow_left().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onArrowUp")]
|
|
||||||
pub fn on_arrow_up(&mut self) -> Result<()> {
|
|
||||||
self.model.on_arrow_up().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onArrowDown")]
|
|
||||||
pub fn on_arrow_down(&mut self) -> Result<()> {
|
|
||||||
self.model.on_arrow_down().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onPageDown")]
|
|
||||||
pub fn on_page_down(&mut self) -> Result<()> {
|
|
||||||
self.model.on_page_down().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onPageUp")]
|
|
||||||
pub fn on_page_up(&mut self) -> Result<()> {
|
|
||||||
self.model.on_page_up().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setWindowWidth")]
|
|
||||||
pub fn set_window_width(&mut self, window_width: f64) {
|
|
||||||
self.model.set_window_width(window_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setWindowHeight")]
|
|
||||||
pub fn set_window_height(&mut self, window_height: f64) {
|
|
||||||
self.model.set_window_height(window_height);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getScrollX")]
|
|
||||||
pub fn get_scroll_x(&self) -> Result<f64> {
|
|
||||||
self.model.get_scroll_x().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getScrollY")]
|
|
||||||
pub fn get_scroll_y(&self) -> Result<f64> {
|
|
||||||
self.model.get_scroll_y().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onExpandSelectedRange")]
|
|
||||||
pub fn on_expand_selected_range(&mut self, key: String) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.on_expand_selected_range(&key)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "onAreaSelecting")]
|
|
||||||
pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.on_area_selecting(target_row, target_column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setAreaWithBorder")]
|
|
||||||
pub fn set_area_with_border(
|
|
||||||
&mut self,
|
|
||||||
env: Env,
|
|
||||||
area: JsUnknown,
|
|
||||||
border_area: JsUnknown,
|
|
||||||
) -> Result<()> {
|
|
||||||
let range: Area = env
|
|
||||||
.from_js_value(area)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
let border: BorderArea = env
|
|
||||||
.from_js_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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "toBytes")]
|
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
|
||||||
self.model.to_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getName")]
|
|
||||||
pub fn get_name(&self) -> String {
|
|
||||||
self.model.get_name()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "setName")]
|
|
||||||
pub fn set_name(&mut self, name: String) {
|
|
||||||
self.model.set_name(&name);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "copyToClipboard")]
|
|
||||||
pub fn copy_to_clipboard(&self, env: Env) -> Result<JsUnknown> {
|
|
||||||
let data = self
|
|
||||||
.model
|
|
||||||
.copy_to_clipboard()
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
|
|
||||||
env
|
|
||||||
.to_js_value(&data)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "pasteFromClipboard")]
|
|
||||||
pub fn paste_from_clipboard(
|
|
||||||
&mut self,
|
|
||||||
env: Env,
|
|
||||||
source_sheet: u32,
|
|
||||||
source_range: JsUnknown,
|
|
||||||
clipboard: JsUnknown,
|
|
||||||
is_cut: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let source_range: (i32, i32, i32, i32) = env
|
|
||||||
.from_js_value(source_range)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
let clipboard: ClipboardData = env
|
|
||||||
.from_js_value(clipboard)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.paste_from_clipboard(source_sheet, source_range, &clipboard, is_cut)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "pasteCsvText")]
|
|
||||||
pub fn paste_csv_string(&mut self, env: Env, area: JsUnknown, csv: String) -> Result<()> {
|
|
||||||
let range: Area = env
|
|
||||||
.from_js_value(area)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))?;
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.paste_csv_string(&range, &csv)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "getDefinedNameList")]
|
|
||||||
pub fn get_defined_name_list(&self, env: Env) -> Result<JsUnknown> {
|
|
||||||
let data: Vec<DefinedName> = self
|
|
||||||
.model
|
|
||||||
.get_defined_name_list()
|
|
||||||
.iter()
|
|
||||||
.map(|s| DefinedName {
|
|
||||||
name: s.0.to_owned(),
|
|
||||||
scope: s.1,
|
|
||||||
formula: s.2.to_owned(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
env
|
|
||||||
.to_js_value(&data)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "newDefinedName")]
|
|
||||||
pub fn new_defined_name(
|
|
||||||
&mut self,
|
|
||||||
name: String,
|
|
||||||
scope: Option<u32>,
|
|
||||||
formula: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.new_defined_name(&name, scope, &formula)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "updateDefinedName")]
|
|
||||||
pub fn update_defined_name(
|
|
||||||
&mut self,
|
|
||||||
name: String,
|
|
||||||
scope: Option<u32>,
|
|
||||||
new_name: String,
|
|
||||||
new_scope: Option<u32>,
|
|
||||||
new_formula: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.update_defined_name(&name, scope, &new_name, new_scope, &new_formula)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(js_name = "deleteDefinedName")]
|
|
||||||
pub fn delete_definedname(&mut self, name: String, scope: Option<u32>) -> Result<()> {
|
|
||||||
self
|
|
||||||
.model
|
|
||||||
.delete_defined_name(&name, scope)
|
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
343
bindings/nodejs/src/model.rs
Normal file
343
bindings/nodejs/src/model.rs
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
|
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use ironcalc::{
|
||||||
|
base::{
|
||||||
|
types::{CellType, Style},
|
||||||
|
Model as BaseModel,
|
||||||
|
},
|
||||||
|
error::XlsxError,
|
||||||
|
export::{save_to_icalc, save_to_xlsx},
|
||||||
|
import::{load_from_icalc, load_from_xlsx},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DefinedName {
|
||||||
|
name: String,
|
||||||
|
scope: Option<u32>,
|
||||||
|
formula: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_js_error(error: String) -> Error {
|
||||||
|
Error::new(Status::Unknown, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_node_error(error: XlsxError) -> Error {
|
||||||
|
Error::new(Status::Unknown, error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub struct Model {
|
||||||
|
model: BaseModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl Model {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(name: String, locale: String, timezone: String) -> Result<Self> {
|
||||||
|
let model = BaseModel::new_empty(&name, &locale, &timezone).map_err(to_js_error)?;
|
||||||
|
Ok(Self { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(factory)]
|
||||||
|
pub fn from_xlsx(file_path: String, locale: String, tz: String) -> Result<Model> {
|
||||||
|
let model = load_from_xlsx(&file_path, &locale, &tz)
|
||||||
|
.map_err(|error| Error::new(Status::Unknown, error.to_string()))?;
|
||||||
|
Ok(Self { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(factory)]
|
||||||
|
pub fn from_icalc(file_name: String) -> Result<Model> {
|
||||||
|
let model = load_from_icalc(&file_name)
|
||||||
|
.map_err(|error| Error::new(Status::Unknown, error.to_string()))?;
|
||||||
|
Ok(Self { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn save_to_xlsx(&self, file: String) -> Result<()> {
|
||||||
|
save_to_xlsx(&self.model, &file).map_err(to_node_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn save_to_icalc(&self, file: String) -> Result<()> {
|
||||||
|
save_to_icalc(&self.model, &file).map_err(to_node_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn evaluate(&mut self) {
|
||||||
|
self.model.evaluate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_user_input(&mut self, sheet: u32, row: i32, column: i32, value: String) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_user_input(sheet, row, column, value)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn clear_cell_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.cell_clear_contents(sheet, row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_cell_content(sheet, row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getCellType")]
|
||||||
|
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32> {
|
||||||
|
Ok(
|
||||||
|
match self
|
||||||
|
.model
|
||||||
|
.get_cell_type(sheet, row, column)
|
||||||
|
.map_err(to_js_error)?
|
||||||
|
{
|
||||||
|
CellType::Number => 1,
|
||||||
|
CellType::Text => 2,
|
||||||
|
CellType::LogicalValue => 4,
|
||||||
|
CellType::ErrorValue => 16,
|
||||||
|
CellType::Array => 64,
|
||||||
|
CellType::CompoundData => 128,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_formatted_cell_value(sheet, row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_cell_style(
|
||||||
|
&mut self,
|
||||||
|
env: Env,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
style: JsUnknown,
|
||||||
|
) -> Result<()> {
|
||||||
|
let style: Style = env
|
||||||
|
.from_js_value(style)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_cell_style(sheet, row, column, &style)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getCellStyle")]
|
||||||
|
pub fn get_cell_style(
|
||||||
|
&mut self,
|
||||||
|
env: Env,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<JsUnknown> {
|
||||||
|
let style = self
|
||||||
|
.model
|
||||||
|
.get_style_for_cell(sheet, row, column)
|
||||||
|
.map_err(to_js_error)?;
|
||||||
|
|
||||||
|
env
|
||||||
|
.to_js_value(&style)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.insert_rows(sheet, row, row_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn insert_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.insert_columns(sheet, column, column_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.delete_rows(sheet, row, row_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn delete_columns(&mut self, sheet: u32, column: i32, column_count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.delete_columns(sheet, column, column_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_column_width(sheet, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64> {
|
||||||
|
self.model.get_row_height(sheet, row).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_column_width(sheet, column, width)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_row_height(sheet, row, height)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_frozen_columns_count(sheet)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32> {
|
||||||
|
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_frozen_columns_count(&mut self, sheet: u32, column_count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_frozen_columns(sheet, column_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_frozen_rows_count(&mut self, sheet: u32, row_count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_frozen_rows(sheet, row_count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I don't _think_ serializing to JsUnknown can't fail
|
||||||
|
// FIXME: Remove this clippy directive
|
||||||
|
#[napi(js_name = "getWorksheetsProperties")]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
|
||||||
|
env
|
||||||
|
.to_js_value(&self.model.get_worksheets_properties())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn set_sheet_color(&mut self, sheet: u32, color: String) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_sheet_color(sheet, &color)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn add_sheet(&mut self, sheet_name: String) -> Result<()> {
|
||||||
|
self.model.add_sheet(&sheet_name).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn new_sheet(&mut self) {
|
||||||
|
self.model.new_sheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn delete_sheet(&mut self, sheet: u32) -> Result<()> {
|
||||||
|
self.model.delete_sheet(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub fn rename_sheet(&mut self, sheet: u32, new_name: String) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.rename_sheet_by_index(sheet, &new_name)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getDefinedNameList")]
|
||||||
|
pub fn get_defined_name_list(&self, env: Env) -> Result<JsUnknown> {
|
||||||
|
let data: Vec<DefinedName> = self
|
||||||
|
.model
|
||||||
|
.workbook
|
||||||
|
.get_defined_names_with_scope()
|
||||||
|
.iter()
|
||||||
|
.map(|s| DefinedName {
|
||||||
|
name: s.0.to_owned(),
|
||||||
|
scope: s.1,
|
||||||
|
formula: s.2.to_owned(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
env
|
||||||
|
.to_js_value(&data)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "newDefinedName")]
|
||||||
|
pub fn new_defined_name(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
scope: Option<u32>,
|
||||||
|
formula: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.new_defined_name(&name, scope, &formula)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "updateDefinedName")]
|
||||||
|
pub fn update_defined_name(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
scope: Option<u32>,
|
||||||
|
new_name: String,
|
||||||
|
new_scope: Option<u32>,
|
||||||
|
new_formula: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.update_defined_name(&name, scope, &new_name, new_scope, &new_formula)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "deleteDefinedName")]
|
||||||
|
pub fn delete_definedname(&mut self, name: String, scope: Option<u32>) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.delete_defined_name(&name, scope)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
654
bindings/nodejs/src/user_model.rs
Normal file
654
bindings/nodejs/src/user_model.rs
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
#![deny(clippy::all)]
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use napi::{self, bindgen_prelude::*, JsUnknown, Result};
|
||||||
|
|
||||||
|
use ironcalc::base::{
|
||||||
|
expressions::types::Area,
|
||||||
|
types::{CellType, Style},
|
||||||
|
BorderArea, ClipboardData, UserModel as BaseModel,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DefinedName {
|
||||||
|
name: String,
|
||||||
|
scope: Option<u32>,
|
||||||
|
formula: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_js_error(error: String) -> Error {
|
||||||
|
Error::new(Status::Unknown, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
pub struct UserModel {
|
||||||
|
model: BaseModel,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi]
|
||||||
|
impl UserModel {
|
||||||
|
#[napi(constructor)]
|
||||||
|
pub fn new(name: String, locale: String, timezone: String) -> Result<Self> {
|
||||||
|
let model = BaseModel::new_empty(&name, &locale, &timezone).map_err(to_js_error)?;
|
||||||
|
Ok(Self { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(factory)]
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<UserModel> {
|
||||||
|
let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?;
|
||||||
|
Ok(UserModel { model })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo(&mut self) -> Result<()> {
|
||||||
|
self.model.undo().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redo(&mut self) -> Result<()> {
|
||||||
|
self.model.redo().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "canUndo")]
|
||||||
|
pub fn can_undo(&self) -> bool {
|
||||||
|
self.model.can_undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "canRedo")]
|
||||||
|
pub fn can_redo(&self) -> bool {
|
||||||
|
self.model.can_redo()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "pauseEvaluation")]
|
||||||
|
pub fn pause_evaluation(&mut self) {
|
||||||
|
self.model.pause_evaluation()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "resumeEvaluation")]
|
||||||
|
pub fn resume_evaluation(&mut self) {
|
||||||
|
self.model.resume_evaluation()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&mut self) {
|
||||||
|
self.model.evaluate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "flushSendQueue")]
|
||||||
|
pub fn flush_send_queue(&mut self) -> Vec<u8> {
|
||||||
|
self.model.flush_send_queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "applyExternalDiffs")]
|
||||||
|
pub fn apply_external_diffs(&mut self, diffs: &[u8]) -> Result<()> {
|
||||||
|
self.model.apply_external_diffs(diffs).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getCellContent")]
|
||||||
|
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_cell_content(sheet, row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "newSheet")]
|
||||||
|
pub fn new_sheet(&mut self) -> Result<()> {
|
||||||
|
self.model.new_sheet().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "deleteSheet")]
|
||||||
|
pub fn delete_sheet(&mut self, sheet: u32) -> Result<()> {
|
||||||
|
self.model.delete_sheet(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "hideSheet")]
|
||||||
|
pub fn hide_sheet(&mut self, sheet: u32) -> Result<()> {
|
||||||
|
self.model.hide_sheet(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "unhideSheet")]
|
||||||
|
pub fn unhide_sheet(&mut self, sheet: u32) -> Result<()> {
|
||||||
|
self.model.unhide_sheet(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "renameSheet")]
|
||||||
|
pub fn rename_sheet(&mut self, sheet: u32, name: String) -> Result<()> {
|
||||||
|
self.model.rename_sheet(sheet, &name).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setSheetColor")]
|
||||||
|
pub fn set_sheet_color(&mut self, sheet: u32, color: String) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_sheet_color(sheet, &color)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "rangeClearAll")]
|
||||||
|
pub fn range_clear_all(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
end_row: i32,
|
||||||
|
end_column: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let range = Area {
|
||||||
|
sheet,
|
||||||
|
row: start_row,
|
||||||
|
column: start_column,
|
||||||
|
width: end_column - start_column + 1,
|
||||||
|
height: end_row - start_row + 1,
|
||||||
|
};
|
||||||
|
self.model.range_clear_all(&range).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "rangeClearContents")]
|
||||||
|
pub fn range_clear_contents(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
end_row: i32,
|
||||||
|
end_column: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let range = Area {
|
||||||
|
sheet,
|
||||||
|
row: start_row,
|
||||||
|
column: start_column,
|
||||||
|
width: end_column - start_column + 1,
|
||||||
|
height: end_row - start_row + 1,
|
||||||
|
};
|
||||||
|
self.model.range_clear_contents(&range).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "rangeClearFormatting")]
|
||||||
|
pub fn range_clear_formatting(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
end_row: i32,
|
||||||
|
end_column: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let range = Area {
|
||||||
|
sheet,
|
||||||
|
row: start_row,
|
||||||
|
column: start_column,
|
||||||
|
width: end_column - start_column + 1,
|
||||||
|
height: end_row - start_row + 1,
|
||||||
|
};
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.range_clear_formatting(&range)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "insertRow")]
|
||||||
|
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
||||||
|
self.model.insert_row(sheet, row).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "insertColumn")]
|
||||||
|
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
||||||
|
self.model.insert_column(sheet, column).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "deleteRow")]
|
||||||
|
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<()> {
|
||||||
|
self.model.delete_row(sheet, row).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "deleteColumn")]
|
||||||
|
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<()> {
|
||||||
|
self.model.delete_column(sheet, column).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setRowsHeight")]
|
||||||
|
pub fn set_rows_height(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
row_start: i32,
|
||||||
|
row_end: i32,
|
||||||
|
height: f64,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_rows_height(sheet, row_start, row_end, height)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setColumnsWidth")]
|
||||||
|
pub fn set_columns_width(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column_start: i32,
|
||||||
|
column_end: i32,
|
||||||
|
width: f64,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_columns_width(sheet, column_start, column_end, width)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getRowHeight")]
|
||||||
|
pub fn get_row_height(&mut self, sheet: u32, row: i32) -> Result<f64> {
|
||||||
|
self.model.get_row_height(sheet, row).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getColumnWidth")]
|
||||||
|
pub fn get_column_width(&mut self, sheet: u32, column: i32) -> Result<f64> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_column_width(sheet, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setUserInput")]
|
||||||
|
pub fn set_user_input(&mut self, sheet: u32, row: i32, column: i32, input: String) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_user_input(sheet, row, column, &input)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getFormattedCellValue")]
|
||||||
|
pub fn get_formatted_cell_value(&self, sheet: u32, row: i32, column: i32) -> Result<String> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_formatted_cell_value(sheet, row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getFrozenRowsCount")]
|
||||||
|
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32> {
|
||||||
|
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getFrozenColumnsCount")]
|
||||||
|
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.get_frozen_columns_count(sheet)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setFrozenRowsCount")]
|
||||||
|
pub fn set_frozen_rows_count(&mut self, sheet: u32, count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_frozen_rows_count(sheet, count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setFrozenColumnsCount")]
|
||||||
|
pub fn set_frozen_columns_count(&mut self, sheet: u32, count: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_frozen_columns_count(sheet, count)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "updateRangeStyle")]
|
||||||
|
pub fn update_range_style(
|
||||||
|
&mut self,
|
||||||
|
env: Env,
|
||||||
|
range: JsUnknown,
|
||||||
|
style_path: String,
|
||||||
|
value: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
let range: Area = env
|
||||||
|
.from_js_value(range)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.update_range_style(&range, &style_path, &value)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getCellStyle")]
|
||||||
|
pub fn get_cell_style(
|
||||||
|
&mut self,
|
||||||
|
env: Env,
|
||||||
|
sheet: u32,
|
||||||
|
row: i32,
|
||||||
|
column: i32,
|
||||||
|
) -> Result<JsUnknown> {
|
||||||
|
let style = self
|
||||||
|
.model
|
||||||
|
.get_cell_style(sheet, row, column)
|
||||||
|
.map_err(to_js_error)?;
|
||||||
|
|
||||||
|
env
|
||||||
|
.to_js_value(&style)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onPasteStyles")]
|
||||||
|
pub fn on_paste_styles(&mut self, env: Env, styles: JsUnknown) -> Result<()> {
|
||||||
|
let styles: &Vec<Vec<Style>> = &env
|
||||||
|
.from_js_value(styles)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
self.model.on_paste_styles(styles).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getCellType")]
|
||||||
|
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32> {
|
||||||
|
Ok(
|
||||||
|
match self
|
||||||
|
.model
|
||||||
|
.get_cell_type(sheet, row, column)
|
||||||
|
.map_err(to_js_error)?
|
||||||
|
{
|
||||||
|
CellType::Number => 1,
|
||||||
|
CellType::Text => 2,
|
||||||
|
CellType::LogicalValue => 4,
|
||||||
|
CellType::ErrorValue => 16,
|
||||||
|
CellType::Array => 64,
|
||||||
|
CellType::CompoundData => 128,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I don't _think_ serializing to JsUnknown can't fail
|
||||||
|
// FIXME: Remove this clippy directive
|
||||||
|
#[napi(js_name = "getWorksheetsProperties")]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
pub fn get_worksheets_properties(&self, env: Env) -> JsUnknown {
|
||||||
|
env
|
||||||
|
.to_js_value(&self.model.get_worksheets_properties())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getSelectedSheet")]
|
||||||
|
pub fn get_selected_sheet(&self) -> u32 {
|
||||||
|
self.model.get_selected_sheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(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]
|
||||||
|
}
|
||||||
|
|
||||||
|
// I don't _think_ serializing to JsUnknown can't fail
|
||||||
|
// FIXME: Remove this clippy directive
|
||||||
|
#[napi(js_name = "getSelectedView")]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
pub fn get_selected_view(&self, env: Env) -> JsUnknown {
|
||||||
|
env.to_js_value(&self.model.get_selected_view()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setSelectedSheet")]
|
||||||
|
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<()> {
|
||||||
|
self.model.set_selected_sheet(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setSelectedCell")]
|
||||||
|
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_selected_cell(row, column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setSelectedRange")]
|
||||||
|
pub fn set_selected_range(
|
||||||
|
&mut self,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
end_row: i32,
|
||||||
|
end_column: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_selected_range(start_row, start_column, end_row, end_column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setTopLeftVisibleCell")]
|
||||||
|
pub fn set_top_left_visible_cell(&mut self, top_row: i32, top_column: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_top_left_visible_cell(top_row, top_column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setShowGridLines")]
|
||||||
|
pub fn set_show_grid_lines(&mut self, sheet: u32, show_grid_lines: bool) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.set_show_grid_lines(sheet, show_grid_lines)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getShowGridLines")]
|
||||||
|
pub fn get_show_grid_lines(&mut self, sheet: u32) -> Result<bool> {
|
||||||
|
self.model.get_show_grid_lines(sheet).map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "autoFillRows")]
|
||||||
|
pub fn auto_fill_rows(&mut self, env: Env, source_area: JsUnknown, to_row: i32) -> Result<()> {
|
||||||
|
let area: Area = env
|
||||||
|
.from_js_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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "autoFillColumns")]
|
||||||
|
pub fn auto_fill_columns(
|
||||||
|
&mut self,
|
||||||
|
env: Env,
|
||||||
|
source_area: JsUnknown,
|
||||||
|
to_column: i32,
|
||||||
|
) -> Result<()> {
|
||||||
|
let area: Area = env
|
||||||
|
.from_js_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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onArrowRight")]
|
||||||
|
pub fn on_arrow_right(&mut self) -> Result<()> {
|
||||||
|
self.model.on_arrow_right().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onArrowLeft")]
|
||||||
|
pub fn on_arrow_left(&mut self) -> Result<()> {
|
||||||
|
self.model.on_arrow_left().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onArrowUp")]
|
||||||
|
pub fn on_arrow_up(&mut self) -> Result<()> {
|
||||||
|
self.model.on_arrow_up().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onArrowDown")]
|
||||||
|
pub fn on_arrow_down(&mut self) -> Result<()> {
|
||||||
|
self.model.on_arrow_down().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onPageDown")]
|
||||||
|
pub fn on_page_down(&mut self) -> Result<()> {
|
||||||
|
self.model.on_page_down().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onPageUp")]
|
||||||
|
pub fn on_page_up(&mut self) -> Result<()> {
|
||||||
|
self.model.on_page_up().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setWindowWidth")]
|
||||||
|
pub fn set_window_width(&mut self, window_width: f64) {
|
||||||
|
self.model.set_window_width(window_width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setWindowHeight")]
|
||||||
|
pub fn set_window_height(&mut self, window_height: f64) {
|
||||||
|
self.model.set_window_height(window_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getScrollX")]
|
||||||
|
pub fn get_scroll_x(&self) -> Result<f64> {
|
||||||
|
self.model.get_scroll_x().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getScrollY")]
|
||||||
|
pub fn get_scroll_y(&self) -> Result<f64> {
|
||||||
|
self.model.get_scroll_y().map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onExpandSelectedRange")]
|
||||||
|
pub fn on_expand_selected_range(&mut self, key: String) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.on_expand_selected_range(&key)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "onAreaSelecting")]
|
||||||
|
pub fn on_area_selecting(&mut self, target_row: i32, target_column: i32) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.on_area_selecting(target_row, target_column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setAreaWithBorder")]
|
||||||
|
pub fn set_area_with_border(
|
||||||
|
&mut self,
|
||||||
|
env: Env,
|
||||||
|
area: JsUnknown,
|
||||||
|
border_area: JsUnknown,
|
||||||
|
) -> Result<()> {
|
||||||
|
let range: Area = env
|
||||||
|
.from_js_value(area)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
let border: BorderArea = env
|
||||||
|
.from_js_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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "toBytes")]
|
||||||
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
|
self.model.to_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getName")]
|
||||||
|
pub fn get_name(&self) -> String {
|
||||||
|
self.model.get_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "setName")]
|
||||||
|
pub fn set_name(&mut self, name: String) {
|
||||||
|
self.model.set_name(&name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "copyToClipboard")]
|
||||||
|
pub fn copy_to_clipboard(&self, env: Env) -> Result<JsUnknown> {
|
||||||
|
let data = self
|
||||||
|
.model
|
||||||
|
.copy_to_clipboard()
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
|
||||||
|
env
|
||||||
|
.to_js_value(&data)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "pasteFromClipboard")]
|
||||||
|
pub fn paste_from_clipboard(
|
||||||
|
&mut self,
|
||||||
|
env: Env,
|
||||||
|
source_sheet: u32,
|
||||||
|
source_range: JsUnknown,
|
||||||
|
clipboard: JsUnknown,
|
||||||
|
is_cut: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let source_range: (i32, i32, i32, i32) = env
|
||||||
|
.from_js_value(source_range)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
let clipboard: ClipboardData = env
|
||||||
|
.from_js_value(clipboard)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.paste_from_clipboard(source_sheet, source_range, &clipboard, is_cut)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "pasteCsvText")]
|
||||||
|
pub fn paste_csv_string(&mut self, env: Env, area: JsUnknown, csv: String) -> Result<()> {
|
||||||
|
let range: Area = env
|
||||||
|
.from_js_value(area)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))?;
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.paste_csv_string(&range, &csv)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "getDefinedNameList")]
|
||||||
|
pub fn get_defined_name_list(&self, env: Env) -> Result<JsUnknown> {
|
||||||
|
let data: Vec<DefinedName> = self
|
||||||
|
.model
|
||||||
|
.get_defined_name_list()
|
||||||
|
.iter()
|
||||||
|
.map(|s| DefinedName {
|
||||||
|
name: s.0.to_owned(),
|
||||||
|
scope: s.1,
|
||||||
|
formula: s.2.to_owned(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
env
|
||||||
|
.to_js_value(&data)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "newDefinedName")]
|
||||||
|
pub fn new_defined_name(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
scope: Option<u32>,
|
||||||
|
formula: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.new_defined_name(&name, scope, &formula)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "updateDefinedName")]
|
||||||
|
pub fn update_defined_name(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
scope: Option<u32>,
|
||||||
|
new_name: String,
|
||||||
|
new_scope: Option<u32>,
|
||||||
|
new_formula: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.update_defined_name(&name, scope, &new_name, new_scope, &new_formula)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[napi(js_name = "deleteDefinedName")]
|
||||||
|
pub fn delete_definedname(&mut self, name: String, scope: Option<u32>) -> Result<()> {
|
||||||
|
self
|
||||||
|
.model
|
||||||
|
.delete_defined_name(&name, scope)
|
||||||
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
210
bindings/python/docs/api_reference.rst
Normal file
210
bindings/python/docs/api_reference.rst
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
|
||||||
|
API Reference
|
||||||
|
-------------
|
||||||
|
|
||||||
|
In general methods in IronCalc use a 0-index base for the the sheet index and 1-index base for the row and column indexes.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: evaluate()
|
||||||
|
|
||||||
|
Evaluates the model. This needs to be done after each change, otherwise the model might be on a broken state.
|
||||||
|
|
||||||
|
.. method:: set_user_input(sheet: int, row: int, column: int, value: str)
|
||||||
|
|
||||||
|
Sets an input in a cell, as would be done by a user typing into a spreadsheet cell.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index (first row is 1).
|
||||||
|
:param column: The 1-based column index (column “A” is 1).
|
||||||
|
:param value: The value to set, e.g. ``"123"`` or ``"=A1*2"``.
|
||||||
|
|
||||||
|
.. method:: clear_cell_contents(sheet: int, row: int, column: int)
|
||||||
|
|
||||||
|
Removes the content of the cell but leaves the style intact.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index (first row is 1).
|
||||||
|
:param column: The 1-based column index (column “A” is 1).
|
||||||
|
|
||||||
|
.. method:: get_cell_content(sheet: int, row: int, column: int) -> str
|
||||||
|
|
||||||
|
Returns the raw content of a cell. If the cell contains a formula,
|
||||||
|
the returned string starts with ``"="``.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index.
|
||||||
|
:param column: The 1-based column index.
|
||||||
|
:returns: The raw content, or an empty string if the cell is empty.
|
||||||
|
|
||||||
|
.. method:: get_cell_type(sheet: int, row: int, column: int) -> PyCellType
|
||||||
|
|
||||||
|
Returns the type of the cell (number, boolean, string, error, etc.).
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index.
|
||||||
|
:param column: The 1-based column index.
|
||||||
|
:rtype: PyCellType
|
||||||
|
|
||||||
|
.. method:: get_formatted_cell_value(sheet: int, row: int, column: int) -> str
|
||||||
|
|
||||||
|
Returns the cell’s value as a formatted string, taking into
|
||||||
|
account any number/currency/date formatting.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index.
|
||||||
|
:param column: The 1-based column index.
|
||||||
|
:returns: Formatted string of the cell’s value.
|
||||||
|
|
||||||
|
.. method:: set_cell_style(sheet: int, row: int, column: int, style: PyStyle)
|
||||||
|
|
||||||
|
Sets the style of the cell at (sheet, row, column).
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index.
|
||||||
|
:param column: The 1-based column index.
|
||||||
|
:param style: A PyStyle object specifying the style.
|
||||||
|
|
||||||
|
.. method:: get_cell_style(sheet: int, row: int, column: int) -> PyStyle
|
||||||
|
|
||||||
|
Retrieves the style of the specified cell.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index.
|
||||||
|
:param column: The 1-based column index.
|
||||||
|
:returns: A PyStyle object describing the cell’s style.
|
||||||
|
|
||||||
|
.. method:: insert_rows(sheet: int, row: int, row_count: int)
|
||||||
|
|
||||||
|
Inserts new rows.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The position before which new rows are inserted (1-based).
|
||||||
|
:param row_count: The number of rows to insert.
|
||||||
|
|
||||||
|
.. method:: insert_columns(sheet: int, column: int, column_count: int)
|
||||||
|
|
||||||
|
Inserts new columns.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param column: The position before which new columns are inserted (1-based).
|
||||||
|
:param column_count: The number of columns to insert.
|
||||||
|
|
||||||
|
.. method:: delete_rows(sheet: int, row: int, row_count: int)
|
||||||
|
|
||||||
|
Deletes a range of rows.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The starting row to delete (1-based).
|
||||||
|
:param row_count: How many rows to delete.
|
||||||
|
|
||||||
|
.. method:: delete_columns(sheet: int, column: int, column_count: int)
|
||||||
|
|
||||||
|
Deletes a range of columns.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param column: The starting column to delete (1-based).
|
||||||
|
:param column_count: How many columns to delete.
|
||||||
|
|
||||||
|
.. method:: get_column_width(sheet: int, column: int) -> float
|
||||||
|
|
||||||
|
Retrieves the width of a given column.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param column: The 1-based column index.
|
||||||
|
:rtype: float
|
||||||
|
|
||||||
|
.. method:: get_row_height(sheet: int, row: int) -> float
|
||||||
|
|
||||||
|
Retrieves the height of a given row.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index.
|
||||||
|
:rtype: float
|
||||||
|
|
||||||
|
.. method:: set_column_width(sheet: int, column: int, width: float)
|
||||||
|
|
||||||
|
Sets the width of a given column.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param column: The 1-based column index.
|
||||||
|
:param width: The desired width (float).
|
||||||
|
|
||||||
|
.. method:: set_row_height(sheet: int, row: int, height: float)
|
||||||
|
|
||||||
|
Sets the height of a given row.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row: The 1-based row index.
|
||||||
|
:param height: The desired height (float).
|
||||||
|
|
||||||
|
.. method:: get_frozen_columns_count(sheet: int) -> int
|
||||||
|
|
||||||
|
Returns the number of columns frozen (pinned) on the left side of the sheet.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:rtype: int
|
||||||
|
|
||||||
|
.. method:: get_frozen_rows_count(sheet: int) -> int
|
||||||
|
|
||||||
|
Returns the number of rows frozen (pinned) at the top of the sheet.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:rtype: int
|
||||||
|
|
||||||
|
.. method:: set_frozen_columns_count(sheet: int, column_count: int)
|
||||||
|
|
||||||
|
Sets how many columns are frozen (pinned) on the left.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param column_count: The number of frozen columns (0-based).
|
||||||
|
|
||||||
|
.. method:: set_frozen_rows_count(sheet: int, row_count: int)
|
||||||
|
|
||||||
|
Sets how many rows are frozen (pinned) at the top.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param row_count: The number of frozen rows (0-based).
|
||||||
|
|
||||||
|
.. method:: get_worksheets_properties() -> List[PySheetProperty]
|
||||||
|
|
||||||
|
Returns a list of :class:`PySheetProperty` describing each worksheet’s
|
||||||
|
name, visibility state, ID, and tab color.
|
||||||
|
|
||||||
|
:rtype: list of PySheetProperty
|
||||||
|
|
||||||
|
.. method:: set_sheet_color(sheet: int, color: str)
|
||||||
|
|
||||||
|
Sets the tab color of a sheet. Use an empty string to clear the color.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param color: A color in “#RRGGBB” format, or empty to remove color.
|
||||||
|
|
||||||
|
.. method:: add_sheet(sheet_name: str)
|
||||||
|
|
||||||
|
Creates a new sheet with the specified name.
|
||||||
|
|
||||||
|
:param sheet_name: The name to give the new sheet.
|
||||||
|
|
||||||
|
.. method:: new_sheet()
|
||||||
|
|
||||||
|
Creates a new sheet with an auto-generated name.
|
||||||
|
|
||||||
|
.. method:: delete_sheet(sheet: int)
|
||||||
|
|
||||||
|
Deletes the sheet at the given index.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
|
||||||
|
.. method:: rename_sheet(sheet: int, new_name: str)
|
||||||
|
|
||||||
|
Renames the sheet at the given index.
|
||||||
|
|
||||||
|
:param sheet: The sheet index (0-based).
|
||||||
|
:param new_name: The new sheet name.
|
||||||
|
|
||||||
|
.. method:: test_panic()
|
||||||
|
|
||||||
|
A test method that deliberately panics in Rust.
|
||||||
|
Used for testing panic handling at the method level.
|
||||||
|
|
||||||
|
:raises WorkbookError: (wrapped Rust panic)
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
IronCalc: The democratization of spreadsheets
|
IronCalc
|
||||||
=============================================
|
========
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
IronCalc is a spreadsheet engine that allows you to create, modify and safe spreadsheets.
|
installation
|
||||||
|
usage_examples
|
||||||
|
top_level_methods
|
||||||
|
api_reference
|
||||||
|
objects
|
||||||
|
|
||||||
|
IronCalc is a spreadsheet engine that allows you to create, modify and save spreadsheets.
|
||||||
|
|
||||||
A simple example that creates a model, sets a formula, evaluates it and gets the result back:
|
A simple example that creates a model, sets a formula, evaluates it and gets the result back:
|
||||||
|
|
||||||
.. literalinclude:: examples/simple.py
|
.. literalinclude:: examples/simple.py
|
||||||
:language: python
|
:language: python
|
||||||
|
|
||||||
|
|||||||
9
bindings/python/docs/installation.rst
Normal file
9
bindings/python/docs/installation.rst
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
You can simply do:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
pip install ironcalc
|
||||||
|
|
||||||
32
bindings/python/docs/objects.rst
Normal file
32
bindings/python/docs/objects.rst
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
Objects
|
||||||
|
-------
|
||||||
|
|
||||||
|
The following examples
|
||||||
|
|
||||||
|
|
||||||
|
``WorkbookError``
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
Exceptions of type ``WorkbookError`` are raised whenever there is a problem with
|
||||||
|
the workbook (e.g., invalid parameters, file I/O error, or even a Rust panic).
|
||||||
|
You can catch these exceptions in Python as follows:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from ironcalc import WorkbookError
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Some operation on PyModel
|
||||||
|
pass
|
||||||
|
except WorkbookError as e:
|
||||||
|
print("Caught a workbook error:", e)
|
||||||
|
|
||||||
|
``PyCellType``
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
Represents the type of a cell (e.g., number, string, boolean, etc.). You can
|
||||||
|
check the type of a cell with :meth:`PyModel.get_cell_type`.
|
||||||
|
|
||||||
|
``PyStyle``
|
||||||
|
^^^^^^^^^^^
|
||||||
|
Represents the style of a cell (font, bold, number formats, alignment, etc.).
|
||||||
|
You can get/set these styles with :meth:`PyModel.get_cell_style`
|
||||||
|
and :meth:`PyModel.set_cell_style`.
|
||||||
6
bindings/python/docs/top_level_methods.rst
Normal file
6
bindings/python/docs/top_level_methods.rst
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Top Level Methods
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
.. autofunction:: ironcalc.create
|
||||||
|
.. autofunction:: ironcalc.load_from_xlsx
|
||||||
|
.. autofunction:: ironcalc.load_from_icalc
|
||||||
37
bindings/python/docs/usage_examples.rst
Normal file
37
bindings/python/docs/usage_examples.rst
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
Usage Examples
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Creating an Empty Model
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import ironcalc as ic
|
||||||
|
|
||||||
|
model = ic.create("My Workbook", "en", "UTC")
|
||||||
|
|
||||||
|
Loading from XLSX
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import ironcalc as ic
|
||||||
|
|
||||||
|
model = ic.load_from_xlsx("example.xlsx", "en", "UTC")
|
||||||
|
|
||||||
|
Modifying and Saving
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
model = ic.create("model", "en", "UTC")
|
||||||
|
model.set_user_input(0, 1, 1, "123")
|
||||||
|
model.set_user_input(0, 1, 2, "=A1*2")
|
||||||
|
model.evaluate()
|
||||||
|
|
||||||
|
# Save to XLSX
|
||||||
|
model.save_to_xlsx("updated.xlsx")
|
||||||
|
|
||||||
|
# Or save to the binary format
|
||||||
|
model.save_to_icalc("my_workbook.icalc")
|
||||||
@@ -16,7 +16,7 @@ classifiers = [
|
|||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Topic :: Software Development :: Libraries",
|
"Topic :: Software Development :: Libraries",
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
"Office/Business :: Financial :: Spreadsheet",
|
"Topic :: Office/Business :: Financial :: Spreadsheet",
|
||||||
]
|
]
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Nicolás Hatcher", email = "nicolas@theuniverse.today" },
|
{ name = "Nicolás Hatcher", email = "nicolas@theuniverse.today" },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wasm"
|
name = "wasm"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
|
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
|
||||||
description = "IronCalc Web bindings"
|
description = "IronCalc Web bindings"
|
||||||
license = "MIT/Apache-2.0"
|
license = "MIT/Apache-2.0"
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ https://www.npmjs.com/package/@ironcalc/wasm?activeTab=readme
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
|
Dependencies:
|
||||||
|
|
||||||
|
* Rust
|
||||||
|
* wasm-pack
|
||||||
|
* TypeScript
|
||||||
|
* Python
|
||||||
|
* binutils (for make)
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -174,6 +174,27 @@ impl Model {
|
|||||||
self.model.range_clear_contents(&range).map_err(to_js_error)
|
self.model.range_clear_contents(&range).map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "rangeClearFormatting")]
|
||||||
|
pub fn range_clear_formatting(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
end_row: i32,
|
||||||
|
end_column: i32,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
|
let range = Area {
|
||||||
|
sheet,
|
||||||
|
row: start_row,
|
||||||
|
column: start_column,
|
||||||
|
width: end_column - start_column + 1,
|
||||||
|
height: end_row - start_row + 1,
|
||||||
|
};
|
||||||
|
self.model
|
||||||
|
.range_clear_formatting(&range)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "insertRow")]
|
#[wasm_bindgen(js_name = "insertRow")]
|
||||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
||||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
self.model.insert_row(sheet, row).map_err(to_js_error)
|
||||||
@@ -194,17 +215,29 @@ impl Model {
|
|||||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
self.model.delete_column(sheet, column).map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setRowHeight")]
|
#[wasm_bindgen(js_name = "setRowsHeight")]
|
||||||
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<(), JsError> {
|
pub fn set_rows_height(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
row_start: i32,
|
||||||
|
row_end: i32,
|
||||||
|
height: f64,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
self.model
|
self.model
|
||||||
.set_row_height(sheet, row, height)
|
.set_rows_height(sheet, row_start, row_end, height)
|
||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setColumnWidth")]
|
#[wasm_bindgen(js_name = "setColumnsWidth")]
|
||||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), JsError> {
|
pub fn set_columns_width(
|
||||||
|
&mut self,
|
||||||
|
sheet: u32,
|
||||||
|
column_start: i32,
|
||||||
|
column_end: i32,
|
||||||
|
width: f64,
|
||||||
|
) -> Result<(), JsError> {
|
||||||
self.model
|
self.model
|
||||||
.set_column_width(sheet, column, width)
|
.set_columns_width(sheet, column_start, column_end, width)
|
||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +304,37 @@ impl Model {
|
|||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This two are only used when we want to compute the automatic width of a column or height of a row
|
||||||
|
#[wasm_bindgen(js_name = "getRowsWithData")]
|
||||||
|
pub fn get_rows_with_data(&self, sheet: u32, column: i32) -> Result<Vec<i32>, JsError> {
|
||||||
|
let sheet_data = &self
|
||||||
|
.model
|
||||||
|
.get_model()
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)
|
||||||
|
.map_err(to_js_error)?
|
||||||
|
.sheet_data;
|
||||||
|
Ok(sheet_data
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, data)| data.contains_key(&column))
|
||||||
|
.map(|(row, _)| *row)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "getColumnsWithData")]
|
||||||
|
pub fn get_columns_with_data(&self, sheet: u32, row: i32) -> Result<Vec<i32>, JsError> {
|
||||||
|
Ok(self
|
||||||
|
.model
|
||||||
|
.get_model()
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet)
|
||||||
|
.map_err(to_js_error)?
|
||||||
|
.sheet_data
|
||||||
|
.get(&row)
|
||||||
|
.map(|row_data| row_data.keys().copied().collect())
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
||||||
pub fn update_range_style(
|
pub fn update_range_style(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ test('Row height', () => {
|
|||||||
let model = new Model('Workbook1', 'en', 'UTC');
|
let model = new Model('Workbook1', 'en', 'UTC');
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), DEFAULT_ROW_HEIGHT);
|
assert.strictEqual(model.getRowHeight(0, 3), DEFAULT_ROW_HEIGHT);
|
||||||
|
|
||||||
model.setRowHeight(0, 3, 32);
|
model.setRowsHeight(0, 3, 3, 32);
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
||||||
|
|
||||||
model.undo();
|
model.undo();
|
||||||
@@ -29,7 +29,7 @@ test('Row height', () => {
|
|||||||
model.redo();
|
model.redo();
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
||||||
|
|
||||||
model.setRowHeight(0, 3, 320);
|
model.setRowsHeight(0, 3, 3, 320);
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 320);
|
assert.strictEqual(model.getRowHeight(0, 3), 320);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ test('Styles work', () => {
|
|||||||
num_fmt: 'general',
|
num_fmt: 'general',
|
||||||
fill: { pattern_type: 'none' },
|
fill: { pattern_type: 'none' },
|
||||||
font: {
|
font: {
|
||||||
sz: 11,
|
sz: 13,
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
name: 'Calibri',
|
name: 'Calibri',
|
||||||
family: 2,
|
family: 2,
|
||||||
@@ -64,7 +64,7 @@ test('Styles work', () => {
|
|||||||
num_fmt: 'general',
|
num_fmt: 'general',
|
||||||
fill: { pattern_type: 'none' },
|
fill: { pattern_type: 'none' },
|
||||||
font: {
|
font: {
|
||||||
sz: 11,
|
sz: 13,
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
name: 'Calibri',
|
name: 'Calibri',
|
||||||
family: 2,
|
family: 2,
|
||||||
@@ -96,7 +96,7 @@ test("Add sheets", (t) => {
|
|||||||
test("invalid sheet index throws an exception", () => {
|
test("invalid sheet index throws an exception", () => {
|
||||||
const model = new Model('Workbook1', 'en', 'UTC');
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
assert.throws(() => {
|
assert.throws(() => {
|
||||||
model.setRowHeight(1, 1, 100);
|
model.setRowsHeight(1, 1, 1, 100);
|
||||||
}, {
|
}, {
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
message: 'Invalid sheet index',
|
message: 'Invalid sheet index',
|
||||||
@@ -106,7 +106,7 @@ test("invalid sheet index throws an exception", () => {
|
|||||||
test("invalid column throws an exception", () => {
|
test("invalid column throws an exception", () => {
|
||||||
const model = new Model('Workbook1', 'en', 'UTC');
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
assert.throws(() => {
|
assert.throws(() => {
|
||||||
model.setRowHeight(0, -1, 100);
|
model.setRowsHeight(0, -1, 0, 100);
|
||||||
}, {
|
}, {
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
message: "Row number '-1' is not valid.",
|
message: "Row number '-1' is not valid.",
|
||||||
@@ -115,7 +115,7 @@ test("invalid column throws an exception", () => {
|
|||||||
|
|
||||||
test("floating column numbers get truncated", () => {
|
test("floating column numbers get truncated", () => {
|
||||||
const model = new Model('Workbook1', 'en', 'UTC');
|
const model = new Model('Workbook1', 'en', 'UTC');
|
||||||
model.setRowHeight(0.8, 5.2, 100.5);
|
model.setRowsHeight(0.8, 5.2, 5.5, 100.5);
|
||||||
|
|
||||||
assert.strictEqual(model.getRowHeight(0.11, 5.99), 100.5);
|
assert.strictEqual(model.getRowHeight(0.11, 5.99), 100.5);
|
||||||
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ lang: en-US
|
|||||||
|
|
||||||
# How to Contribute
|
# How to Contribute
|
||||||
|
|
||||||
If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action), join our [Discord Channel](https://discord.gg/sjaefMWE) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
|
If you want to give us a hand, take a look at our [GitHub repository](https://github.com/ironcalc/IronCalc?tab=readme-ov-file#collaborators-needed-call-to-action), join our [Discord Channel](https://discord.com/invite/zZYWfh3RHJ) or send us an email to [hello@ironcalc.com](mailto:hello@ironcalc.com).
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ Although IronCalc is ready for use, it’s important to understand its current l
|
|||||||
|
|
||||||
IronCalc currently does not implement arrays or array formulas. These are planned and are coming very soon, as they are the highest priority on the engine side.
|
IronCalc currently does not implement arrays or array formulas. These are planned and are coming very soon, as they are the highest priority on the engine side.
|
||||||
|
|
||||||
## **Name Manager** <Badge type="info" text="Planned" />
|
|
||||||
|
|
||||||
While IronCalc supports importing and exporting defined names, it does not yet allow you to create, delete, or update them in the UI. This feature is expected to be implemented shortly.
|
|
||||||
|
|
||||||
## **Only English Supported**
|
## **Only English Supported**
|
||||||
|
|
||||||
The MVP version of IronCalc supports only the English language. However, version 1.0 will include support for three languages: **English**, **German**, and **Spanish**.
|
The MVP version of IronCalc supports only the English language. However, version 1.0 will include support for three languages: **English**, **German**, and **Spanish**.
|
||||||
|
|||||||
@@ -6,7 +6,20 @@ lang: en-US
|
|||||||
|
|
||||||
# Name Manager
|
# Name Manager
|
||||||
|
|
||||||
::: warning
|
The **Name Manager** makes working with specific cells or ranges easier by letting you assign custom names and set their scope.
|
||||||
**Note:** This draft page is under construction 🚧
|
|
||||||
:::
|
|
||||||
|
|
||||||
|
## How to Use It
|
||||||
|
|
||||||
|
1. Click the **Named Ranges** button in the toolbar.
|
||||||
|
- A dialog will open.
|
||||||
|
2. Click **Add New**.
|
||||||
|
3. Input a name to identify the range.
|
||||||
|
4. Set the scope:
|
||||||
|
- **Global**: Applies to the entire workbook.
|
||||||
|
- **Sheet-specific**: Applies only to the selected sheet.
|
||||||
|
5. Click the check icon to save your changes.
|
||||||
|
|
||||||
|
## Managing Named Ranges
|
||||||
|
|
||||||
|
- Use **Edit** to modify name, range, or scope.
|
||||||
|
- Use **Delete** to remove ranges when they’re no longer needed.
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { StorybookConfig } from "@storybook/react-vite";
|
|||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||||
addons: [
|
addons: [
|
||||||
"@storybook/addon-onboarding",
|
|
||||||
"@storybook/addon-essentials",
|
"@storybook/addon-essentials",
|
||||||
"@chromatic-com/storybook",
|
"@chromatic-com/storybook",
|
||||||
"@storybook/addon-interactions",
|
"@storybook/addon-interactions",
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ npm install
|
|||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev
|
npm run storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
## Linter and formatting
|
## Linter and formatting
|
||||||
@@ -39,20 +38,9 @@ npm run test
|
|||||||
|
|
||||||
Warning: There is only the testing infrastructure in place.
|
Warning: There is only the testing infrastructure in place.
|
||||||
|
|
||||||
## Deploy
|
## Build package
|
||||||
|
|
||||||
Deploying is a bit of a manual hassle right now:
|
|
||||||
To build a deployable frontend:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
Please copy the `inroncalc.svg` icon and the models you want to have as 'examples' in the internal 'ic' format.
|
|
||||||
I normally compress the wasm and js files with brotli
|
|
||||||
|
|
||||||
```
|
|
||||||
brotli wasm_bg-*****.wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy to the final destination and you are good to go.
|
|
||||||
2744
webapp/IronCalc/package-lock.json
generated
2744
webapp/IronCalc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@ironcalc/workbook",
|
"name": "@ironcalc/workbook",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/ironcalc.js",
|
"main": "./dist/ironcalc.js",
|
||||||
"module": "./dist/ironcalc.js",
|
"module": "./dist/ironcalc.js",
|
||||||
"source": "./src/index.ts",
|
"source": "./src/index.ts",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
|
||||||
"build": "vite build && tsc",
|
"build": "vite build && tsc",
|
||||||
"check": "biome check ./src",
|
"check": "biome check ./src",
|
||||||
"check-write": "biome check --write ./src",
|
"check-write": "biome check --write ./src",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006 --no-open",
|
||||||
"build-storybook": "storybook build"
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -27,29 +26,28 @@
|
|||||||
"react-i18next": "^15.4.0"
|
"react-i18next": "^15.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@chromatic-com/storybook": "^3.2.4",
|
"@chromatic-com/storybook": "^3.2.4",
|
||||||
"@storybook/addon-essentials": "^8.6.0-alpha.0",
|
"@storybook/addon-essentials": "^8.6.0",
|
||||||
"@storybook/addon-interactions": "^8.6.0-alpha.0",
|
"@storybook/addon-interactions": "^8.6.0",
|
||||||
"@storybook/addon-onboarding": "^8.6.0-alpha.0",
|
"@storybook/blocks": "^8.6.0",
|
||||||
"@storybook/blocks": "^8.6.0-alpha.0",
|
"@storybook/react": "^8.6.0",
|
||||||
"@storybook/react": "^8.6.0-alpha.0",
|
"@storybook/react-vite": "^8.6.0",
|
||||||
"@storybook/react-vite": "^8.6.0-alpha.0",
|
"@storybook/test": "^8.6.0",
|
||||||
"@storybook/test": "^8.6.0-alpha.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"react": "^18.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"storybook": "^8.6.0-alpha.0",
|
"storybook": "^8.6.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.5",
|
"vite": "^6.2.0",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"vitest": "^2.0.5"
|
"vitest": "^3.0.7"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0",
|
"@types/react": "^18.0.0 || ^19.0.0",
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "./index.css";
|
import "./index.css";
|
||||||
import type { Model } from "@ironcalc/wasm";
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import ThemeProvider from "@mui/material/styles/ThemeProvider";
|
import ThemeProvider from "@mui/material/styles/ThemeProvider";
|
||||||
import Workbook from "./components/workbook.tsx";
|
import Workbook from "./components/Workbook/Workbook.tsx";
|
||||||
import { WorkbookState } from "./components/workbookState.ts";
|
import { WorkbookState } from "./components/workbookState.ts";
|
||||||
import { theme } from "./theme.ts";
|
import { theme } from "./theme.ts";
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ import {
|
|||||||
BorderRightIcon,
|
BorderRightIcon,
|
||||||
BorderStyleIcon,
|
BorderStyleIcon,
|
||||||
BorderTopIcon,
|
BorderTopIcon,
|
||||||
} from "../icons";
|
} from "../../icons";
|
||||||
import { theme } from "../theme";
|
import { theme } from "../../theme";
|
||||||
import ColorPicker from "./colorPicker";
|
import ColorPicker from "../ColorPicker/ColorPicker";
|
||||||
|
|
||||||
type BorderPickerProps = {
|
type BorderPickerProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange: (border: BorderOptions) => void;
|
onChange: (border: BorderOptions) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
anchorEl: React.RefObject<HTMLElement>;
|
anchorEl: React.RefObject<HTMLElement | null>;
|
||||||
anchorOrigin?: PopoverOrigin;
|
anchorOrigin?: PopoverOrigin;
|
||||||
transformOrigin?: PopoverOrigin;
|
transformOrigin?: PopoverOrigin;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -1,33 +1,36 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import Popover, { type PopoverOrigin } from "@mui/material/Popover";
|
import Popover, { type PopoverOrigin } from "@mui/material/Popover";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||||
import { theme } from "../theme";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
|
||||||
type ColorPickerProps = {
|
type ColorPickerProps = {
|
||||||
className?: string;
|
|
||||||
color: string;
|
color: string;
|
||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
anchorEl: React.RefObject<HTMLElement>;
|
anchorEl: React.RefObject<HTMLElement | null>;
|
||||||
anchorOrigin?: PopoverOrigin;
|
anchorOrigin?: PopoverOrigin;
|
||||||
transformOrigin?: PopoverOrigin;
|
transformOrigin?: PopoverOrigin;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const colorPickerWidth = 240;
|
const colorPickerWidth = 240;
|
||||||
const colorfulHeight = 185; // 150 + 15 + 20
|
const colorfulHeight = 240;
|
||||||
|
|
||||||
const ColorPicker = (properties: ColorPickerProps) => {
|
const ColorPicker = (properties: ColorPickerProps) => {
|
||||||
const [color, setColor] = useState<string>(properties.color);
|
const [color, setColor] = useState<string>(properties.color);
|
||||||
const recentColors = useRef<string[]>([]);
|
const recentColors = useRef<string[]>([]);
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const closePicker = (newColor: string): void => {
|
const closePicker = (newColor: string): void => {
|
||||||
const maxRecentColors = 14;
|
const maxRecentColors = 14;
|
||||||
properties.onChange(newColor);
|
|
||||||
const colors = recentColors.current.filter((c) => c !== newColor);
|
const colors = recentColors.current.filter((c) => c !== newColor);
|
||||||
recentColors.current = [newColor, ...colors].slice(0, maxRecentColors);
|
recentColors.current = [newColor, ...colors].slice(0, maxRecentColors);
|
||||||
|
properties.onChange(newColor);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = (): void => {
|
const handleClose = (): void => {
|
||||||
@@ -85,21 +88,16 @@ const ColorPicker = (properties: ColorPickerProps) => {
|
|||||||
/>
|
/>
|
||||||
</HexColorInputBox>
|
</HexColorInputBox>
|
||||||
</HexWrapper>
|
</HexWrapper>
|
||||||
<Swatch
|
<Swatch $color={color} />
|
||||||
$color={color}
|
|
||||||
onClick={(): void => {
|
|
||||||
closePicker(color);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ColorPickerInput>
|
</ColorPickerInput>
|
||||||
<HorizontalDivider />
|
<HorizontalDivider />
|
||||||
<ColorList>
|
<ColorList>
|
||||||
{presetColors.map((presetColor) => (
|
{presetColors.map((presetColor) => (
|
||||||
<Button
|
<RecentColorButton
|
||||||
key={presetColor}
|
key={presetColor}
|
||||||
$color={presetColor}
|
$color={presetColor}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
closePicker(presetColor);
|
setColor(presetColor);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -111,11 +109,11 @@ const ColorPicker = (properties: ColorPickerProps) => {
|
|||||||
<RecentLabel>{"Recent"}</RecentLabel>
|
<RecentLabel>{"Recent"}</RecentLabel>
|
||||||
<ColorList>
|
<ColorList>
|
||||||
{recentColors.current.map((recentColor) => (
|
{recentColors.current.map((recentColor) => (
|
||||||
<Button
|
<RecentColorButton
|
||||||
key={recentColor}
|
key={recentColor}
|
||||||
$color={recentColor}
|
$color={recentColor}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
closePicker(recentColor);
|
setColor(recentColor);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -124,11 +122,46 @@ const ColorPicker = (properties: ColorPickerProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<div />
|
<div />
|
||||||
)}
|
)}
|
||||||
|
<Buttons>
|
||||||
|
<StyledButton
|
||||||
|
onClick={(): void => {
|
||||||
|
closePicker(color);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
style={{ width: "16px", height: "16px", marginRight: "8px" }}
|
||||||
|
/>
|
||||||
|
{t("color_picker.apply")}
|
||||||
|
</StyledButton>
|
||||||
|
</Buttons>
|
||||||
</ColorPickerDialog>
|
</ColorPickerDialog>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Buttons = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled("div")`
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ffffff;
|
||||||
|
background: #f2994a;
|
||||||
|
padding: 0px 10px;
|
||||||
|
height: 36px;
|
||||||
|
line-height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: "Inter";
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover {
|
||||||
|
background: #d68742;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const RecentLabel = styled.div`
|
const RecentLabel = styled.div`
|
||||||
font-family: "Inter";
|
font-family: "Inter";
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -146,7 +179,7 @@ const ColorList = styled.div`
|
|||||||
gap: 4.7px;
|
gap: 4.7px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Button = styled.button<{ $color: string }>`
|
const RecentColorButton = styled.button<{ $color: string }>`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
${({ $color }): string => {
|
${({ $color }): string => {
|
||||||
@@ -174,20 +207,6 @@ const HorizontalDivider = styled.div`
|
|||||||
border-top: 1px solid ${theme.palette.grey["200"]};
|
border-top: 1px solid ${theme.palette.grey["200"]};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// const StyledPopover = styled(Popover)`
|
|
||||||
// .MuiPopover-paper {
|
|
||||||
// border-radius: 10px;
|
|
||||||
// border: 0px solid ${theme.palette.background.default};
|
|
||||||
// box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
|
||||||
// }
|
|
||||||
// .MuiPopover-padding {
|
|
||||||
// padding: 0px;
|
|
||||||
// }
|
|
||||||
// .MuiList-padding {
|
|
||||||
// padding: 0px;
|
|
||||||
// }
|
|
||||||
// `;
|
|
||||||
|
|
||||||
const ColorPickerDialog = styled.div`
|
const ColorPickerDialog = styled.div`
|
||||||
background: ${theme.palette.background.default};
|
background: ${theme.palette.background.default};
|
||||||
width: ${colorPickerWidth}px;
|
width: ${colorPickerWidth}px;
|
||||||
@@ -9,7 +9,7 @@ interface Options {
|
|||||||
onEditEnd: () => void;
|
onEditEnd: () => void;
|
||||||
onTextUpdated: () => void;
|
onTextUpdated: () => void;
|
||||||
workbookState: WorkbookState;
|
workbookState: WorkbookState;
|
||||||
textareaRef: RefObject<HTMLTextAreaElement>;
|
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useKeyDown = (
|
export const useKeyDown = (
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
type TokenType,
|
type TokenType,
|
||||||
getTokens,
|
getTokens,
|
||||||
} from "@ironcalc/wasm";
|
} from "@ironcalc/wasm";
|
||||||
|
import type { JSX } from "react";
|
||||||
import type { ActiveRange } from "../workbookState";
|
import type { ActiveRange } from "../workbookState";
|
||||||
|
|
||||||
function sliceString(
|
function sliceString(
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Menu, MenuItem, styled } from "@mui/material";
|
import { Menu, MenuItem, styled } from "@mui/material";
|
||||||
import { type ComponentProps, useCallback, useRef, useState } from "react";
|
import { type ComponentProps, useCallback, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import FormatPicker from "./formatPicker";
|
import FormatPicker from "./FormatPicker";
|
||||||
import { NumberFormats } from "./formatUtil";
|
import { NumberFormats } from "./formatUtil";
|
||||||
|
|
||||||
type FormatMenuProps = {
|
type FormatMenuProps = {
|
||||||
@@ -3,7 +3,7 @@ import { Dialog, TextField } from "@mui/material";
|
|||||||
import { Check, X } from "lucide-react";
|
import { Check, X } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { theme } from "../theme";
|
import { theme } from "../../theme";
|
||||||
|
|
||||||
type FormatPickerProps = {
|
type FormatPickerProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -129,7 +129,6 @@ const StyledDialogContent = styled("div")`
|
|||||||
|
|
||||||
const StyledTextField = styled(TextField)`
|
const StyledTextField = styled(TextField)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 320px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
& .MuiInputBase-input {
|
& .MuiInputBase-input {
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { Model } from "@ironcalc/wasm";
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import { styled } from "@mui/material";
|
import { styled } from "@mui/material";
|
||||||
import { Fx } from "../icons";
|
import { Fx } from "../../icons";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
import Editor from "../Editor/Editor";
|
||||||
import {
|
import {
|
||||||
COLUMN_WIDTH_SCALE,
|
COLUMN_WIDTH_SCALE,
|
||||||
ROW_HEIGH_SCALE,
|
ROW_HEIGH_SCALE,
|
||||||
} from "./WorksheetCanvas/constants";
|
} from "../WorksheetCanvas/constants";
|
||||||
import { FORMULA_BAR_HEIGHT } from "./constants";
|
import { FORMULA_BAR_HEIGHT } from "../constants";
|
||||||
import Editor from "./editor/editor";
|
import type { WorkbookState } from "../workbookState";
|
||||||
import type { WorkbookState } from "./workbookState";
|
|
||||||
|
|
||||||
type FormulaBarProps = {
|
type FormulaBarProps = {
|
||||||
cellAddress: string;
|
cellAddress: string;
|
||||||
@@ -18,8 +19,6 @@ type FormulaBarProps = {
|
|||||||
onTextUpdated: () => void;
|
onTextUpdated: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const headerColumnWidth = 35;
|
|
||||||
|
|
||||||
function FormulaBar(properties: FormulaBarProps) {
|
function FormulaBar(properties: FormulaBarProps) {
|
||||||
const {
|
const {
|
||||||
cellAddress,
|
cellAddress,
|
||||||
@@ -99,7 +98,7 @@ const FormulaSymbolButton = styled(StyledButton)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Divider = styled("div")`
|
const Divider = styled("div")`
|
||||||
background-color: #e0e0e0;
|
background-color: ${theme.palette.grey["300"]};
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
@@ -127,14 +126,13 @@ const Container = styled("div")`
|
|||||||
|
|
||||||
const AddressContainer = styled("div")`
|
const AddressContainer = styled("div")`
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
color: #333;
|
color: ${theme.palette.common.black};
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
flex-grow: row;
|
flex-grow: row;
|
||||||
min-width: ${headerColumnWidth}px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CellBarAddress = styled("div")`
|
const CellBarAddress = styled("div")`
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { BookOpen, Plus, X } from "lucide-react";
|
import { BookOpen, PackageOpen, Plus, X } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import NamedRangeActive from "./NamedRangeActive";
|
import NamedRangeActive from "./NamedRangeActive";
|
||||||
@@ -79,65 +79,79 @@ function NameManagerDialog(properties: NameManagerDialogProperties) {
|
|||||||
</Cross>
|
</Cross>
|
||||||
</StyledDialogTitle>
|
</StyledDialogTitle>
|
||||||
<StyledDialogContent>
|
<StyledDialogContent>
|
||||||
<StyledRangesHeader>
|
{(definedNameList.length > 0 || editingNameIndex !== -2) && (
|
||||||
<StyledBox>{t("name_manager_dialog.name")}</StyledBox>
|
<StyledRangesHeader>
|
||||||
<StyledBox>{t("name_manager_dialog.range")}</StyledBox>
|
<StyledBox>{t("name_manager_dialog.name")}</StyledBox>
|
||||||
<StyledBox>{t("name_manager_dialog.scope")}</StyledBox>
|
<StyledBox>{t("name_manager_dialog.range")}</StyledBox>
|
||||||
</StyledRangesHeader>
|
<StyledBox>{t("name_manager_dialog.scope")}</StyledBox>
|
||||||
<NameListWrapper>
|
</StyledRangesHeader>
|
||||||
{definedNameList.map((definedName, index) => {
|
)}
|
||||||
const scopeName = definedName.scope
|
{definedNameList.length === 0 && editingNameIndex === -2 ? (
|
||||||
? worksheets[definedName.scope].name
|
<EmptyStateMessage>
|
||||||
: "[global]";
|
<IconWrapper>
|
||||||
if (index === editingNameIndex) {
|
<PackageOpen />
|
||||||
|
</IconWrapper>
|
||||||
|
{t("name_manager_dialog.empty_message1")}
|
||||||
|
<br />
|
||||||
|
{t("name_manager_dialog.empty_message2")}
|
||||||
|
</EmptyStateMessage>
|
||||||
|
) : (
|
||||||
|
<NameListWrapper>
|
||||||
|
{definedNameList.map((definedName, index) => {
|
||||||
|
const scopeName =
|
||||||
|
definedName.scope !== undefined
|
||||||
|
? worksheets[definedName.scope].name
|
||||||
|
: "[global]";
|
||||||
|
if (index === editingNameIndex) {
|
||||||
|
return (
|
||||||
|
<NamedRangeActive
|
||||||
|
worksheets={worksheets}
|
||||||
|
name={definedName.name}
|
||||||
|
scope={scopeName}
|
||||||
|
formula={definedName.formula}
|
||||||
|
key={definedName.name + definedName.scope}
|
||||||
|
onSave={(
|
||||||
|
newName,
|
||||||
|
newScope,
|
||||||
|
newFormula,
|
||||||
|
): string | undefined => {
|
||||||
|
const scope_index = worksheets.findIndex(
|
||||||
|
(s) => s.name === newScope,
|
||||||
|
);
|
||||||
|
const scope = scope_index >= 0 ? scope_index : undefined;
|
||||||
|
try {
|
||||||
|
updateDefinedName(
|
||||||
|
definedName.name,
|
||||||
|
definedName.scope,
|
||||||
|
newName,
|
||||||
|
scope,
|
||||||
|
newFormula,
|
||||||
|
);
|
||||||
|
setEditingNameIndex(-2);
|
||||||
|
} catch (e) {
|
||||||
|
return `${e}`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => setEditingNameIndex(-2)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<NamedRangeActive
|
<NamedRangeInactive
|
||||||
worksheets={worksheets}
|
|
||||||
name={definedName.name}
|
name={definedName.name}
|
||||||
scope={scopeName}
|
scope={scopeName}
|
||||||
formula={definedName.formula}
|
formula={definedName.formula}
|
||||||
key={definedName.name + definedName.scope}
|
key={definedName.name + definedName.scope}
|
||||||
onSave={(
|
showOptions={editingNameIndex === -2}
|
||||||
newName,
|
onEdit={() => setEditingNameIndex(index)}
|
||||||
newScope,
|
onDelete={() => {
|
||||||
newFormula,
|
deleteDefinedName(definedName.name, definedName.scope);
|
||||||
): string | undefined => {
|
|
||||||
const scope_index = worksheets.findIndex(
|
|
||||||
(s) => s.name === newScope,
|
|
||||||
);
|
|
||||||
const scope = scope_index > 0 ? scope_index : undefined;
|
|
||||||
try {
|
|
||||||
updateDefinedName(
|
|
||||||
definedName.name,
|
|
||||||
definedName.scope,
|
|
||||||
newName,
|
|
||||||
scope,
|
|
||||||
newFormula,
|
|
||||||
);
|
|
||||||
setEditingNameIndex(-2);
|
|
||||||
} catch (e) {
|
|
||||||
return `${e}`;
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onCancel={() => setEditingNameIndex(-2)}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
return (
|
</NameListWrapper>
|
||||||
<NamedRangeInactive
|
)}
|
||||||
name={definedName.name}
|
|
||||||
scope={scopeName}
|
|
||||||
formula={definedName.formula}
|
|
||||||
key={definedName.name + definedName.scope}
|
|
||||||
showOptions={editingNameIndex === -2}
|
|
||||||
onEdit={() => setEditingNameIndex(index)}
|
|
||||||
onDelete={() => {
|
|
||||||
deleteDefinedName(definedName.name, definedName.scope);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</NameListWrapper>
|
|
||||||
{editingNameIndex === -1 && (
|
{editingNameIndex === -1 && (
|
||||||
<NamedRangeActive
|
<NamedRangeActive
|
||||||
worksheets={worksheets}
|
worksheets={worksheets}
|
||||||
@@ -230,6 +244,39 @@ const NameListWrapper = styled(Stack)`
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const EmptyStateMessage = styled(Box)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${theme.palette.grey["600"]};
|
||||||
|
font-family: "Inter";
|
||||||
|
z-index: 0;
|
||||||
|
margin: auto 0px;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconWrapper = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: ${theme.palette.grey["100"]};
|
||||||
|
color: ${theme.palette.grey["600"]};
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledBox = styled(Box)`
|
const StyledBox = styled(Box)`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
142
webapp/IronCalc/src/components/SheetTabBar/SheetDeleteDialog.tsx
Normal file
142
webapp/IronCalc/src/components/SheetTabBar/SheetDeleteDialog.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import styled from "@emotion/styled";
|
||||||
|
import { Button, Dialog } from "@mui/material";
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
|
||||||
|
interface SheetDeleteDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
sheetName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDeleteDialog({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onDelete,
|
||||||
|
sheetName,
|
||||||
|
}: SheetDeleteDialogProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose}>
|
||||||
|
<DialogWrapper>
|
||||||
|
<IconWrapper>
|
||||||
|
<Trash2 />
|
||||||
|
</IconWrapper>
|
||||||
|
<Title>{t("sheet_delete.title")}</Title>
|
||||||
|
<Body>
|
||||||
|
{t("sheet_delete.message", {
|
||||||
|
sheetName,
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
<ButtonGroup>
|
||||||
|
<DeleteButton onClick={onDelete} autoFocus>
|
||||||
|
{t("sheet_delete.confirm")}
|
||||||
|
</DeleteButton>
|
||||||
|
<CancelButton onClick={onClose}>
|
||||||
|
{t("sheet_delete.cancel")}
|
||||||
|
</CancelButton>
|
||||||
|
</ButtonGroup>
|
||||||
|
</DialogWrapper>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogWrapper = styled.div`
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0px 1px 3px 0px ${theme.palette.common.black}1A;
|
||||||
|
width: 280px;
|
||||||
|
max-width: calc(100% - 40px);
|
||||||
|
z-index: 50;
|
||||||
|
font-family: "Inter", sans-serif;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const IconWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: ${theme.palette.error.main}1A;
|
||||||
|
margin: 12px auto 8px auto;
|
||||||
|
color: ${theme.palette.error.main};
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Title = styled.h2`
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: ${theme.palette.grey["900"]};
|
||||||
|
text-align: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Body = styled.p`
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
color: ${theme.palette.grey["900"]};
|
||||||
|
font-size: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ButtonGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled.button`
|
||||||
|
cursor: pointer;
|
||||||
|
color: ${theme.palette.common.white};
|
||||||
|
background-color: ${theme.palette.primary.main};
|
||||||
|
padding: 0px 10px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: background-color 150ms;
|
||||||
|
text-transform: none;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.palette.primary.dark};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DeleteButton = styled(Button)`
|
||||||
|
background-color: ${theme.palette.error.main};
|
||||||
|
color: ${theme.palette.common.white};
|
||||||
|
text-transform: none;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.palette.error.dark};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CancelButton = styled(Button)`
|
||||||
|
background-color: ${theme.palette.grey["200"]};
|
||||||
|
color: ${theme.palette.grey["700"]};
|
||||||
|
text-transform: none;
|
||||||
|
&:hover {
|
||||||
|
background-color: ${theme.palette.grey["300"]};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default SheetDeleteDialog;
|
||||||
@@ -3,9 +3,10 @@ import type { MenuItemProps } from "@mui/material";
|
|||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import ColorPicker from "../colorPicker";
|
import ColorPicker from "../ColorPicker/ColorPicker";
|
||||||
import { isInReferenceMode } from "../editor/util";
|
import { isInReferenceMode } from "../Editor/util";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
|
import SheetDeleteDialog from "./SheetDeleteDialog";
|
||||||
import SheetRenameDialog from "./SheetRenameDialog";
|
import SheetRenameDialog from "./SheetRenameDialog";
|
||||||
|
|
||||||
interface SheetTabProps {
|
interface SheetTabProps {
|
||||||
@@ -25,7 +26,7 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
const { name, color, selected, workbookState, onSelected } = props;
|
const { name, color, selected, workbookState, onSelected } = props;
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
||||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||||
const colorButton = useRef(null);
|
const colorButton = useRef<HTMLDivElement>(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@@ -37,20 +38,31 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
const handleCloseRenameDialog = () => {
|
const handleCloseRenameDialog = () => {
|
||||||
setRenameDialogOpen(false);
|
setRenameDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenRenameDialog = () => {
|
const handleOpenRenameDialog = () => {
|
||||||
setRenameDialogOpen(true);
|
setRenameDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleOpenDeleteDialog = () => {
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDeleteDialog = () => {
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TabWrapper
|
<TabWrapper
|
||||||
$color={color}
|
$color={color}
|
||||||
$selected={selected}
|
$selected={selected}
|
||||||
onClick={(event) => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
onSelected();
|
onSelected();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event: React.PointerEvent) => {
|
||||||
// If it is in browse mode stop he event
|
// If it is in browse mode stop he event
|
||||||
const cell = workbookState.getEditingCell();
|
const cell = workbookState.getEditingCell();
|
||||||
if (cell && isInReferenceMode(cell.text, cell.cursorStart)) {
|
if (cell && isInReferenceMode(cell.text, cell.cursorStart)) {
|
||||||
@@ -97,7 +109,7 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
<StyledMenuItem
|
<StyledMenuItem
|
||||||
disabled={!props.canDelete}
|
disabled={!props.canDelete}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.onDeleted();
|
handleOpenDeleteDialog();
|
||||||
handleClose();
|
handleClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -134,6 +146,15 @@ function SheetTab(props: SheetTabProps) {
|
|||||||
anchorEl={colorButton}
|
anchorEl={colorButton}
|
||||||
open={colorPickerOpen}
|
open={colorPickerOpen}
|
||||||
/>
|
/>
|
||||||
|
<SheetDeleteDialog
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
onClose={handleCloseDeleteDialog}
|
||||||
|
onDelete={() => {
|
||||||
|
props.onDeleted();
|
||||||
|
handleCloseDeleteDialog();
|
||||||
|
}}
|
||||||
|
sheetName={name}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Menu, Plus } from "lucide-react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
|
import { StyledButton } from "../Toolbar/Toolbar";
|
||||||
import { NAVIGATION_HEIGHT } from "../constants";
|
import { NAVIGATION_HEIGHT } from "../constants";
|
||||||
import { StyledButton } from "../toolbar";
|
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
import SheetListMenu from "./SheetListMenu";
|
import SheetListMenu from "./SheetListMenu";
|
||||||
import SheetTab from "./SheetTab";
|
import SheetTab from "./SheetTab";
|
||||||
@@ -123,6 +123,10 @@ const Container = styled("div")`
|
|||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
background-color: ${theme.palette.common.white};
|
background-color: ${theme.palette.common.white};
|
||||||
border-top: 1px solid ${theme.palette.grey["300"]};
|
border-top: 1px solid ${theme.palette.grey["300"]};
|
||||||
|
@media (max-width: 769px) {
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Sheets = styled("div")`
|
const Sheets = styled("div")`
|
||||||
@@ -152,7 +156,7 @@ const Advert = styled("a")`
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
@media (max-width: 769px) {
|
@media (max-width: 769px) {
|
||||||
height: 100%;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -161,6 +165,9 @@ const LeftButtonsContainer = styled("div")`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
|
@media (max-width: 769px) {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const VerticalDivider = styled("div")`
|
const VerticalDivider = styled("div")`
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import type {
|
|||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
import type {} from "@mui/system";
|
import type {} from "@mui/system";
|
||||||
import {
|
import {
|
||||||
|
AArrowDown,
|
||||||
|
AArrowUp,
|
||||||
AlignCenter,
|
AlignCenter,
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
AlignRight,
|
AlignRight,
|
||||||
@@ -18,11 +20,13 @@ import {
|
|||||||
Grid2X2,
|
Grid2X2,
|
||||||
Grid2x2Check,
|
Grid2x2Check,
|
||||||
Grid2x2X,
|
Grid2x2X,
|
||||||
|
ImageDown,
|
||||||
Italic,
|
Italic,
|
||||||
PaintBucket,
|
PaintBucket,
|
||||||
PaintRoller,
|
PaintRoller,
|
||||||
Percent,
|
Percent,
|
||||||
Redo2,
|
Redo2,
|
||||||
|
RemoveFormatting,
|
||||||
Strikethrough,
|
Strikethrough,
|
||||||
Tags,
|
Tags,
|
||||||
Type,
|
Type,
|
||||||
@@ -35,19 +39,19 @@ import {
|
|||||||
ArrowMiddleFromLine,
|
ArrowMiddleFromLine,
|
||||||
DecimalPlacesDecreaseIcon,
|
DecimalPlacesDecreaseIcon,
|
||||||
DecimalPlacesIncreaseIcon,
|
DecimalPlacesIncreaseIcon,
|
||||||
} from "../icons";
|
} from "../../icons";
|
||||||
import { theme } from "../theme";
|
import { theme } from "../../theme";
|
||||||
import NameManagerDialog from "./NameManagerDialog";
|
import BorderPicker from "../BorderPicker/BorderPicker";
|
||||||
import type { NameManagerProperties } from "./NameManagerDialog/NameManagerDialog";
|
import ColorPicker from "../ColorPicker/ColorPicker";
|
||||||
import BorderPicker from "./borderPicker";
|
import FormatMenu from "../FormatMenu/FormatMenu";
|
||||||
import ColorPicker from "./colorPicker";
|
|
||||||
import { TOOLBAR_HEIGHT } from "./constants";
|
|
||||||
import FormatMenu from "./formatMenu";
|
|
||||||
import {
|
import {
|
||||||
NumberFormats,
|
NumberFormats,
|
||||||
decreaseDecimalPlaces,
|
decreaseDecimalPlaces,
|
||||||
increaseDecimalPlaces,
|
increaseDecimalPlaces,
|
||||||
} from "./formatUtil";
|
} from "../FormatMenu/formatUtil";
|
||||||
|
import NameManagerDialog from "../NameManagerDialog";
|
||||||
|
import type { NameManagerProperties } from "../NameManagerDialog/NameManagerDialog";
|
||||||
|
import { TOOLBAR_HEIGHT } from "../constants";
|
||||||
|
|
||||||
type ToolbarProperties = {
|
type ToolbarProperties = {
|
||||||
canUndo: boolean;
|
canUndo: boolean;
|
||||||
@@ -65,6 +69,9 @@ type ToolbarProperties = {
|
|||||||
onFillColorPicked: (hex: string) => void;
|
onFillColorPicked: (hex: string) => void;
|
||||||
onNumberFormatPicked: (numberFmt: string) => void;
|
onNumberFormatPicked: (numberFmt: string) => void;
|
||||||
onBorderChanged: (border: BorderOptions) => void;
|
onBorderChanged: (border: BorderOptions) => void;
|
||||||
|
onClearFormatting: () => void;
|
||||||
|
onIncreaseFontSize: (delta: number) => void;
|
||||||
|
onDownloadPNG: () => void;
|
||||||
fillColor: string;
|
fillColor: string;
|
||||||
fontColor: string;
|
fontColor: string;
|
||||||
bold: boolean;
|
bold: boolean;
|
||||||
@@ -246,6 +253,28 @@ function Toolbar(properties: ToolbarProperties) {
|
|||||||
>
|
>
|
||||||
<Type />
|
<Type />
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
type="button"
|
||||||
|
$pressed={false}
|
||||||
|
disabled={!canEdit}
|
||||||
|
onClick={() => {
|
||||||
|
properties.onIncreaseFontSize(1);
|
||||||
|
}}
|
||||||
|
title={t("toolbar.increase_font_size")}
|
||||||
|
>
|
||||||
|
<AArrowUp />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
type="button"
|
||||||
|
$pressed={false}
|
||||||
|
disabled={!canEdit}
|
||||||
|
onClick={() => {
|
||||||
|
properties.onIncreaseFontSize(-1);
|
||||||
|
}}
|
||||||
|
title={t("toolbar.decrease_font_size")}
|
||||||
|
>
|
||||||
|
<AArrowDown />
|
||||||
|
</StyledButton>
|
||||||
<StyledButton
|
<StyledButton
|
||||||
type="button"
|
type="button"
|
||||||
$pressed={false}
|
$pressed={false}
|
||||||
@@ -360,6 +389,30 @@ function Toolbar(properties: ToolbarProperties) {
|
|||||||
<Tags />
|
<Tags />
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
<StyledButton
|
||||||
|
type="button"
|
||||||
|
$pressed={false}
|
||||||
|
disabled={!canEdit}
|
||||||
|
onClick={() => {
|
||||||
|
properties.onClearFormatting();
|
||||||
|
}}
|
||||||
|
title={t("toolbar.clear_formatting")}
|
||||||
|
>
|
||||||
|
<RemoveFormatting />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
type="button"
|
||||||
|
$pressed={false}
|
||||||
|
disabled={!canEdit}
|
||||||
|
onClick={() => {
|
||||||
|
properties.onDownloadPNG();
|
||||||
|
}}
|
||||||
|
title={t("toolbar.selected_png")}
|
||||||
|
>
|
||||||
|
<ImageDown />
|
||||||
|
</StyledButton>
|
||||||
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={properties.fontColor}
|
color={properties.fontColor}
|
||||||
onChange={(color): void => {
|
onChange={(color): void => {
|
||||||
@@ -6,30 +6,35 @@ import type {
|
|||||||
} from "@ironcalc/wasm";
|
} from "@ironcalc/wasm";
|
||||||
import { styled } from "@mui/material/styles";
|
import { styled } from "@mui/material/styles";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import SheetTabBar from "./SheetTabBar/SheetTabBar";
|
import FormulaBar from "../FormulaBar/FormulaBar";
|
||||||
|
import SheetTabBar from "../SheetTabBar";
|
||||||
|
import Toolbar from "../Toolbar/Toolbar";
|
||||||
|
import Worksheet from "../Worksheet/Worksheet";
|
||||||
import {
|
import {
|
||||||
COLUMN_WIDTH_SCALE,
|
COLUMN_WIDTH_SCALE,
|
||||||
LAST_COLUMN,
|
LAST_COLUMN,
|
||||||
ROW_HEIGH_SCALE,
|
ROW_HEIGH_SCALE,
|
||||||
} from "./WorksheetCanvas/constants";
|
} from "../WorksheetCanvas/constants";
|
||||||
|
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
|
||||||
|
import { devicePixelRatio } from "../WorksheetCanvas/worksheetCanvas";
|
||||||
import {
|
import {
|
||||||
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
CLIPBOARD_ID_SESSION_STORAGE_KEY,
|
||||||
getNewClipboardId,
|
getNewClipboardId,
|
||||||
} from "./clipboard";
|
} from "../clipboard";
|
||||||
import FormulaBar from "./formulabar";
|
|
||||||
import Toolbar from "./toolbar";
|
|
||||||
import useKeyboardNavigation from "./useKeyboardNavigation";
|
|
||||||
import {
|
import {
|
||||||
type NavigationKey,
|
type NavigationKey,
|
||||||
getCellAddress,
|
getCellAddress,
|
||||||
getFullRangeToString,
|
getFullRangeToString,
|
||||||
} from "./util";
|
} from "../util";
|
||||||
import type { WorkbookState } from "./workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
import Worksheet from "./worksheet";
|
import useKeyboardNavigation from "./useKeyboardNavigation";
|
||||||
|
|
||||||
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
||||||
const { model, workbookState } = props;
|
const { model, workbookState } = props;
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const worksheetRef = useRef<{
|
||||||
|
getCanvas: () => WorksheetCanvas | null;
|
||||||
|
}>(null);
|
||||||
|
|
||||||
// Calling `setRedrawId((id) => id + 1);` forces a redraw
|
// Calling `setRedrawId((id) => id + 1);` forces a redraw
|
||||||
// This is needed because `model` or `workbookState` can change without React being aware of it
|
// This is needed because `model` or `workbookState` can change without React being aware of it
|
||||||
@@ -119,6 +124,10 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
updateRangeStyle("num_fmt", numberFmt);
|
updateRangeStyle("num_fmt", numberFmt);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onIncreaseFontSize = (delta: number) => {
|
||||||
|
updateRangeStyle("font.size_delta", `${delta}`);
|
||||||
|
};
|
||||||
|
|
||||||
const onCopyStyles = () => {
|
const onCopyStyles = () => {
|
||||||
const {
|
const {
|
||||||
sheet,
|
sheet,
|
||||||
@@ -222,17 +231,17 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
},
|
},
|
||||||
onBold: () => {
|
onBold: () => {
|
||||||
const { sheet, row, column } = model.getSelectedView();
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
const value = !model.getCellStyle(sheet, row, column).font.b;
|
const value = model.getCellStyle(sheet, row, column).font.b;
|
||||||
onToggleBold(!value);
|
onToggleBold(!value);
|
||||||
},
|
},
|
||||||
onItalic: () => {
|
onItalic: () => {
|
||||||
const { sheet, row, column } = model.getSelectedView();
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
const value = !model.getCellStyle(sheet, row, column).font.i;
|
const value = model.getCellStyle(sheet, row, column).font.i;
|
||||||
onToggleItalic(!value);
|
onToggleItalic(!value);
|
||||||
},
|
},
|
||||||
onUnderline: () => {
|
onUnderline: () => {
|
||||||
const { sheet, row, column } = model.getSelectedView();
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
const value = !model.getCellStyle(sheet, row, column).font.u;
|
const value = model.getCellStyle(sheet, row, column).font.u;
|
||||||
onToggleUnderline(!value);
|
onToggleUnderline(!value);
|
||||||
},
|
},
|
||||||
onNavigationToEdge: (direction: NavigationKey): void => {
|
onNavigationToEdge: (direction: NavigationKey): void => {
|
||||||
@@ -350,7 +359,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(event) => {
|
onClick={(event: React.MouseEvent) => {
|
||||||
if (!workbookState.getEditingCell()) {
|
if (!workbookState.getEditingCell()) {
|
||||||
focusWorkbook();
|
focusWorkbook();
|
||||||
} else {
|
} else {
|
||||||
@@ -527,6 +536,76 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
onTextColorPicked={onTextColorPicked}
|
onTextColorPicked={onTextColorPicked}
|
||||||
onFillColorPicked={onFillColorPicked}
|
onFillColorPicked={onFillColorPicked}
|
||||||
onNumberFormatPicked={onNumberFormatPicked}
|
onNumberFormatPicked={onNumberFormatPicked}
|
||||||
|
onClearFormatting={() => {
|
||||||
|
const {
|
||||||
|
sheet,
|
||||||
|
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||||
|
} = model.getSelectedView();
|
||||||
|
model.rangeClearFormatting(
|
||||||
|
sheet,
|
||||||
|
rowStart,
|
||||||
|
columnStart,
|
||||||
|
rowEnd,
|
||||||
|
columnEnd,
|
||||||
|
);
|
||||||
|
setRedrawId((id) => id + 1);
|
||||||
|
}}
|
||||||
|
onIncreaseFontSize={(delta: number) => {
|
||||||
|
onIncreaseFontSize(delta);
|
||||||
|
}}
|
||||||
|
onDownloadPNG={() => {
|
||||||
|
// creates a new canvas element in the visible part of the the selected area
|
||||||
|
const worksheetCanvas = worksheetRef.current?.getCanvas();
|
||||||
|
if (!worksheetCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||||
|
} = model.getSelectedView();
|
||||||
|
const { topLeftCell, bottomRightCell } =
|
||||||
|
worksheetCanvas.getVisibleCells();
|
||||||
|
const firstRow = Math.max(rowStart, topLeftCell.row);
|
||||||
|
const firstColumn = Math.max(columnStart, topLeftCell.column);
|
||||||
|
const lastRow = Math.min(rowEnd, bottomRightCell.row);
|
||||||
|
const lastColumn = Math.min(columnEnd, bottomRightCell.column);
|
||||||
|
let [x, y] = worksheetCanvas.getCoordinatesByCell(
|
||||||
|
firstRow,
|
||||||
|
firstColumn,
|
||||||
|
);
|
||||||
|
const [x1, y1] = worksheetCanvas.getCoordinatesByCell(
|
||||||
|
lastRow + 1,
|
||||||
|
lastColumn + 1,
|
||||||
|
);
|
||||||
|
const width = (x1 - x) * devicePixelRatio;
|
||||||
|
const height = (y1 - y) * devicePixelRatio;
|
||||||
|
x *= devicePixelRatio;
|
||||||
|
y *= devicePixelRatio;
|
||||||
|
|
||||||
|
const capturedCanvas = document.createElement("canvas");
|
||||||
|
capturedCanvas.width = width;
|
||||||
|
capturedCanvas.height = height;
|
||||||
|
const ctx = capturedCanvas.getContext("2d");
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(
|
||||||
|
worksheetCanvas.canvas,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadLink = document.createElement("a");
|
||||||
|
downloadLink.href = capturedCanvas.toDataURL("image/png");
|
||||||
|
downloadLink.download = "ironcalc.png";
|
||||||
|
downloadLink.click();
|
||||||
|
}}
|
||||||
onBorderChanged={(border: BorderOptions): void => {
|
onBorderChanged={(border: BorderOptions): void => {
|
||||||
const {
|
const {
|
||||||
sheet,
|
sheet,
|
||||||
@@ -619,6 +698,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
refresh={(): void => {
|
refresh={(): void => {
|
||||||
setRedrawId((id) => id + 1);
|
setRedrawId((id) => id + 1);
|
||||||
}}
|
}}
|
||||||
|
ref={worksheetRef}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SheetTabBar
|
<SheetTabBar
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type KeyboardEvent, type RefObject, useCallback } from "react";
|
import { type KeyboardEvent, type RefObject, useCallback } from "react";
|
||||||
import { type NavigationKey, isEditingKey, isNavigationKey } from "./util";
|
import { type NavigationKey, isEditingKey, isNavigationKey } from "../util";
|
||||||
|
|
||||||
export enum Border {
|
export enum Border {
|
||||||
Top = "top",
|
Top = "top",
|
||||||
@@ -32,7 +32,7 @@ interface Options {
|
|||||||
onNextSheet: () => void;
|
onNextSheet: () => void;
|
||||||
onPreviousSheet: () => void;
|
onPreviousSheet: () => void;
|
||||||
onEscape: () => void;
|
onEscape: () => void;
|
||||||
root: RefObject<HTMLDivElement>;
|
root: RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// # IronCalc Keyboard accessibility:
|
// # IronCalc Keyboard accessibility:
|
||||||
289
webapp/IronCalc/src/components/Worksheet/CellContextMenu.tsx
Normal file
289
webapp/IronCalc/src/components/Worksheet/CellContextMenu.tsx
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import { Menu, MenuItem, styled } from "@mui/material";
|
||||||
|
import {
|
||||||
|
BetweenHorizontalStart,
|
||||||
|
BetweenVerticalStart,
|
||||||
|
ChevronRight,
|
||||||
|
Snowflake,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { theme } from "../../theme";
|
||||||
|
|
||||||
|
const red_color = theme.palette.error.main;
|
||||||
|
|
||||||
|
interface CellContextMenuProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
anchorEl: HTMLDivElement | null;
|
||||||
|
onInsertRowAbove: () => void;
|
||||||
|
onInsertRowBelow: () => void;
|
||||||
|
onInsertColumnLeft: () => void;
|
||||||
|
onInsertColumnRight: () => void;
|
||||||
|
onFreezeColumns: () => void;
|
||||||
|
onFreezeRows: () => void;
|
||||||
|
onUnfreezeColumns: () => void;
|
||||||
|
onUnfreezeRows: () => void;
|
||||||
|
onDeleteRow: () => void;
|
||||||
|
onDeleteColumn: () => void;
|
||||||
|
row: number;
|
||||||
|
column: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CellContextMenu = (properties: CellContextMenuProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
anchorEl,
|
||||||
|
onInsertRowAbove,
|
||||||
|
onInsertRowBelow,
|
||||||
|
onInsertColumnLeft,
|
||||||
|
onInsertColumnRight,
|
||||||
|
onFreezeColumns,
|
||||||
|
onFreezeRows,
|
||||||
|
onUnfreezeColumns,
|
||||||
|
onUnfreezeRows,
|
||||||
|
onDeleteRow,
|
||||||
|
onDeleteColumn,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
} = properties;
|
||||||
|
const [freezeMenuOpen, setFreezeMenuOpen] = useState(false);
|
||||||
|
const freezeRef = useRef(null);
|
||||||
|
|
||||||
|
const [insertRowMenuOpen, setInsertRowMenuOpen] = useState(false);
|
||||||
|
const insertRowRef = useRef(null);
|
||||||
|
|
||||||
|
const [insertColumnMenuOpen, setInsertColumnMenuOpen] = useState(false);
|
||||||
|
const insertColumnRef = useRef(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledMenu
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: 6,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledMenuItem
|
||||||
|
ref={insertColumnRef}
|
||||||
|
onClick={() => setInsertColumnMenuOpen(true)}
|
||||||
|
>
|
||||||
|
<BetweenVerticalStartStyled />
|
||||||
|
<ItemNameStyled>{t("cell_context.insert_column")}</ItemNameStyled>
|
||||||
|
<ChevronRightStyled />
|
||||||
|
</StyledMenuItem>
|
||||||
|
<StyledMenuItem
|
||||||
|
ref={insertRowRef}
|
||||||
|
onClick={() => setInsertRowMenuOpen(true)}
|
||||||
|
>
|
||||||
|
<BetweenHorizontalStartStyled />
|
||||||
|
<ItemNameStyled>{t("cell_context.insert_row")}</ItemNameStyled>
|
||||||
|
<ChevronRightStyled />
|
||||||
|
</StyledMenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
<StyledMenuItem ref={freezeRef} onClick={() => setFreezeMenuOpen(true)}>
|
||||||
|
<StyledSnowflake />
|
||||||
|
<ItemNameStyled>{t("cell_context.freeze")}</ItemNameStyled>
|
||||||
|
<ChevronRightStyled />
|
||||||
|
</StyledMenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
<StyledMenuItem onClick={onDeleteRow}>
|
||||||
|
<StyledTrash />
|
||||||
|
<ItemNameStyled style={{ color: red_color }}>
|
||||||
|
{t("cell_context.delete_row", { row })}
|
||||||
|
</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
<StyledMenuItem onClick={onDeleteColumn}>
|
||||||
|
<StyledTrash />
|
||||||
|
<ItemNameStyled style={{ color: red_color }}>
|
||||||
|
{t("cell_context.delete_column", { column })}
|
||||||
|
</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
</StyledMenu>
|
||||||
|
<StyledMenu
|
||||||
|
open={insertRowMenuOpen}
|
||||||
|
onClose={() => setInsertRowMenuOpen(false)}
|
||||||
|
anchorEl={insertRowRef.current}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setInsertRowMenuOpen(false);
|
||||||
|
onInsertRowAbove();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>{t("cell_context.insert_row_above")}</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setInsertRowMenuOpen(false);
|
||||||
|
onInsertRowBelow();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>{t("cell_context.insert_row_below")}</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
</StyledMenu>
|
||||||
|
<StyledMenu
|
||||||
|
open={insertColumnMenuOpen}
|
||||||
|
onClose={() => setInsertColumnMenuOpen(false)}
|
||||||
|
anchorEl={insertColumnRef.current}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setInsertColumnMenuOpen(false);
|
||||||
|
onInsertColumnLeft();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>
|
||||||
|
{t("cell_context.insert_column_before")}
|
||||||
|
</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
setInsertColumnMenuOpen(false);
|
||||||
|
onInsertColumnRight();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>
|
||||||
|
{t("cell_context.insert_column_after")}
|
||||||
|
</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
</StyledMenu>
|
||||||
|
<StyledMenu
|
||||||
|
open={freezeMenuOpen}
|
||||||
|
onClose={() => setFreezeMenuOpen(false)}
|
||||||
|
anchorEl={freezeRef.current}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onFreezeColumns();
|
||||||
|
setFreezeMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>
|
||||||
|
{t("cell_context.freeze_columns", { column })}
|
||||||
|
</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onFreezeRows();
|
||||||
|
setFreezeMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>
|
||||||
|
{t("cell_context.freeze_rows", { row })}
|
||||||
|
</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onUnfreezeColumns();
|
||||||
|
setFreezeMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>{t("cell_context.unfreeze_columns")}</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
<StyledMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onUnfreezeRows();
|
||||||
|
setFreezeMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ItemNameStyled>{t("cell_context.unfreeze_rows")}</ItemNameStyled>
|
||||||
|
</StyledMenuItem>
|
||||||
|
</StyledMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const BetweenVerticalStartStyled = styled(BetweenVerticalStart)`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: ${theme.palette.grey[900]};
|
||||||
|
padding-right: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BetweenHorizontalStartStyled = styled(BetweenHorizontalStart)`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: ${theme.palette.grey[900]};
|
||||||
|
padding-right: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSnowflake = styled(Snowflake)`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: ${theme.palette.grey[900]};
|
||||||
|
padding-right: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTrash = styled(Trash2)`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: ${red_color};
|
||||||
|
padding-right: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMenu = styled(Menu)({
|
||||||
|
"& .MuiPaper-root": {
|
||||||
|
borderRadius: 8,
|
||||||
|
paddingTop: 4,
|
||||||
|
paddingBottom: 4,
|
||||||
|
},
|
||||||
|
"& .MuiList-padding": {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledMenuItem = styled(MenuItem)`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
font-size: 14px;
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
min-width: 172px;
|
||||||
|
margin: 0px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
height: 32px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MenuDivider = styled("div")`
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-top: 1px solid ${theme.palette.grey[200]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ItemNameStyled = styled("div")`
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${theme.palette.grey[900]};
|
||||||
|
flex-grow: 2;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ChevronRightStyled = styled(ChevronRight)`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default CellContextMenu;
|
||||||
676
webapp/IronCalc/src/components/Worksheet/Worksheet.tsx
Normal file
676
webapp/IronCalc/src/components/Worksheet/Worksheet.tsx
Normal file
@@ -0,0 +1,676 @@
|
|||||||
|
import { type Model, columnNameFromNumber } from "@ironcalc/wasm";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useLayoutEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import Editor from "../Editor/Editor";
|
||||||
|
import {
|
||||||
|
COLUMN_WIDTH_SCALE,
|
||||||
|
LAST_COLUMN,
|
||||||
|
LAST_ROW,
|
||||||
|
ROW_HEIGH_SCALE,
|
||||||
|
outlineBackgroundColor,
|
||||||
|
outlineColor,
|
||||||
|
} from "../WorksheetCanvas/constants";
|
||||||
|
import WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
|
||||||
|
import {
|
||||||
|
FORMULA_BAR_HEIGHT,
|
||||||
|
NAVIGATION_HEIGHT,
|
||||||
|
TOOLBAR_HEIGHT,
|
||||||
|
} from "../constants";
|
||||||
|
import type { Cell } from "../types";
|
||||||
|
import { AreaType, type WorkbookState } from "../workbookState";
|
||||||
|
import CellContextMenu from "./CellContextMenu";
|
||||||
|
import usePointer from "./usePointer";
|
||||||
|
|
||||||
|
function useWindowSize() {
|
||||||
|
const [size, setSize] = useState([0, 0]);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
function updateSize() {
|
||||||
|
setSize([window.innerWidth, window.innerHeight]);
|
||||||
|
}
|
||||||
|
window.addEventListener("resize", updateSize);
|
||||||
|
updateSize();
|
||||||
|
return () => window.removeEventListener("resize", updateSize);
|
||||||
|
}, []);
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Worksheet = forwardRef(
|
||||||
|
(
|
||||||
|
props: {
|
||||||
|
model: Model;
|
||||||
|
workbookState: WorkbookState;
|
||||||
|
refresh: () => void;
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const canvasElement = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
const worksheetElement = useRef<HTMLDivElement>(null);
|
||||||
|
const scrollElement = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const editorElement = useRef<HTMLDivElement>(null);
|
||||||
|
const spacerElement = useRef<HTMLDivElement>(null);
|
||||||
|
const cellOutline = useRef<HTMLDivElement>(null);
|
||||||
|
const areaOutline = useRef<HTMLDivElement>(null);
|
||||||
|
const cellOutlineHandle = useRef<HTMLDivElement>(null);
|
||||||
|
const extendToOutline = useRef<HTMLDivElement>(null);
|
||||||
|
const columnResizeGuide = useRef<HTMLDivElement>(null);
|
||||||
|
const rowResizeGuide = useRef<HTMLDivElement>(null);
|
||||||
|
const columnHeaders = useRef<HTMLDivElement>(null);
|
||||||
|
const worksheetCanvas = useRef<WorksheetCanvas | null>(null);
|
||||||
|
|
||||||
|
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
const ignoreScrollEventRef = useRef(false);
|
||||||
|
|
||||||
|
const { model, workbookState, refresh } = props;
|
||||||
|
const [clientWidth, clientHeight] = useWindowSize();
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getCanvas: () => worksheetCanvas.current,
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvasRef = canvasElement.current;
|
||||||
|
const columnGuideRef = columnResizeGuide.current;
|
||||||
|
const rowGuideRef = rowResizeGuide.current;
|
||||||
|
const columnHeadersRef = columnHeaders.current;
|
||||||
|
const worksheetRef = worksheetElement.current;
|
||||||
|
|
||||||
|
const outline = cellOutline.current;
|
||||||
|
const handle = cellOutlineHandle.current;
|
||||||
|
const area = areaOutline.current;
|
||||||
|
const extendTo = extendToOutline.current;
|
||||||
|
const editor = editorElement.current;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!canvasRef ||
|
||||||
|
!columnGuideRef ||
|
||||||
|
!rowGuideRef ||
|
||||||
|
!columnHeadersRef ||
|
||||||
|
!worksheetRef ||
|
||||||
|
!outline ||
|
||||||
|
!handle ||
|
||||||
|
!area ||
|
||||||
|
!extendTo ||
|
||||||
|
!scrollElement.current ||
|
||||||
|
!editor
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
// FIXME: This two need to be computed.
|
||||||
|
model.setWindowWidth(clientWidth - 37);
|
||||||
|
model.setWindowHeight(clientHeight - 190);
|
||||||
|
const canvas = new WorksheetCanvas({
|
||||||
|
width: worksheetRef.clientWidth,
|
||||||
|
height: worksheetRef.clientHeight,
|
||||||
|
model,
|
||||||
|
workbookState,
|
||||||
|
elements: {
|
||||||
|
canvas: canvasRef,
|
||||||
|
columnGuide: columnGuideRef,
|
||||||
|
rowGuide: rowGuideRef,
|
||||||
|
columnHeaders: columnHeadersRef,
|
||||||
|
cellOutline: outline,
|
||||||
|
cellOutlineHandle: handle,
|
||||||
|
areaOutline: area,
|
||||||
|
extendToOutline: extendTo,
|
||||||
|
editor: editor,
|
||||||
|
},
|
||||||
|
onColumnWidthChanges(sheet, column, width) {
|
||||||
|
if (width < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { range } = model.getSelectedView();
|
||||||
|
let columnStart = column;
|
||||||
|
let columnEnd = column;
|
||||||
|
const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
|
||||||
|
const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
|
||||||
|
if (
|
||||||
|
fullColumn &&
|
||||||
|
column >= range[1] &&
|
||||||
|
column <= range[3] &&
|
||||||
|
!fullRow
|
||||||
|
) {
|
||||||
|
columnStart = Math.min(range[1], column, range[3]);
|
||||||
|
columnEnd = Math.max(range[1], column, range[3]);
|
||||||
|
}
|
||||||
|
model.setColumnsWidth(sheet, columnStart, columnEnd, width);
|
||||||
|
worksheetCanvas.current?.renderSheet();
|
||||||
|
},
|
||||||
|
onRowHeightChanges(sheet, row, height) {
|
||||||
|
if (height < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { range } = model.getSelectedView();
|
||||||
|
let rowStart = row;
|
||||||
|
let rowEnd = row;
|
||||||
|
const fullColumn = range[0] === 1 && range[2] === LAST_ROW;
|
||||||
|
const fullRow = range[1] === 1 && range[3] === LAST_COLUMN;
|
||||||
|
if (fullRow && row >= range[0] && row <= range[2] && !fullColumn) {
|
||||||
|
rowStart = Math.min(range[0], row, range[2]);
|
||||||
|
rowEnd = Math.max(range[0], row, range[2]);
|
||||||
|
}
|
||||||
|
model.setRowsHeight(sheet, rowStart, rowEnd, height);
|
||||||
|
worksheetCanvas.current?.renderSheet();
|
||||||
|
},
|
||||||
|
refresh,
|
||||||
|
});
|
||||||
|
const scrollX = model.getScrollX();
|
||||||
|
const scrollY = model.getScrollY();
|
||||||
|
const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000];
|
||||||
|
if (spacerElement.current) {
|
||||||
|
spacerElement.current.style.height = `${sheetHeight}px`;
|
||||||
|
spacerElement.current.style.width = `${sheetWidth}px`;
|
||||||
|
}
|
||||||
|
const left = scrollElement.current.scrollLeft;
|
||||||
|
const top = scrollElement.current.scrollTop;
|
||||||
|
if (scrollX !== left) {
|
||||||
|
ignoreScrollEventRef.current = true;
|
||||||
|
scrollElement.current.scrollLeft = scrollX;
|
||||||
|
setTimeout(() => {
|
||||||
|
ignoreScrollEventRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollY !== top) {
|
||||||
|
ignoreScrollEventRef.current = true;
|
||||||
|
scrollElement.current.scrollTop = scrollY;
|
||||||
|
setTimeout(() => {
|
||||||
|
ignoreScrollEventRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.renderSheet();
|
||||||
|
worksheetCanvas.current = canvas;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { onPointerMove, onPointerDown, onPointerHandleDown, onPointerUp } =
|
||||||
|
usePointer({
|
||||||
|
model,
|
||||||
|
workbookState,
|
||||||
|
refresh,
|
||||||
|
onColumnSelected: (column: number, shift: boolean) => {
|
||||||
|
let firstColumn = column;
|
||||||
|
let lastColumn = column;
|
||||||
|
if (shift) {
|
||||||
|
const { range } = model.getSelectedView();
|
||||||
|
firstColumn = Math.min(range[1], column, range[3]);
|
||||||
|
lastColumn = Math.max(range[3], column, range[1]);
|
||||||
|
}
|
||||||
|
model.setSelectedCell(1, firstColumn);
|
||||||
|
model.setSelectedRange(1, firstColumn, LAST_ROW, lastColumn);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onRowSelected: (row: number, shift: boolean) => {
|
||||||
|
let firstRow = row;
|
||||||
|
let lastRow = row;
|
||||||
|
if (shift) {
|
||||||
|
const { range } = model.getSelectedView();
|
||||||
|
firstRow = Math.min(range[0], row, range[2]);
|
||||||
|
lastRow = Math.max(range[2], row, range[0]);
|
||||||
|
}
|
||||||
|
model.setSelectedCell(firstRow, 1);
|
||||||
|
model.setSelectedRange(firstRow, 1, lastRow, LAST_COLUMN);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onAllSheetSelected: () => {
|
||||||
|
model.setSelectedCell(1, 1);
|
||||||
|
model.setSelectedRange(1, 1, LAST_ROW, LAST_COLUMN);
|
||||||
|
},
|
||||||
|
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
model.setSelectedCell(cell.row, cell.column);
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onAreaSelecting: (cell: Cell) => {
|
||||||
|
const canvas = worksheetCanvas.current;
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { row, column } = cell;
|
||||||
|
model.onAreaSelecting(row, column);
|
||||||
|
canvas.renderSheet();
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onAreaSelected: () => {
|
||||||
|
const styles = workbookState.getCopyStyles();
|
||||||
|
if (styles?.length) {
|
||||||
|
model.onPasteStyles(styles);
|
||||||
|
const canvas = worksheetCanvas.current;
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.renderSheet();
|
||||||
|
}
|
||||||
|
workbookState.setCopyStyles(null);
|
||||||
|
if (worksheetElement.current) {
|
||||||
|
worksheetElement.current.style.cursor = "auto";
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
},
|
||||||
|
onExtendToCell: (cell) => {
|
||||||
|
const canvas = worksheetCanvas.current;
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { row, column } = cell;
|
||||||
|
const {
|
||||||
|
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||||
|
} = model.getSelectedView();
|
||||||
|
// We are either extending by rows or by columns
|
||||||
|
// And we could be doing it in the positive direction (downwards or right)
|
||||||
|
// or the negative direction (upwards or left)
|
||||||
|
|
||||||
|
if (
|
||||||
|
row > rowEnd &&
|
||||||
|
((column <= columnEnd && column >= columnStart) ||
|
||||||
|
(column < columnStart && columnStart - column < row - rowEnd) ||
|
||||||
|
(column > columnEnd && column - columnEnd < row - rowEnd))
|
||||||
|
) {
|
||||||
|
// rows downwards
|
||||||
|
const area = {
|
||||||
|
type: AreaType.rowsDown,
|
||||||
|
rowStart: rowEnd + 1,
|
||||||
|
rowEnd: row,
|
||||||
|
columnStart,
|
||||||
|
columnEnd,
|
||||||
|
};
|
||||||
|
workbookState.setExtendToArea(area);
|
||||||
|
canvas.renderSheet();
|
||||||
|
} else if (
|
||||||
|
row < rowStart &&
|
||||||
|
((column <= columnEnd && column >= columnStart) ||
|
||||||
|
(column < columnStart && columnStart - column < rowStart - row) ||
|
||||||
|
(column > columnEnd && column - columnEnd < rowStart - row))
|
||||||
|
) {
|
||||||
|
// rows upwards
|
||||||
|
const area = {
|
||||||
|
type: AreaType.rowsUp,
|
||||||
|
rowStart: row,
|
||||||
|
rowEnd: rowStart,
|
||||||
|
columnStart,
|
||||||
|
columnEnd,
|
||||||
|
};
|
||||||
|
workbookState.setExtendToArea(area);
|
||||||
|
canvas.renderSheet();
|
||||||
|
} else if (
|
||||||
|
column > columnEnd &&
|
||||||
|
((row <= rowEnd && row >= rowStart) ||
|
||||||
|
(row < rowStart && rowStart - row < column - columnEnd) ||
|
||||||
|
(row > rowEnd && row - rowEnd < column - columnEnd))
|
||||||
|
) {
|
||||||
|
// columns right
|
||||||
|
const area = {
|
||||||
|
type: AreaType.columnsRight,
|
||||||
|
rowStart,
|
||||||
|
rowEnd,
|
||||||
|
columnStart: columnEnd + 1,
|
||||||
|
columnEnd: column,
|
||||||
|
};
|
||||||
|
workbookState.setExtendToArea(area);
|
||||||
|
canvas.renderSheet();
|
||||||
|
} else if (
|
||||||
|
column < columnStart &&
|
||||||
|
((row <= rowEnd && row >= rowStart) ||
|
||||||
|
(row < rowStart && rowStart - row < columnStart - column) ||
|
||||||
|
(row > rowEnd && row - rowEnd < columnStart - column))
|
||||||
|
) {
|
||||||
|
// columns left
|
||||||
|
const area = {
|
||||||
|
type: AreaType.columnsLeft,
|
||||||
|
rowStart,
|
||||||
|
rowEnd,
|
||||||
|
columnStart: column,
|
||||||
|
columnEnd: columnStart,
|
||||||
|
};
|
||||||
|
workbookState.setExtendToArea(area);
|
||||||
|
canvas.renderSheet();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onExtendToEnd: () => {
|
||||||
|
const canvas = worksheetCanvas.current;
|
||||||
|
if (!canvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { sheet, range } = model.getSelectedView();
|
||||||
|
const extendedArea = workbookState.getExtendToArea();
|
||||||
|
if (!extendedArea) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rowStart = Math.min(range[0], range[2]);
|
||||||
|
const height = Math.abs(range[2] - range[0]) + 1;
|
||||||
|
const width = Math.abs(range[3] - range[1]) + 1;
|
||||||
|
const columnStart = Math.min(range[1], range[3]);
|
||||||
|
|
||||||
|
const area = {
|
||||||
|
sheet,
|
||||||
|
row: rowStart,
|
||||||
|
column: columnStart,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (extendedArea.type) {
|
||||||
|
case AreaType.rowsDown:
|
||||||
|
model.autoFillRows(area, extendedArea.rowEnd);
|
||||||
|
break;
|
||||||
|
case AreaType.rowsUp: {
|
||||||
|
model.autoFillRows(area, extendedArea.rowStart);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AreaType.columnsRight: {
|
||||||
|
model.autoFillColumns(area, extendedArea.columnEnd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AreaType.columnsLeft: {
|
||||||
|
model.autoFillColumns(area, extendedArea.columnStart);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.setSelectedRange(
|
||||||
|
Math.min(rowStart, extendedArea.rowStart),
|
||||||
|
Math.min(columnStart, extendedArea.columnStart),
|
||||||
|
Math.max(rowStart + height - 1, extendedArea.rowEnd),
|
||||||
|
Math.max(columnStart + width - 1, extendedArea.columnEnd),
|
||||||
|
);
|
||||||
|
workbookState.clearExtendToArea();
|
||||||
|
canvas.renderSheet();
|
||||||
|
},
|
||||||
|
canvasElement,
|
||||||
|
worksheetElement,
|
||||||
|
worksheetCanvas,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onScroll = (): void => {
|
||||||
|
if (!scrollElement.current || !worksheetCanvas.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ignoreScrollEventRef.current) {
|
||||||
|
// Programmatic scroll ignored
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const left = scrollElement.current.scrollLeft;
|
||||||
|
const top = scrollElement.current.scrollTop;
|
||||||
|
|
||||||
|
worksheetCanvas.current.setScrollPosition({ left, top });
|
||||||
|
worksheetCanvas.current.renderSheet();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper ref={scrollElement} onScroll={onScroll} className="scroll">
|
||||||
|
<Spacer ref={spacerElement} />
|
||||||
|
<SheetContainer
|
||||||
|
className="sheet-container"
|
||||||
|
ref={worksheetElement}
|
||||||
|
onPointerDown={onPointerDown}
|
||||||
|
onPointerMove={onPointerMove}
|
||||||
|
onPointerUp={onPointerUp}
|
||||||
|
onContextMenu={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
setContextMenuOpen(true);
|
||||||
|
}}
|
||||||
|
onDoubleClick={(event) => {
|
||||||
|
// Starts editing cell
|
||||||
|
const { sheet, row, column } = model.getSelectedView();
|
||||||
|
const text = model.getCellContent(sheet, row, column);
|
||||||
|
const editorWidth =
|
||||||
|
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
|
||||||
|
const editorHeight =
|
||||||
|
model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
|
||||||
|
workbookState.setEditingCell({
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
text,
|
||||||
|
cursorStart: text.length,
|
||||||
|
cursorEnd: text.length,
|
||||||
|
focus: "cell",
|
||||||
|
referencedRange: null,
|
||||||
|
activeRanges: [],
|
||||||
|
mode: "accept",
|
||||||
|
editorWidth,
|
||||||
|
editorHeight,
|
||||||
|
});
|
||||||
|
event.stopPropagation();
|
||||||
|
// event.preventDefault();
|
||||||
|
props.refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SheetCanvas ref={canvasElement} />
|
||||||
|
<CellOutline ref={cellOutline} />
|
||||||
|
<EditorWrapper ref={editorElement}>
|
||||||
|
<Editor
|
||||||
|
originalText={workbookState.getEditingText()}
|
||||||
|
onEditEnd={(): void => {
|
||||||
|
props.refresh();
|
||||||
|
}}
|
||||||
|
onTextUpdated={(): void => {
|
||||||
|
props.refresh();
|
||||||
|
}}
|
||||||
|
model={model}
|
||||||
|
workbookState={workbookState}
|
||||||
|
type={"cell"}
|
||||||
|
/>
|
||||||
|
</EditorWrapper>
|
||||||
|
<AreaOutline ref={areaOutline} />
|
||||||
|
<ExtendToOutline ref={extendToOutline} />
|
||||||
|
<CellOutlineHandle
|
||||||
|
ref={cellOutlineHandle}
|
||||||
|
onPointerDown={onPointerHandleDown}
|
||||||
|
/>
|
||||||
|
<ColumnResizeGuide ref={columnResizeGuide} />
|
||||||
|
<RowResizeGuide ref={rowResizeGuide} />
|
||||||
|
<ColumnHeaders ref={columnHeaders} />
|
||||||
|
</SheetContainer>
|
||||||
|
<CellContextMenu
|
||||||
|
open={contextMenuOpen}
|
||||||
|
onClose={() => setContextMenuOpen(false)}
|
||||||
|
anchorEl={cellOutline.current}
|
||||||
|
onInsertRowAbove={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.insertRow(view.sheet, view.row);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onInsertRowBelow={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.insertRow(view.sheet, view.row + 1);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onInsertColumnLeft={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.insertColumn(view.sheet, view.column);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onInsertColumnRight={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.insertColumn(view.sheet, view.column + 1);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onFreezeColumns={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.setFrozenColumnsCount(view.sheet, view.column);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onFreezeRows={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.setFrozenRowsCount(view.sheet, view.row);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onUnfreezeColumns={(): void => {
|
||||||
|
const sheet = model.getSelectedSheet();
|
||||||
|
model.setFrozenColumnsCount(sheet, 0);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onUnfreezeRows={(): void => {
|
||||||
|
const sheet = model.getSelectedSheet();
|
||||||
|
model.setFrozenRowsCount(sheet, 0);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onDeleteRow={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.deleteRow(view.sheet, view.row);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
onDeleteColumn={(): void => {
|
||||||
|
const view = model.getSelectedView();
|
||||||
|
model.deleteColumn(view.sheet, view.column);
|
||||||
|
setContextMenuOpen(false);
|
||||||
|
}}
|
||||||
|
row={model.getSelectedView().row}
|
||||||
|
column={columnNameFromNumber(model.getSelectedView().column)}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const Spacer = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
height: 5000px;
|
||||||
|
width: 5000px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SheetContainer = styled("div")`
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.column-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
width: 3px;
|
||||||
|
opacity: 0;
|
||||||
|
background: ${outlineColor};
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: col-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-resize-handle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.row-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
height: 3px;
|
||||||
|
opacity: 0;
|
||||||
|
background: ${outlineColor};
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: row-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-resize-handle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Wrapper = styled("div")({
|
||||||
|
position: "absolute",
|
||||||
|
overflow: "scroll",
|
||||||
|
top: TOOLBAR_HEIGHT + FORMULA_BAR_HEIGHT + 1,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: NAVIGATION_HEIGHT + 1,
|
||||||
|
overscrollBehavior: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
const SheetCanvas = styled("canvas")`
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 40px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ColumnResizeGuide = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
display: none;
|
||||||
|
height: 100%;
|
||||||
|
width: 0px;
|
||||||
|
border-left: 1px dashed ${outlineColor};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ColumnHeaders = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
& .column-header {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RowResizeGuide = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
left: 0px;
|
||||||
|
height: 0px;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px dashed ${outlineColor};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AreaOutline = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid ${outlineColor};
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: ${outlineBackgroundColor};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CellOutline = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid ${outlineColor};
|
||||||
|
border-radius: 3px;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const CellOutlineHandle = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
background: ${outlineColor};
|
||||||
|
cursor: crosshair;
|
||||||
|
border-radius: 1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ExtendToOutline = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
border: 1px dashed ${outlineColor};
|
||||||
|
border-radius: 3px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EditorWrapper = styled("div")`
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
border-width: 0px;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
vertical-align: bottom;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
span {
|
||||||
|
min-width: 1px;
|
||||||
|
}
|
||||||
|
font-family: monospace;
|
||||||
|
border: 2px solid ${outlineColor};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default Worksheet;
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
import type { Model } from "@ironcalc/wasm";
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import { type PointerEvent, type RefObject, useCallback, useRef } from "react";
|
import { type PointerEvent, type RefObject, useCallback, useRef } from "react";
|
||||||
import type WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
|
import { isInReferenceMode } from "../Editor/util";
|
||||||
|
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
|
||||||
import {
|
import {
|
||||||
headerColumnWidth,
|
headerColumnWidth,
|
||||||
headerRowHeight,
|
headerRowHeight,
|
||||||
} from "./WorksheetCanvas/worksheetCanvas";
|
} from "../WorksheetCanvas/worksheetCanvas";
|
||||||
import { isInReferenceMode } from "./editor/util";
|
import type { Cell } from "../types";
|
||||||
import type { Cell } from "./types";
|
import { rangeToStr } from "../util";
|
||||||
import { rangeToStr } from "./util";
|
import type { WorkbookState } from "../workbookState";
|
||||||
import type { WorkbookState } from "./workbookState";
|
|
||||||
|
|
||||||
interface PointerSettings {
|
interface PointerSettings {
|
||||||
canvasElement: RefObject<HTMLCanvasElement>;
|
canvasElement: RefObject<HTMLCanvasElement | null>;
|
||||||
worksheetCanvas: RefObject<WorksheetCanvas | null>;
|
worksheetCanvas: RefObject<WorksheetCanvas | null>;
|
||||||
worksheetElement: RefObject<HTMLDivElement>;
|
worksheetElement: RefObject<HTMLDivElement | null>;
|
||||||
onCellSelected: (cell: Cell, event: React.MouseEvent) => void;
|
onCellSelected: (cell: Cell, event: React.MouseEvent) => void;
|
||||||
|
onRowSelected: (row: number, shift: boolean) => void;
|
||||||
|
onColumnSelected: (column: number, shift: boolean) => void;
|
||||||
|
onAllSheetSelected: () => void;
|
||||||
onAreaSelecting: (cell: Cell) => void;
|
onAreaSelecting: (cell: Cell) => void;
|
||||||
onAreaSelected: () => void;
|
onAreaSelected: () => void;
|
||||||
onExtendToCell: (cell: Cell) => void;
|
onExtendToCell: (cell: Cell) => void;
|
||||||
@@ -116,6 +119,11 @@ const usePointer = (options: PointerSettings): PointerEvents => {
|
|||||||
|
|
||||||
const onPointerDown = useCallback(
|
const onPointerDown = useCallback(
|
||||||
(event: PointerEvent) => {
|
(event: PointerEvent) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (target !== null && target.className === "column-resize-handle") {
|
||||||
|
// we are resizing a column
|
||||||
|
return;
|
||||||
|
}
|
||||||
let x = event.clientX;
|
let x = event.clientX;
|
||||||
let y = event.clientY;
|
let y = event.clientY;
|
||||||
const {
|
const {
|
||||||
@@ -125,6 +133,9 @@ const usePointer = (options: PointerSettings): PointerEvents => {
|
|||||||
worksheetElement,
|
worksheetElement,
|
||||||
worksheetCanvas,
|
worksheetCanvas,
|
||||||
workbookState,
|
workbookState,
|
||||||
|
onRowSelected,
|
||||||
|
onColumnSelected,
|
||||||
|
onAllSheetSelected,
|
||||||
} = options;
|
} = options;
|
||||||
const worksheet = worksheetCanvas.current;
|
const worksheet = worksheetCanvas.current;
|
||||||
const canvas = canvasElement.current;
|
const canvas = canvasElement.current;
|
||||||
@@ -143,7 +154,10 @@ const usePointer = (options: PointerSettings): PointerEvents => {
|
|||||||
y < headerRowHeight ||
|
y < headerRowHeight ||
|
||||||
y > canvasRect.height
|
y > canvasRect.height
|
||||||
) {
|
) {
|
||||||
if (
|
if (x < headerColumnWidth && y < headerRowHeight) {
|
||||||
|
// Click on the top left corner
|
||||||
|
onAllSheetSelected();
|
||||||
|
} else if (
|
||||||
x > 0 &&
|
x > 0 &&
|
||||||
x < headerColumnWidth &&
|
x < headerColumnWidth &&
|
||||||
y > headerRowHeight &&
|
y > headerRowHeight &&
|
||||||
@@ -152,8 +166,18 @@ const usePointer = (options: PointerSettings): PointerEvents => {
|
|||||||
// Click on a row number
|
// Click on a row number
|
||||||
const cell = worksheet.getCellByCoordinates(headerColumnWidth, y);
|
const cell = worksheet.getCellByCoordinates(headerColumnWidth, y);
|
||||||
if (cell) {
|
if (cell) {
|
||||||
// TODO
|
onRowSelected(cell.row, event.shiftKey);
|
||||||
// Row selected
|
}
|
||||||
|
} else if (
|
||||||
|
x > headerColumnWidth &&
|
||||||
|
x < canvasRect.width &&
|
||||||
|
y > 0 &&
|
||||||
|
y < headerRowHeight
|
||||||
|
) {
|
||||||
|
// Click on a column letter
|
||||||
|
const cell = worksheet.getCellByCoordinates(x, headerRowHeight);
|
||||||
|
if (cell) {
|
||||||
|
onColumnSelected(cell.column, event.shiftKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -240,7 +264,6 @@ const usePointer = (options: PointerSettings): PointerEvents => {
|
|||||||
onPointerMove,
|
onPointerMove,
|
||||||
onPointerUp,
|
onPointerUp,
|
||||||
onPointerHandleDown,
|
onPointerHandleDown,
|
||||||
// onContextMenu,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Model } from "@ironcalc/wasm";
|
import type { Model } from "@ironcalc/wasm";
|
||||||
import { columnNameFromNumber } from "@ironcalc/wasm";
|
import { columnNameFromNumber } from "@ironcalc/wasm";
|
||||||
import { getColor } from "../editor/util";
|
import { getColor } from "../Editor/util";
|
||||||
import type { Cell } from "../types";
|
import type { Cell } from "../types";
|
||||||
import type { WorkbookState } from "../workbookState";
|
import type { WorkbookState } from "../workbookState";
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +37,7 @@ export interface CanvasSettings {
|
|||||||
};
|
};
|
||||||
onColumnWidthChanges: (sheet: number, column: number, width: number) => void;
|
onColumnWidthChanges: (sheet: number, column: number, width: number) => void;
|
||||||
onRowHeightChanges: (sheet: number, row: number, height: number) => void;
|
onRowHeightChanges: (sheet: number, row: number, height: number) => void;
|
||||||
|
refresh: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fonts = {
|
export const fonts = {
|
||||||
@@ -106,6 +107,8 @@ export default class WorksheetCanvas {
|
|||||||
|
|
||||||
onRowHeightChanges: (sheet: number, row: number, height: number) => void;
|
onRowHeightChanges: (sheet: number, row: number, height: number) => void;
|
||||||
|
|
||||||
|
refresh: () => void;
|
||||||
|
|
||||||
constructor(options: CanvasSettings) {
|
constructor(options: CanvasSettings) {
|
||||||
this.model = options.model;
|
this.model = options.model;
|
||||||
this.sheetWidth = 0;
|
this.sheetWidth = 0;
|
||||||
@@ -116,6 +119,7 @@ export default class WorksheetCanvas {
|
|||||||
this.ctx = this.setContext();
|
this.ctx = this.setContext();
|
||||||
this.workbookState = options.workbookState;
|
this.workbookState = options.workbookState;
|
||||||
this.editor = options.elements.editor;
|
this.editor = options.elements.editor;
|
||||||
|
this.refresh = options.refresh;
|
||||||
|
|
||||||
this.cellOutline = options.elements.cellOutline;
|
this.cellOutline = options.elements.cellOutline;
|
||||||
this.cellOutlineHandle = options.elements.cellOutlineHandle;
|
this.cellOutlineHandle = options.elements.cellOutlineHandle;
|
||||||
@@ -349,7 +353,7 @@ export default class WorksheetCanvas {
|
|||||||
? gridColor
|
? gridColor
|
||||||
: backgroundColor;
|
: backgroundColor;
|
||||||
|
|
||||||
const fontSize = 13;
|
const fontSize = style.font?.sz || 13;
|
||||||
let font = `${fontSize}px ${defaultCellFontFamily}`;
|
let font = `${fontSize}px ${defaultCellFontFamily}`;
|
||||||
let textColor = defaultTextColor;
|
let textColor = defaultTextColor;
|
||||||
if (style.font) {
|
if (style.font) {
|
||||||
@@ -577,23 +581,54 @@ export default class WorksheetCanvas {
|
|||||||
let resizeHandleUp = (event: MouseEvent): void => {
|
let resizeHandleUp = (event: MouseEvent): void => {
|
||||||
div.style.opacity = "0";
|
div.style.opacity = "0";
|
||||||
this.columnGuide.style.display = "none";
|
this.columnGuide.style.display = "none";
|
||||||
document.removeEventListener("mousemove", resizeHandleMove);
|
document.removeEventListener("pointermove", resizeHandleMove);
|
||||||
document.removeEventListener("mouseup", resizeHandleUp);
|
document.removeEventListener("pointerup", resizeHandleUp);
|
||||||
const newColumnWidth = columnWidth + event.pageX - initPageX;
|
const newColumnWidth = columnWidth + event.pageX - initPageX;
|
||||||
this.onColumnWidthChanges(
|
if (newColumnWidth !== columnWidth) {
|
||||||
this.model.getSelectedSheet(),
|
this.onColumnWidthChanges(
|
||||||
column,
|
this.model.getSelectedSheet(),
|
||||||
newColumnWidth,
|
column,
|
||||||
);
|
newColumnWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
resizeHandleUp = resizeHandleUp.bind(this);
|
resizeHandleUp = resizeHandleUp.bind(this);
|
||||||
div.addEventListener("mousedown", (event) => {
|
div.addEventListener("pointerdown", (event) => {
|
||||||
div.style.opacity = "1";
|
div.style.opacity = "1";
|
||||||
this.columnGuide.style.display = "block";
|
this.columnGuide.style.display = "block";
|
||||||
this.columnGuide.style.left = `${headerColumnWidth + x}px`;
|
this.columnGuide.style.left = `${headerColumnWidth + x}px`;
|
||||||
initPageX = event.pageX;
|
initPageX = event.pageX;
|
||||||
document.addEventListener("mousemove", resizeHandleMove);
|
document.addEventListener("pointermove", resizeHandleMove);
|
||||||
document.addEventListener("mouseup", resizeHandleUp);
|
document.addEventListener("pointerup", resizeHandleUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener("dblclick", (event) => {
|
||||||
|
// This is tough. We should have a call like this.model.setAutofitColumn(sheet, column)
|
||||||
|
// but we can't do that because the back end knows nothing about the rendering engine.
|
||||||
|
const sheet = this.model.getSelectedSheet();
|
||||||
|
const rows = this.model.getRowsWithData(sheet, column);
|
||||||
|
let width = 0;
|
||||||
|
// This is a bit of a HACK. We should use the actual font size and weather is bold or not
|
||||||
|
const fontSize = 13;
|
||||||
|
this.ctx.font = `${fontSize}px ${defaultCellFontFamily}`;
|
||||||
|
for (const row of rows) {
|
||||||
|
const fullText = this.model.getFormattedCellValue(sheet, row, column);
|
||||||
|
if (fullText === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lines = fullText.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const textWidth = this.ctx.measureText(line).width;
|
||||||
|
width = Math.max(width, textWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the width is 0, we do nothing
|
||||||
|
if (width !== 0) {
|
||||||
|
// The +8 is so that the text is in the same position regardless of the horizontal alignment
|
||||||
|
this.model.setColumnsWidth(sheet, column, column, width + 8);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,20 +650,52 @@ export default class WorksheetCanvas {
|
|||||||
let resizeHandleUp = (event: MouseEvent): void => {
|
let resizeHandleUp = (event: MouseEvent): void => {
|
||||||
div.style.opacity = "0";
|
div.style.opacity = "0";
|
||||||
this.rowGuide.style.display = "none";
|
this.rowGuide.style.display = "none";
|
||||||
document.removeEventListener("mousemove", resizeHandleMove);
|
document.removeEventListener("pointermove", resizeHandleMove);
|
||||||
document.removeEventListener("mouseup", resizeHandleUp);
|
document.removeEventListener("pointerup", resizeHandleUp);
|
||||||
const newRowHeight = rowHeight + event.pageY - initPageY - 1;
|
const newRowHeight = rowHeight + event.pageY - initPageY;
|
||||||
this.onRowHeightChanges(sheet, row, newRowHeight);
|
if (newRowHeight !== rowHeight) {
|
||||||
|
this.onRowHeightChanges(sheet, row, newRowHeight);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
resizeHandleUp = resizeHandleUp.bind(this);
|
resizeHandleUp = resizeHandleUp.bind(this);
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
div.addEventListener("mousedown", (event) => {
|
div.addEventListener("pointerdown", (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
div.style.opacity = "1";
|
div.style.opacity = "1";
|
||||||
this.rowGuide.style.display = "block";
|
this.rowGuide.style.display = "block";
|
||||||
this.rowGuide.style.top = `${y}px`;
|
this.rowGuide.style.top = `${y}px`;
|
||||||
initPageY = event.pageY;
|
initPageY = event.pageY;
|
||||||
document.addEventListener("mousemove", resizeHandleMove);
|
document.addEventListener("pointermove", resizeHandleMove);
|
||||||
document.addEventListener("mouseup", resizeHandleUp);
|
document.addEventListener("pointerup", resizeHandleUp);
|
||||||
|
});
|
||||||
|
|
||||||
|
div.addEventListener("dblclick", (event) => {
|
||||||
|
// This is tough. We should have a call like this.model.setAutofitRow(sheet, row)
|
||||||
|
// but we can't do that because the back end knows nothing about the rendering engine.
|
||||||
|
const sheet = this.model.getSelectedSheet();
|
||||||
|
const columns = this.model.getColumnsWithData(sheet, row);
|
||||||
|
let height = 0;
|
||||||
|
const lineHeight = 22;
|
||||||
|
// This is a bit of a HACK. We should use the actual font size and weather is bold or not
|
||||||
|
const fontSize = 13;
|
||||||
|
this.ctx.font = `${fontSize}px ${defaultCellFontFamily}`;
|
||||||
|
for (const column of columns) {
|
||||||
|
const fullText = this.model.getFormattedCellValue(sheet, row, column);
|
||||||
|
if (fullText === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const lines = fullText.split("\n");
|
||||||
|
const lineCount = lines.length;
|
||||||
|
// This si computed so that the y position of the text is independent of the vertical alignment
|
||||||
|
const textHeight = (lineCount - 1) * lineHeight + 8 + fontSize;
|
||||||
|
height = Math.max(height, textHeight);
|
||||||
|
}
|
||||||
|
// If the height is 0, we do nothing
|
||||||
|
if (height !== 0) {
|
||||||
|
this.model.setRowsHeight(sheet, row, row, height);
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1354,7 +1421,7 @@ export default class WorksheetCanvas {
|
|||||||
let y = headerRowHeight + 0.5;
|
let y = headerRowHeight + 0.5;
|
||||||
for (let row = 1; row <= frozenRows; row += 1) {
|
for (let row = 1; row <= frozenRows; row += 1) {
|
||||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||||
x = headerColumnWidth;
|
x = headerColumnWidth + 0.5;
|
||||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||||
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
const columnWidth = this.getColumnWidth(selectedSheet, column);
|
||||||
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
this.renderCell(row, column, x, y, columnWidth, rowHeight);
|
||||||
@@ -1363,7 +1430,7 @@ export default class WorksheetCanvas {
|
|||||||
y += rowHeight;
|
y += rowHeight;
|
||||||
}
|
}
|
||||||
if (frozenRows === 0 && frozenColumns !== 0) {
|
if (frozenRows === 0 && frozenColumns !== 0) {
|
||||||
x = headerColumnWidth;
|
x = headerColumnWidth + 0.5;
|
||||||
for (let column = 1; column <= frozenColumns; column += 1) {
|
for (let column = 1; column <= frozenColumns; column += 1) {
|
||||||
x += this.getColumnWidth(selectedSheet, column);
|
x += this.getColumnWidth(selectedSheet, column);
|
||||||
}
|
}
|
||||||
@@ -1397,7 +1464,7 @@ export default class WorksheetCanvas {
|
|||||||
const frozenX = x;
|
const frozenX = x;
|
||||||
const frozenY = y;
|
const frozenY = y;
|
||||||
// Draw frozen rows (top-right pane)
|
// Draw frozen rows (top-right pane)
|
||||||
y = headerRowHeight;
|
y = headerRowHeight + 0.5;
|
||||||
for (let row = 1; row <= frozenRows; row += 1) {
|
for (let row = 1; row <= frozenRows; row += 1) {
|
||||||
x = frozenX;
|
x = frozenX;
|
||||||
const rowHeight = this.getRowHeight(selectedSheet, row);
|
const rowHeight = this.getRowHeight(selectedSheet, row);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { type SelectedView, initSync } from "@ironcalc/wasm";
|
import { type SelectedView, initSync } from "@ironcalc/wasm";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { decreaseDecimalPlaces, increaseDecimalPlaces } from "../formatUtil";
|
import {
|
||||||
|
decreaseDecimalPlaces,
|
||||||
|
increaseDecimalPlaces,
|
||||||
|
} from "../FormatMenu/formatUtil";
|
||||||
import { getFullRangeToString, isNavigationKey } from "../util";
|
import { getFullRangeToString, isNavigationKey } from "../util";
|
||||||
|
|
||||||
test("checks arrow left is a navigation key", () => {
|
test("checks arrow left is a navigation key", () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Area, Cell } from "./types";
|
import type { Area, Cell } from "./types";
|
||||||
|
|
||||||
import { type SelectedView, columnNameFromNumber } from "@ironcalc/wasm";
|
import { type SelectedView, columnNameFromNumber } from "@ironcalc/wasm";
|
||||||
|
import { LAST_COLUMN, LAST_ROW } from "./WorksheetCanvas/constants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the keypress should start editing
|
* Returns true if the keypress should start editing
|
||||||
@@ -34,11 +35,23 @@ export const getCellAddress = (selectedArea: Area, selectedCell: Cell) => {
|
|||||||
selectedArea.rowStart === selectedArea.rowEnd &&
|
selectedArea.rowStart === selectedArea.rowEnd &&
|
||||||
selectedArea.columnEnd === selectedArea.columnStart;
|
selectedArea.columnEnd === selectedArea.columnStart;
|
||||||
|
|
||||||
return isSingleCell
|
if (isSingleCell) {
|
||||||
? `${columnNameFromNumber(selectedCell.column)}${selectedCell.row}`
|
return `${columnNameFromNumber(selectedCell.column)}${selectedCell.row}`;
|
||||||
: `${columnNameFromNumber(selectedArea.columnStart)}${
|
}
|
||||||
selectedArea.rowStart
|
if (selectedArea.rowStart === 1 && selectedArea.rowEnd === LAST_ROW) {
|
||||||
}:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`;
|
return `${columnNameFromNumber(selectedArea.columnStart)}:${columnNameFromNumber(
|
||||||
|
selectedArea.columnEnd,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
selectedArea.columnStart === 1 &&
|
||||||
|
selectedArea.columnEnd === LAST_COLUMN
|
||||||
|
) {
|
||||||
|
return `${selectedArea.rowStart}:${selectedArea.rowEnd}`;
|
||||||
|
}
|
||||||
|
return `${columnNameFromNumber(selectedArea.columnStart)}${
|
||||||
|
selectedArea.rowStart
|
||||||
|
}:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function rangeToStr(
|
export function rangeToStr(
|
||||||
|
|||||||
@@ -1,539 +0,0 @@
|
|||||||
import type { Model } from "@ironcalc/wasm";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
||||||
import {
|
|
||||||
COLUMN_WIDTH_SCALE,
|
|
||||||
ROW_HEIGH_SCALE,
|
|
||||||
outlineBackgroundColor,
|
|
||||||
outlineColor,
|
|
||||||
} from "./WorksheetCanvas/constants";
|
|
||||||
import WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
|
|
||||||
import {
|
|
||||||
FORMULA_BAR_HEIGHT,
|
|
||||||
NAVIGATION_HEIGHT,
|
|
||||||
TOOLBAR_HEIGHT,
|
|
||||||
} from "./constants";
|
|
||||||
import Editor from "./editor/editor";
|
|
||||||
import type { Cell } from "./types";
|
|
||||||
import usePointer from "./usePointer";
|
|
||||||
import { AreaType, type WorkbookState } from "./workbookState";
|
|
||||||
|
|
||||||
function useWindowSize() {
|
|
||||||
const [size, setSize] = useState([0, 0]);
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
function updateSize() {
|
|
||||||
setSize([window.innerWidth, window.innerHeight]);
|
|
||||||
}
|
|
||||||
window.addEventListener("resize", updateSize);
|
|
||||||
updateSize();
|
|
||||||
return () => window.removeEventListener("resize", updateSize);
|
|
||||||
}, []);
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Worksheet(props: {
|
|
||||||
model: Model;
|
|
||||||
workbookState: WorkbookState;
|
|
||||||
refresh: () => void;
|
|
||||||
}) {
|
|
||||||
const canvasElement = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
const worksheetElement = useRef<HTMLDivElement>(null);
|
|
||||||
const scrollElement = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const editorElement = useRef<HTMLDivElement>(null);
|
|
||||||
const spacerElement = useRef<HTMLDivElement>(null);
|
|
||||||
const cellOutline = useRef<HTMLDivElement>(null);
|
|
||||||
const areaOutline = useRef<HTMLDivElement>(null);
|
|
||||||
const cellOutlineHandle = useRef<HTMLDivElement>(null);
|
|
||||||
const extendToOutline = useRef<HTMLDivElement>(null);
|
|
||||||
const columnResizeGuide = useRef<HTMLDivElement>(null);
|
|
||||||
const rowResizeGuide = useRef<HTMLDivElement>(null);
|
|
||||||
const columnHeaders = useRef<HTMLDivElement>(null);
|
|
||||||
const worksheetCanvas = useRef<WorksheetCanvas | null>(null);
|
|
||||||
|
|
||||||
const ignoreScrollEventRef = useRef(false);
|
|
||||||
|
|
||||||
const { model, workbookState, refresh } = props;
|
|
||||||
const [clientWidth, clientHeight] = useWindowSize();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvasRef = canvasElement.current;
|
|
||||||
const columnGuideRef = columnResizeGuide.current;
|
|
||||||
const rowGuideRef = rowResizeGuide.current;
|
|
||||||
const columnHeadersRef = columnHeaders.current;
|
|
||||||
const worksheetRef = worksheetElement.current;
|
|
||||||
|
|
||||||
const outline = cellOutline.current;
|
|
||||||
const handle = cellOutlineHandle.current;
|
|
||||||
const area = areaOutline.current;
|
|
||||||
const extendTo = extendToOutline.current;
|
|
||||||
const editor = editorElement.current;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!canvasRef ||
|
|
||||||
!columnGuideRef ||
|
|
||||||
!rowGuideRef ||
|
|
||||||
!columnHeadersRef ||
|
|
||||||
!worksheetRef ||
|
|
||||||
!outline ||
|
|
||||||
!handle ||
|
|
||||||
!area ||
|
|
||||||
!extendTo ||
|
|
||||||
!scrollElement.current ||
|
|
||||||
!editor
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
// FIXME: This two need to be computed.
|
|
||||||
model.setWindowWidth(clientWidth - 37);
|
|
||||||
model.setWindowHeight(clientHeight - 190);
|
|
||||||
const canvas = new WorksheetCanvas({
|
|
||||||
width: worksheetRef.clientWidth,
|
|
||||||
height: worksheetRef.clientHeight,
|
|
||||||
model,
|
|
||||||
workbookState,
|
|
||||||
elements: {
|
|
||||||
canvas: canvasRef,
|
|
||||||
columnGuide: columnGuideRef,
|
|
||||||
rowGuide: rowGuideRef,
|
|
||||||
columnHeaders: columnHeadersRef,
|
|
||||||
cellOutline: outline,
|
|
||||||
cellOutlineHandle: handle,
|
|
||||||
areaOutline: area,
|
|
||||||
extendToOutline: extendTo,
|
|
||||||
editor: editor,
|
|
||||||
},
|
|
||||||
onColumnWidthChanges(sheet, column, width) {
|
|
||||||
if (width < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.setColumnWidth(sheet, column, width);
|
|
||||||
worksheetCanvas.current?.renderSheet();
|
|
||||||
},
|
|
||||||
onRowHeightChanges(sheet, row, height) {
|
|
||||||
if (height < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
model.setRowHeight(sheet, row, height);
|
|
||||||
worksheetCanvas.current?.renderSheet();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const scrollX = model.getScrollX();
|
|
||||||
const scrollY = model.getScrollY();
|
|
||||||
const [sheetWidth, sheetHeight] = [scrollX + 100_000, scrollY + 500_000];
|
|
||||||
if (spacerElement.current) {
|
|
||||||
spacerElement.current.style.height = `${sheetHeight}px`;
|
|
||||||
spacerElement.current.style.width = `${sheetWidth}px`;
|
|
||||||
}
|
|
||||||
const left = scrollElement.current.scrollLeft;
|
|
||||||
const top = scrollElement.current.scrollTop;
|
|
||||||
if (scrollX !== left) {
|
|
||||||
ignoreScrollEventRef.current = true;
|
|
||||||
scrollElement.current.scrollLeft = scrollX;
|
|
||||||
setTimeout(() => {
|
|
||||||
ignoreScrollEventRef.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollY !== top) {
|
|
||||||
ignoreScrollEventRef.current = true;
|
|
||||||
scrollElement.current.scrollTop = scrollY;
|
|
||||||
setTimeout(() => {
|
|
||||||
ignoreScrollEventRef.current = false;
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.renderSheet();
|
|
||||||
worksheetCanvas.current = canvas;
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
onPointerMove,
|
|
||||||
onPointerDown,
|
|
||||||
onPointerHandleDown,
|
|
||||||
onPointerUp,
|
|
||||||
// onContextMenu,
|
|
||||||
} = usePointer({
|
|
||||||
model,
|
|
||||||
workbookState,
|
|
||||||
refresh,
|
|
||||||
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
model.setSelectedCell(cell.row, cell.column);
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
onAreaSelecting: (cell: Cell) => {
|
|
||||||
const canvas = worksheetCanvas.current;
|
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { row, column } = cell;
|
|
||||||
model.onAreaSelecting(row, column);
|
|
||||||
canvas.renderSheet();
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
onAreaSelected: () => {
|
|
||||||
const styles = workbookState.getCopyStyles();
|
|
||||||
if (styles?.length) {
|
|
||||||
model.onPasteStyles(styles);
|
|
||||||
const canvas = worksheetCanvas.current;
|
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
canvas.renderSheet();
|
|
||||||
}
|
|
||||||
workbookState.setCopyStyles(null);
|
|
||||||
if (worksheetElement.current) {
|
|
||||||
worksheetElement.current.style.cursor = "auto";
|
|
||||||
}
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
onExtendToCell: (cell) => {
|
|
||||||
const canvas = worksheetCanvas.current;
|
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { row, column } = cell;
|
|
||||||
const {
|
|
||||||
range: [rowStart, columnStart, rowEnd, columnEnd],
|
|
||||||
} = model.getSelectedView();
|
|
||||||
// We are either extending by rows or by columns
|
|
||||||
// And we could be doing it in the positive direction (downwards or right)
|
|
||||||
// or the negative direction (upwards or left)
|
|
||||||
|
|
||||||
if (
|
|
||||||
row > rowEnd &&
|
|
||||||
((column <= columnEnd && column >= columnStart) ||
|
|
||||||
(column < columnStart && columnStart - column < row - rowEnd) ||
|
|
||||||
(column > columnEnd && column - columnEnd < row - rowEnd))
|
|
||||||
) {
|
|
||||||
// rows downwards
|
|
||||||
const area = {
|
|
||||||
type: AreaType.rowsDown,
|
|
||||||
rowStart: rowEnd + 1,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
};
|
|
||||||
workbookState.setExtendToArea(area);
|
|
||||||
canvas.renderSheet();
|
|
||||||
} else if (
|
|
||||||
row < rowStart &&
|
|
||||||
((column <= columnEnd && column >= columnStart) ||
|
|
||||||
(column < columnStart && columnStart - column < rowStart - row) ||
|
|
||||||
(column > columnEnd && column - columnEnd < rowStart - row))
|
|
||||||
) {
|
|
||||||
// rows upwards
|
|
||||||
const area = {
|
|
||||||
type: AreaType.rowsUp,
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: rowStart,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
};
|
|
||||||
workbookState.setExtendToArea(area);
|
|
||||||
canvas.renderSheet();
|
|
||||||
} else if (
|
|
||||||
column > columnEnd &&
|
|
||||||
((row <= rowEnd && row >= rowStart) ||
|
|
||||||
(row < rowStart && rowStart - row < column - columnEnd) ||
|
|
||||||
(row > rowEnd && row - rowEnd < column - columnEnd))
|
|
||||||
) {
|
|
||||||
// columns right
|
|
||||||
const area = {
|
|
||||||
type: AreaType.columnsRight,
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: columnEnd + 1,
|
|
||||||
columnEnd: column,
|
|
||||||
};
|
|
||||||
workbookState.setExtendToArea(area);
|
|
||||||
canvas.renderSheet();
|
|
||||||
} else if (
|
|
||||||
column < columnStart &&
|
|
||||||
((row <= rowEnd && row >= rowStart) ||
|
|
||||||
(row < rowStart && rowStart - row < columnStart - column) ||
|
|
||||||
(row > rowEnd && row - rowEnd < columnStart - column))
|
|
||||||
) {
|
|
||||||
// columns left
|
|
||||||
const area = {
|
|
||||||
type: AreaType.columnsLeft,
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: columnStart,
|
|
||||||
};
|
|
||||||
workbookState.setExtendToArea(area);
|
|
||||||
canvas.renderSheet();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onExtendToEnd: () => {
|
|
||||||
const canvas = worksheetCanvas.current;
|
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { sheet, range } = model.getSelectedView();
|
|
||||||
const extendedArea = workbookState.getExtendToArea();
|
|
||||||
if (!extendedArea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rowStart = Math.min(range[0], range[2]);
|
|
||||||
const height = Math.abs(range[2] - range[0]) + 1;
|
|
||||||
const width = Math.abs(range[3] - range[1]) + 1;
|
|
||||||
const columnStart = Math.min(range[1], range[3]);
|
|
||||||
|
|
||||||
const area = { sheet, row: rowStart, column: columnStart, width, height };
|
|
||||||
|
|
||||||
switch (extendedArea.type) {
|
|
||||||
case AreaType.rowsDown:
|
|
||||||
model.autoFillRows(area, extendedArea.rowEnd);
|
|
||||||
break;
|
|
||||||
case AreaType.rowsUp: {
|
|
||||||
model.autoFillRows(area, extendedArea.rowStart);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AreaType.columnsRight: {
|
|
||||||
model.autoFillColumns(area, extendedArea.columnEnd);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case AreaType.columnsLeft: {
|
|
||||||
model.autoFillColumns(area, extendedArea.columnStart);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
model.setSelectedRange(
|
|
||||||
Math.min(rowStart, extendedArea.rowStart),
|
|
||||||
Math.min(columnStart, extendedArea.columnStart),
|
|
||||||
Math.max(rowStart + height - 1, extendedArea.rowEnd),
|
|
||||||
Math.max(columnStart + width - 1, extendedArea.columnEnd),
|
|
||||||
);
|
|
||||||
workbookState.clearExtendToArea();
|
|
||||||
canvas.renderSheet();
|
|
||||||
},
|
|
||||||
canvasElement,
|
|
||||||
worksheetElement,
|
|
||||||
worksheetCanvas,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onScroll = (): void => {
|
|
||||||
if (!scrollElement.current || !worksheetCanvas.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (ignoreScrollEventRef.current) {
|
|
||||||
// Programmatic scroll ignored
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const left = scrollElement.current.scrollLeft;
|
|
||||||
const top = scrollElement.current.scrollTop;
|
|
||||||
|
|
||||||
worksheetCanvas.current.setScrollPosition({ left, top });
|
|
||||||
worksheetCanvas.current.renderSheet();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper ref={scrollElement} onScroll={onScroll} className="scroll">
|
|
||||||
<Spacer ref={spacerElement} />
|
|
||||||
<SheetContainer
|
|
||||||
className="sheet-container"
|
|
||||||
ref={worksheetElement}
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
onPointerMove={onPointerMove}
|
|
||||||
onPointerUp={onPointerUp}
|
|
||||||
onDoubleClick={(event) => {
|
|
||||||
// Starts editing cell
|
|
||||||
const { sheet, row, column } = model.getSelectedView();
|
|
||||||
const text = model.getCellContent(sheet, row, column);
|
|
||||||
const editorWidth =
|
|
||||||
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
|
|
||||||
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
|
|
||||||
workbookState.setEditingCell({
|
|
||||||
sheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
text,
|
|
||||||
cursorStart: text.length,
|
|
||||||
cursorEnd: text.length,
|
|
||||||
focus: "cell",
|
|
||||||
referencedRange: null,
|
|
||||||
activeRanges: [],
|
|
||||||
mode: "accept",
|
|
||||||
editorWidth,
|
|
||||||
editorHeight,
|
|
||||||
});
|
|
||||||
event.stopPropagation();
|
|
||||||
// event.preventDefault();
|
|
||||||
props.refresh();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SheetCanvas ref={canvasElement} />
|
|
||||||
<CellOutline ref={cellOutline} />
|
|
||||||
<EditorWrapper ref={editorElement}>
|
|
||||||
<Editor
|
|
||||||
originalText={workbookState.getEditingText()}
|
|
||||||
onEditEnd={(): void => {
|
|
||||||
props.refresh();
|
|
||||||
}}
|
|
||||||
onTextUpdated={(): void => {
|
|
||||||
props.refresh();
|
|
||||||
}}
|
|
||||||
model={model}
|
|
||||||
workbookState={workbookState}
|
|
||||||
type={"cell"}
|
|
||||||
/>
|
|
||||||
</EditorWrapper>
|
|
||||||
<AreaOutline ref={areaOutline} />
|
|
||||||
<ExtendToOutline ref={extendToOutline} />
|
|
||||||
<CellOutlineHandle
|
|
||||||
ref={cellOutlineHandle}
|
|
||||||
onPointerDown={onPointerHandleDown}
|
|
||||||
/>
|
|
||||||
<ColumnResizeGuide ref={columnResizeGuide} />
|
|
||||||
<RowResizeGuide ref={rowResizeGuide} />
|
|
||||||
<ColumnHeaders ref={columnHeaders} />
|
|
||||||
</SheetContainer>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const Spacer = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
height: 5000px;
|
|
||||||
width: 5000px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SheetContainer = styled("div")`
|
|
||||||
position: sticky;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.column-resize-handle {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
width: 3px;
|
|
||||||
opacity: 0;
|
|
||||||
background: ${outlineColor};
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: col-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-resize-handle:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.row-resize-handle {
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
height: 3px;
|
|
||||||
opacity: 0;
|
|
||||||
background: ${outlineColor};
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: row-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-resize-handle:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled("div")({
|
|
||||||
position: "absolute",
|
|
||||||
overflow: "scroll",
|
|
||||||
top: TOOLBAR_HEIGHT + FORMULA_BAR_HEIGHT + 1,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: NAVIGATION_HEIGHT + 1,
|
|
||||||
overscrollBehavior: "none",
|
|
||||||
});
|
|
||||||
|
|
||||||
const SheetCanvas = styled("canvas")`
|
|
||||||
position: relative;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
bottom: 40px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColumnResizeGuide = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
display: none;
|
|
||||||
height: 100%;
|
|
||||||
width: 0px;
|
|
||||||
border-left: 1px dashed ${outlineColor};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColumnHeaders = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
top: 0px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
& .column-header {
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RowResizeGuide = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
left: 0px;
|
|
||||||
height: 0px;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px dashed ${outlineColor};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AreaOutline = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
border: 1px solid ${outlineColor};
|
|
||||||
border-radius: 3px;
|
|
||||||
background-color: ${outlineBackgroundColor};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CellOutline = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
border: 2px solid ${outlineColor};
|
|
||||||
border-radius: 3px;
|
|
||||||
word-break: break-word;
|
|
||||||
font-size: 13px;
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CellOutlineHandle = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
width: 5px;
|
|
||||||
height: 5px;
|
|
||||||
background: ${outlineColor};
|
|
||||||
cursor: crosshair;
|
|
||||||
border-radius: 1px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ExtendToOutline = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
border: 1px dashed ${outlineColor};
|
|
||||||
border-radius: 3px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const EditorWrapper = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0px;
|
|
||||||
border-width: 0px;
|
|
||||||
outline: none;
|
|
||||||
resize: none;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
vertical-align: bottom;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: left;
|
|
||||||
span {
|
|
||||||
min-width: 1px;
|
|
||||||
}
|
|
||||||
font-family: monospace;
|
|
||||||
border: 2px solid ${outlineColor};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Worksheet;
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
"redo": "Redo",
|
"redo": "Redo",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"copy_styles": "Copy styles",
|
"copy_styles": "Copy styles",
|
||||||
|
"clear_formatting": "Clear formatting",
|
||||||
"euro": "Format as Euro",
|
"euro": "Format as Euro",
|
||||||
"percentage": "Format as Percentage",
|
"percentage": "Format as Percentage",
|
||||||
"bold": "Bold",
|
"bold": "Bold",
|
||||||
@@ -15,6 +16,8 @@
|
|||||||
"format_number": "Format number",
|
"format_number": "Format number",
|
||||||
"font_color": "Font color",
|
"font_color": "Font color",
|
||||||
"fill_color": "Fill color",
|
"fill_color": "Fill color",
|
||||||
|
"increase_font_size": "Increase font size",
|
||||||
|
"decrease_font_size": "Decrease font size",
|
||||||
"decimal_places_increase": "Increase decimal places",
|
"decimal_places_increase": "Increase decimal places",
|
||||||
"decimal_places_decrease": "Decrease decimal places",
|
"decimal_places_decrease": "Decrease decimal places",
|
||||||
"show_hide_grid_lines": "Show/hide grid lines",
|
"show_hide_grid_lines": "Show/hide grid lines",
|
||||||
@@ -22,6 +25,7 @@
|
|||||||
"vertical_align_bottom": "Align bottom",
|
"vertical_align_bottom": "Align bottom",
|
||||||
"vertical_align_middle": " Align middle",
|
"vertical_align_middle": " Align middle",
|
||||||
"vertical_align_top": "Align top",
|
"vertical_align_top": "Align top",
|
||||||
|
"selected_png": "Export Selected area as PNG",
|
||||||
"format_menu": {
|
"format_menu": {
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"number": "Number",
|
"number": "Number",
|
||||||
@@ -68,6 +72,12 @@
|
|||||||
"title": "Rename Sheet",
|
"title": "Rename Sheet",
|
||||||
"close": "Close dialog"
|
"close": "Close dialog"
|
||||||
},
|
},
|
||||||
|
"sheet_delete": {
|
||||||
|
"title": "Are you sure?",
|
||||||
|
"message": "The sheet '{{sheetName}}' will be deleted.",
|
||||||
|
"confirm": "Yes, delete sheet",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
"formula_input": {
|
"formula_input": {
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"label": "Formula",
|
"label": "Formula",
|
||||||
@@ -79,6 +89,8 @@
|
|||||||
},
|
},
|
||||||
"name_manager_dialog": {
|
"name_manager_dialog": {
|
||||||
"title": "Named Ranges",
|
"title": "Named Ranges",
|
||||||
|
"empty_message1": "No named ranges added yet.",
|
||||||
|
"empty_message2": "Click on 'Add new' to add one.",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"range": "Scope",
|
"range": "Scope",
|
||||||
"scope": "Range",
|
"scope": "Range",
|
||||||
@@ -91,5 +103,23 @@
|
|||||||
"edit": "Edit Range",
|
"edit": "Edit Range",
|
||||||
"apply": "Apply changes",
|
"apply": "Apply changes",
|
||||||
"discard": "Discard changes"
|
"discard": "Discard changes"
|
||||||
|
},
|
||||||
|
"cell_context": {
|
||||||
|
"insert_row_above": "Insert 1 row above",
|
||||||
|
"insert_row_below": "Insert 1 row below",
|
||||||
|
"insert_column_before": "Insert 1 column left",
|
||||||
|
"insert_column_after": "Insert 1 column right",
|
||||||
|
"freeze_columns": "Freeze up to column '{{column}}'",
|
||||||
|
"freeze_rows": "Freeze up to row '{{row}}'",
|
||||||
|
"unfreeze_rows": "Unfreeze rows",
|
||||||
|
"unfreeze_columns": "Unfreeze columns",
|
||||||
|
"delete_row": "Delete row '{{row}}'",
|
||||||
|
"delete_column": "Delete column '{{column}}'",
|
||||||
|
"freeze": "Freeze",
|
||||||
|
"insert_row": "Insert row",
|
||||||
|
"insert_column": "Insert column"
|
||||||
|
},
|
||||||
|
"color_picker": {
|
||||||
|
"apply": "Apply"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
webapp/README.md
Normal file
7
webapp/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# WebApp
|
||||||
|
|
||||||
|
The folder app.ironcalc.com contains the frontend and backend code deployed at https://app.ironcalc.com
|
||||||
|
|
||||||
|
The folder IronCalc contains the actual code for the spreadsheet widget as found in:
|
||||||
|
|
||||||
|
https://www.npmjs.com/package/@ironcalc/workbook
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
reverse_proxy /api/* 127.0.0.1:8000
|
reverse_proxy /api/* 127.0.0.1:8000
|
||||||
|
|
||||||
# everything else is the frontend
|
# everything else is the frontend
|
||||||
reverse_proxy :5173
|
reverse_proxy localhost:5173
|
||||||
|
|||||||
5
webapp/app.ironcalc.com/frontend/deploy.sh
Executable file
5
webapp/app.ironcalc.com/frontend/deploy.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
rm -rf dist/*
|
||||||
|
npm run build
|
||||||
|
cd dist/assets && brotli wasm* && brotli index-*
|
||||||
|
cd ..
|
||||||
|
scp -r * app.ironcalc.com:~/app/
|
||||||
780
webapp/app.ironcalc.com/frontend/package-lock.json
generated
780
webapp/app.ironcalc.com/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,17 +13,17 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@ironcalc/workbook": "^0.3.1",
|
"@ironcalc/workbook": "file:../../IronCalc/",
|
||||||
"@mui/material": "^6.3.1",
|
"@mui/material": "^6.4",
|
||||||
"lucide": "^0.469.0",
|
"lucide-react": "^0.473.0",
|
||||||
"lucide-react": "^0.469.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.9.4",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"typescript": "~5.6.2",
|
"typescript": "~5.6.2",
|
||||||
"vite": "^6.0.5",
|
"vite": "^6.0.5",
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ function DeleteWorkbookDialog(properties: DeleteWorkbookDialogProperties) {
|
|||||||
properties.onClose();
|
properties.onClose();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
role="dialog"
|
|
||||||
aria-labelledby="delete-dialog-title"
|
aria-labelledby="delete-dialog-title"
|
||||||
aria-describedby="delete-dialog-description"
|
aria-describedby="delete-dialog-description"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import type { Model } from "@ironcalc/workbook";
|
import type { Model } from "@ironcalc/workbook";
|
||||||
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
|
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
|
||||||
import { CircleCheck } from "lucide-react";
|
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
// import { IronCalcIcon, IronCalcLogo } from "./../icons";
|
|
||||||
import { FileMenu } from "./FileMenu";
|
import { FileMenu } from "./FileMenu";
|
||||||
import { ShareButton } from "./ShareButton";
|
import { ShareButton } from "./ShareButton";
|
||||||
|
import ShareWorkbookDialog from "./ShareWorkbookDialog";
|
||||||
import { WorkbookTitle } from "./WorkbookTitle";
|
import { WorkbookTitle } from "./WorkbookTitle";
|
||||||
import { downloadModel, shareModel } from "./rpc";
|
import { downloadModel } from "./rpc";
|
||||||
import { updateNameSelectedWorkbook } from "./storage";
|
import { updateNameSelectedWorkbook } from "./storage";
|
||||||
|
|
||||||
export function FileBar(properties: {
|
export function FileBar(properties: {
|
||||||
@@ -18,7 +17,8 @@ export function FileBar(properties: {
|
|||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) {
|
}) {
|
||||||
const hiddenInputRef = useRef<HTMLInputElement>(null);
|
const hiddenInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [toast, setToast] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileBarWrapper>
|
<FileBarWrapper>
|
||||||
<StyledDesktopLogo />
|
<StyledDesktopLogo />
|
||||||
@@ -53,37 +53,17 @@ export function FileBar(properties: {
|
|||||||
type="text"
|
type="text"
|
||||||
style={{ position: "absolute", left: -9999, top: -9999 }}
|
style={{ position: "absolute", left: -9999, top: -9999 }}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginLeft: "auto" }}>
|
<div style={{ marginLeft: "auto" }} />
|
||||||
{toast ? (
|
<DialogContainer>
|
||||||
<Toast>
|
<ShareButton onClick={() => setIsDialogOpen(true)} />
|
||||||
<CircleCheck style={{ width: 12 }} />
|
{isDialogOpen && (
|
||||||
<span
|
<ShareWorkbookDialog
|
||||||
style={{ marginLeft: 8, marginRight: 12, fontFamily: "Inter" }}
|
onClose={() => setIsDialogOpen(false)}
|
||||||
>
|
onModelUpload={properties.onModelUpload}
|
||||||
URL copied to clipboard
|
model={properties.model}
|
||||||
</span>
|
/>
|
||||||
</Toast>
|
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</DialogContainer>
|
||||||
<ShareButton
|
|
||||||
onClick={async () => {
|
|
||||||
const model = properties.model;
|
|
||||||
const bytes = model.toBytes();
|
|
||||||
const fileName = model.getName();
|
|
||||||
const hash = await shareModel(bytes, fileName);
|
|
||||||
const value = `${location.origin}/?model=${hash}`;
|
|
||||||
if (hiddenInputRef.current) {
|
|
||||||
hiddenInputRef.current.value = value;
|
|
||||||
hiddenInputRef.current.select();
|
|
||||||
document.execCommand("copy");
|
|
||||||
setToast(true);
|
|
||||||
setTimeout(() => setToast(false), 5000);
|
|
||||||
}
|
|
||||||
console.log(value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FileBarWrapper>
|
</FileBarWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -117,14 +97,6 @@ const HelpButton = styled("div")`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Toast = styled("div")`
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #9e9e9e;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Divider = styled("div")`
|
const Divider = styled("div")`
|
||||||
margin: 0px 8px 0px 16px;
|
margin: 0px 8px 0px 16px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@@ -141,3 +113,17 @@ const FileBarWrapper = styled("div")`
|
|||||||
position: relative;
|
position: relative;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const DialogContainer = styled("div")`
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
button {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.MuiDialog-root {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -110,14 +110,12 @@ export function FileMenu(props: {
|
|||||||
aria-labelledby="modal-modal-title"
|
aria-labelledby="modal-modal-title"
|
||||||
aria-describedby="modal-modal-description"
|
aria-describedby="modal-modal-description"
|
||||||
>
|
>
|
||||||
<>
|
<UploadFileDialog
|
||||||
<UploadFileDialog
|
onClose={() => {
|
||||||
onClose={() => {
|
setImportMenuOpen(false);
|
||||||
setImportMenuOpen(false);
|
}}
|
||||||
}}
|
onModelUpload={props.onModelUpload}
|
||||||
onModelUpload={props.onModelUpload}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
<Modal
|
<Modal
|
||||||
open={isDeleteDialogOpen}
|
open={isDeleteDialogOpen}
|
||||||
@@ -125,13 +123,11 @@ export function FileMenu(props: {
|
|||||||
aria-labelledby="delete-dialog-title"
|
aria-labelledby="delete-dialog-title"
|
||||||
aria-describedby="delete-dialog-description"
|
aria-describedby="delete-dialog-description"
|
||||||
>
|
>
|
||||||
<>
|
<DeleteWorkbookDialog
|
||||||
<DeleteWorkbookDialog
|
onClose={() => setDeleteDialogOpen(false)}
|
||||||
onClose={() => setDeleteDialogOpen(false)}
|
onConfirm={props.onDelete}
|
||||||
onConfirm={props.onDelete}
|
workbookName={selectedUuid ? models[selectedUuid] : ""}
|
||||||
workbookName={selectedUuid ? models[selectedUuid] : ""}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import type { Model } from "@ironcalc/workbook";
|
||||||
|
import { Button, Dialog, TextField, styled } from "@mui/material";
|
||||||
|
import { Check, Copy, GlobeLock } from "lucide-react";
|
||||||
|
import { QRCodeSVG } from "qrcode.react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { shareModel } from "./rpc";
|
||||||
|
|
||||||
|
function ShareWorkbookDialog(properties: {
|
||||||
|
onClose: () => void;
|
||||||
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
|
model?: Model;
|
||||||
|
}) {
|
||||||
|
const [url, setUrl] = useState<string>("");
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generateUrl = async () => {
|
||||||
|
if (properties.model) {
|
||||||
|
const bytes = properties.model.toBytes();
|
||||||
|
const fileName = properties.model.getName();
|
||||||
|
const hash = await shareModel(bytes, fileName);
|
||||||
|
setUrl(`${location.origin}/?model=${hash}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
generateUrl();
|
||||||
|
}, [properties.model]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
|
if (copied) {
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [copied]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
properties.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url);
|
||||||
|
setCopied(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to copy text: ", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogWrapper
|
||||||
|
open={true}
|
||||||
|
tabIndex={0}
|
||||||
|
onClose={handleClose}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.code === "Escape") {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<QRCodeWrapper>
|
||||||
|
<QRCodeSVG value={url} size={80} />{" "}
|
||||||
|
</QRCodeWrapper>
|
||||||
|
<URLWrapper>
|
||||||
|
<StyledTextField
|
||||||
|
hiddenLabel
|
||||||
|
disabled
|
||||||
|
value={url}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<StyledButton
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="small"
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
{copied ? <StyledCheck /> : <StyledCopy />}
|
||||||
|
{copied ? "Copied!" : "Copy URL"}
|
||||||
|
</StyledButton>
|
||||||
|
</URLWrapper>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<UploadFooter>
|
||||||
|
<GlobeLock />
|
||||||
|
Anyone with the link will be able to access a copy of this workbook
|
||||||
|
</UploadFooter>
|
||||||
|
</DialogWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogWrapper = styled(Dialog)`
|
||||||
|
.MuiDialog-paper {
|
||||||
|
width: 440px;
|
||||||
|
position: absolute;
|
||||||
|
top: 44px;
|
||||||
|
right: 0px;
|
||||||
|
margin: 10px;
|
||||||
|
max-width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
.MuiBackdrop-root {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const DialogContent = styled("div")`
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
height: 80px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const URLWrapper = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextField = styled(TextField)`
|
||||||
|
margin: 0px;
|
||||||
|
.MuiInputBase-root {
|
||||||
|
max-height: 36px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
.MuiOutlinedInput-input {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 4px;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
height: 36px;
|
||||||
|
color: #616161;
|
||||||
|
box-shadow: none;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
gap: 10px;
|
||||||
|
&:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background-color: #d4d4d4;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCopy = styled(Copy)`
|
||||||
|
width: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCheck = styled(Check)`
|
||||||
|
width: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const QRCodeWrapper = styled("div")`
|
||||||
|
min-height: 80px;
|
||||||
|
min-width: 80px;
|
||||||
|
background-color: grey;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UploadFooter = styled("div")`
|
||||||
|
height: 44px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #757575;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: Inter;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0px 12px;
|
||||||
|
svg {
|
||||||
|
max-width: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default ShareWorkbookDialog;
|
||||||
@@ -96,7 +96,6 @@ function UploadFileDialog(properties: {
|
|||||||
<DialogWrapper
|
<DialogWrapper
|
||||||
open={true}
|
open={true}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="dialog"
|
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.code === "Escape") {
|
if (event.code === "Escape") {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use ironcalc::compare::{test_file, test_load_and_saving};
|
|||||||
use ironcalc::export::save_to_xlsx;
|
use ironcalc::export::save_to_xlsx;
|
||||||
use ironcalc::import::{load_from_icalc, load_from_xlsx, load_from_xlsx_bytes};
|
use ironcalc::import::{load_from_icalc, load_from_xlsx, load_from_xlsx_bytes};
|
||||||
use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment};
|
use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment};
|
||||||
use ironcalc_base::Model;
|
use ironcalc_base::{Model, UserModel};
|
||||||
|
|
||||||
// This is a functional test.
|
// This is a functional test.
|
||||||
// We check that the output of example.xlsx is what we expect.
|
// We check that the output of example.xlsx is what we expect.
|
||||||
@@ -496,3 +496,17 @@ fn test_documentation_xlsx() {
|
|||||||
}
|
}
|
||||||
fs::remove_dir_all(&dir).unwrap();
|
fs::remove_dir_all(&dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_user_model() {
|
||||||
|
let temp_file_name = "temp_file_test_user_model.xlsx";
|
||||||
|
let mut model = UserModel::new_empty("my_model", "en", "UTC").unwrap();
|
||||||
|
model.set_user_input(0, 1, 1, "=1+1").unwrap();
|
||||||
|
|
||||||
|
// test we can use `get_model` to save the model
|
||||||
|
save_to_xlsx(model.get_model(), temp_file_name).unwrap();
|
||||||
|
fs::remove_file(temp_file_name).unwrap();
|
||||||
|
|
||||||
|
// we can still use the model afterwards
|
||||||
|
model.set_rows_height(0, 1, 1, 100.0).unwrap();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user