Compare commits
1 Commits
feature/ni
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afecf29356 |
18
.github/workflows/publish-wiki.yml
vendored
18
.github/workflows/publish-wiki.yml
vendored
@@ -1,18 +0,0 @@
|
|||||||
name: Publish wiki
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- wiki/**
|
|
||||||
- .github/workflows/publish-wiki.yml
|
|
||||||
concurrency:
|
|
||||||
group: publish-wiki
|
|
||||||
cancel-in-progress: true
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
jobs:
|
|
||||||
publish-wiki:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: Andrew-Chen-Wang/github-wiki-action@v4
|
|
||||||
3
.github/workflows/rust-build-test.yaml
vendored
3
.github/workflows/rust-build-test.yaml
vendored
@@ -16,9 +16,6 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install wasm-pack
|
|
||||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release --verbose
|
run: cargo build --release --verbose
|
||||||
|
|
||||||
|
|||||||
597
Cargo.lock
generated
597
Cargo.lock
generated
@@ -8,22 +8,11 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aes"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cipher",
|
|
||||||
"cpufeatures",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.3"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -45,57 +34,34 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.2.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "bincode"
|
||||||
version = "1.6.0"
|
version = "2.0.0-rc.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bitcode"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "48bc1c27654127a24c476d40198746860ef56475f41a601bfa5c4d0f832968f0"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcode_derive",
|
"bincode_derive",
|
||||||
"bytemuck",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcode_derive"
|
name = "bincode_derive"
|
||||||
version = "0.6.0"
|
version = "2.0.0-rc.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2966755a19aad59ee2aae91e2d48842c667a99d818ec72168efdab07200701cc"
|
checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"virtue",
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-buffer"
|
|
||||||
version = "0.10.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.15.4"
|
version = "3.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytemuck"
|
|
||||||
version = "1.15.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
@@ -126,11 +92,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.90"
|
version = "1.0.83"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -142,9 +107,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.37"
|
version = "0.4.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
|
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
@@ -156,9 +121,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono-tz"
|
name = "chrono-tz"
|
||||||
version = "0.9.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
|
checksum = "bbc529705a6e0028189c83f0a5dd9fb214105116f7e3c0eeab7ff0369766b0d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz-build",
|
"chrono-tz-build",
|
||||||
@@ -167,106 +132,35 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono-tz-build"
|
name = "chrono-tz-build"
|
||||||
version = "0.3.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
|
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"parse-zoneinfo",
|
"parse-zoneinfo",
|
||||||
"phf",
|
"phf",
|
||||||
"phf_codegen",
|
"phf_codegen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cipher"
|
|
||||||
version = "0.4.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
|
||||||
dependencies = [
|
|
||||||
"crypto-common",
|
|
||||||
"inout",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "console_error_panic_hook"
|
|
||||||
version = "0.1.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "constant_time_eq"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.6"
|
version = "0.8.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cpufeatures"
|
|
||||||
version = "0.2.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.4.0"
|
version = "1.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
|
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-utils"
|
|
||||||
version = "0.8.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crypto-common"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
"typenum",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "deranged"
|
|
||||||
version = "0.3.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
|
||||||
dependencies = [
|
|
||||||
"powerfmt",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "digest"
|
|
||||||
version = "0.10.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|
||||||
dependencies = [
|
|
||||||
"block-buffer",
|
|
||||||
"crypto-common",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.10.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
@@ -278,41 +172,22 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "generic-array"
|
|
||||||
version = "0.14.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.13"
|
version = "0.2.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a06fddc2749e0528d2813f95e050e87e52c8cbbae56223b9babf73b3e53b0cc6"
|
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hmac"
|
|
||||||
version = "0.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
|
||||||
dependencies = [
|
|
||||||
"digest",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.60"
|
version = "0.1.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android_system_properties",
|
"android_system_properties",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
@@ -331,20 +206,11 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "inout"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
|
||||||
dependencies = [
|
|
||||||
"generic-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironcalc"
|
name = "ironcalc"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"ironcalc_base",
|
"ironcalc_base",
|
||||||
"itertools",
|
"itertools",
|
||||||
@@ -358,9 +224,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ironcalc_base"
|
name = "ironcalc_base"
|
||||||
version = "0.1.3"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcode",
|
"bincode",
|
||||||
"chrono",
|
"chrono",
|
||||||
"chrono-tz",
|
"chrono-tz",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -375,84 +241,69 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.10.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jobserver"
|
|
||||||
version = "0.1.28"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.69"
|
version = "0.3.65"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.153"
|
version = "0.2.150"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.21"
|
version = "0.4.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.2"
|
version = "2.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.7.2"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
|
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler",
|
"adler",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-conv"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.18"
|
version = "0.2.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.19.0"
|
version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parse-zoneinfo"
|
name = "parse-zoneinfo"
|
||||||
@@ -463,29 +314,6 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "password-hash"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
|
|
||||||
dependencies = [
|
|
||||||
"base64ct",
|
|
||||||
"rand_core",
|
|
||||||
"subtle",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pbkdf2"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
|
|
||||||
dependencies = [
|
|
||||||
"digest",
|
|
||||||
"hmac",
|
|
||||||
"password-hash",
|
|
||||||
"sha2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
@@ -526,15 +354,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.30"
|
version = "0.3.27"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "powerfmt"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
@@ -544,18 +366,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.79"
|
version = "1.0.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
|
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.35"
|
version = "1.0.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -592,9 +414,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.4"
|
version = "1.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -604,9 +426,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.6"
|
version = "0.4.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -615,53 +437,39 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.3"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.19.0"
|
version = "0.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671"
|
||||||
|
dependencies = [
|
||||||
|
"xmlparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.17"
|
version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scoped-tls"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.197"
|
version = "1.0.192"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde-wasm-bindgen"
|
|
||||||
version = "0.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
|
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"serde",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.197"
|
version = "1.0.192"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -670,9 +478,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.115"
|
version = "1.0.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
|
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
@@ -681,54 +489,26 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.18"
|
version = "0.1.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sha1"
|
|
||||||
version = "0.10.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"digest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sha2"
|
|
||||||
version = "0.10.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"digest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "subtle"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.58"
|
version = "2.0.39"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
|
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -737,18 +517,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.58"
|
version = "1.0.50"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.58"
|
version = "1.0.50"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -757,29 +537,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.34"
|
version = "0.1.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
|
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"libc",
|
||||||
"num-conv",
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
"powerfmt",
|
"winapi",
|
||||||
"serde",
|
|
||||||
"time-core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time-core"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typenum"
|
|
||||||
version = "1.17.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
@@ -788,19 +554,25 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.8.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "virtue"
|
||||||
version = "0.9.4"
|
version = "0.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
@@ -808,22 +580,11 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm"
|
|
||||||
version = "0.1.3"
|
|
||||||
dependencies = [
|
|
||||||
"ironcalc_base",
|
|
||||||
"serde",
|
|
||||||
"serde-wasm-bindgen",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-test",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.92"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
@@ -831,9 +592,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.92"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
@@ -844,23 +605,11 @@ dependencies = [
|
|||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-futures"
|
|
||||||
version = "0.4.42"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"web-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.92"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -868,9 +617,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.92"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -881,59 +630,46 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.92"
|
version = "0.2.88"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-test"
|
name = "winapi"
|
||||||
version = "0.3.42"
|
version = "0.3.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b"
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"console_error_panic_hook",
|
"winapi-i686-pc-windows-gnu",
|
||||||
"js-sys",
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
"scoped-tls",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"wasm-bindgen-futures",
|
|
||||||
"wasm-bindgen-test-macro",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-test-macro"
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
version = "0.3.42"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.3.69"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
dependencies = [
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.52.0"
|
version = "0.51.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-targets"
|
name = "windows-targets"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
|
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows_aarch64_gnullvm",
|
"windows_aarch64_gnullvm",
|
||||||
"windows_aarch64_msvc",
|
"windows_aarch64_msvc",
|
||||||
@@ -946,91 +682,62 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
|
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
|
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
|
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
|
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
|
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
|
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.52.4"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xmlparser"
|
||||||
|
version = "0.13.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "0.6.6"
|
version = "0.5.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
|
checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"bzip2",
|
"bzip2",
|
||||||
"constant_time_eq",
|
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"crossbeam-utils",
|
|
||||||
"flate2",
|
"flate2",
|
||||||
"hmac",
|
"thiserror",
|
||||||
"pbkdf2",
|
|
||||||
"sha1",
|
|
||||||
"time",
|
"time",
|
||||||
"zstd",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zstd"
|
|
||||||
version = "0.11.2+zstd.1.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
|
|
||||||
dependencies = [
|
|
||||||
"zstd-safe",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zstd-safe"
|
|
||||||
version = "5.0.2+zstd.1.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"zstd-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zstd-sys"
|
|
||||||
version = "2.0.10+zstd.1.5.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"base",
|
"base",
|
||||||
"xlsx",
|
"xlsx",
|
||||||
"bindings/wasm",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
exclude = [
|
exclude = [
|
||||||
|
|||||||
24
Makefile
24
Makefile
@@ -1,48 +1,32 @@
|
|||||||
all:
|
|
||||||
cargo build --release
|
|
||||||
cd bindings/wasm/ && make
|
|
||||||
cd webapp && npm install && npm run build
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
cargo fmt -- --check
|
cargo fmt -- --check
|
||||||
# TODO: See issue #33
|
cargo clippy --all-targets --all-features
|
||||||
# cargo clippy --all-targets --all-features -- -D warnings -D clippy::expect_used -D clippy::unwrap_used -D clippy::panic
|
|
||||||
cargo clippy --all-targets --all-features -- -D warnings
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
cargo fmt
|
cargo fmt
|
||||||
|
|
||||||
tests: lint
|
tests: lint
|
||||||
cargo test
|
cargo test
|
||||||
./target/debug/documentation
|
make remove-xlsx
|
||||||
cmp functions.md wiki/functions.md || exit 1
|
|
||||||
make remove-artifacts
|
|
||||||
cd bindings/wasm/ && make tests
|
|
||||||
|
|
||||||
remove-artifacts:
|
remove-xlsx:
|
||||||
rm -f xlsx/hello-calc.xlsx
|
rm -f xlsx/hello-calc.xlsx
|
||||||
rm -f xlsx/hello-styles.xlsx
|
rm -f xlsx/hello-styles.xlsx
|
||||||
rm -f xlsx/widths-and-heights.xlsx
|
rm -f xlsx/widths-and-heights.xlsx
|
||||||
rm -f functions.md
|
|
||||||
|
|
||||||
clean: remove-artifacts
|
clean: remove-xlsx
|
||||||
cargo clean
|
cargo clean
|
||||||
rm -r -f base/target
|
rm -r -f base/target
|
||||||
rm -r -f xlsx/target
|
rm -r -f xlsx/target
|
||||||
rm -f cargo-test-*
|
rm -f cargo-test-*
|
||||||
rm -f base/cargo-test-*
|
rm -f base/cargo-test-*
|
||||||
rm -f xlsx/cargo-test-*
|
rm -f xlsx/cargo-test-*
|
||||||
rm -r -f webapp/node_modules
|
|
||||||
|
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
CARGO_INCREMENTAL=0 RUSTFLAGS='-C instrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
|
CARGO_INCREMENTAL=0 RUSTFLAGS='-C instrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test
|
||||||
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
|
grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html
|
||||||
|
|
||||||
update-docs:
|
|
||||||
cargo build
|
|
||||||
./target/debug/documentation -o wiki/functions.md
|
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
cargo doc --no-deps
|
cargo doc --no-deps
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ironcalc_base"
|
name = "ironcalc_base"
|
||||||
version = "0.1.3"
|
version = "0.1.2"
|
||||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
homepage = "https://www.ironcalc.com"
|
homepage = "https://www.ironcalc.com"
|
||||||
@@ -16,15 +16,15 @@ serde_json = "1.0"
|
|||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
ryu = "1.0"
|
ryu = "1.0"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
chrono-tz = "0.9"
|
chrono-tz = "0.7.0"
|
||||||
regex = "1.0"
|
regex = "1.0"
|
||||||
once_cell = "1.16.0"
|
once_cell = "1.16.0"
|
||||||
bitcode = "0.6.0"
|
bincode = "=2.0.0-rc.3"
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
js-sys = { version = "0.3.69" }
|
js-sys = { version = "0.3.60" }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
rand = "0.8.5"
|
rand = "0.8.4"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use ironcalc_base::{types::CellType, Model};
|
use ironcalc_base::{model::Model, types::CellType};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?;
|
let mut model = Model::new_empty("formulas-and-errors", "en", "UTC")?;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use ironcalc_base::{cell::CellValue, Model};
|
use ironcalc_base::{cell::CellValue, model::Model};
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut model = Model::new_empty("hello-world", "en", "UTC")?;
|
let mut model = Model::new_empty("hello-world", "en", "UTC")?;
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ impl Model {
|
|||||||
let style = source_cell.get_style();
|
let style = source_cell.get_style();
|
||||||
// FIXME: we need some user_input getter instead of get_text
|
// FIXME: we need some user_input getter instead of get_text
|
||||||
let formula_or_value = self
|
let formula_or_value = self
|
||||||
.get_cell_formula(sheet, source_row, source_column)?
|
.cell_formula(sheet, source_row, source_column)?
|
||||||
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
|
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||||
self.workbook
|
self.workbook
|
||||||
.worksheet_mut(sheet)?
|
.worksheet_mut(sheet)?
|
||||||
.set_cell_style(target_row, target_column, style);
|
.set_cell_style(target_row, target_column, style);
|
||||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
self.delete_cell(sheet, source_row, source_column)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,11 +157,6 @@ impl Model {
|
|||||||
return Err("Please use insert columns instead".to_string());
|
return Err("Please use insert columns instead".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// first column being deleted
|
|
||||||
let column_start = column;
|
|
||||||
// last column being deleted
|
|
||||||
let column_end = column + column_count - 1;
|
|
||||||
|
|
||||||
// Move cells
|
// Move cells
|
||||||
let worksheet = &self.workbook.worksheet(sheet)?;
|
let worksheet = &self.workbook.worksheet(sheet)?;
|
||||||
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||||
@@ -171,11 +166,11 @@ impl Model {
|
|||||||
for r in all_rows {
|
for r in all_rows {
|
||||||
let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
|
let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
|
||||||
for col in columns {
|
for col in columns {
|
||||||
if col >= column_start {
|
if col >= column {
|
||||||
if col > column_end {
|
if col >= column + column_count {
|
||||||
self.move_cell(sheet, r, col, r, col - column_count)?;
|
self.move_cell(sheet, r, col, r, col - column_count)?;
|
||||||
} else {
|
} else {
|
||||||
self.cell_clear_all(sheet, r, col)?;
|
self.delete_cell(sheet, r, col)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,64 +184,6 @@ impl Model {
|
|||||||
delta: -column_count,
|
delta: -column_count,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
|
||||||
|
|
||||||
// deletes all the column styles
|
|
||||||
let mut new_columns = Vec::new();
|
|
||||||
for col in worksheet.cols.iter_mut() {
|
|
||||||
// range under study
|
|
||||||
let min = col.min;
|
|
||||||
let max = col.max;
|
|
||||||
// In the diagram:
|
|
||||||
// |xxxxx| range we are studying [min, max]
|
|
||||||
// |*****| range we are deleting [column_start, column_end]
|
|
||||||
// we are going to split it in three big cases:
|
|
||||||
// ----------------|xxxxxxxx|-----------------
|
|
||||||
// -----|*****|------------------------------- Case A
|
|
||||||
// -------|**********|------------------------ Case B
|
|
||||||
// -------------|**************|-------------- Case C
|
|
||||||
// ------------------|****|------------------- Case D
|
|
||||||
// ---------------------|**********|---------- Case E
|
|
||||||
// -----------------------------|*****|------- Case F
|
|
||||||
if column_start < min {
|
|
||||||
if column_end < min {
|
|
||||||
// Case A
|
|
||||||
// We displace all columns
|
|
||||||
let mut new_column = col.clone();
|
|
||||||
new_column.min = min - column_count;
|
|
||||||
new_column.max = max - column_count;
|
|
||||||
new_columns.push(new_column);
|
|
||||||
} else if column_end < max {
|
|
||||||
// Case B
|
|
||||||
// We displace the end
|
|
||||||
let mut new_column = col.clone();
|
|
||||||
new_column.min = column_start;
|
|
||||||
new_column.max = max - column_count;
|
|
||||||
new_columns.push(new_column);
|
|
||||||
} else {
|
|
||||||
// Case C
|
|
||||||
// skip this, we are deleting the whole range
|
|
||||||
}
|
|
||||||
} else if column_start <= max {
|
|
||||||
if column_end <= max {
|
|
||||||
// Case D
|
|
||||||
// We displace the end
|
|
||||||
let mut new_column = col.clone();
|
|
||||||
new_column.max = max - column_count;
|
|
||||||
new_columns.push(new_column);
|
|
||||||
} else {
|
|
||||||
// Case E
|
|
||||||
let mut new_column = col.clone();
|
|
||||||
new_column.max = column_start - 1;
|
|
||||||
new_columns.push(new_column);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Case F
|
|
||||||
// No action required
|
|
||||||
new_columns.push(col.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
worksheet.cols = new_columns;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -346,7 +283,7 @@ impl Model {
|
|||||||
// remove all cells in row
|
// remove all cells in row
|
||||||
// FIXME: We could just remove the entire row in one go
|
// FIXME: We could just remove the entire row in one go
|
||||||
for column in columns {
|
for column in columns {
|
||||||
self.cell_clear_all(sheet, r, column)?;
|
self.delete_cell(sheet, r, column)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,9 +308,9 @@ impl Lexer {
|
|||||||
return self.consume_range(None);
|
return self.consume_range(None);
|
||||||
}
|
}
|
||||||
let name_upper = name.to_ascii_uppercase();
|
let name_upper = name.to_ascii_uppercase();
|
||||||
if name_upper == self.language.booleans.r#true {
|
if name_upper == self.language.booleans.true_value {
|
||||||
return TokenType::Boolean(true);
|
return TokenType::Boolean(true);
|
||||||
} else if name_upper == self.language.booleans.r#false {
|
} else if name_upper == self.language.booleans.false_value {
|
||||||
return TokenType::Boolean(false);
|
return TokenType::Boolean(false);
|
||||||
}
|
}
|
||||||
if self.mode == LexerMode::A1 {
|
if self.mode == LexerMode::A1 {
|
||||||
@@ -660,8 +660,8 @@ impl Lexer {
|
|||||||
fn consume_error(&mut self) -> TokenType {
|
fn consume_error(&mut self) -> TokenType {
|
||||||
let errors = &self.language.errors;
|
let errors = &self.language.errors;
|
||||||
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
|
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
|
||||||
if rest_of_formula.starts_with(&errors.r#ref) {
|
if rest_of_formula.starts_with(&errors.ref_value) {
|
||||||
self.position += errors.r#ref.chars().count() - 1;
|
self.position += errors.ref_value.chars().count() - 1;
|
||||||
return TokenType::Error(Error::REF);
|
return TokenType::Error(Error::REF);
|
||||||
} else if rest_of_formula.starts_with(&errors.name) {
|
} else if rest_of_formula.starts_with(&errors.name) {
|
||||||
self.position += errors.name.chars().count() - 1;
|
self.position += errors.name.chars().count() - 1;
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ use crate::{
|
|||||||
token::TokenType,
|
token::TokenType,
|
||||||
},
|
},
|
||||||
language::get_language,
|
language::get_language,
|
||||||
locale::get_locale,
|
locale::get_locale_fix,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
|
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
|
||||||
let locale = get_locale(locale).unwrap();
|
let locale = get_locale_fix(locale).unwrap();
|
||||||
let language = get_language(language).unwrap();
|
let language = get_language(language).unwrap();
|
||||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use bitcode::{Decode, Encode};
|
use bincode::{Decode, Encode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ impl fmt::Display for OpProduct {
|
|||||||
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
|
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
|
||||||
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
|
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
|
||||||
/// Note that they are serialized/deserialized by index
|
/// Note that they are serialized/deserialized by index
|
||||||
#[derive(Serialize_repr, Deserialize_repr, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize_repr, Deserialize_repr, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
REF,
|
REF,
|
||||||
@@ -120,7 +120,7 @@ impl Error {
|
|||||||
pub fn to_localized_error_string(&self, language: &Language) -> String {
|
pub fn to_localized_error_string(&self, language: &Language) -> String {
|
||||||
match self {
|
match self {
|
||||||
Error::NULL => language.errors.null.to_string(),
|
Error::NULL => language.errors.null.to_string(),
|
||||||
Error::REF => language.errors.r#ref.to_string(),
|
Error::REF => language.errors.ref_value.to_string(),
|
||||||
Error::NAME => language.errors.name.to_string(),
|
Error::NAME => language.errors.name.to_string(),
|
||||||
Error::VALUE => language.errors.value.to_string(),
|
Error::VALUE => language.errors.value.to_string(),
|
||||||
Error::DIV => language.errors.div.to_string(),
|
Error::DIV => language.errors.div.to_string(),
|
||||||
@@ -137,7 +137,7 @@ impl Error {
|
|||||||
|
|
||||||
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
|
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
|
||||||
let errors = &language.errors;
|
let errors = &language.errors;
|
||||||
if name == errors.r#ref {
|
if name == errors.ref_value {
|
||||||
return Some(Error::REF);
|
return Some(Error::REF);
|
||||||
} else if name == errors.name {
|
} else if name == errors.name {
|
||||||
return Some(Error::NAME);
|
return Some(Error::NAME);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use chrono::DateTime;
|
|
||||||
use chrono::Datelike;
|
use chrono::Datelike;
|
||||||
use chrono::Months;
|
use chrono::Months;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use chrono::TimeZone;
|
||||||
use chrono::Timelike;
|
use chrono::Timelike;
|
||||||
|
|
||||||
use crate::expressions::types::CellReferenceIndex;
|
use crate::expressions::types::CellReferenceIndex;
|
||||||
@@ -257,8 +258,8 @@ impl Model {
|
|||||||
// milliseconds since January 1, 1970 00:00:00 UTC.
|
// milliseconds since January 1, 1970 00:00:00 UTC.
|
||||||
let milliseconds = get_milliseconds_since_epoch();
|
let milliseconds = get_milliseconds_since_epoch();
|
||||||
let seconds = milliseconds / 1000;
|
let seconds = milliseconds / 1000;
|
||||||
let local_time = match DateTime::from_timestamp(seconds, 0) {
|
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||||
Some(dt) => dt.with_timezone(&self.tz),
|
Some(dt) => dt,
|
||||||
None => {
|
None => {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
@@ -267,6 +268,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let local_time = self.tz.from_utc_datetime(&dt);
|
||||||
// 693_594 is computed as:
|
// 693_594 is computed as:
|
||||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||||
// The 2 days offset is because of Excel 1900 bug
|
// The 2 days offset is because of Excel 1900 bug
|
||||||
@@ -287,8 +289,8 @@ impl Model {
|
|||||||
// milliseconds since January 1, 1970 00:00:00 UTC.
|
// milliseconds since January 1, 1970 00:00:00 UTC.
|
||||||
let milliseconds = get_milliseconds_since_epoch();
|
let milliseconds = get_milliseconds_since_epoch();
|
||||||
let seconds = milliseconds / 1000;
|
let seconds = milliseconds / 1000;
|
||||||
let local_time = match DateTime::from_timestamp(seconds, 0) {
|
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||||
Some(dt) => dt.with_timezone(&self.tz),
|
Some(dt) => dt,
|
||||||
None => {
|
None => {
|
||||||
return CalcResult::Error {
|
return CalcResult::Error {
|
||||||
error: Error::ERROR,
|
error: Error::ERROR,
|
||||||
@@ -297,6 +299,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let local_time = self.tz.from_utc_datetime(&dt);
|
||||||
// 693_594 is computed as:
|
// 693_594 is computed as:
|
||||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||||
// The 2 days offset is because of Excel 1900 bug
|
// The 2 days offset is because of Excel 1900 bug
|
||||||
|
|||||||
@@ -165,8 +165,7 @@ impl Model {
|
|||||||
message: "argument must be a reference to a single cell".to_string(),
|
message: "argument must be a reference to a single cell".to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let is_formula = if let Ok(f) = self.get_cell_formula(left.sheet, left.row, left.column)
|
let is_formula = if let Ok(f) = self.cell_formula(left.sheet, left.row, left.column) {
|
||||||
{
|
|
||||||
f.is_some()
|
f.is_some()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use core::fmt;
|
use core::fmt;
|
||||||
use std::array::IntoIter;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
calc_result::CalcResult,
|
calc_result::CalcResult,
|
||||||
@@ -245,206 +244,6 @@ pub enum Function {
|
|||||||
Subtotal,
|
Subtotal,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Function {
|
|
||||||
pub fn into_iter() -> IntoIter<Function, 192> {
|
|
||||||
[
|
|
||||||
Function::And,
|
|
||||||
Function::False,
|
|
||||||
Function::If,
|
|
||||||
Function::Iferror,
|
|
||||||
Function::Ifna,
|
|
||||||
Function::Ifs,
|
|
||||||
Function::Not,
|
|
||||||
Function::Or,
|
|
||||||
Function::Switch,
|
|
||||||
Function::True,
|
|
||||||
Function::Xor,
|
|
||||||
Function::Sin,
|
|
||||||
Function::Cos,
|
|
||||||
Function::Tan,
|
|
||||||
Function::Asin,
|
|
||||||
Function::Acos,
|
|
||||||
Function::Atan,
|
|
||||||
Function::Sinh,
|
|
||||||
Function::Cosh,
|
|
||||||
Function::Tanh,
|
|
||||||
Function::Asinh,
|
|
||||||
Function::Acosh,
|
|
||||||
Function::Atanh,
|
|
||||||
Function::Abs,
|
|
||||||
Function::Pi,
|
|
||||||
Function::Sqrt,
|
|
||||||
Function::Sqrtpi,
|
|
||||||
Function::Atan2,
|
|
||||||
Function::Power,
|
|
||||||
Function::Max,
|
|
||||||
Function::Min,
|
|
||||||
Function::Product,
|
|
||||||
Function::Rand,
|
|
||||||
Function::Randbetween,
|
|
||||||
Function::Round,
|
|
||||||
Function::Rounddown,
|
|
||||||
Function::Roundup,
|
|
||||||
Function::Sum,
|
|
||||||
Function::Sumif,
|
|
||||||
Function::Sumifs,
|
|
||||||
Function::Choose,
|
|
||||||
Function::Column,
|
|
||||||
Function::Columns,
|
|
||||||
Function::Index,
|
|
||||||
Function::Indirect,
|
|
||||||
Function::Hlookup,
|
|
||||||
Function::Lookup,
|
|
||||||
Function::Match,
|
|
||||||
Function::Offset,
|
|
||||||
Function::Row,
|
|
||||||
Function::Rows,
|
|
||||||
Function::Vlookup,
|
|
||||||
Function::Xlookup,
|
|
||||||
Function::Concatenate,
|
|
||||||
Function::Exact,
|
|
||||||
Function::Value,
|
|
||||||
Function::T,
|
|
||||||
Function::Valuetotext,
|
|
||||||
Function::Concat,
|
|
||||||
Function::Find,
|
|
||||||
Function::Left,
|
|
||||||
Function::Len,
|
|
||||||
Function::Lower,
|
|
||||||
Function::Mid,
|
|
||||||
Function::Right,
|
|
||||||
Function::Search,
|
|
||||||
Function::Text,
|
|
||||||
Function::Trim,
|
|
||||||
Function::Upper,
|
|
||||||
Function::Isnumber,
|
|
||||||
Function::Isnontext,
|
|
||||||
Function::Istext,
|
|
||||||
Function::Islogical,
|
|
||||||
Function::Isblank,
|
|
||||||
Function::Iserr,
|
|
||||||
Function::Iserror,
|
|
||||||
Function::Isna,
|
|
||||||
Function::Na,
|
|
||||||
Function::Isref,
|
|
||||||
Function::Isodd,
|
|
||||||
Function::Iseven,
|
|
||||||
Function::ErrorType,
|
|
||||||
Function::Isformula,
|
|
||||||
Function::Type,
|
|
||||||
Function::Sheet,
|
|
||||||
Function::Average,
|
|
||||||
Function::Averagea,
|
|
||||||
Function::Averageif,
|
|
||||||
Function::Averageifs,
|
|
||||||
Function::Count,
|
|
||||||
Function::Counta,
|
|
||||||
Function::Countblank,
|
|
||||||
Function::Countif,
|
|
||||||
Function::Countifs,
|
|
||||||
Function::Maxifs,
|
|
||||||
Function::Minifs,
|
|
||||||
Function::Year,
|
|
||||||
Function::Day,
|
|
||||||
Function::Month,
|
|
||||||
Function::Eomonth,
|
|
||||||
Function::Date,
|
|
||||||
Function::Edate,
|
|
||||||
Function::Today,
|
|
||||||
Function::Now,
|
|
||||||
Function::Pmt,
|
|
||||||
Function::Pv,
|
|
||||||
Function::Rate,
|
|
||||||
Function::Nper,
|
|
||||||
Function::Fv,
|
|
||||||
Function::Ppmt,
|
|
||||||
Function::Ipmt,
|
|
||||||
Function::Npv,
|
|
||||||
Function::Mirr,
|
|
||||||
Function::Irr,
|
|
||||||
Function::Xirr,
|
|
||||||
Function::Xnpv,
|
|
||||||
Function::Rept,
|
|
||||||
Function::Textafter,
|
|
||||||
Function::Textbefore,
|
|
||||||
Function::Textjoin,
|
|
||||||
Function::Substitute,
|
|
||||||
Function::Ispmt,
|
|
||||||
Function::Rri,
|
|
||||||
Function::Sln,
|
|
||||||
Function::Syd,
|
|
||||||
Function::Nominal,
|
|
||||||
Function::Effect,
|
|
||||||
Function::Pduration,
|
|
||||||
Function::Tbillyield,
|
|
||||||
Function::Tbillprice,
|
|
||||||
Function::Tbilleq,
|
|
||||||
Function::Dollarde,
|
|
||||||
Function::Dollarfr,
|
|
||||||
Function::Ddb,
|
|
||||||
Function::Db,
|
|
||||||
Function::Cumprinc,
|
|
||||||
Function::Cumipmt,
|
|
||||||
Function::Besseli,
|
|
||||||
Function::Besselj,
|
|
||||||
Function::Besselk,
|
|
||||||
Function::Bessely,
|
|
||||||
Function::Erf,
|
|
||||||
Function::ErfPrecise,
|
|
||||||
Function::Erfc,
|
|
||||||
Function::ErfcPrecise,
|
|
||||||
Function::Bin2dec,
|
|
||||||
Function::Bin2hex,
|
|
||||||
Function::Bin2oct,
|
|
||||||
Function::Dec2Bin,
|
|
||||||
Function::Dec2hex,
|
|
||||||
Function::Dec2oct,
|
|
||||||
Function::Hex2bin,
|
|
||||||
Function::Hex2dec,
|
|
||||||
Function::Hex2oct,
|
|
||||||
Function::Oct2bin,
|
|
||||||
Function::Oct2dec,
|
|
||||||
Function::Oct2hex,
|
|
||||||
Function::Bitand,
|
|
||||||
Function::Bitlshift,
|
|
||||||
Function::Bitor,
|
|
||||||
Function::Bitrshift,
|
|
||||||
Function::Bitxor,
|
|
||||||
Function::Complex,
|
|
||||||
Function::Imabs,
|
|
||||||
Function::Imaginary,
|
|
||||||
Function::Imargument,
|
|
||||||
Function::Imconjugate,
|
|
||||||
Function::Imcos,
|
|
||||||
Function::Imcosh,
|
|
||||||
Function::Imcot,
|
|
||||||
Function::Imcsc,
|
|
||||||
Function::Imcsch,
|
|
||||||
Function::Imdiv,
|
|
||||||
Function::Imexp,
|
|
||||||
Function::Imln,
|
|
||||||
Function::Imlog10,
|
|
||||||
Function::Imlog2,
|
|
||||||
Function::Impower,
|
|
||||||
Function::Improduct,
|
|
||||||
Function::Imreal,
|
|
||||||
Function::Imsec,
|
|
||||||
Function::Imsech,
|
|
||||||
Function::Imsin,
|
|
||||||
Function::Imsinh,
|
|
||||||
Function::Imsqrt,
|
|
||||||
Function::Imsub,
|
|
||||||
Function::Imsum,
|
|
||||||
Function::Imtan,
|
|
||||||
Function::Convert,
|
|
||||||
Function::Delta,
|
|
||||||
Function::Gestep,
|
|
||||||
Function::Subtotal,
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
/// Some functions in Excel like CONCAT are stringified as `_xlfn.CONCAT`.
|
/// Some functions in Excel like CONCAT are stringified as `_xlfn.CONCAT`.
|
||||||
pub fn to_xlsx_string(&self) -> String {
|
pub fn to_xlsx_string(&self) -> String {
|
||||||
@@ -909,23 +708,7 @@ impl fmt::Display for Function {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Documentation for one function
|
|
||||||
pub struct Documentation {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Model {
|
impl Model {
|
||||||
/// Produces documentation for all implemented functions
|
|
||||||
pub fn documentation() -> Vec<Documentation> {
|
|
||||||
let mut doc = Vec::new();
|
|
||||||
for function in Function::into_iter() {
|
|
||||||
doc.push(Documentation {
|
|
||||||
name: function.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
doc
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn evaluate_function(
|
pub(crate) fn evaluate_function(
|
||||||
&mut self,
|
&mut self,
|
||||||
kind: &Function,
|
kind: &Function,
|
||||||
@@ -1145,66 +928,3 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::{
|
|
||||||
fs::File,
|
|
||||||
io::{BufRead, BufReader},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::functions::Function;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn function_iterator() {
|
|
||||||
// This checks that the number of functions in the enum is the same
|
|
||||||
// as the number of functions in the Iterator.
|
|
||||||
|
|
||||||
// This is tricky. In Rust we cannot loop over all the members of an enum.
|
|
||||||
// There are alternatives like using an external crate like strum.
|
|
||||||
// But I am not in the mood for that.
|
|
||||||
|
|
||||||
// What we do here is read this file , extract the functions in the enum
|
|
||||||
// and check they are the same as in the iterator
|
|
||||||
let file = File::open("src/functions/mod.rs").unwrap();
|
|
||||||
let reader = BufReader::new(file);
|
|
||||||
let mut start = false;
|
|
||||||
let mut list = Vec::new();
|
|
||||||
|
|
||||||
for line in reader.lines() {
|
|
||||||
let text = line.unwrap();
|
|
||||||
let text = text.trim().trim_end_matches(',');
|
|
||||||
if text == "pub enum Function {" {
|
|
||||||
start = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if start {
|
|
||||||
if text == "}" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if text.starts_with("//") {
|
|
||||||
// skip comments
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if text.is_empty() {
|
|
||||||
// skip empty lines
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
list.push(text.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
|
|
||||||
let iter_list = Function::into_iter()
|
|
||||||
.map(|f| format!("{}", f).replace('.', ""))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let len = iter_list.len();
|
|
||||||
|
|
||||||
assert_eq!(list.len(), len);
|
|
||||||
// We still need to check there are no duplicates. This will fail if a function in iter_list
|
|
||||||
// is included twice and one is missing
|
|
||||||
for function in list {
|
|
||||||
assert!(iter_list.contains(&function.to_uppercase()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Booleans {
|
pub struct Booleans {
|
||||||
pub r#true: String,
|
#[serde(rename = "true")]
|
||||||
pub r#false: String,
|
pub true_value: String,
|
||||||
|
#[serde(rename = "false")]
|
||||||
|
pub false_value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct Errors {
|
pub struct Errors {
|
||||||
pub r#ref: String,
|
#[serde(rename = "ref")]
|
||||||
|
pub ref_value: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub div: String,
|
pub div: String,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! [dependencies]
|
//! [dependencies]
|
||||||
//! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc" }
|
//! ironcalc_base = { git = "https://github.com/ironcalc/IronCalc", version = "0.1"}
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! <small> until version 0.5.0 you should use the git dependencies as stated </small>
|
//! <small> until version 0.5.0 you should use the git dependencies as stated </small>
|
||||||
@@ -31,21 +31,23 @@ pub mod expressions;
|
|||||||
pub mod formatter;
|
pub mod formatter;
|
||||||
pub mod language;
|
pub mod language;
|
||||||
pub mod locale;
|
pub mod locale;
|
||||||
|
pub mod model;
|
||||||
pub mod new_empty;
|
pub mod new_empty;
|
||||||
pub mod number_format;
|
pub mod number_format;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod worksheet;
|
pub mod worksheet;
|
||||||
|
|
||||||
|
mod functions;
|
||||||
|
|
||||||
mod actions;
|
mod actions;
|
||||||
mod cast;
|
mod cast;
|
||||||
mod constants;
|
mod constants;
|
||||||
mod diffs;
|
|
||||||
mod functions;
|
|
||||||
mod implicit_intersection;
|
|
||||||
mod model;
|
|
||||||
mod styles;
|
mod styles;
|
||||||
|
|
||||||
|
mod diffs;
|
||||||
|
mod implicit_intersection;
|
||||||
|
|
||||||
mod units;
|
mod units;
|
||||||
mod user_model;
|
|
||||||
mod utils;
|
mod utils;
|
||||||
mod workbook;
|
mod workbook;
|
||||||
|
|
||||||
@@ -54,7 +56,3 @@ mod test;
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod mock_time;
|
pub mod mock_time;
|
||||||
|
|
||||||
pub use model::get_milliseconds_since_epoch;
|
|
||||||
pub use model::Model;
|
|
||||||
pub use user_model::UserModel;
|
|
||||||
|
|||||||
@@ -80,8 +80,14 @@ static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| {
|
|||||||
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
|
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
|
||||||
});
|
});
|
||||||
|
|
||||||
pub fn get_locale(id: &str) -> Result<&Locale, String> {
|
pub fn get_locale(_id: &str) -> Result<&Locale, String> {
|
||||||
// TODO: pass the locale once we implement locales in Rust
|
// TODO: pass the locale once we implement locales in Rust
|
||||||
|
let locale = LOCALES.get("en").ok_or("Invalid locale")?;
|
||||||
|
Ok(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this function one we implement locales properly
|
||||||
|
pub fn get_locale_fix(id: &str) -> Result<&Locale, String> {
|
||||||
let locale = LOCALES.get(id).ok_or("Invalid locale")?;
|
let locale = LOCALES.get(id).ok_or("Invalid locale")?;
|
||||||
Ok(locale)
|
Ok(locale)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::cell::RefCell;
|
|||||||
// 8 November 2022 12:13 Berlin time
|
// 8 November 2022 12:13 Berlin time
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static MOCK_TIME: RefCell<i64> = const { RefCell::new(1667906008578) };
|
static MOCK_TIME: RefCell<i64> = RefCell::new(1667906008578);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_milliseconds_since_epoch() -> i64 {
|
pub fn get_milliseconds_since_epoch() -> i64 {
|
||||||
@@ -20,18 +20,3 @@ pub fn get_milliseconds_since_epoch() -> i64 {
|
|||||||
pub fn set_mock_time(time: i64) {
|
pub fn set_mock_time(time: i64) {
|
||||||
MOCK_TIME.with(|cell| *cell.borrow_mut() = time);
|
MOCK_TIME.with(|cell| *cell.borrow_mut() = time);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::mock_time::MOCK_TIME;
|
|
||||||
|
|
||||||
use super::get_milliseconds_since_epoch;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mock_time() {
|
|
||||||
let t = get_milliseconds_since_epoch();
|
|
||||||
assert_eq!(t, 1667906008578);
|
|
||||||
|
|
||||||
MOCK_TIME.with_borrow(|v| assert_eq!(*v, 1667906008578));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
//! # Model
|
||||||
|
//!
|
||||||
|
//! Note that sheets are 0-indexed and rows and columns are 1-indexed.
|
||||||
|
//!
|
||||||
|
//! IronCalc is row first. A cell is referenced by (`sheet`, `row`, `column`)
|
||||||
|
//!
|
||||||
|
|
||||||
|
use bincode::config;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::vec::Vec;
|
use std::vec::Vec;
|
||||||
|
|
||||||
@@ -38,11 +48,7 @@ use chrono_tz::Tz;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use crate::mock_time::get_milliseconds_since_epoch;
|
pub use crate::mock_time::get_milliseconds_since_epoch;
|
||||||
|
|
||||||
/// Number of milliseconds since January 1, 1970
|
/// wasm implementation for time
|
||||||
/// Used by time and date functions. It takes the value from the environment:
|
|
||||||
/// * The Operative System
|
|
||||||
/// * The JavaScript environment
|
|
||||||
/// * Or mocked for tests
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn get_milliseconds_since_epoch() -> i64 {
|
pub fn get_milliseconds_since_epoch() -> i64 {
|
||||||
@@ -53,11 +59,6 @@ pub fn get_milliseconds_since_epoch() -> i64 {
|
|||||||
.as_millis() as i64
|
.as_millis() as i64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Number of milliseconds since January 1, 1970
|
|
||||||
/// Used by time and date functions. It takes the value from the environment:
|
|
||||||
/// * The Operative System
|
|
||||||
/// * The JavaScript environment
|
|
||||||
/// * Or mocked for tests
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn get_milliseconds_since_epoch() -> i64 {
|
pub fn get_milliseconds_since_epoch() -> i64 {
|
||||||
@@ -67,7 +68,7 @@ pub fn get_milliseconds_since_epoch() -> i64 {
|
|||||||
|
|
||||||
/// A cell might be evaluated or being evaluated
|
/// A cell might be evaluated or being evaluated
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) enum CellState {
|
pub enum CellState {
|
||||||
/// The cell has already been evaluated
|
/// The cell has already been evaluated
|
||||||
Evaluated,
|
Evaluated,
|
||||||
/// The cell is being evaluated
|
/// The cell is being evaluated
|
||||||
@@ -75,7 +76,7 @@ pub(crate) enum CellState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A parsed formula for a defined name
|
/// A parsed formula for a defined name
|
||||||
pub(crate) enum ParsedDefinedName {
|
pub enum ParsedDefinedName {
|
||||||
/// CellReference (`=C4`)
|
/// CellReference (`=C4`)
|
||||||
CellReference(CellReferenceIndex),
|
CellReference(CellReferenceIndex),
|
||||||
/// A Range (`=C4:D6`)
|
/// A Range (`=C4:D6`)
|
||||||
@@ -105,19 +106,19 @@ pub struct Model {
|
|||||||
/// A list of parsed formulas
|
/// A list of parsed formulas
|
||||||
pub parsed_formulas: Vec<Vec<Node>>,
|
pub parsed_formulas: Vec<Vec<Node>>,
|
||||||
/// A list of parsed defined names
|
/// A list of parsed defined names
|
||||||
pub(crate) parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
|
pub parsed_defined_names: HashMap<(Option<u32>, String), ParsedDefinedName>,
|
||||||
/// An optimization to lookup strings faster
|
/// An optimization to lookup strings faster
|
||||||
pub(crate) shared_strings: HashMap<String, usize>,
|
pub shared_strings: HashMap<String, usize>,
|
||||||
/// An instance of the parser
|
/// An instance of the parser
|
||||||
pub(crate) parser: Parser,
|
pub parser: Parser,
|
||||||
/// The list of cells with formulas that are evaluated of being evaluated
|
/// The list of cells with formulas that are evaluated of being evaluated
|
||||||
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
|
pub cells: HashMap<(u32, i32, i32), CellState>,
|
||||||
/// The locale of the model
|
/// The locale of the model
|
||||||
pub(crate) locale: Locale,
|
pub locale: Locale,
|
||||||
/// Tha language used
|
/// Tha language used
|
||||||
pub(crate) language: Language,
|
pub language: Language,
|
||||||
/// The timezone used to evaluate the model
|
/// The timezone used to evaluate the model
|
||||||
pub(crate) tz: Tz,
|
pub tz: Tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Maybe this should be the same as CellReference
|
// FIXME: Maybe this should be the same as CellReference
|
||||||
@@ -659,7 +660,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// assert_eq!(model.workbook.worksheet(0)?.color, None);
|
/// assert_eq!(model.workbook.worksheet(0)?.color, None);
|
||||||
@@ -726,7 +727,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// assert_eq!(model.is_empty_cell(0, 1, 1)?, true);
|
/// assert_eq!(model.is_empty_cell(0, 1, 1)?, true);
|
||||||
@@ -798,17 +799,17 @@ impl Model {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a model from an internal binary representation of a workbook
|
/// Returns a model from a String representation of a workbook
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # use ironcalc_base::cell::CellValue;
|
/// # use ironcalc_base::cell::CellValue;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// model.set_user_input(0, 1, 1, "Stella!".to_string());
|
/// model.set_user_input(0, 1, 1, "Stella!".to_string());
|
||||||
/// let model2 = Model::from_bytes(&model.to_bytes())?;
|
/// let model2 = Model::from_json(&model.to_json_str())?;
|
||||||
/// assert_eq!(
|
/// assert_eq!(
|
||||||
/// model2.get_cell_value_by_index(0, 1, 1),
|
/// model2.get_cell_value_by_index(0, 1, 1),
|
||||||
/// Ok(CellValue::String("Stella!".to_string()))
|
/// Ok(CellValue::String("Stella!".to_string()))
|
||||||
@@ -816,12 +817,9 @@ impl Model {
|
|||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
pub fn from_json(s: &str) -> Result<Model, String> {
|
||||||
/// See also:
|
|
||||||
/// * [Model::to_bytes]
|
|
||||||
pub fn from_bytes(s: &[u8]) -> Result<Model, String> {
|
|
||||||
let workbook: Workbook =
|
let workbook: Workbook =
|
||||||
bitcode::decode(s).map_err(|e| format!("Error parsing workbook: {e}"))?;
|
serde_json::from_str(s).map_err(|_| "Error parsing workbook".to_string())?;
|
||||||
Model::from_workbook(workbook)
|
Model::from_workbook(workbook)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -830,7 +828,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # use ironcalc_base::cell::CellValue;
|
/// # use ironcalc_base::cell::CellValue;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
@@ -899,7 +897,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
@@ -968,7 +966,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # use ironcalc_base::expressions::types::{Area, CellReferenceIndex};
|
/// # use ironcalc_base::expressions::types::{Area, CellReferenceIndex};
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
@@ -1039,7 +1037,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
/// let (sheet, row, column) = (0, 1, 1);
|
||||||
@@ -1086,7 +1084,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
/// # use ironcalc_base::expressions::types::CellReferenceIndex;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
@@ -1141,13 +1139,13 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
/// let (sheet, row, column) = (0, 1, 1);
|
||||||
/// model.set_user_input(sheet, row, column, "=SIN(B1*C3)+1".to_string());
|
/// model.set_user_input(sheet, row, column, "=SIN(B1*C3)+1".to_string());
|
||||||
/// model.evaluate();
|
/// model.evaluate();
|
||||||
/// let result = model.get_cell_formula(sheet, row, column)?;
|
/// let result = model.cell_formula(sheet, row, column)?;
|
||||||
/// assert_eq!(result, Some("=SIN(B1*C3)+1".to_string()));
|
/// assert_eq!(result, Some("=SIN(B1*C3)+1".to_string()));
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
@@ -1155,33 +1153,24 @@ impl Model {
|
|||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
/// * [Model::get_cell_content()]
|
/// * [Model::get_cell_content()]
|
||||||
pub fn get_cell_formula(
|
pub fn cell_formula(
|
||||||
&self,
|
&self,
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
row: i32,
|
row: i32,
|
||||||
column: i32,
|
column: i32,
|
||||||
) -> Result<Option<String>, String> {
|
) -> Result<Option<String>, String> {
|
||||||
let worksheet = self.workbook.worksheet(sheet)?;
|
let worksheet = self.workbook.worksheet(sheet)?;
|
||||||
match worksheet.cell(row, column) {
|
Ok(worksheet.cell(row, column).and_then(|cell| {
|
||||||
Some(cell) => match cell.get_formula() {
|
cell.get_formula().map(|formula_index| {
|
||||||
Some(formula_index) => {
|
let formula = &self.parsed_formulas[sheet as usize][formula_index as usize];
|
||||||
let formula = &self
|
|
||||||
.parsed_formulas
|
|
||||||
.get(sheet as usize)
|
|
||||||
.ok_or("missing sheet")?
|
|
||||||
.get(formula_index as usize)
|
|
||||||
.ok_or("missing formula")?;
|
|
||||||
let cell_ref = CellReferenceRC {
|
let cell_ref = CellReferenceRC {
|
||||||
sheet: worksheet.get_name(),
|
sheet: worksheet.get_name(),
|
||||||
row,
|
row,
|
||||||
column,
|
column,
|
||||||
};
|
};
|
||||||
Ok(Some(format!("={}", to_string(formula, &cell_ref))))
|
format!("={}", to_string(formula, &cell_ref))
|
||||||
}
|
})
|
||||||
None => Ok(None),
|
}))
|
||||||
},
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the value of a cell with some text
|
/// Updates the value of a cell with some text
|
||||||
@@ -1190,7 +1179,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
/// let (sheet, row, column) = (0, 1, 1);
|
||||||
@@ -1233,7 +1222,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
/// let (sheet, row, column) = (0, 1, 1);
|
||||||
@@ -1270,7 +1259,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
/// let (sheet, row, column) = (0, 1, 1);
|
||||||
@@ -1308,7 +1297,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
/// let (sheet, row, column) = (0, 1, 1);
|
||||||
@@ -1360,7 +1349,7 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # use ironcalc_base::cell::CellValue;
|
/// # use ironcalc_base::cell::CellValue;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
@@ -1370,7 +1359,7 @@ impl Model {
|
|||||||
/// model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string());
|
/// model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string());
|
||||||
/// model.evaluate();
|
/// model.evaluate();
|
||||||
/// assert_eq!(model.get_cell_value_by_index(0, 1, 2), Ok(CellValue::Number(215.0)));
|
/// assert_eq!(model.get_cell_value_by_index(0, 1, 2), Ok(CellValue::Number(215.0)));
|
||||||
/// assert_eq!(model.get_formatted_cell_value(0, 1, 2), Ok("215$".to_string()));
|
/// assert_eq!(model.formatted_cell_value(0, 1, 2), Ok("215$".to_string()));
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
@@ -1541,7 +1530,7 @@ impl Model {
|
|||||||
/// Returns the cell value for (`sheet`, `row`, `column`)
|
/// Returns the cell value for (`sheet`, `row`, `column`)
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
/// * [Model::get_formatted_cell_value()]
|
/// * [Model::formatted_cell_value()]
|
||||||
pub fn get_cell_value_by_index(
|
pub fn get_cell_value_by_index(
|
||||||
&self,
|
&self,
|
||||||
sheet_index: u32,
|
sheet_index: u32,
|
||||||
@@ -1567,43 +1556,36 @@ impl Model {
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use ironcalc_base::Model;
|
/// # use ironcalc_base::model::Model;
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
/// let (sheet, row, column) = (0, 1, 1);
|
||||||
/// model.set_user_input(sheet, row, column, "=1/3".to_string());
|
/// model.set_user_input(sheet, row, column, "=1/3".to_string());
|
||||||
/// model.evaluate();
|
/// model.evaluate();
|
||||||
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
|
/// let result = model.formatted_cell_value(sheet, row, column)?;
|
||||||
/// assert_eq!(result, "0.333333333".to_string());
|
/// assert_eq!(result, "0.333333333".to_string());
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get_formatted_cell_value(
|
pub fn formatted_cell_value(
|
||||||
&self,
|
&self,
|
||||||
sheet_index: u32,
|
sheet_index: u32,
|
||||||
row: i32,
|
row: i32,
|
||||||
column: i32,
|
column: i32,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
match self.workbook.worksheet(sheet_index)?.cell(row, column) {
|
|
||||||
Some(cell) => {
|
|
||||||
let format = self.get_style_for_cell(sheet_index, row, column).num_fmt;
|
let format = self.get_style_for_cell(sheet_index, row, column).num_fmt;
|
||||||
|
let cell = self
|
||||||
|
.workbook
|
||||||
|
.worksheet(sheet_index)?
|
||||||
|
.cell(row, column)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
let formatted_value =
|
let formatted_value =
|
||||||
cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| {
|
cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| {
|
||||||
format_number(value, &format, &self.locale).text
|
format_number(value, &format, &self.locale).text
|
||||||
});
|
});
|
||||||
Ok(formatted_value)
|
Ok(formatted_value)
|
||||||
}
|
}
|
||||||
None => Ok("".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the typeof a cell
|
|
||||||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<CellType, String> {
|
|
||||||
Ok(match self.workbook.worksheet(sheet)?.cell(row, column) {
|
|
||||||
Some(c) => c.get_type(),
|
|
||||||
None => CellType::Number,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a string with the cell content. If there is a formula returns the formula
|
/// Returns a string with the cell content. If there is a formula returns the formula
|
||||||
/// If the cell is empty returns the empty string
|
/// If the cell is empty returns the empty string
|
||||||
@@ -1666,53 +1648,15 @@ impl Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the content of the cell but leaves the style.
|
/// Sets cell to empty. Can be used to delete value without affecting style.
|
||||||
///
|
pub fn set_cell_empty(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||||
/// See also:
|
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||||
/// * [Model::cell_clear_all()]
|
worksheet.set_cell_empty(row, column);
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use ironcalc_base::Model;
|
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
|
||||||
/// model.set_user_input(sheet, row, column, "100$".to_string());
|
|
||||||
/// model.cell_clear_contents(sheet, row, column);
|
|
||||||
/// model.set_user_input(sheet, row, column, "10".to_string());
|
|
||||||
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
|
|
||||||
/// assert_eq!(result, "10$".to_string());
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn cell_clear_contents(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
|
||||||
self.workbook
|
|
||||||
.worksheet_mut(sheet)?
|
|
||||||
.cell_clear_contents(row, column);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes a cell by removing it from worksheet data. All content and style is removed.
|
/// Deletes a cell by removing it from worksheet data.
|
||||||
///
|
pub fn delete_cell(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
||||||
/// See also:
|
|
||||||
/// * [Model::cell_clear_contents()]
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use ironcalc_base::Model;
|
|
||||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let mut model = Model::new_empty("model", "en", "UTC")?;
|
|
||||||
/// let (sheet, row, column) = (0, 1, 1);
|
|
||||||
/// model.set_user_input(sheet, row, column, "100$".to_string());
|
|
||||||
/// model.cell_clear_all(sheet, row, column);
|
|
||||||
/// model.set_user_input(sheet, row, column, "10".to_string());
|
|
||||||
/// let result = model.get_formatted_cell_value(sheet, row, column)?;
|
|
||||||
/// assert_eq!(result, "10".to_string());
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
pub fn cell_clear_all(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> {
|
|
||||||
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
let worksheet = self.workbook.worksheet_mut(sheet)?;
|
||||||
|
|
||||||
let sheet_data = &mut worksheet.sheet_data;
|
let sheet_data = &mut worksheet.sheet_data;
|
||||||
@@ -1739,10 +1683,11 @@ impl Model {
|
|||||||
if r.r == row {
|
if r.r == row {
|
||||||
if r.custom_format {
|
if r.custom_format {
|
||||||
return r.s;
|
return r.s;
|
||||||
}
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let cols = &self.workbook.worksheets[sheet as usize].cols;
|
let cols = &self.workbook.worksheets[sheet as usize].cols;
|
||||||
for c in cols.iter() {
|
for c in cols.iter() {
|
||||||
let min = c.min;
|
let min = c.min;
|
||||||
@@ -1763,30 +1708,25 @@ impl Model {
|
|||||||
.get_style(self.get_cell_style_index(sheet, row, column))
|
.get_style(self.get_cell_style_index(sheet, row, column))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns an internal binary representation of the workbook
|
/// Returns a JSON string of the workbook
|
||||||
///
|
pub fn to_json_str(&self) -> String {
|
||||||
/// See also:
|
match serde_json::to_string(&self.workbook) {
|
||||||
/// * [Model::from_bytes]
|
Ok(s) => s,
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
Err(_) => {
|
||||||
bitcode::encode(&self.workbook)
|
// TODO, is this branch possible at all?
|
||||||
|
json!({"error": "Error stringifying workbook"}).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns data about the worksheets
|
/// bin
|
||||||
pub fn get_worksheets_properties(&self) -> Vec<SheetProperties> {
|
pub fn to_binary_str(&self) -> Vec<u8> {
|
||||||
self.workbook
|
let config = config::standard();
|
||||||
.worksheets
|
bincode::encode_to_vec(&self.workbook, config).expect("")
|
||||||
.iter()
|
|
||||||
.map(|worksheet| SheetProperties {
|
|
||||||
name: worksheet.get_name(),
|
|
||||||
state: worksheet.state.to_string(),
|
|
||||||
color: worksheet.color.clone(),
|
|
||||||
sheet_id: worksheet.sheet_id,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns markup representation of the given `sheet`.
|
/// Returns markup representation of the given `sheet`.
|
||||||
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
pub fn sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
||||||
let worksheet = self.workbook.worksheet(sheet)?;
|
let worksheet = self.workbook.worksheet(sheet)?;
|
||||||
let dimension = worksheet.dimension();
|
let dimension = worksheet.dimension();
|
||||||
|
|
||||||
@@ -1796,9 +1736,9 @@ impl Model {
|
|||||||
let mut row_markup: Vec<String> = Vec::new();
|
let mut row_markup: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for column in 1..(dimension.max_column + 1) {
|
for column in 1..(dimension.max_column + 1) {
|
||||||
let mut cell_markup = match self.get_cell_formula(sheet, row, column)? {
|
let mut cell_markup = match self.cell_formula(sheet, row, column)? {
|
||||||
Some(formula) => formula,
|
Some(formula) => formula,
|
||||||
None => self.get_formatted_cell_value(sheet, row, column)?,
|
None => self.formatted_cell_value(sheet, row, column)?,
|
||||||
};
|
};
|
||||||
let style = self.get_style_for_cell(sheet, row, column);
|
let style = self.get_style_for_cell(sheet, row, column);
|
||||||
if style.font.b {
|
if style.font.b {
|
||||||
@@ -1837,7 +1777,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of frozen rows in `sheet`
|
/// Returns the number of frozen rows in `sheet`
|
||||||
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, String> {
|
pub fn get_frozen_rows(&self, sheet: u32) -> Result<i32, String> {
|
||||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||||
Ok(worksheet.frozen_rows)
|
Ok(worksheet.frozen_rows)
|
||||||
} else {
|
} else {
|
||||||
@@ -1846,7 +1786,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the number of frozen columns in `sheet`
|
/// Return the number of frozen columns in `sheet`
|
||||||
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, String> {
|
pub fn get_frozen_columns(&self, sheet: u32) -> Result<i32, String> {
|
||||||
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
if let Some(worksheet) = self.workbook.worksheets.get(sheet as usize) {
|
||||||
Ok(worksheet.frozen_columns)
|
Ok(worksheet.frozen_columns)
|
||||||
} else {
|
} else {
|
||||||
@@ -1887,34 +1827,6 @@ impl Model {
|
|||||||
Err("Invalid sheet".to_string())
|
Err("Invalid sheet".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the width of a column
|
|
||||||
#[inline]
|
|
||||||
pub fn get_column_width(&self, sheet: u32, column: i32) -> Result<f64, String> {
|
|
||||||
self.workbook.worksheet(sheet)?.get_column_width(column)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the width of a column
|
|
||||||
#[inline]
|
|
||||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), String> {
|
|
||||||
self.workbook
|
|
||||||
.worksheet_mut(sheet)?
|
|
||||||
.set_column_width(column, width)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the height of a row
|
|
||||||
#[inline]
|
|
||||||
pub fn get_row_height(&self, sheet: u32, row: i32) -> Result<f64, String> {
|
|
||||||
self.workbook.worksheet(sheet)?.row_height(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the height of a row
|
|
||||||
#[inline]
|
|
||||||
pub fn set_row_height(&mut self, sheet: u32, column: i32, height: f64) -> Result<(), String> {
|
|
||||||
self.workbook
|
|
||||||
.worksheet_mut(sheet)?
|
|
||||||
.set_row_height(column, height)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use chrono::DateTime;
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reparses all formulas and defined names
|
// Reparses all formulas and defined names
|
||||||
pub(crate) fn reset_parsed_structures(&mut self) {
|
fn reset_parsed_structures(&mut self) {
|
||||||
self.parser
|
self.parser
|
||||||
.set_worksheets(self.workbook.get_worksheet_names());
|
.set_worksheets(self.workbook.get_worksheet_names());
|
||||||
self.parsed_formulas = vec![];
|
self.parsed_formulas = vec![];
|
||||||
@@ -134,10 +134,10 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a sheet with a automatically generated name
|
/// Adds a sheet with a automatically generated name
|
||||||
pub fn new_sheet(&mut self) -> (String, u32) {
|
pub fn new_sheet(&mut self) {
|
||||||
// First we find a name
|
// First we find a name
|
||||||
|
|
||||||
// TODO: The name should depend on the locale
|
// TODO: When/if we support i18n the name could depend on the locale
|
||||||
let base_name = "Sheet";
|
let base_name = "Sheet";
|
||||||
let base_name_uppercase = base_name.to_uppercase();
|
let base_name_uppercase = base_name.to_uppercase();
|
||||||
let mut index = 1;
|
let mut index = 1;
|
||||||
@@ -156,7 +156,6 @@ impl Model {
|
|||||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
|
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
|
||||||
self.workbook.worksheets.push(worksheet);
|
self.workbook.worksheets.push(worksheet);
|
||||||
self.reset_parsed_structures();
|
self.reset_parsed_structures();
|
||||||
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts a sheet with a particular index
|
/// Inserts a sheet with a particular index
|
||||||
@@ -224,10 +223,10 @@ impl Model {
|
|||||||
new_name: &str,
|
new_name: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if !is_valid_sheet_name(new_name) {
|
if !is_valid_sheet_name(new_name) {
|
||||||
return Err(format!("Invalid name for a sheet: '{}'.", new_name));
|
return Err(format!("Invalid name for a sheet: '{}'", new_name));
|
||||||
}
|
}
|
||||||
if self.get_sheet_index_by_name(new_name).is_some() {
|
if self.get_sheet_index_by_name(new_name).is_some() {
|
||||||
return Err(format!("Sheet already exists: '{}'.", new_name));
|
return Err(format!("Sheet already exists: '{}'", new_name));
|
||||||
}
|
}
|
||||||
let worksheets = &self.workbook.worksheets;
|
let worksheets = &self.workbook.worksheets;
|
||||||
let sheet_count = worksheets.len() as u32;
|
let sheet_count = worksheets.len() as u32;
|
||||||
@@ -271,7 +270,7 @@ impl Model {
|
|||||||
if sheet_count == 1 {
|
if sheet_count == 1 {
|
||||||
return Err("Cannot delete only sheet".to_string());
|
return Err("Cannot delete only sheet".to_string());
|
||||||
};
|
};
|
||||||
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);
|
||||||
@@ -324,7 +323,7 @@ impl Model {
|
|||||||
|
|
||||||
let milliseconds = get_milliseconds_since_epoch();
|
let milliseconds = get_milliseconds_since_epoch();
|
||||||
let seconds = milliseconds / 1000;
|
let seconds = milliseconds / 1000;
|
||||||
let dt = match DateTime::from_timestamp(seconds, 0) {
|
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
|
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
mod test_actions;
|
mod test_actions;
|
||||||
mod test_binary_search;
|
mod test_binary_search;
|
||||||
mod test_cell;
|
mod test_cell;
|
||||||
mod test_cell_clear_contents;
|
|
||||||
mod test_circular_references;
|
mod test_circular_references;
|
||||||
mod test_column_width;
|
mod test_column_width;
|
||||||
mod test_criteria;
|
mod test_criteria;
|
||||||
@@ -29,8 +28,9 @@ mod test_frozen_rows_columns;
|
|||||||
mod test_general;
|
mod test_general;
|
||||||
mod test_math;
|
mod test_math;
|
||||||
mod test_metadata;
|
mod test_metadata;
|
||||||
mod test_model_cell_clear_all;
|
mod test_model_delete_cell;
|
||||||
mod test_model_is_empty_cell;
|
mod test_model_is_empty_cell;
|
||||||
|
mod test_model_set_cell_empty;
|
||||||
mod test_move_formula;
|
mod test_move_formula;
|
||||||
mod test_quote_prefix;
|
mod test_quote_prefix;
|
||||||
mod test_set_user_input;
|
mod test_set_user_input;
|
||||||
@@ -53,4 +53,3 @@ mod test_frozen_rows_and_columns;
|
|||||||
mod test_get_cell_content;
|
mod test_get_cell_content;
|
||||||
mod test_percentage;
|
mod test_percentage;
|
||||||
mod test_today;
|
mod test_today;
|
||||||
mod user_model;
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
use crate::constants::LAST_COLUMN;
|
use crate::constants::LAST_COLUMN;
|
||||||
use crate::model::Model;
|
use crate::model::Model;
|
||||||
use crate::test::util::new_empty_model;
|
use crate::test::util::new_empty_model;
|
||||||
use crate::types::Col;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_insert_columns() {
|
fn test_insert_columns() {
|
||||||
@@ -196,250 +195,6 @@ fn test_delete_columns() {
|
|||||||
assert_eq!(model._get_formula("A3"), *"=SUM(#REF!:K4)");
|
assert_eq!(model._get_formula("A3"), *"=SUM(#REF!:K4)");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete_column_width() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
let (sheet, column) = (0, 5);
|
|
||||||
let normal_width = model.get_column_width(sheet, column).unwrap();
|
|
||||||
// Set the width of one column to 5 times the normal width
|
|
||||||
assert!(model
|
|
||||||
.set_column_width(sheet, column, normal_width * 5.0)
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
// delete it
|
|
||||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
|
||||||
|
|
||||||
// all the columns around have the expected width
|
|
||||||
assert_eq!(
|
|
||||||
model.get_column_width(sheet, column - 1).unwrap(),
|
|
||||||
normal_width
|
|
||||||
);
|
|
||||||
assert_eq!(model.get_column_width(sheet, column).unwrap(), normal_width);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_column_width(sheet, column + 1).unwrap(),
|
|
||||||
normal_width
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// We set the style of columns 4 to 7 and delete column 4
|
|
||||||
// We check that columns 4 to 6 have the new style
|
|
||||||
fn test_delete_first_column_width() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 4,
|
|
||||||
max: 7,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
let (sheet, column) = (0, 4);
|
|
||||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
cols[0],
|
|
||||||
Col {
|
|
||||||
min: 4,
|
|
||||||
max: 6,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// Delete the last column in the range
|
|
||||||
fn test_delete_last_column_width() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 4,
|
|
||||||
max: 7,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
let (sheet, column) = (0, 7);
|
|
||||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
cols[0],
|
|
||||||
Col {
|
|
||||||
min: 4,
|
|
||||||
max: 6,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// Deletes columns at the end
|
|
||||||
fn test_delete_last_few_columns_width() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 4,
|
|
||||||
max: 17,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
let (sheet, column) = (0, 13);
|
|
||||||
assert!(model.delete_columns(sheet, column, 10).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
cols[0],
|
|
||||||
Col {
|
|
||||||
min: 4,
|
|
||||||
max: 12,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete_columns_non_overlapping_left() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 10,
|
|
||||||
max: 17,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
let (sheet, column) = (0, 3);
|
|
||||||
assert!(model.delete_columns(sheet, column, 4).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
cols[0],
|
|
||||||
Col {
|
|
||||||
min: 6,
|
|
||||||
max: 13,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete_columns_overlapping_left() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 10,
|
|
||||||
max: 20,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
let (sheet, column) = (0, 8);
|
|
||||||
assert!(model.delete_columns(sheet, column, 4).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
cols[0],
|
|
||||||
Col {
|
|
||||||
min: 8,
|
|
||||||
max: 16,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete_columns_non_overlapping_right() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 10,
|
|
||||||
max: 17,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
let (sheet, column) = (0, 23);
|
|
||||||
assert!(model.delete_columns(sheet, column, 4).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
cols[0],
|
|
||||||
Col {
|
|
||||||
min: 10,
|
|
||||||
max: 17,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// deletes some columns in the middle of the range
|
|
||||||
fn test_delete_middle_column_width() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
// styled columns [4, 17]
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 4,
|
|
||||||
max: 17,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
|
|
||||||
// deletes columns 10, 11, 12
|
|
||||||
let (sheet, column) = (0, 10);
|
|
||||||
assert!(model.delete_columns(sheet, column, 3).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
cols[0],
|
|
||||||
Col {
|
|
||||||
min: 4,
|
|
||||||
max: 14,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// the range is inside the deleted columns
|
|
||||||
fn delete_range_in_columns() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
// styled columns [6, 10]
|
|
||||||
model.workbook.worksheets[0].cols = vec![Col {
|
|
||||||
min: 6,
|
|
||||||
max: 10,
|
|
||||||
width: 300.0,
|
|
||||||
custom_width: true,
|
|
||||||
style: None,
|
|
||||||
}];
|
|
||||||
|
|
||||||
// deletes columns [4, 17]
|
|
||||||
let (sheet, column) = (0, 4);
|
|
||||||
assert!(model.delete_columns(sheet, column, 8).is_ok());
|
|
||||||
let cols = &model.workbook.worksheets[0].cols;
|
|
||||||
assert_eq!(cols.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_delete_columns_error() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
let (sheet, column) = (0, 5);
|
|
||||||
assert!(model.delete_columns(sheet, column, -1).is_err());
|
|
||||||
assert!(model.delete_columns(sheet, column, 0).is_err());
|
|
||||||
assert!(model.delete_columns(sheet, column, 1).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_delete_rows() {
|
fn test_delete_rows() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ fn test_column_width() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 3);
|
assert_eq!(model.workbook.worksheets[0].cols.len(), 3);
|
||||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||||
assert!((worksheet.get_column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
assert!((worksheet.column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||||
assert!((worksheet.get_column_width(2).unwrap() - 30.0).abs() < f64::EPSILON);
|
assert!((worksheet.column_width(2).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||||
assert!((worksheet.get_column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
assert!((worksheet.column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||||
assert_eq!(model.get_cell_style_index(0, 23, 2), 6);
|
assert_eq!(model.get_cell_style_index(0, 23, 2), 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +48,9 @@ fn test_column_width_lower_edge() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
||||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||||
assert!((worksheet.get_column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
assert!((worksheet.column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||||
assert!((worksheet.get_column_width(5).unwrap() - 30.0).abs() < f64::EPSILON);
|
assert!((worksheet.column_width(5).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||||
assert!(
|
assert!((worksheet.column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON);
|
||||||
(worksheet.get_column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
|
|
||||||
);
|
|
||||||
assert_eq!(model.get_cell_style_index(0, 23, 5), 1);
|
assert_eq!(model.get_cell_style_index(0, 23, 5), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +74,9 @@ fn test_column_width_higher_edge() {
|
|||||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
||||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
(worksheet.get_column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
|
(worksheet.column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
|
||||||
);
|
);
|
||||||
assert!((worksheet.get_column_width(16).unwrap() - 30.0).abs() < f64::EPSILON);
|
assert!((worksheet.column_width(16).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||||
assert!((worksheet.get_column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
assert!((worksheet.column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||||
assert_eq!(model.get_cell_style_index(0, 23, 16), 1);
|
assert_eq!(model.get_cell_style_index(0, 23, 16), 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,37 +8,34 @@ use crate::{
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_empty_model() {
|
fn test_empty_model() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
assert_eq!(model.get_frozen_rows_count(0), Ok(0));
|
assert_eq!(model.get_frozen_rows(0), Ok(0));
|
||||||
assert_eq!(model.get_frozen_columns_count(0), Ok(0));
|
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||||
|
|
||||||
let e = model.set_frozen_rows(0, 3);
|
let e = model.set_frozen_rows(0, 3);
|
||||||
assert!(e.is_ok());
|
assert!(e.is_ok());
|
||||||
assert_eq!(model.get_frozen_rows_count(0), Ok(3));
|
assert_eq!(model.get_frozen_rows(0), Ok(3));
|
||||||
assert_eq!(model.get_frozen_columns_count(0), Ok(0));
|
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||||
|
|
||||||
let e = model.set_frozen_columns(0, 53);
|
let e = model.set_frozen_columns(0, 53);
|
||||||
assert!(e.is_ok());
|
assert!(e.is_ok());
|
||||||
assert_eq!(model.get_frozen_rows_count(0), Ok(3));
|
assert_eq!(model.get_frozen_rows(0), Ok(3));
|
||||||
assert_eq!(model.get_frozen_columns_count(0), Ok(53));
|
assert_eq!(model.get_frozen_columns(0), Ok(53));
|
||||||
|
|
||||||
// Set them back to zero
|
// Set them back to zero
|
||||||
let e = model.set_frozen_rows(0, 0);
|
let e = model.set_frozen_rows(0, 0);
|
||||||
assert!(e.is_ok());
|
assert!(e.is_ok());
|
||||||
let e = model.set_frozen_columns(0, 0);
|
let e = model.set_frozen_columns(0, 0);
|
||||||
assert!(e.is_ok());
|
assert!(e.is_ok());
|
||||||
assert_eq!(model.get_frozen_rows_count(0), Ok(0));
|
assert_eq!(model.get_frozen_rows(0), Ok(0));
|
||||||
assert_eq!(model.get_frozen_columns_count(0), Ok(0));
|
assert_eq!(model.get_frozen_columns(0), Ok(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_sheet() {
|
fn test_invalid_sheet() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
|
assert_eq!(model.get_frozen_rows(1), Err("Invalid sheet".to_string()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_frozen_rows_count(1),
|
model.get_frozen_columns(3),
|
||||||
Err("Invalid sheet".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_frozen_columns_count(3),
|
|
||||||
Err("Invalid sheet".to_string())
|
Err("Invalid sheet".to_string())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -411,11 +411,11 @@ fn test_get_formatted_cell_value() {
|
|||||||
|
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1).unwrap(), "foobar");
|
assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "foobar");
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 2, 1).unwrap(), "TRUE");
|
assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "TRUE");
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 3, 1).unwrap(), "");
|
assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), "");
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 4, 1).unwrap(), "123.456");
|
assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "123.456");
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1).unwrap(), "$123.46");
|
assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "$123.46");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -426,20 +426,20 @@ fn test_cell_formula() {
|
|||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 1, 1), // A1
|
model.cell_formula(0, 1, 1), // A1
|
||||||
Ok(Some("=1+2+3".to_string())),
|
Ok(Some("=1+2+3".to_string())),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 2, 1), // A2
|
model.cell_formula(0, 2, 1), // A2
|
||||||
Ok(None),
|
Ok(None),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 3, 1), // A3 - empty cell
|
model.cell_formula(0, 3, 1), // A3 - empty cell
|
||||||
Ok(None),
|
Ok(None),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(42, 1, 1),
|
model.cell_formula(42, 1, 1),
|
||||||
Err("Invalid sheet index".to_string()),
|
Err("Invalid sheet index".to_string()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -453,16 +453,16 @@ fn test_xlfn() {
|
|||||||
model.evaluate();
|
model.evaluate();
|
||||||
// Only modern formulas strip the '_xlfn.'
|
// Only modern formulas strip the '_xlfn.'
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 1, 1).unwrap(),
|
model.cell_formula(0, 1, 1).unwrap(),
|
||||||
Some("=_xlfn.SIN(1)".to_string())
|
Some("=_xlfn.SIN(1)".to_string())
|
||||||
);
|
);
|
||||||
// unknown formulas keep the '_xlfn.' prefix
|
// unknown formulas keep the '_xlfn.' prefix
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 2, 1).unwrap(),
|
model.cell_formula(0, 2, 1).unwrap(),
|
||||||
Some("=_xlfn.SINY(1)".to_string())
|
Some("=_xlfn.SINY(1)".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 3, 1).unwrap(),
|
model.cell_formula(0, 3, 1).unwrap(),
|
||||||
Some("=CONCAT(3,4)".to_string())
|
Some("=CONCAT(3,4)".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -474,11 +474,11 @@ fn test_letter_case() {
|
|||||||
model._set("A2", "=sIn(2)");
|
model._set("A2", "=sIn(2)");
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 1, 1).unwrap(),
|
model.cell_formula(0, 1, 1).unwrap(),
|
||||||
Some("=SIN(1)".to_string())
|
Some("=SIN(1)".to_string())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_cell_formula(0, 2, 1).unwrap(),
|
model.cell_formula(0, 2, 1).unwrap(),
|
||||||
Some("=SIN(2)".to_string())
|
Some("=SIN(2)".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,22 @@
|
|||||||
use crate::test::util::new_empty_model;
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_all_non_existing_sheet() {
|
fn test_delete_cell_non_existing_sheet() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.cell_clear_all(13, 1, 1),
|
model.delete_cell(13, 1, 1),
|
||||||
Err("Invalid sheet index".to_string())
|
Err("Invalid sheet index".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_all_unset_cell() {
|
fn test_delete_cell_unset_cell() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
assert!(model.cell_clear_all(0, 1, 1).is_ok());
|
assert!(model.delete_cell(0, 1, 1).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_all_with_value() {
|
fn test_delete_cell_with_value() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", "hello");
|
model._set("A1", "hello");
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
@@ -25,7 +25,7 @@ fn test_cell_clear_all_with_value() {
|
|||||||
assert_eq!(model._get_text_at(0, 1, 1), "hello");
|
assert_eq!(model._get_text_at(0, 1, 1), "hello");
|
||||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||||
|
|
||||||
model.cell_clear_all(0, 1, 1).unwrap();
|
model.delete_cell(0, 1, 1).unwrap();
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||||
@@ -33,7 +33,7 @@ fn test_cell_clear_all_with_value() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_all_referenced_elsewhere() {
|
fn test_delete_cell_referenced_elsewhere() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", "35");
|
model._set("A1", "35");
|
||||||
model._set("A2", "=2*A1");
|
model._set("A2", "=2*A1");
|
||||||
@@ -44,7 +44,7 @@ fn test_cell_clear_all_referenced_elsewhere() {
|
|||||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||||
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
|
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
|
||||||
|
|
||||||
model.cell_clear_all(0, 1, 1).unwrap();
|
model.delete_cell(0, 1, 1).unwrap();
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||||
@@ -16,7 +16,7 @@ fn test_is_empty_cell() {
|
|||||||
assert!(model.is_empty_cell(0, 3, 1).unwrap());
|
assert!(model.is_empty_cell(0, 3, 1).unwrap());
|
||||||
model.set_user_input(0, 3, 1, "Hello World".to_string());
|
model.set_user_input(0, 3, 1, "Hello World".to_string());
|
||||||
assert!(!model.is_empty_cell(0, 3, 1).unwrap());
|
assert!(!model.is_empty_cell(0, 3, 1).unwrap());
|
||||||
model.cell_clear_contents(0, 3, 1).unwrap();
|
model.set_cell_empty(0, 3, 1).unwrap();
|
||||||
assert!(model.is_empty_cell(0, 3, 1).unwrap());
|
assert!(model.is_empty_cell(0, 3, 1).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,25 @@
|
|||||||
use crate::test::util::new_empty_model;
|
use crate::test::util::new_empty_model;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_contents_non_existing_sheet() {
|
fn test_set_cell_empty_non_existing_sheet() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.cell_clear_contents(13, 1, 1),
|
model.set_cell_empty(13, 1, 1),
|
||||||
Err("Invalid sheet index".to_string())
|
Err("Invalid sheet index".to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_contents_unset_cell() {
|
fn test_set_cell_empty_unset_cell() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model.cell_clear_contents(0, 1, 1).unwrap();
|
model.set_cell_empty(0, 1, 1).unwrap();
|
||||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true));
|
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true));
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_contents_with_value() {
|
fn test_set_cell_empty_with_value() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", "hello");
|
model._set("A1", "hello");
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
@@ -28,7 +28,7 @@ fn test_cell_clear_contents_with_value() {
|
|||||||
assert_eq!(model._get_text_at(0, 1, 1), "hello");
|
assert_eq!(model._get_text_at(0, 1, 1), "hello");
|
||||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||||
|
|
||||||
model.cell_clear_contents(0, 1, 1).unwrap();
|
model.set_cell_empty(0, 1, 1).unwrap();
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||||
@@ -36,7 +36,7 @@ fn test_cell_clear_contents_with_value() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_clear_contents_referenced_elsewhere() {
|
fn test_set_cell_empty_referenced_elsewhere() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", "35");
|
model._set("A1", "35");
|
||||||
model._set("A2", "=2*A1");
|
model._set("A2", "=2*A1");
|
||||||
@@ -47,7 +47,7 @@ fn test_cell_clear_contents_referenced_elsewhere() {
|
|||||||
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false));
|
||||||
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
|
assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false));
|
||||||
|
|
||||||
model.cell_clear_contents(0, 1, 1).unwrap();
|
model.set_cell_empty(0, 1, 1).unwrap();
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
|
|
||||||
assert_eq!(model._get_text_at(0, 1, 1), "");
|
assert_eq!(model._get_text_at(0, 1, 1), "");
|
||||||
@@ -21,7 +21,7 @@ fn test_sheet_markup() {
|
|||||||
model.set_cell_style(0, 4, 1, &style).unwrap();
|
model.set_cell_style(0, 4, 1, &style).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_sheet_markup(0),
|
model.sheet_markup(0),
|
||||||
Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()),
|
Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,11 +236,3 @@ fn test_delete_sheet_by_index() {
|
|||||||
assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]);
|
assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]);
|
||||||
assert_eq!(model._get_text("Sheet2!A1"), "#REF!");
|
assert_eq!(model._get_text("Sheet2!A1"), "#REF!");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delete_sheet_error() {
|
|
||||||
let mut model = new_empty_model();
|
|
||||||
model.new_sheet();
|
|
||||||
assert!(model.delete_sheet(2).is_err());
|
|
||||||
assert!(model.delete_sheet(1).is_ok());
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
#![allow(clippy::unwrap_used)]
|
||||||
|
|
||||||
use crate::{test::util::new_empty_model, types::SheetProperties};
|
use crate::{test::util::new_empty_model, types::SheetInfo};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workbook_worksheets_info() {
|
fn workbook_worksheets_info() {
|
||||||
let model = new_empty_model();
|
let model = new_empty_model();
|
||||||
let sheets_info = model.get_worksheets_properties();
|
let sheets_info = model.workbook.get_worksheets_info();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sheets_info[0],
|
sheets_info[0],
|
||||||
SheetProperties {
|
SheetInfo {
|
||||||
name: "Sheet1".to_string(),
|
name: "Sheet1".to_string(),
|
||||||
state: "visible".to_string(),
|
state: "visible".to_string(),
|
||||||
sheet_id: 1,
|
sheet_id: 1,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ fn test_worksheet_dimension_single_cell() {
|
|||||||
fn test_worksheet_dimension_single_cell_set_empty() {
|
fn test_worksheet_dimension_single_cell_set_empty() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("W11", "1");
|
model._set("W11", "1");
|
||||||
model.cell_clear_contents(0, 11, 23).unwrap();
|
model.set_cell_empty(0, 11, 23).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.workbook.worksheet(0).unwrap().dimension(),
|
model.workbook.worksheet(0).unwrap().dimension(),
|
||||||
WorksheetDimension {
|
WorksheetDimension {
|
||||||
@@ -55,7 +55,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
|
|||||||
fn test_worksheet_dimension_single_cell_deleted() {
|
fn test_worksheet_dimension_single_cell_deleted() {
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("W11", "1");
|
model._set("W11", "1");
|
||||||
model.cell_clear_all(0, 11, 23).unwrap();
|
model.delete_cell(0, 11, 23).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.workbook.worksheet(0).unwrap().dimension(),
|
model.workbook.worksheet(0).unwrap().dimension(),
|
||||||
WorksheetDimension {
|
WorksheetDimension {
|
||||||
@@ -75,7 +75,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
|||||||
model._set("AA17", "1");
|
model._set("AA17", "1");
|
||||||
model._set("G17", "1");
|
model._set("G17", "1");
|
||||||
model._set("B19", "1");
|
model._set("B19", "1");
|
||||||
model.cell_clear_all(0, 11, 23).unwrap();
|
model.delete_cell(0, 11, 23).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.workbook.worksheet(0).unwrap().dimension(),
|
model.workbook.worksheet(0).unwrap().dimension(),
|
||||||
WorksheetDimension {
|
WorksheetDimension {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
mod test_add_delete_sheets;
|
|
||||||
mod test_clear_cells;
|
|
||||||
mod test_diff_queue;
|
|
||||||
mod test_evaluation;
|
|
||||||
mod test_general;
|
|
||||||
mod test_rename_sheet;
|
|
||||||
mod test_row_column;
|
|
||||||
mod test_styles;
|
|
||||||
mod test_to_from_bytes;
|
|
||||||
mod test_undo_redo;
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{constants::DEFAULT_COLUMN_WIDTH, UserModel};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn add_undo_redo() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.new_sheet();
|
|
||||||
model.set_user_input(1, 1, 1, "=1 + 1").unwrap();
|
|
||||||
model.set_user_input(1, 1, 2, "=A1*3").unwrap();
|
|
||||||
model
|
|
||||||
.set_column_width(1, 5, 5.0 * DEFAULT_COLUMN_WIDTH)
|
|
||||||
.unwrap();
|
|
||||||
model.new_sheet();
|
|
||||||
model.set_user_input(2, 1, 1, "=Sheet2!B1").unwrap();
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
assert!(model.get_formatted_cell_value(2, 1, 1).is_err());
|
|
||||||
|
|
||||||
model.redo().unwrap();
|
|
||||||
model.redo().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(2, 1, 1), Ok("6".to_string()));
|
|
||||||
|
|
||||||
model.delete_sheet(1).unwrap();
|
|
||||||
|
|
||||||
assert!(!model.can_undo());
|
|
||||||
assert!(!model.can_redo());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn set_sheet_color() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.set_sheet_color(0, "#343434").unwrap();
|
|
||||||
let worksheets_properties = model.get_worksheets_properties();
|
|
||||||
assert_eq!(worksheets_properties.len(), 1);
|
|
||||||
assert_eq!(worksheets_properties[0].color, Some("#343434".to_owned()));
|
|
||||||
model.undo().unwrap();
|
|
||||||
assert_eq!(model.get_worksheets_properties()[0].color, None);
|
|
||||||
|
|
||||||
model.redo().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_worksheets_properties()[0].color,
|
|
||||||
Some("#343434".to_owned())
|
|
||||||
);
|
|
||||||
// changes the color if there is one
|
|
||||||
model.set_sheet_color(0, "#2534FF").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_worksheets_properties()[0].color,
|
|
||||||
Some("#2534FF".to_owned())
|
|
||||||
);
|
|
||||||
// Setting it back to none
|
|
||||||
model.set_sheet_color(0, "").unwrap();
|
|
||||||
assert_eq!(model.get_worksheets_properties()[0].color, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_sheet_propagates() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.new_sheet();
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
let worksheets_properties = model2.get_worksheets_properties();
|
|
||||||
assert_eq!(worksheets_properties.len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delete_sheet_propagates() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.new_sheet();
|
|
||||||
model.delete_sheet(0).unwrap();
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
let sheets_info = model2.get_worksheets_properties();
|
|
||||||
assert_eq!(sheets_info.len(), 1);
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{expressions::types::Area, UserModel};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.set_user_input(0, 1, 1, "100$").unwrap();
|
|
||||||
model
|
|
||||||
.range_clear_contents(&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
model.undo().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 1),
|
|
||||||
Ok("100$".to_string())
|
|
||||||
);
|
|
||||||
model.redo().unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
|
|
||||||
model.set_user_input(0, 1, 1, "300").unwrap();
|
|
||||||
// clear contents keeps the formatting
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 1),
|
|
||||||
Ok("300$".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.range_clear_all(&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
model.undo().unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 1),
|
|
||||||
Ok("300$".to_string())
|
|
||||||
);
|
|
||||||
model.redo().unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
model.set_user_input(0, 1, 1, "400").unwrap();
|
|
||||||
// clear contents keeps the formatting
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 1),
|
|
||||||
Ok("400".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn clear_empty_cell() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model
|
|
||||||
.range_clear_contents(&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
model.undo().unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn clear_all_empty_cell() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model
|
|
||||||
.range_clear_all(&Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
model.undo().unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT},
|
|
||||||
test::util::new_empty_model,
|
|
||||||
UserModel,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn send_queue() {
|
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
|
||||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
|
||||||
model1.set_column_width(0, 3, width).unwrap();
|
|
||||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
|
||||||
let send_queue = model1.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::from_model(new_empty_model());
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model2.get_column_width(0, 3), Ok(width));
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 1, 2),
|
|
||||||
Ok("Hello IronCalc!".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn apply_external_diffs_wrong_str() {
|
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
|
||||||
assert!(model1.apply_external_diffs("invalid".as_bytes()).is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn queue_undo_redo() {
|
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
|
||||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
|
||||||
model1.set_column_width(0, 3, width).unwrap();
|
|
||||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
|
||||||
assert!(model1.undo().is_ok());
|
|
||||||
assert!(model1.redo().is_ok());
|
|
||||||
let send_queue = model1.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::from_model(new_empty_model());
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model2.get_column_width(0, 3), Ok(width));
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 1, 2),
|
|
||||||
Ok("Hello IronCalc!".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn queue_undo_redo_multiple() {
|
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
|
||||||
|
|
||||||
// do a bunch of things
|
|
||||||
model1.set_frozen_columns_count(0, 5).unwrap();
|
|
||||||
model1.set_frozen_rows_count(0, 6).unwrap();
|
|
||||||
model1.set_column_width(0, 7, 300.0).unwrap();
|
|
||||||
model1.set_row_height(0, 23, 123.0).unwrap();
|
|
||||||
model1.set_user_input(0, 55, 55, "=42+8").unwrap();
|
|
||||||
|
|
||||||
for row in 1..5 {
|
|
||||||
model1.set_user_input(0, row, 17, "=ROW()").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
model1.insert_row(0, 3).unwrap();
|
|
||||||
model1.insert_row(0, 3).unwrap();
|
|
||||||
|
|
||||||
// undo al of them
|
|
||||||
while model1.can_undo() {
|
|
||||||
model1.undo().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// check it is an empty model
|
|
||||||
assert_eq!(model1.get_frozen_columns_count(0), Ok(0));
|
|
||||||
assert_eq!(model1.get_frozen_rows_count(0), Ok(0));
|
|
||||||
assert_eq!(model1.get_column_width(0, 7), Ok(DEFAULT_COLUMN_WIDTH));
|
|
||||||
assert_eq!(
|
|
||||||
model1.get_formatted_cell_value(0, 55, 55),
|
|
||||||
Ok("".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(model1.get_row_height(0, 23), Ok(DEFAULT_ROW_HEIGHT));
|
|
||||||
assert_eq!(
|
|
||||||
model1.get_formatted_cell_value(0, 57, 55),
|
|
||||||
Ok("".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(model1.get_row_height(0, 25), Ok(DEFAULT_ROW_HEIGHT));
|
|
||||||
|
|
||||||
// redo all of them
|
|
||||||
while model1.can_redo() {
|
|
||||||
model1.redo().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// now send all this to a new model
|
|
||||||
let send_queue = model1.flush_send_queue();
|
|
||||||
let mut model2 = UserModel::from_model(new_empty_model());
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
// Check everything is as expected
|
|
||||||
assert_eq!(model2.get_frozen_columns_count(0), Ok(5));
|
|
||||||
assert_eq!(model2.get_frozen_rows_count(0), Ok(6));
|
|
||||||
assert_eq!(model2.get_column_width(0, 7), Ok(300.0));
|
|
||||||
// I inserted two rows
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 57, 55),
|
|
||||||
Ok("50".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(model2.get_row_height(0, 25), Ok(123.0));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 1, 17),
|
|
||||||
Ok("1".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 2, 17),
|
|
||||||
Ok("2".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 3, 17),
|
|
||||||
Ok("".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 4, 17),
|
|
||||||
Ok("".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 5, 17),
|
|
||||||
Ok("5".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 6, 17),
|
|
||||||
Ok("6".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn new_sheet() {
|
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
|
||||||
model1.new_sheet();
|
|
||||||
model1.set_user_input(0, 1, 1, "42").unwrap();
|
|
||||||
model1.set_user_input(1, 1, 1, "=Sheet1!A1*2").unwrap();
|
|
||||||
|
|
||||||
let send_queue = model1.flush_send_queue();
|
|
||||||
let mut model2 = UserModel::from_model(new_empty_model());
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(1, 1, 1),
|
|
||||||
Ok("84".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn wrong_diffs_handled() {
|
|
||||||
let mut model = UserModel::from_model(new_empty_model());
|
|
||||||
assert!(model
|
|
||||||
.apply_external_diffs("Hello world".as_bytes())
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::UserModel;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn model_evaluates_automatically() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.set_user_input(0, 1, 1, "=1 + 1").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string()));
|
|
||||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pause_resume_evaluation() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.pause_evaluation();
|
|
||||||
model.set_user_input(0, 1, 1, "=1+1").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 1),
|
|
||||||
Ok("#ERROR!".to_string())
|
|
||||||
);
|
|
||||||
model.evaluate();
|
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("2".to_string()));
|
|
||||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+1".to_string()));
|
|
||||||
|
|
||||||
model.resume_evaluation();
|
|
||||||
model.set_user_input(0, 2, 1, "=1+4").unwrap();
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 2, 1), Ok("5".to_string()));
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
|
||||||
use crate::test::util::new_empty_model;
|
|
||||||
use crate::types::CellType;
|
|
||||||
use crate::UserModel;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn set_user_input_errors() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// Wrong sheet
|
|
||||||
assert!(model.set_user_input(1, 1, 1, "1").is_err());
|
|
||||||
// Wrong row
|
|
||||||
assert!(model.set_user_input(0, 0, 1, "1").is_err());
|
|
||||||
// Wrong column
|
|
||||||
assert!(model.set_user_input(0, 1, 0, "1").is_err());
|
|
||||||
// row too large
|
|
||||||
assert!(model.set_user_input(0, LAST_ROW, 1, "1").is_ok());
|
|
||||||
assert!(model.set_user_input(0, LAST_ROW + 1, 1, "1").is_err());
|
|
||||||
// column too large
|
|
||||||
assert!(model.set_user_input(0, 1, LAST_COLUMN, "1").is_ok());
|
|
||||||
assert!(model.set_user_input(0, 1, LAST_COLUMN + 1, "1").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_model_debug_message() {
|
|
||||||
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let s = &format!("{:?}", model);
|
|
||||||
assert_eq!(s, "UserModel");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cell_type() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.set_user_input(0, 1, 1, "1").unwrap();
|
|
||||||
model.set_user_input(0, 1, 2, "Wish you were here").unwrap();
|
|
||||||
model.set_user_input(0, 1, 3, "true").unwrap();
|
|
||||||
model.set_user_input(0, 1, 4, "=1/0").unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_cell_type(0, 1, 1).unwrap(), CellType::Number);
|
|
||||||
assert_eq!(model.get_cell_type(0, 1, 2).unwrap(), CellType::Text);
|
|
||||||
assert_eq!(
|
|
||||||
model.get_cell_type(0, 1, 3).unwrap(),
|
|
||||||
CellType::LogicalValue
|
|
||||||
);
|
|
||||||
assert_eq!(model.get_cell_type(0, 1, 4).unwrap(), CellType::ErrorValue);
|
|
||||||
|
|
||||||
// empty cells are number type
|
|
||||||
assert_eq!(model.get_cell_type(0, 40, 40).unwrap(), CellType::Number);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn insert_remove_rows() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
let height = model.get_row_height(0, 5).unwrap();
|
|
||||||
|
|
||||||
// Insert some data in row 5 (and change the style)
|
|
||||||
assert!(model.set_user_input(0, 5, 1, "100$").is_ok());
|
|
||||||
// Change the height of the column
|
|
||||||
assert!(model.set_row_height(0, 5, 3.0 * height).is_ok());
|
|
||||||
|
|
||||||
// remove the row
|
|
||||||
assert!(model.delete_row(0, 5).is_ok());
|
|
||||||
// Row 5 has now the normal height
|
|
||||||
assert_eq!(model.get_row_height(0, 5), Ok(height));
|
|
||||||
// There is no value in A5
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 5, 1), Ok("".to_string()));
|
|
||||||
// Setting a value will not format it
|
|
||||||
assert!(model.set_user_input(0, 5, 1, "125").is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 5, 1),
|
|
||||||
Ok("125".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// undo twice
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
|
|
||||||
assert_eq!(model.get_row_height(0, 5), Ok(3.0 * height));
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 5, 1),
|
|
||||||
Ok("100$".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn insert_remove_columns() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// column E
|
|
||||||
let column_width = model.get_column_width(0, 5).unwrap();
|
|
||||||
println!("{column_width}");
|
|
||||||
|
|
||||||
// Insert some data in row 5 (and change the style) in E1
|
|
||||||
assert!(model.set_user_input(0, 1, 5, "100$").is_ok());
|
|
||||||
// Change the width of the column
|
|
||||||
assert!(model.set_column_width(0, 5, 3.0 * column_width).is_ok());
|
|
||||||
assert_eq!(model.get_column_width(0, 5).unwrap(), 3.0 * column_width);
|
|
||||||
|
|
||||||
// remove the column
|
|
||||||
assert!(model.delete_column(0, 5).is_ok());
|
|
||||||
// Column 5 has now the normal width
|
|
||||||
assert_eq!(model.get_column_width(0, 5), Ok(column_width));
|
|
||||||
// There is no value in E5
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 5), Ok("".to_string()));
|
|
||||||
// Setting a value will not format it
|
|
||||||
assert!(model.set_user_input(0, 1, 5, "125").is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 5),
|
|
||||||
Ok("125".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// undo twice (set_user_input and delete_column)
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
|
|
||||||
assert_eq!(model.get_column_width(0, 5), Ok(3.0 * column_width));
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 5),
|
|
||||||
Ok("100$".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delete_remove_cell() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let (sheet, row, column) = (0, 1, 1);
|
|
||||||
model.set_user_input(sheet, row, column, "100$").unwrap();
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::UserModel;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_rename() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.rename_sheet(0, "NewSheet").unwrap();
|
|
||||||
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn undo_redo() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model.rename_sheet(0, "NewSheet").unwrap();
|
|
||||||
model.undo().unwrap();
|
|
||||||
assert_eq!(model.get_worksheets_properties()[0].name, "Sheet1");
|
|
||||||
model.redo().unwrap();
|
|
||||||
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
assert_eq!(model.get_worksheets_properties()[0].name, "NewSheet");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn errors() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
model.rename_sheet(0, ""),
|
|
||||||
Err("Invalid name for a sheet: ''.".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.rename_sheet(1, "Hello"),
|
|
||||||
Err("Invalid sheet index".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
constants::{DEFAULT_COLUMN_WIDTH, DEFAULT_ROW_HEIGHT, LAST_COLUMN},
|
|
||||||
test::util::new_empty_model,
|
|
||||||
UserModel,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple_insert_row() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
let (sheet, column) = (0, 5);
|
|
||||||
for row in 1..5 {
|
|
||||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
|
||||||
}
|
|
||||||
assert!(model.insert_row(sheet, 3).is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
|
||||||
""
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
|
||||||
"123"
|
|
||||||
);
|
|
||||||
assert!(model.redo().is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(sheet, 3, column).unwrap(),
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple_insert_column() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
let (sheet, row) = (0, 5);
|
|
||||||
for column in 1..5 {
|
|
||||||
assert!(model.set_user_input(sheet, row, column, "123").is_ok());
|
|
||||||
}
|
|
||||||
assert!(model.insert_column(sheet, 3).is_ok());
|
|
||||||
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
|
|
||||||
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(sheet, row, 3).unwrap(),
|
|
||||||
"123"
|
|
||||||
);
|
|
||||||
assert!(model.redo().is_ok());
|
|
||||||
assert_eq!(model.get_formatted_cell_value(sheet, row, 3).unwrap(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple_delete_column() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.set_user_input(0, 1, 5, "3").unwrap();
|
|
||||||
model.set_user_input(0, 2, 5, "=E1*2").unwrap();
|
|
||||||
model
|
|
||||||
.set_column_width(0, 5, DEFAULT_COLUMN_WIDTH * 3.0)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
model.delete_column(0, 5).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("".to_string()));
|
|
||||||
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH));
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 2, 5), Ok("6".to_string()));
|
|
||||||
assert_eq!(model.get_column_width(0, 5), Ok(DEFAULT_COLUMN_WIDTH * 3.0));
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 2, 5),
|
|
||||||
Ok("6".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_column_width(0, 5),
|
|
||||||
Ok(DEFAULT_COLUMN_WIDTH * 3.0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn delete_column_errors() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
assert_eq!(
|
|
||||||
model.delete_column(1, 1),
|
|
||||||
Err("Invalid sheet index".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.delete_column(0, 0),
|
|
||||||
Err("Column number '0' is not valid.".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.delete_column(0, LAST_COLUMN + 1),
|
|
||||||
Err("Column number '16385' is not valid.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(model.delete_column(0, LAST_COLUMN), Ok(()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple_delete_row() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.set_user_input(0, 15, 4, "3").unwrap();
|
|
||||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
|
||||||
|
|
||||||
model
|
|
||||||
.set_row_height(0, 15, DEFAULT_ROW_HEIGHT * 3.0)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
model.delete_row(0, 15).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
|
||||||
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT));
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 15, 6),
|
|
||||||
Ok("6".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(model.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0));
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 15, 6),
|
|
||||||
Ok("6".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(model2.get_row_height(0, 15), Ok(DEFAULT_ROW_HEIGHT * 3.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple_delete_row_no_style() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
model.set_user_input(0, 15, 4, "3").unwrap();
|
|
||||||
model.set_user_input(0, 15, 6, "=D15*2").unwrap();
|
|
||||||
model.delete_row(0, 15).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 15, 6), Ok("".to_string()));
|
|
||||||
}
|
|
||||||
@@ -1,711 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
expressions::types::Area,
|
|
||||||
types::{Alignment, BorderItem, BorderStyle, HorizontalAlignment, VerticalAlignment},
|
|
||||||
UserModel,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_fonts() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
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()));
|
|
||||||
|
|
||||||
// bold
|
|
||||||
model.update_range_style(&range, "font.b", "true").unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert!(style.font.b);
|
|
||||||
|
|
||||||
// italics
|
|
||||||
model.update_range_style(&range, "font.i", "true").unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert!(style.font.i);
|
|
||||||
|
|
||||||
// underline
|
|
||||||
model.update_range_style(&range, "font.u", "true").unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert!(style.font.u);
|
|
||||||
|
|
||||||
// strike
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "font.strike", "true")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert!(style.font.strike);
|
|
||||||
|
|
||||||
// color
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "font.color", "#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.font.color, Some("#F1F1F1".to_owned()));
|
|
||||||
|
|
||||||
while model.can_undo() {
|
|
||||||
model.undo().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
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()));
|
|
||||||
|
|
||||||
while model.can_redo() {
|
|
||||||
model.redo().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
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("#F1F1F1".to_owned()));
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
let style = model2.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("#F1F1F1".to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn font_errors() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "font.b", "True"),
|
|
||||||
Err("Invalid value for boolean: 'True'.".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "font.i", "FALSE"),
|
|
||||||
Err("Invalid value for boolean: 'FALSE'.".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "font.bold", "true"),
|
|
||||||
Err("Invalid style path: 'font.bold'.".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "font.strike", ""),
|
|
||||||
Err("Invalid value for boolean: ''.".to_string())
|
|
||||||
);
|
|
||||||
// There is no cast for booleans
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "font.b", "1"),
|
|
||||||
Err("Invalid value for boolean: '1'.".to_string())
|
|
||||||
);
|
|
||||||
// colors don't work by name
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "font.color", "blue"),
|
|
||||||
Err("Invalid color: 'blue'.".to_string())
|
|
||||||
);
|
|
||||||
// No short form
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "font.color", "#FFF"),
|
|
||||||
Err("Invalid color: '#FFF'.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_fill() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, None);
|
|
||||||
|
|
||||||
// bg_color
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "fill.bg_color", "#F2F2F2")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fill_errors() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
assert!(model
|
|
||||||
.update_range_style(&range, "fill.bg_color", "#FFF")
|
|
||||||
.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_format() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.num_fmt, "general");
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "num_fmt", "$#,##0.0000")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.num_fmt, "$#,##0.0000");
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.num_fmt, "general");
|
|
||||||
|
|
||||||
model.redo().unwrap();
|
|
||||||
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.num_fmt, "$#,##0.0000");
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.num_fmt, "$#,##0.0000");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_borders() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "thin,#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Thin,
|
|
||||||
color: Some("#F1F1F1".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "thin,")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Thin,
|
|
||||||
color: None,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.right", "dotted,#F1F1F2")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.right,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Dotted,
|
|
||||||
color: Some("#F1F1F2".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.top", "double,#F1F1F3")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.top,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Double,
|
|
||||||
color: Some("#F1F1F3".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.bottom", "medium,#F1F1F4")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.bottom,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Medium,
|
|
||||||
color: Some("#F1F1F4".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
while model.can_undo() {
|
|
||||||
model.undo().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.border.left, None);
|
|
||||||
assert_eq!(style.border.top, None);
|
|
||||||
assert_eq!(style.border.right, None);
|
|
||||||
assert_eq!(style.border.bottom, None);
|
|
||||||
|
|
||||||
while model.can_redo() {
|
|
||||||
model.redo().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Thin,
|
|
||||||
color: None,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style.border.right,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Dotted,
|
|
||||||
color: Some("#F1F1F2".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style.border.top,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Double,
|
|
||||||
color: Some("#F1F1F3".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style.border.bottom,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Medium,
|
|
||||||
color: Some("#F1F1F4".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let send_queue = model.flush_send_queue();
|
|
||||||
|
|
||||||
let mut model2 = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
model2.apply_external_diffs(&send_queue).unwrap();
|
|
||||||
|
|
||||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Thin,
|
|
||||||
color: None,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style.border.right,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Dotted,
|
|
||||||
color: Some("#F1F1F2".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style.border.top,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Double,
|
|
||||||
color: Some("#F1F1F3".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
style.border.bottom,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Medium,
|
|
||||||
color: Some("#F1F1F4".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_alignment() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
|
||||||
assert_eq!(alignment, None);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "alignment.horizontal", "center")
|
|
||||||
.unwrap();
|
|
||||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
|
||||||
assert_eq!(
|
|
||||||
alignment,
|
|
||||||
Some(Alignment {
|
|
||||||
horizontal: HorizontalAlignment::Center,
|
|
||||||
vertical: VerticalAlignment::Bottom,
|
|
||||||
wrap_text: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "alignment.horizontal", "centerContinuous")
|
|
||||||
.unwrap();
|
|
||||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
|
||||||
assert_eq!(
|
|
||||||
alignment,
|
|
||||||
Some(Alignment {
|
|
||||||
horizontal: HorizontalAlignment::CenterContinuous,
|
|
||||||
vertical: VerticalAlignment::Bottom,
|
|
||||||
wrap_text: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 2,
|
|
||||||
column: 2,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "alignment.vertical", "distributed")
|
|
||||||
.unwrap();
|
|
||||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
|
||||||
assert_eq!(
|
|
||||||
alignment,
|
|
||||||
Some(Alignment {
|
|
||||||
horizontal: HorizontalAlignment::General,
|
|
||||||
vertical: VerticalAlignment::Distributed,
|
|
||||||
wrap_text: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "alignment.vertical", "justify")
|
|
||||||
.unwrap();
|
|
||||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
|
||||||
assert_eq!(
|
|
||||||
alignment,
|
|
||||||
Some(Alignment {
|
|
||||||
horizontal: HorizontalAlignment::General,
|
|
||||||
vertical: VerticalAlignment::Justify,
|
|
||||||
wrap_text: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model.update_range_style(&range, "alignment", "").unwrap();
|
|
||||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
|
||||||
assert_eq!(alignment, None);
|
|
||||||
|
|
||||||
model.undo().unwrap();
|
|
||||||
|
|
||||||
let alignment = model.get_cell_style(0, 2, 2).unwrap().alignment;
|
|
||||||
assert_eq!(
|
|
||||||
alignment,
|
|
||||||
Some(Alignment {
|
|
||||||
horizontal: HorizontalAlignment::General,
|
|
||||||
vertical: VerticalAlignment::Justify,
|
|
||||||
wrap_text: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn alignment_errors() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment", "some"),
|
|
||||||
Err("Alignment must be empty, but found: 'some'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment.vertical", "justified"),
|
|
||||||
Err("Invalid value for vertical alignment: 'justified'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment.horizontal", "unjustified"),
|
|
||||||
Err("Invalid value for horizontal alignment: 'unjustified'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "alignment.vertical", "justify")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Also fail if there is an alignment
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment", "some"),
|
|
||||||
Err("Alignment must be empty, but found: 'some'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment.vertical", "justified"),
|
|
||||||
Err("Invalid value for vertical alignment: 'justified'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment.horizontal", "unjustified"),
|
|
||||||
Err("Invalid value for horizontal alignment: 'unjustified'.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_wrap_text() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment.wrap_text", "T"),
|
|
||||||
Err("Invalid value for boolean: 'T'.".to_string())
|
|
||||||
);
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "alignment.wrap_text", "true")
|
|
||||||
.unwrap();
|
|
||||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
|
||||||
assert_eq!(
|
|
||||||
alignment,
|
|
||||||
Some(Alignment {
|
|
||||||
horizontal: HorizontalAlignment::General,
|
|
||||||
vertical: VerticalAlignment::Bottom,
|
|
||||||
wrap_text: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
model.undo().unwrap();
|
|
||||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
|
||||||
assert_eq!(alignment, None);
|
|
||||||
|
|
||||||
model.redo().unwrap();
|
|
||||||
|
|
||||||
let alignment = model.get_cell_style(0, 1, 1).unwrap().alignment;
|
|
||||||
assert_eq!(
|
|
||||||
alignment,
|
|
||||||
Some(Alignment {
|
|
||||||
horizontal: HorizontalAlignment::General,
|
|
||||||
vertical: VerticalAlignment::Bottom,
|
|
||||||
wrap_text: true
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "alignment.wrap_text", "True"),
|
|
||||||
Err("Invalid value for boolean: 'True'.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn more_basic_borders() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "thick,#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::Thick,
|
|
||||||
color: Some("#F1F1F1".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "slantDashDot,#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::SlantDashDot,
|
|
||||||
color: Some("#F1F1F1".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "mediumDashDot,#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::MediumDashDot,
|
|
||||||
color: Some("#F1F1F1".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::MediumDashDotDot,
|
|
||||||
color: Some("#F1F1F1".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "mediumDashed,#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::MediumDashed,
|
|
||||||
color: Some("#F1F1F1".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn border_errors() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "border.lef", "thick,#F1F1F1"),
|
|
||||||
Err("Invalid style path: 'border.lef'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "border.left", "thic,#F1F1F1"),
|
|
||||||
Err("Invalid border style: 'thic'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "border.left", "thick,#F1F1F"),
|
|
||||||
Err("Invalid color: '#F1F1F'.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "border.left", " "),
|
|
||||||
Err("Invalid border value: ' '.".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
model.update_range_style(&range, "border.left", "thick,#F1F1F1,thin"),
|
|
||||||
Err("Invalid border value: 'thick,#F1F1F1,thin'.".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_removes_border() {
|
|
||||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
|
||||||
let range = Area {
|
|
||||||
sheet: 0,
|
|
||||||
row: 1,
|
|
||||||
column: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
};
|
|
||||||
model
|
|
||||||
.update_range_style(&range, "border.left", "mediumDashDotDot,#F1F1F1")
|
|
||||||
.unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
style.border.left,
|
|
||||||
Some(BorderItem {
|
|
||||||
style: BorderStyle::MediumDashDotDot,
|
|
||||||
color: Some("#F1F1F1".to_owned()),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
model.update_range_style(&range, "border.left", "").unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert_eq!(style.border.left, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn false_removes_value() {
|
|
||||||
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();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert!(style.font.b);
|
|
||||||
|
|
||||||
model.update_range_style(&range, "font.b", "false").unwrap();
|
|
||||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
|
||||||
assert!(!style.font.b);
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{test::util::new_empty_model, UserModel};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic() {
|
|
||||||
let mut model1 = UserModel::from_model(new_empty_model());
|
|
||||||
let width = model1.get_column_width(0, 3).unwrap() * 3.0;
|
|
||||||
model1.set_column_width(0, 3, width).unwrap();
|
|
||||||
model1.set_user_input(0, 1, 2, "Hello IronCalc!").unwrap();
|
|
||||||
|
|
||||||
let model_bytes = model1.to_bytes();
|
|
||||||
|
|
||||||
let model2 = UserModel::from_bytes(&model_bytes).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(model2.get_column_width(0, 3), Ok(width));
|
|
||||||
assert_eq!(
|
|
||||||
model2.get_formatted_cell_value(0, 1, 2),
|
|
||||||
Ok("Hello IronCalc!".to_string())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn errors() {
|
|
||||||
let model_bytes = "Early in the morning, late in the century, Cricklewood Broadway.".as_bytes();
|
|
||||||
assert_eq!(
|
|
||||||
&UserModel::from_bytes(model_bytes).unwrap_err(),
|
|
||||||
"Error parsing workbook: invalid packing"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#![allow(clippy::unwrap_used)]
|
|
||||||
|
|
||||||
use crate::{test::util::new_empty_model, UserModel};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple_undo_redo() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
// at the beginning I cannot undo or redo
|
|
||||||
assert!(!model.can_undo());
|
|
||||||
assert!(!model.can_redo());
|
|
||||||
assert!(model.set_user_input(0, 1, 1, "=1+2").is_ok());
|
|
||||||
|
|
||||||
// Once I enter a value I can undo but not redo
|
|
||||||
assert!(model.can_undo());
|
|
||||||
assert!(!model.can_redo());
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string()));
|
|
||||||
|
|
||||||
// If I undo, I can't undo anymore, but I can redo
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
assert!(!model.can_undo());
|
|
||||||
assert!(model.can_redo());
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("".to_string()));
|
|
||||||
|
|
||||||
// If I redo, I have the old value and formula
|
|
||||||
assert!(model.redo().is_ok());
|
|
||||||
assert_eq!(model.get_formatted_cell_value(0, 1, 1), Ok("3".to_string()));
|
|
||||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("=1+2".to_string()));
|
|
||||||
assert!(model.can_undo());
|
|
||||||
assert!(!model.can_redo());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn undo_redo_respect_styles() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
assert!(model.set_user_input(0, 1, 1, "100").is_ok());
|
|
||||||
assert!(model.set_user_input(0, 1, 1, "125$").is_ok());
|
|
||||||
// The content of the cell is just the number 125
|
|
||||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string()));
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
// The cell has no currency number formatting
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 1),
|
|
||||||
Ok("100".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("100".to_string()));
|
|
||||||
assert!(model.redo().is_ok());
|
|
||||||
// The cell has the number 125 formatted as '125$'
|
|
||||||
assert_eq!(
|
|
||||||
model.get_formatted_cell_value(0, 1, 1),
|
|
||||||
Ok("125$".to_string())
|
|
||||||
);
|
|
||||||
assert_eq!(model.get_cell_content(0, 1, 1), Ok("125".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn can_undo_can_redo() {
|
|
||||||
let model = new_empty_model();
|
|
||||||
let mut model = UserModel::from_model(model);
|
|
||||||
assert!(!model.can_undo());
|
|
||||||
assert!(!model.can_redo());
|
|
||||||
|
|
||||||
assert!(model.undo().is_ok());
|
|
||||||
assert!(model.redo().is_ok());
|
|
||||||
}
|
|
||||||
@@ -32,11 +32,11 @@ impl Model {
|
|||||||
let cell_reference = self._parse_reference(cell);
|
let cell_reference = self._parse_reference(cell);
|
||||||
let column = cell_reference.column;
|
let column = cell_reference.column;
|
||||||
let row = cell_reference.row;
|
let row = cell_reference.row;
|
||||||
self.get_cell_formula(cell_reference.sheet, row, column)
|
self.cell_formula(cell_reference.sheet, row, column)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
pub fn _get_text_at(&self, sheet: u32, row: i32, column: i32) -> String {
|
pub fn _get_text_at(&self, sheet: u32, row: i32, column: i32) -> String {
|
||||||
self.get_formatted_cell_value(sheet, row, column).unwrap()
|
self.formatted_cell_value(sheet, row, column).unwrap()
|
||||||
}
|
}
|
||||||
pub fn _get_text(&self, cell: &str) -> String {
|
pub fn _get_text(&self, cell: &str) -> String {
|
||||||
let CellReferenceIndex { sheet, row, column } = self._parse_reference(cell);
|
let CellReferenceIndex { sheet, row, column } = self._parse_reference(cell);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use bitcode::{Decode, Encode};
|
use bincode::{Decode, Encode};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, fmt::Display};
|
use std::{collections::HashMap, fmt::Display};
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ fn hashmap_is_empty(h: &HashMap<String, Table>) -> bool {
|
|||||||
h.values().len() == 0
|
h.values().len() == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
pub application: String,
|
pub application: String,
|
||||||
pub app_version: String,
|
pub app_version: String,
|
||||||
@@ -44,13 +44,13 @@ pub struct Metadata {
|
|||||||
pub last_modified: String, //"2020-11-20T16:24:35"
|
pub last_modified: String, //"2020-11-20T16:24:35"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct WorkbookSettings {
|
pub struct WorkbookSettings {
|
||||||
pub tz: String,
|
pub tz: String,
|
||||||
pub locale: String,
|
pub locale: String,
|
||||||
}
|
}
|
||||||
/// An internal representation of an IronCalc Workbook
|
/// An internal representation of an IronCalc Workbook
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Clone)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Workbook {
|
pub struct Workbook {
|
||||||
pub shared_strings: Vec<String>,
|
pub shared_strings: Vec<String>,
|
||||||
@@ -66,7 +66,7 @@ pub struct Workbook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A defined name. The `sheet_id` is the sheet index in case the name is local
|
/// A defined name. The `sheet_id` is the sheet index in case the name is local
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct DefinedName {
|
pub struct DefinedName {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub formula: String,
|
pub formula: String,
|
||||||
@@ -80,7 +80,7 @@ pub struct DefinedName {
|
|||||||
/// * state:
|
/// * state:
|
||||||
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
||||||
/// hidden, veryHidden, visible
|
/// hidden, veryHidden, visible
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum SheetState {
|
pub enum SheetState {
|
||||||
Visible,
|
Visible,
|
||||||
@@ -99,7 +99,7 @@ impl Display for SheetState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Internal representation of a worksheet Excel object
|
/// Internal representation of a worksheet Excel object
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Clone)]
|
||||||
pub struct Worksheet {
|
pub struct Worksheet {
|
||||||
pub dimension: String,
|
pub dimension: String,
|
||||||
pub cols: Vec<Col>,
|
pub cols: Vec<Col>,
|
||||||
@@ -126,7 +126,7 @@ pub struct Worksheet {
|
|||||||
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
|
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
|
||||||
|
|
||||||
// ECMA-376-1:2016 section 18.3.1.73
|
// ECMA-376-1:2016 section 18.3.1.73
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Clone)]
|
||||||
pub struct Row {
|
pub struct Row {
|
||||||
/// Row index
|
/// Row index
|
||||||
pub r: i32,
|
pub r: i32,
|
||||||
@@ -140,7 +140,7 @@ pub struct Row {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ECMA-376-1:2016 section 18.3.1.13
|
// ECMA-376-1:2016 section 18.3.1.13
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Clone)]
|
||||||
pub struct Col {
|
pub struct Col {
|
||||||
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
|
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
|
||||||
/// First column affected by this record. Settings apply to column in \[min, max\] range.
|
/// First column affected by this record. Settings apply to column in \[min, max\] range.
|
||||||
@@ -165,7 +165,7 @@ pub enum CellType {
|
|||||||
CompoundData = 128,
|
CompoundData = 128,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq)]
|
||||||
#[serde(tag = "t", deny_unknown_fields)]
|
#[serde(tag = "t", deny_unknown_fields)]
|
||||||
pub enum Cell {
|
pub enum Cell {
|
||||||
#[serde(rename = "empty")]
|
#[serde(rename = "empty")]
|
||||||
@@ -209,7 +209,7 @@ impl Default for Cell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub author_name: String,
|
pub author_name: String,
|
||||||
@@ -219,7 +219,7 @@ pub struct Comment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ECMA-376-1:2016 section 18.5.1.2
|
// ECMA-376-1:2016 section 18.5.1.2
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Table {
|
pub struct Table {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
@@ -242,7 +242,7 @@ pub struct Table {
|
|||||||
|
|
||||||
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
|
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
|
||||||
// the totals_row_function is an enum not String methinks
|
// the totals_row_function is an enum not String methinks
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct TableColumn {
|
pub struct TableColumn {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -272,7 +272,7 @@ impl Default for TableColumn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone, Default)]
|
||||||
pub struct TableStyleInfo {
|
pub struct TableStyleInfo {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
@@ -290,7 +290,7 @@ pub struct TableStyleInfo {
|
|||||||
pub show_column_stripes: bool,
|
pub show_column_stripes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Styles {
|
pub struct Styles {
|
||||||
pub num_fmts: Vec<NumFmt>,
|
pub num_fmts: Vec<NumFmt>,
|
||||||
pub fonts: Vec<Font>,
|
pub fonts: Vec<Font>,
|
||||||
@@ -315,9 +315,8 @@ impl Default for Styles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Style {
|
pub struct Style {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub alignment: Option<Alignment>,
|
pub alignment: Option<Alignment>,
|
||||||
pub num_fmt: String,
|
pub num_fmt: String,
|
||||||
pub fill: Fill,
|
pub fill: Fill,
|
||||||
@@ -326,7 +325,7 @@ pub struct Style {
|
|||||||
pub quote_prefix: bool,
|
pub quote_prefix: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct NumFmt {
|
pub struct NumFmt {
|
||||||
pub num_fmt_id: i32,
|
pub num_fmt_id: i32,
|
||||||
pub format_code: String,
|
pub format_code: String,
|
||||||
@@ -344,7 +343,7 @@ impl Default for NumFmt {
|
|||||||
// ST_FontScheme simple type (§18.18.33).
|
// ST_FontScheme simple type (§18.18.33).
|
||||||
// Usually major fonts are used for styles like headings,
|
// Usually major fonts are used for styles like headings,
|
||||||
// and minor fonts are used for body and paragraph text.
|
// and minor fonts are used for body and paragraph text.
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub enum FontScheme {
|
pub enum FontScheme {
|
||||||
@@ -364,7 +363,7 @@ impl Display for FontScheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
#[serde(default = "default_as_false")]
|
#[serde(default = "default_as_false")]
|
||||||
#[serde(skip_serializing_if = "is_false")]
|
#[serde(skip_serializing_if = "is_false")]
|
||||||
@@ -407,7 +406,7 @@ impl Default for Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Maybe use an enum for the pattern_type values here?
|
// TODO: Maybe use an enum for the pattern_type values here?
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Fill {
|
pub struct Fill {
|
||||||
pub pattern_type: String,
|
pub pattern_type: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
@@ -426,7 +425,7 @@ impl Default for Fill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum HorizontalAlignment {
|
pub enum HorizontalAlignment {
|
||||||
Center,
|
Center,
|
||||||
@@ -468,7 +467,7 @@ impl Display for HorizontalAlignment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum VerticalAlignment {
|
pub enum VerticalAlignment {
|
||||||
Bottom,
|
Bottom,
|
||||||
@@ -503,7 +502,7 @@ impl Display for VerticalAlignment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 1762
|
// 1762
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone, Default)]
|
||||||
pub struct Alignment {
|
pub struct Alignment {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(skip_serializing_if = "HorizontalAlignment::is_default")]
|
#[serde(skip_serializing_if = "HorizontalAlignment::is_default")]
|
||||||
@@ -516,7 +515,7 @@ pub struct Alignment {
|
|||||||
pub wrap_text: bool,
|
pub wrap_text: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct CellStyleXfs {
|
pub struct CellStyleXfs {
|
||||||
pub num_fmt_id: i32,
|
pub num_fmt_id: i32,
|
||||||
pub font_id: i32,
|
pub font_id: i32,
|
||||||
@@ -559,7 +558,7 @@ impl Default for CellStyleXfs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone, Default)]
|
||||||
pub struct CellXfs {
|
pub struct CellXfs {
|
||||||
pub xf_id: i32,
|
pub xf_id: i32,
|
||||||
pub num_fmt_id: i32,
|
pub num_fmt_id: i32,
|
||||||
@@ -591,7 +590,7 @@ pub struct CellXfs {
|
|||||||
pub alignment: Option<Alignment>,
|
pub alignment: Option<Alignment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct CellStyles {
|
pub struct CellStyles {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub xf_id: i32,
|
pub xf_id: i32,
|
||||||
@@ -608,7 +607,7 @@ impl Default for CellStyles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum BorderStyle {
|
pub enum BorderStyle {
|
||||||
Thin,
|
Thin,
|
||||||
@@ -638,13 +637,13 @@ impl Display for BorderStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct BorderItem {
|
pub struct BorderItem {
|
||||||
pub style: BorderStyle,
|
pub style: BorderStyle,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq, Eq, Clone, Default)]
|
||||||
pub struct Border {
|
pub struct Border {
|
||||||
#[serde(default = "default_as_false")]
|
#[serde(default = "default_as_false")]
|
||||||
#[serde(skip_serializing_if = "is_false")]
|
#[serde(skip_serializing_if = "is_false")]
|
||||||
@@ -666,11 +665,10 @@ pub struct Border {
|
|||||||
|
|
||||||
/// Information need to show a sheet tab in the UI
|
/// Information need to show a sheet tab in the UI
|
||||||
/// The color is serialized only if it is not Color::None
|
/// The color is serialized only if it is not Color::None
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
#[derive(Serialize, Deserialize, Decode, Encode, Debug, PartialEq)]
|
||||||
pub struct SheetProperties {
|
pub struct SheetInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
pub sheet_id: u32,
|
pub sheet_id: u32,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -27,4 +27,16 @@ impl Workbook {
|
|||||||
.get_mut(worksheet_index as usize)
|
.get_mut(worksheet_index as usize)
|
||||||
.ok_or_else(|| "Invalid sheet index".to_string())
|
.ok_or_else(|| "Invalid sheet index".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_worksheets_info(&self) -> Vec<SheetInfo> {
|
||||||
|
self.worksheets
|
||||||
|
.iter()
|
||||||
|
.map(|worksheet| SheetInfo {
|
||||||
|
name: worksheet.get_name(),
|
||||||
|
state: worksheet.state.to_string(),
|
||||||
|
color: worksheet.color.clone(),
|
||||||
|
sheet_id: worksheet.sheet_id,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ impl Worksheet {
|
|||||||
self.sheet_data.get_mut(&row)?.get_mut(&column)
|
self.sheet_data.get_mut(&row)?.get_mut(&column)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) {
|
fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) {
|
||||||
match self.sheet_data.get_mut(&row) {
|
match self.sheet_data.get_mut(&row) {
|
||||||
Some(column_data) => match column_data.get(&column) {
|
Some(column_data) => match column_data.get(&column) {
|
||||||
Some(_cell) => {
|
Some(_cell) => {
|
||||||
@@ -68,10 +68,11 @@ impl Worksheet {
|
|||||||
if row.r == row_index {
|
if row.r == row_index {
|
||||||
if row.custom_format {
|
if row.custom_format {
|
||||||
return row.s;
|
return row.s;
|
||||||
}
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let cols = &self.cols;
|
let cols = &self.cols;
|
||||||
for column in cols.iter() {
|
for column in cols.iter() {
|
||||||
let min = column.min;
|
let min = column.min;
|
||||||
@@ -105,8 +106,64 @@ impl Worksheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 cols = &mut self.cols;
|
||||||
self.set_column_width_and_style(column, width, Some(style_index))
|
let col = Col {
|
||||||
|
min: column,
|
||||||
|
max: column,
|
||||||
|
width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR,
|
||||||
|
custom_width: true,
|
||||||
|
style: Some(style_index),
|
||||||
|
};
|
||||||
|
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 {
|
||||||
|
if min == column && max == column {
|
||||||
|
c.style = Some(style_index);
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
// We need to split the result
|
||||||
|
split = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if column < min {
|
||||||
|
// We passed, we should insert at index
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
if split {
|
||||||
|
let min = cols[index].min;
|
||||||
|
let max = cols[index].max;
|
||||||
|
let pre = Col {
|
||||||
|
min,
|
||||||
|
max: column - 1,
|
||||||
|
width: cols[index].width,
|
||||||
|
custom_width: cols[index].custom_width,
|
||||||
|
style: cols[index].style,
|
||||||
|
};
|
||||||
|
let post = Col {
|
||||||
|
min: column + 1,
|
||||||
|
max,
|
||||||
|
width: cols[index].width,
|
||||||
|
custom_width: cols[index].custom_width,
|
||||||
|
style: cols[index].style,
|
||||||
|
};
|
||||||
|
cols.remove(index);
|
||||||
|
if column != max {
|
||||||
|
cols.insert(index, post);
|
||||||
|
}
|
||||||
|
cols.insert(index, col);
|
||||||
|
if column != min {
|
||||||
|
cols.insert(index, pre);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cols.insert(index, col);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
||||||
@@ -134,7 +191,7 @@ impl Worksheet {
|
|||||||
cell.set_style(style_index);
|
cell.set_style(style_index);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.cell_clear_contents_with_style(row, column, style_index);
|
self.set_cell_empty_with_style(row, column, style_index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,13 +223,13 @@ impl Worksheet {
|
|||||||
self.update_cell(row, column, cell);
|
self.update_cell(row, column, cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cell_clear_contents(&mut self, row: i32, column: i32) {
|
pub fn set_cell_empty(&mut self, row: i32, column: i32) {
|
||||||
let s = self.get_style(row, column);
|
let s = self.get_style(row, column);
|
||||||
let cell = Cell::EmptyCell { s };
|
let cell = Cell::EmptyCell { s };
|
||||||
self.update_cell(row, column, cell);
|
self.update_cell(row, column, cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cell_clear_contents_with_style(&mut self, row: i32, column: i32, style: i32) {
|
pub fn set_cell_empty_with_style(&mut self, row: i32, column: i32, style: i32) {
|
||||||
let cell = Cell::EmptyCell { s: style };
|
let cell = Cell::EmptyCell { s: style };
|
||||||
self.update_cell(row, column, cell);
|
self.update_cell(row, column, cell);
|
||||||
}
|
}
|
||||||
@@ -180,8 +237,7 @@ impl Worksheet {
|
|||||||
pub fn set_frozen_rows(&mut self, frozen_rows: i32) -> Result<(), String> {
|
pub fn set_frozen_rows(&mut self, frozen_rows: i32) -> Result<(), String> {
|
||||||
if frozen_rows < 0 {
|
if frozen_rows < 0 {
|
||||||
return Err("Frozen rows cannot be negative".to_string());
|
return Err("Frozen rows cannot be negative".to_string());
|
||||||
}
|
} else if frozen_rows >= constants::LAST_ROW {
|
||||||
if frozen_rows >= constants::LAST_ROW {
|
|
||||||
return Err("Too many rows".to_string());
|
return Err("Too many rows".to_string());
|
||||||
}
|
}
|
||||||
self.frozen_rows = frozen_rows;
|
self.frozen_rows = frozen_rows;
|
||||||
@@ -191,8 +247,7 @@ impl Worksheet {
|
|||||||
pub fn set_frozen_columns(&mut self, frozen_columns: i32) -> Result<(), String> {
|
pub fn set_frozen_columns(&mut self, frozen_columns: i32) -> Result<(), String> {
|
||||||
if frozen_columns < 0 {
|
if frozen_columns < 0 {
|
||||||
return Err("Frozen columns cannot be negative".to_string());
|
return Err("Frozen columns cannot be negative".to_string());
|
||||||
}
|
} else if frozen_columns >= constants::LAST_COLUMN {
|
||||||
if frozen_columns >= constants::LAST_COLUMN {
|
|
||||||
return Err("Too many columns".to_string());
|
return Err("Too many columns".to_string());
|
||||||
}
|
}
|
||||||
self.frozen_columns = frozen_columns;
|
self.frozen_columns = frozen_columns;
|
||||||
@@ -226,21 +281,11 @@ impl Worksheet {
|
|||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 ned to split the range.
|
||||||
/// Fails if column index is outside allowed range.
|
/// Fails if column index is outside allowed range.
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_column_width_and_style(
|
|
||||||
&mut self,
|
|
||||||
column: i32,
|
|
||||||
width: f64,
|
|
||||||
style: Option<i32>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
if !is_valid_column_number(column) {
|
if !is_valid_column_number(column) {
|
||||||
return Err(format!("Column number '{column}' is not valid."));
|
return Err(format!("Column number '{column}' is not valid."));
|
||||||
}
|
}
|
||||||
@@ -250,7 +295,7 @@ impl Worksheet {
|
|||||||
max: column,
|
max: column,
|
||||||
width: width / constants::COLUMN_WIDTH_FACTOR,
|
width: width / constants::COLUMN_WIDTH_FACTOR,
|
||||||
custom_width: true,
|
custom_width: true,
|
||||||
style,
|
style: None,
|
||||||
};
|
};
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
let mut split = false;
|
let mut split = false;
|
||||||
@@ -261,10 +306,12 @@ impl Worksheet {
|
|||||||
if min == column && max == column {
|
if min == column && max == column {
|
||||||
c.width = width / constants::COLUMN_WIDTH_FACTOR;
|
c.width = width / constants::COLUMN_WIDTH_FACTOR;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
} else {
|
||||||
|
// We need to split the result
|
||||||
split = true;
|
split = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if column < min {
|
if column < min {
|
||||||
// We passed, we should insert at index
|
// We passed, we should insert at index
|
||||||
break;
|
break;
|
||||||
@@ -304,7 +351,7 @@ impl Worksheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return the width of a column in pixels
|
/// Return the width of a column in pixels
|
||||||
pub fn get_column_width(&self, column: i32) -> Result<f64, String> {
|
pub fn column_width(&self, column: i32) -> Result<f64, String> {
|
||||||
if !is_valid_column_number(column) {
|
if !is_valid_column_number(column) {
|
||||||
return Err(format!("Column number '{column}' is not valid."));
|
return Err(format!("Column number '{column}' is not valid."));
|
||||||
}
|
}
|
||||||
@@ -316,10 +363,11 @@ impl Worksheet {
|
|||||||
if column >= min && column <= max {
|
if column >= min && column <= max {
|
||||||
if col.custom_width {
|
if col.custom_width {
|
||||||
return Ok(col.width * constants::COLUMN_WIDTH_FACTOR);
|
return Ok(col.width * constants::COLUMN_WIDTH_FACTOR);
|
||||||
}
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(constants::DEFAULT_COLUMN_WIDTH)
|
Ok(constants::DEFAULT_COLUMN_WIDTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
bindings/wasm/.gitignore
vendored
1
bindings/wasm/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
target/*
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "wasm"
|
|
||||||
version = "0.1.3"
|
|
||||||
authors = ["Nicolas Hatcher <nicolas@theuniverse.today>"]
|
|
||||||
description = "IronCalc Web bindings"
|
|
||||||
license = "MIT/Apache-2.0"
|
|
||||||
repository = "https://github.com/ironcalc/web-bindings"
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# Uses `../ironcalc/base` when used locally, and uses
|
|
||||||
# the inicated version from crates.io when published.
|
|
||||||
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
|
||||||
ironcalc_base = { path = "../../base", version = "0.1" }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
wasm-bindgen = "0.2.92"
|
|
||||||
serde-wasm-bindgen = "0.4"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
wasm-bindgen-test = "0.3.38"
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
all:
|
|
||||||
wasm-pack build --target web --scope ironcalc --release
|
|
||||||
cp README.pkg.md pkg/README.md
|
|
||||||
tsc types.ts --target esnext --module esnext
|
|
||||||
python fix_types.py
|
|
||||||
|
|
||||||
tests:
|
|
||||||
wasm-pack build --target nodejs && node tests/test.mjs
|
|
||||||
|
|
||||||
lint:
|
|
||||||
cargo check
|
|
||||||
cargo fmt -- --check
|
|
||||||
cargo clippy --all-targets --all-features -- -D warnings
|
|
||||||
|
|
||||||
clean:
|
|
||||||
cargo clean
|
|
||||||
rm -rf pkg
|
|
||||||
rm -f types.js
|
|
||||||
|
|
||||||
.PHONY: all lint clean
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# IronCalc Web bindings
|
|
||||||
|
|
||||||
This crate is used to build the web bindings for IronCalc.
|
|
||||||
Note that it does not contain the xlsx writer and reader, only the engine.
|
|
||||||
|
|
||||||
https://www.npmjs.com/package/@ironcalc/wasm?activeTab=readme
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Right now this is a manual process and only carries out a smoke test:
|
|
||||||
|
|
||||||
1. Build the package
|
|
||||||
2. Run `python -m http.server`
|
|
||||||
3. In your browser open <http://0.0.0.0:8000/test.html>
|
|
||||||
|
|
||||||
## Publishing
|
|
||||||
|
|
||||||
Follow the commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
wasm-pack login
|
|
||||||
make
|
|
||||||
cd pkg
|
|
||||||
npm publish --access=public
|
|
||||||
```
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# IronCalc Web bindings
|
|
||||||
|
|
||||||
This package contains web bindings for IronCalc. Note that it does not contain the xlsx writer and reader, only the engine.
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
In your project
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install @ironcalc/wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
And then in your TypeScript
|
|
||||||
|
|
||||||
```TypeScript
|
|
||||||
import init, { Model } from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
await init();
|
|
||||||
|
|
||||||
function compute() {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
|
|
||||||
model.setUserInput(0, 1, 1, "23");
|
|
||||||
model.setUserInput(0, 1, 2, "=A1*3+1");
|
|
||||||
|
|
||||||
const result = model.getFormattedCellValue(0, 1, 2);
|
|
||||||
|
|
||||||
console.log("Result: ", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
compute();
|
|
||||||
```
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# Regrettably at the time of writing there is not a perfect way to
|
|
||||||
# generate the TypeScript types from Rust so we basically fix them manually
|
|
||||||
# Hopefully this will suffice for our needs and one day will be automatic
|
|
||||||
|
|
||||||
header = r"""
|
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
get_tokens_str = r"""
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
export function getTokens(formula: string): any;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
get_tokens_str_types = r"""
|
|
||||||
* @returns {MarkedToken[]}
|
|
||||||
*/
|
|
||||||
export function getTokens(formula: string): MarkedToken[];
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
update_style_str = r"""
|
|
||||||
/**
|
|
||||||
* @param {any} range
|
|
||||||
* @param {string} style_path
|
|
||||||
* @param {string} value
|
|
||||||
*/
|
|
||||||
updateRangeStyle(range: any, style_path: string, value: string): void;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
update_style_str_types = r"""
|
|
||||||
/**
|
|
||||||
* @param {Area} range
|
|
||||||
* @param {string} style_path
|
|
||||||
* @param {string} value
|
|
||||||
*/
|
|
||||||
updateRangeStyle(range: Area, style_path: string, value: string): void;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
properties = r"""
|
|
||||||
/**
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
getWorksheetsProperties(): any;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
properties_types = r"""
|
|
||||||
/**
|
|
||||||
* @returns {WorksheetProperties[]}
|
|
||||||
*/
|
|
||||||
getWorksheetsProperties(): WorksheetProperties[];
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
style = r"""
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
getCellStyle(sheet: number, row: number, column: number): any;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
style_types = r"""
|
|
||||||
* @returns {CellStyle}
|
|
||||||
*/
|
|
||||||
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
def fix_types(text):
|
|
||||||
text = text.replace(get_tokens_str, get_tokens_str_types)
|
|
||||||
text = text.replace(update_style_str, update_style_str_types)
|
|
||||||
text = text.replace(properties, properties_types)
|
|
||||||
text = text.replace(style, style_types)
|
|
||||||
with open("types.ts") as f:
|
|
||||||
types_str = f.read()
|
|
||||||
header_types = "{}\n\n{}".format(header, types_str)
|
|
||||||
text = text.replace(header, header_types)
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
types_file = "pkg/wasm.d.ts"
|
|
||||||
with open(types_file) as f:
|
|
||||||
text = f.read()
|
|
||||||
text = fix_types(text)
|
|
||||||
with open(types_file, "wb") as f:
|
|
||||||
f.write(bytes(text, "utf8"))
|
|
||||||
|
|
||||||
js_file = "pkg/wasm.js"
|
|
||||||
with open("types.js") as f:
|
|
||||||
text_js = f.read()
|
|
||||||
with open(js_file) as f:
|
|
||||||
text = f.read()
|
|
||||||
|
|
||||||
with open(js_file, "wb") as f:
|
|
||||||
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
use wasm_bindgen::{
|
|
||||||
prelude::{wasm_bindgen, JsError},
|
|
||||||
JsValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ironcalc_base::{
|
|
||||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
|
||||||
types::CellType,
|
|
||||||
UserModel as BaseModel,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn to_js_error(error: String) -> JsError {
|
|
||||||
JsError::new(&error.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return an array with a list of all the tokens from a formula
|
|
||||||
/// This is used by the UI to color them according to a theme.
|
|
||||||
#[wasm_bindgen(js_name = "getTokens")]
|
|
||||||
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
|
|
||||||
let tokens = tokenizer(formula);
|
|
||||||
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
|
|
||||||
}
|
|
||||||
#[wasm_bindgen]
|
|
||||||
pub struct Model {
|
|
||||||
model: BaseModel,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
|
||||||
impl Model {
|
|
||||||
#[wasm_bindgen(constructor)]
|
|
||||||
pub fn new(locale: &str, timezone: &str) -> Result<Model, JsError> {
|
|
||||||
let model = BaseModel::new_empty("workbook", locale, timezone).map_err(to_js_error)?;
|
|
||||||
Ok(Model { model })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(bytes: &[u8]) -> Result<Model, JsError> {
|
|
||||||
let model = BaseModel::from_bytes(bytes).map_err(to_js_error)?;
|
|
||||||
Ok(Model { model })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn undo(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.undo().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn redo(&mut self) -> Result<(), JsError> {
|
|
||||||
self.model.redo().map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "canUndo")]
|
|
||||||
pub fn can_undo(&self) -> bool {
|
|
||||||
self.model.can_undo()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "canRedo")]
|
|
||||||
pub fn can_redo(&self) -> bool {
|
|
||||||
self.model.can_redo()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "pauseEvaluation")]
|
|
||||||
pub fn pause_evaluation(&mut self) {
|
|
||||||
self.model.pause_evaluation()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "resumeEvaluation")]
|
|
||||||
pub fn resume_evaluation(&mut self) {
|
|
||||||
self.model.resume_evaluation()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn evaluate(&mut self) {
|
|
||||||
self.model.evaluate();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "flushSendQueue")]
|
|
||||||
pub fn flush_send_queue(&mut self) -> Vec<u8> {
|
|
||||||
self.model.flush_send_queue()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "applyExternalDiffs")]
|
|
||||||
pub fn apply_external_diffs(&mut self, diffs: &[u8]) -> Result<(), JsError> {
|
|
||||||
self.model.apply_external_diffs(diffs).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getCellContent")]
|
|
||||||
pub fn get_cell_content(&self, sheet: u32, row: i32, column: i32) -> Result<String, JsError> {
|
|
||||||
self.model
|
|
||||||
.get_cell_content(sheet, row, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "newSheet")]
|
|
||||||
pub fn new_sheet(&mut self) {
|
|
||||||
self.model.new_sheet()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "deleteSheet")]
|
|
||||||
pub fn delete_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
|
|
||||||
self.model.delete_sheet(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "renameSheet")]
|
|
||||||
pub fn rename_sheet(&mut self, sheet: u32, name: &str) -> Result<(), JsError> {
|
|
||||||
self.model.rename_sheet(sheet, name).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(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<(), 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_all(&range).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(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<(), 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_contents(&range).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "insertRow")]
|
|
||||||
pub fn insert_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
|
||||||
self.model.insert_row(sheet, row).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "insertColumn")]
|
|
||||||
pub fn insert_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
|
||||||
self.model.insert_column(sheet, column).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "deleteRow")]
|
|
||||||
pub fn delete_row(&mut self, sheet: u32, row: i32) -> Result<(), JsError> {
|
|
||||||
self.model.delete_row(sheet, row).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "deleteColumn")]
|
|
||||||
pub fn delete_column(&mut self, sheet: u32, column: i32) -> Result<(), JsError> {
|
|
||||||
self.model.delete_column(sheet, column).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setRowHeight")]
|
|
||||||
pub fn set_row_height(&mut self, sheet: u32, row: i32, height: f64) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.set_row_height(sheet, row, height)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setColumnWidth")]
|
|
||||||
pub fn set_column_width(&mut self, sheet: u32, column: i32, width: f64) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.set_column_width(sheet, column, width)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getRowHeight")]
|
|
||||||
pub fn get_row_height(&mut self, sheet: u32, row: i32) -> Result<f64, JsError> {
|
|
||||||
self.model.get_row_height(sheet, row).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getColumnWidth")]
|
|
||||||
pub fn get_column_width(&mut self, sheet: u32, column: i32) -> Result<f64, JsError> {
|
|
||||||
self.model
|
|
||||||
.get_column_width(sheet, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setUserInput")]
|
|
||||||
pub fn set_user_input(
|
|
||||||
&mut self,
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
input: &str,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.set_user_input(sheet, row, column, input)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getFormattedCellValue")]
|
|
||||||
pub fn get_formatted_cell_value(
|
|
||||||
&self,
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
) -> Result<String, JsError> {
|
|
||||||
self.model
|
|
||||||
.get_formatted_cell_value(sheet, row, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getFrozenRowsCount")]
|
|
||||||
pub fn get_frozen_rows_count(&self, sheet: u32) -> Result<i32, JsError> {
|
|
||||||
self.model.get_frozen_rows_count(sheet).map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getFrozenColumnsCount")]
|
|
||||||
pub fn get_frozen_columns_count(&self, sheet: u32) -> Result<i32, JsError> {
|
|
||||||
self.model
|
|
||||||
.get_frozen_columns_count(sheet)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setFrozenRowsCount")]
|
|
||||||
pub fn set_frozen_rows_count(&mut self, sheet: u32, count: i32) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.set_frozen_rows_count(sheet, count)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "setFrozenColumnsCount")]
|
|
||||||
pub fn set_frozen_columns_count(&mut self, sheet: u32, count: i32) -> Result<(), JsError> {
|
|
||||||
self.model
|
|
||||||
.set_frozen_columns_count(sheet, count)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
|
||||||
pub fn update_range_style(
|
|
||||||
&mut self,
|
|
||||||
range: JsValue,
|
|
||||||
style_path: &str,
|
|
||||||
value: &str,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
let range: Area =
|
|
||||||
serde_wasm_bindgen::from_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)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getCellStyle")]
|
|
||||||
pub fn get_cell_style(
|
|
||||||
&mut self,
|
|
||||||
sheet: u32,
|
|
||||||
row: i32,
|
|
||||||
column: i32,
|
|
||||||
) -> Result<JsValue, JsError> {
|
|
||||||
self.model
|
|
||||||
.get_cell_style(sheet, row, column)
|
|
||||||
.map_err(to_js_error)
|
|
||||||
.map(|x| serde_wasm_bindgen::to_value(&x).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getCellType")]
|
|
||||||
pub fn get_cell_type(&self, sheet: u32, row: i32, column: i32) -> Result<i32, JsError> {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getWorksheetsProperties")]
|
|
||||||
pub fn get_worksheets_properties(&self) -> JsValue {
|
|
||||||
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Test IronCalc Web Bindings</title>
|
|
||||||
<script type="module">
|
|
||||||
import init, { Model } from "./pkg/wasm.js";
|
|
||||||
|
|
||||||
await init();
|
|
||||||
|
|
||||||
function test() {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
model.setUserInput(0, 1, 1, "23");
|
|
||||||
model.setUserInput(0, 1, 2, "=A1*3+1");
|
|
||||||
|
|
||||||
const result = model.getFormattedCellValue(0, 1, 2);
|
|
||||||
console.assert(result === "70");
|
|
||||||
console.log("Hoooray! Tests passed");
|
|
||||||
}
|
|
||||||
|
|
||||||
test();
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div>Please have a look at the console</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import test from 'node:test';
|
|
||||||
import assert from 'node:assert'
|
|
||||||
import { Model } from "../pkg/wasm.js";
|
|
||||||
|
|
||||||
test('Frozen rows and columns', () => {
|
|
||||||
let model = new Model('en', 'UTC');
|
|
||||||
assert.strictEqual(model.getFrozenRowsCount(0), 0);
|
|
||||||
assert.strictEqual(model.getFrozenColumnsCount(0), 0);
|
|
||||||
|
|
||||||
model.setFrozenColumnsCount(0, 4);
|
|
||||||
model.setFrozenRowsCount(0, 3)
|
|
||||||
|
|
||||||
assert.strictEqual(model.getFrozenRowsCount(0), 3);
|
|
||||||
assert.strictEqual(model.getFrozenColumnsCount(0), 4);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Row height', () => {
|
|
||||||
let model = new Model('en', 'UTC');
|
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 21);
|
|
||||||
|
|
||||||
model.setRowHeight(0, 3, 32);
|
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
|
||||||
|
|
||||||
model.undo();
|
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 21);
|
|
||||||
|
|
||||||
model.redo();
|
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 32);
|
|
||||||
|
|
||||||
model.setRowHeight(0, 3, 320);
|
|
||||||
assert.strictEqual(model.getRowHeight(0, 3), 320);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Evaluates correctly', (t) => {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
model.setUserInput(0, 1, 1, "23");
|
|
||||||
model.setUserInput(0, 1, 2, "=A1*3+1");
|
|
||||||
|
|
||||||
const result = model.getFormattedCellValue(0, 1, 2);
|
|
||||||
assert.strictEqual(result, "70");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Styles work', () => {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
let style = model.getCellStyle(0, 1, 1);
|
|
||||||
assert.deepEqual(style, {
|
|
||||||
num_fmt: 'general',
|
|
||||||
fill: { pattern_type: 'none' },
|
|
||||||
font: {
|
|
||||||
sz: 11,
|
|
||||||
color: '#000000',
|
|
||||||
name: 'Calibri',
|
|
||||||
family: 2,
|
|
||||||
scheme: 'minor'
|
|
||||||
},
|
|
||||||
border: {},
|
|
||||||
quote_prefix: false
|
|
||||||
});
|
|
||||||
model.setUserInput(0, 1, 1, "'=1+1");
|
|
||||||
style = model.getCellStyle(0, 1, 1);
|
|
||||||
assert.deepEqual(style, {
|
|
||||||
num_fmt: 'general',
|
|
||||||
fill: { pattern_type: 'none' },
|
|
||||||
font: {
|
|
||||||
sz: 11,
|
|
||||||
color: '#000000',
|
|
||||||
name: 'Calibri',
|
|
||||||
family: 2,
|
|
||||||
scheme: 'minor'
|
|
||||||
},
|
|
||||||
border: {},
|
|
||||||
quote_prefix: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Add sheets", (t) => {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
model.newSheet();
|
|
||||||
model.renameSheet(1, "NewName");
|
|
||||||
let props = model.getWorksheetsProperties();
|
|
||||||
assert.deepEqual(props, [{
|
|
||||||
name: 'Sheet1',
|
|
||||||
sheet_id: 1,
|
|
||||||
state: 'visible'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'NewName',
|
|
||||||
sheet_id: 2,
|
|
||||||
state: 'visible'
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("invalid sheet index throws an exception", () => {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
assert.throws(() => {
|
|
||||||
model.setRowHeight(1, 1, 100);
|
|
||||||
}, {
|
|
||||||
name: 'Error',
|
|
||||||
message: 'Invalid sheet index',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("invalid column throws an exception", () => {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
assert.throws(() => {
|
|
||||||
model.setRowHeight(0, -1, 100);
|
|
||||||
}, {
|
|
||||||
name: 'Error',
|
|
||||||
message: "Row number '-1' is not valid.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("floating column numbers get truncated", () => {
|
|
||||||
const model = new Model('en', 'UTC');
|
|
||||||
model.setRowHeight(0.8, 5.2, 100.5);
|
|
||||||
|
|
||||||
assert.strictEqual(model.getRowHeight(0.11, 5.99), 100.5);
|
|
||||||
assert.strictEqual(model.getRowHeight(0, 5), 100.5);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
export interface Area {
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrorType =
|
|
||||||
| "REF"
|
|
||||||
| "NAME"
|
|
||||||
| "VALUE"
|
|
||||||
| "DIV"
|
|
||||||
| "NA"
|
|
||||||
| "NUM"
|
|
||||||
| "ERROR"
|
|
||||||
| "NIMPL"
|
|
||||||
| "SPILL"
|
|
||||||
| "CALC"
|
|
||||||
| "CIRC";
|
|
||||||
|
|
||||||
type OpCompareType =
|
|
||||||
| "LessThan"
|
|
||||||
| "GreaterThan"
|
|
||||||
| "Equal"
|
|
||||||
| "LessOrEqualThan"
|
|
||||||
| "GreaterOrEqualThan"
|
|
||||||
| "NonEqual";
|
|
||||||
|
|
||||||
type OpSumType = "Add" | "Minus";
|
|
||||||
|
|
||||||
type OpProductType = "Times" | "Divide";
|
|
||||||
|
|
||||||
interface ReferenceType {
|
|
||||||
sheet: string | null;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
absolute_column: boolean;
|
|
||||||
absolute_row: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParsedReferenceType {
|
|
||||||
column: number;
|
|
||||||
row: number;
|
|
||||||
absolute_column: boolean;
|
|
||||||
absolute_row: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Reference {
|
|
||||||
Reference: ReferenceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Range {
|
|
||||||
Range: {
|
|
||||||
sheet: string | null;
|
|
||||||
left: ParsedReferenceType;
|
|
||||||
right: ParsedReferenceType;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TokenType =
|
|
||||||
| "Illegal"
|
|
||||||
| "Eof"
|
|
||||||
| { Ident: string }
|
|
||||||
| { String: string }
|
|
||||||
| { Boolean: boolean }
|
|
||||||
| { Number: number }
|
|
||||||
| { ERROR: ErrorType }
|
|
||||||
| { COMPARE: OpCompareType }
|
|
||||||
| { SUM: OpSumType }
|
|
||||||
| { PRODUCT: OpProductType }
|
|
||||||
| "POWER"
|
|
||||||
| "LPAREN"
|
|
||||||
| "RPAREN"
|
|
||||||
| "COLON"
|
|
||||||
| "SEMICOLON"
|
|
||||||
| "LBRACKET"
|
|
||||||
| "RBRACKET"
|
|
||||||
| "LBRACE"
|
|
||||||
| "RBRACE"
|
|
||||||
| "COMMA"
|
|
||||||
| "BANG"
|
|
||||||
| "PERCENT"
|
|
||||||
| "AND"
|
|
||||||
| Reference
|
|
||||||
| Range;
|
|
||||||
|
|
||||||
export interface MarkedToken {
|
|
||||||
token: TokenType;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorksheetProperties {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
sheet_id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CellStyleFill {
|
|
||||||
pattern_type: string;
|
|
||||||
fg_color?: string;
|
|
||||||
bg_color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CellStyleFont {
|
|
||||||
u: boolean;
|
|
||||||
b: boolean;
|
|
||||||
i: boolean;
|
|
||||||
strike: boolean;
|
|
||||||
sz: number;
|
|
||||||
color: string;
|
|
||||||
name: string;
|
|
||||||
family: number;
|
|
||||||
scheme: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BorderType {
|
|
||||||
BorderAll,
|
|
||||||
BorderInner,
|
|
||||||
BorderCenterH,
|
|
||||||
BorderCenterV,
|
|
||||||
BorderOuter,
|
|
||||||
BorderNone,
|
|
||||||
BorderTop,
|
|
||||||
BorderRight,
|
|
||||||
BorderBottom,
|
|
||||||
BorderLeft,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BorderOptions {
|
|
||||||
color: string;
|
|
||||||
style: BorderStyle;
|
|
||||||
border: BorderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BorderStyle {
|
|
||||||
Thin = "thin",
|
|
||||||
Medium = "medium",
|
|
||||||
Thick = "thick",
|
|
||||||
Dashed = "dashed",
|
|
||||||
Dotted = "dotted",
|
|
||||||
Double = "double",
|
|
||||||
None = "none",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BorderItem {
|
|
||||||
style: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CellStyleBorder {
|
|
||||||
diagonal_up?: boolean;
|
|
||||||
diagonal_down?: boolean;
|
|
||||||
left: BorderItem;
|
|
||||||
right: BorderItem;
|
|
||||||
top: BorderItem;
|
|
||||||
bottom: BorderItem;
|
|
||||||
diagonal: BorderItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VerticalAlignment =
|
|
||||||
| "bottom"
|
|
||||||
| "center"
|
|
||||||
| "distributed"
|
|
||||||
| "justify"
|
|
||||||
| "top";
|
|
||||||
|
|
||||||
export type HorizontalAlignment =
|
|
||||||
| "left"
|
|
||||||
| "center"
|
|
||||||
| "right"
|
|
||||||
| "general"
|
|
||||||
| "centerContinuous"
|
|
||||||
| "distributed"
|
|
||||||
| "fill"
|
|
||||||
| "justify";
|
|
||||||
|
|
||||||
interface Alignment {
|
|
||||||
horizontal: HorizontalAlignment;
|
|
||||||
vertical: VerticalAlignment;
|
|
||||||
wrap_text: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CellStyle {
|
|
||||||
read_only: boolean;
|
|
||||||
quote_prefix: boolean;
|
|
||||||
fill: CellStyleFill;
|
|
||||||
font: CellStyleFont;
|
|
||||||
border: CellStyleBorder;
|
|
||||||
num_fmt: string;
|
|
||||||
alignment?: Alignment;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
ignore:
|
|
||||||
- "xlsx/src/bin"
|
|
||||||
- "bindings/wasm"
|
|
||||||
3
webapp/.gitignore
vendored
3
webapp/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
node_modules/*
|
|
||||||
dist/*
|
|
||||||
example.json
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { StorybookConfig } from '@storybook/react-vite';
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
|
||||||
addons: [
|
|
||||||
'@storybook/addon-links',
|
|
||||||
'@storybook/addon-essentials',
|
|
||||||
'@storybook/addon-onboarding',
|
|
||||||
'@storybook/addon-interactions',
|
|
||||||
],
|
|
||||||
framework: {
|
|
||||||
name: '@storybook/react-vite',
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
docs: {
|
|
||||||
autodocs: 'tag',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { Preview } from '@storybook/react';
|
|
||||||
import i18n from '../src/i18n';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
|
|
||||||
const withI18next = (Story: any) => {
|
|
||||||
return (
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<Story />
|
|
||||||
</I18nextProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
parameters: {
|
|
||||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
||||||
controls: {
|
|
||||||
matchers: {
|
|
||||||
color: /(background|color)$/i,
|
|
||||||
date: /Date$/i,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const decorators = [withI18next];
|
|
||||||
export default preview;
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# IronCalc Web App
|
|
||||||
|
|
||||||
## Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,16 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<!-- <meta name="theme-color" content="#1bb566"> -->
|
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#F2994A" />
|
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
|
||||||
<title>Spreadsheet</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { Config } from "jest";
|
|
||||||
// import {defaults} from 'jest-config';
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
// testMatch:["**.jest.mjs"],
|
|
||||||
moduleFileExtensions: ["js", "ts", "mts", "mjs"],
|
|
||||||
transform: {
|
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
|
||||||
},
|
|
||||||
moduleNameMapper: {
|
|
||||||
"^@ironcalc/wasm$": "<rootDir>/node_modules/@ironcalc/nodejs/"
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
17153
webapp/package-lock.json
generated
17153
webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,55 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "frontend",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
|
|
||||||
"dev": "vite",
|
|
||||||
"test": "jest",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"storybook": "storybook dev -p 6006",
|
|
||||||
"build-storybook": "storybook build"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@emotion/react": "^11.11.4",
|
|
||||||
"@emotion/styled": "^11.11.5",
|
|
||||||
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
|
|
||||||
"@mui/material": "^5.15.15",
|
|
||||||
"i18next": "^23.11.1",
|
|
||||||
"lucide-react": "^0.292.0",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-colorful": "^5.6.1",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-i18next": "^13.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@storybook/addon-essentials": "^7.6.17",
|
|
||||||
"@storybook/addon-interactions": "^7.6.17",
|
|
||||||
"@storybook/addon-links": "^7.6.17",
|
|
||||||
"@storybook/addon-onboarding": "^1.0.11",
|
|
||||||
"@storybook/blocks": "^7.5.3",
|
|
||||||
"@storybook/react": "^7.5.3",
|
|
||||||
"@storybook/react-vite": "^7.6.17",
|
|
||||||
"@storybook/testing-library": "^0.2.2",
|
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/react": "^18.2.75",
|
|
||||||
"@types/react-dom": "^18.2.24",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"eslint": "^8.57.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.6",
|
|
||||||
"eslint-plugin-storybook": "^0.6.15",
|
|
||||||
"jest": "^29.7.0",
|
|
||||||
"storybook": "^7.6.17",
|
|
||||||
"ts-jest": "^29.1.2",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.4.5",
|
|
||||||
"vite": "^5.2.8",
|
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
#root {
|
|
||||||
position: absolute;
|
|
||||||
inset: 10px;
|
|
||||||
border: 1px solid #AAA;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import "./App.css";
|
|
||||||
import Workbook from "./components/workbook";
|
|
||||||
import "./i18n";
|
|
||||||
import { createContext, useEffect, useState } from "react";
|
|
||||||
import init, { Model } from "@ironcalc/wasm";
|
|
||||||
import { WorkbookState } from "./components/workbookState";
|
|
||||||
import WorkbookContext from "./components/workbookContext";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [model, setModel] = useState<Model | null>(null);
|
|
||||||
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
useEffect(() => {
|
|
||||||
async function start() {
|
|
||||||
await init();
|
|
||||||
const model_bytes = new Uint8Array(await (await fetch("./example.ic")).arrayBuffer());
|
|
||||||
const _model = Model.from_bytes(model_bytes);
|
|
||||||
// const _model = new Model("en", "UTC");
|
|
||||||
if (!model) setModel(_model);
|
|
||||||
if (!workbookState) setWorkbookState(new WorkbookState());
|
|
||||||
}
|
|
||||||
start();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!model || !workbookState) {
|
|
||||||
return <div>Loading</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could use context for model, but the problem is that it should initialized to null.
|
|
||||||
// Passing the property down makes sure it is always defined.
|
|
||||||
return (
|
|
||||||
// <WorkbookContext.Provider value={{}}>
|
|
||||||
<Workbook model={model} workbookState={workbookState} />
|
|
||||||
// </WorkbookContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Keyboard and mouse events architecture
|
|
||||||
|
|
||||||
This document describes the architecture of the keyboard navigation and mouse events in IronCalc Web
|
|
||||||
|
|
||||||
There are two modes for mouse events:
|
|
||||||
|
|
||||||
* Normal mode: clicking a cell selects it, clicking on a sheet opens it
|
|
||||||
* Browse mode: clicking on a cell updates the formula, etc
|
|
||||||
|
|
||||||
While in browse mode some mouse events might end the browse mode
|
|
||||||
|
|
||||||
We follow Excel's way of navigating a spreadsheet
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export const headerCornerBackground = '#FFF';
|
|
||||||
export const headerTextColor = '#333';
|
|
||||||
export const headerBackground = '#FFF';
|
|
||||||
export const headerGlobalSelectorColor = '#EAECF4';
|
|
||||||
export const headerSelectedBackground = '#EEEEEE';
|
|
||||||
export const headerFullSelectedBackground = '#D3D6E9';
|
|
||||||
export const headerSelectedColor = '#333';
|
|
||||||
export const headerBorderColor = '#DEE0EF';
|
|
||||||
|
|
||||||
export const gridColor = '#D3D6E9';
|
|
||||||
export const gridSeparatorColor = '#D3D6E9';
|
|
||||||
export const defaultTextColor = '#2E414D';
|
|
||||||
|
|
||||||
export const outlineColor = '#F2994A';
|
|
||||||
export const outlineBackgroundColor = '#F2994A1A';
|
|
||||||
|
|
||||||
export const LAST_COLUMN = 16_384;
|
|
||||||
export const LAST_ROW = 1_048_576;
|
|
||||||
|
|
||||||
// FIXME: Browsers cannot have a height that big
|
|
||||||
// For now we will go A-IZ and 10_000 rows
|
|
||||||
export const lastColumn = 260; // TODO: Excel supports up to 16_384
|
|
||||||
// I know of a world with one million moons.
|
|
||||||
// Carl Sagan in The cosmic connection Chapter 7 "Space Exploration as a Human Enterprise"
|
|
||||||
export const lastRow = 10_000; // TODO: Excel supports up to 1_048_576
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
export interface Cell {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SheetArea extends Area {
|
|
||||||
sheet: number;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AreaWithBorderInterface extends Area {
|
|
||||||
border: "left" | "top" | "right" | "bottom";
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
|
||||||
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
const letters = [
|
|
||||||
'A',
|
|
||||||
'B',
|
|
||||||
'C',
|
|
||||||
'D',
|
|
||||||
'E',
|
|
||||||
'F',
|
|
||||||
'G',
|
|
||||||
'H',
|
|
||||||
'I',
|
|
||||||
'J',
|
|
||||||
'K',
|
|
||||||
'L',
|
|
||||||
'M',
|
|
||||||
'N',
|
|
||||||
'O',
|
|
||||||
'P',
|
|
||||||
'Q',
|
|
||||||
'R',
|
|
||||||
'S',
|
|
||||||
'T',
|
|
||||||
'U',
|
|
||||||
'V',
|
|
||||||
'W',
|
|
||||||
'X',
|
|
||||||
'Y',
|
|
||||||
'Z',
|
|
||||||
];
|
|
||||||
interface Reference {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
absoluteRow: boolean;
|
|
||||||
absoluteColumn: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function referenceToString(rf: Reference): string {
|
|
||||||
const absC = rf.absoluteColumn ? '$' : '';
|
|
||||||
const absR = rf.absoluteRow ? '$' : '';
|
|
||||||
return absC + columnNameFromNumber(rf.column) + absR + rf.row;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNameFromNumber(column: number): string {
|
|
||||||
let columnName = '';
|
|
||||||
let index = column;
|
|
||||||
while (index > 0) {
|
|
||||||
columnName = `${letters[(index - 1) % 26]}${columnName}`;
|
|
||||||
index = Math.floor((index - 1) / 26);
|
|
||||||
}
|
|
||||||
return columnName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNumberFromName(columnName: string): number {
|
|
||||||
let column = 0;
|
|
||||||
for (const character of columnName) {
|
|
||||||
const index = (character.codePointAt(0) ?? 0) - 64;
|
|
||||||
column = column * 26 + index;
|
|
||||||
}
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EqualTo Color Palette
|
|
||||||
export function getColor(index: number, alpha = 1): string {
|
|
||||||
const colors = [
|
|
||||||
{
|
|
||||||
name: 'Cyan',
|
|
||||||
rgba: [89, 185, 188, 1],
|
|
||||||
hex: '#59B9BC',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Flamingo',
|
|
||||||
rgba: [236, 87, 83, 1],
|
|
||||||
hex: '#EC5753',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#3358B7',
|
|
||||||
rgba: [51, 88, 183, 1],
|
|
||||||
name: 'Blue',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#F8CD3C',
|
|
||||||
rgba: [248, 205, 60, 1],
|
|
||||||
name: 'Yellow',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#3BB68A',
|
|
||||||
rgba: [59, 182, 138, 1],
|
|
||||||
name: 'Emerald',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#523E93',
|
|
||||||
rgba: [82, 62, 147, 1],
|
|
||||||
name: 'Violet',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#A23C52',
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: 'Burgundy',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#8CB354',
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: 'Wasabi',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#D03627',
|
|
||||||
rgba: [208, 54, 39, 1],
|
|
||||||
name: 'Red',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: '#1B717E',
|
|
||||||
rgba: [27, 113, 126, 1],
|
|
||||||
name: 'Teal',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (alpha === 1) {
|
|
||||||
return colors[index % 10].hex;
|
|
||||||
}
|
|
||||||
const { rgba } = colors[index % 10];
|
|
||||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mergedAreas(area1: Area, area2: Area): Area {
|
|
||||||
return {
|
|
||||||
rowStart: Math.min(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
|
||||||
rowEnd: Math.max(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
|
||||||
columnStart: Math.min(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
|
||||||
columnEnd: Math.max(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getExpandToArea(area: Area, cell: Cell): AreaWithBorder {
|
|
||||||
let { rowStart, rowEnd, columnStart, columnEnd } = area;
|
|
||||||
if (rowStart > rowEnd) {
|
|
||||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
|
||||||
}
|
|
||||||
if (columnStart > columnEnd) {
|
|
||||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
|
||||||
}
|
|
||||||
const { row, column } = cell;
|
|
||||||
if (row <= rowEnd && row >= rowStart && column >= columnStart && column <= columnEnd) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Two rules:
|
|
||||||
// * The extendTo area must be larger than the selected area
|
|
||||||
// * The extendTo area must be of the same width or the same height as the selected area
|
|
||||||
if (row >= rowEnd && column >= columnStart) {
|
|
||||||
// Normal case: we are expanding down and right
|
|
||||||
if (row - rowEnd > column - columnEnd) {
|
|
||||||
// Expanding by rows (down)
|
|
||||||
return {
|
|
||||||
rowStart: rowEnd + 1,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'top',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// expanding by columns (right)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: columnEnd + 1,
|
|
||||||
columnEnd: column,
|
|
||||||
border: 'left',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (row >= rowEnd && column <= columnStart) {
|
|
||||||
// We are expanding down and left
|
|
||||||
if (row - rowEnd > columnStart - column) {
|
|
||||||
// Expanding by rows (down)
|
|
||||||
return {
|
|
||||||
rowStart: rowEnd + 1,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'top',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Expanding by columns (left)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: columnStart - 1,
|
|
||||||
border: 'right',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (row <= rowEnd && column >= columnEnd) {
|
|
||||||
// We are expanding up and right
|
|
||||||
if (rowStart - row > column - columnEnd) {
|
|
||||||
// Expanding by rows (up)
|
|
||||||
return {
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: rowStart - 1,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'bottom',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Expanding by columns (right)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: columnEnd + 1,
|
|
||||||
columnEnd: column,
|
|
||||||
border: 'left',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (row <= rowEnd && column <= columnStart) {
|
|
||||||
// We are expanding up and left
|
|
||||||
if (rowStart - row > columnStart - column) {
|
|
||||||
// Expanding by rows (up)
|
|
||||||
return {
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: rowStart - 1,
|
|
||||||
columnStart,
|
|
||||||
columnEnd,
|
|
||||||
border: 'bottom',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Expanding by columns (left)
|
|
||||||
return {
|
|
||||||
rowStart,
|
|
||||||
rowEnd,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: columnStart - 1,
|
|
||||||
border: 'right',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the keypress should start editing
|
|
||||||
*/
|
|
||||||
export function isEditingKey(key: string): boolean {
|
|
||||||
if (key.length !== 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const code = key.codePointAt(0) ?? 0;
|
|
||||||
if (code > 0 && code < 255) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// / Common types
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AreaWithBorderInterface extends Area {
|
|
||||||
border: 'left' | 'top' | 'right' | 'bottom';
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
|
||||||
|
|
||||||
export interface Cell {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScrollPosition {
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StateSettings {
|
|
||||||
selectedCell: Cell;
|
|
||||||
selectedArea: Area;
|
|
||||||
scrollPosition: ScrollPosition;
|
|
||||||
extendToArea: AreaWithBorder;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Dispatch<A> = (value: A) => void;
|
|
||||||
export type SetStateAction<S> = S | ((prevState: S) => S);
|
|
||||||
|
|
||||||
export enum FocusType {
|
|
||||||
Cell = 'cell',
|
|
||||||
FormulaBar = 'formula-bar',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In Excel there are two "modes" of editing
|
|
||||||
* * `init`: When you start typing in a cell. In this mode arrow keys will move away from the cell
|
|
||||||
* * `edit`: If you double click on a cell or click in the cell while editing.
|
|
||||||
* In this mode arrow keys will move within the cell.
|
|
||||||
*
|
|
||||||
* In a formula bar mode is always `edit`.
|
|
||||||
*/
|
|
||||||
export type CellEditMode = 'init' | 'edit';
|
|
||||||
export interface CellEditingType {
|
|
||||||
/**
|
|
||||||
* ID of cell editing. Useful when one edit transforms into another and some code needs to run
|
|
||||||
* when target changes.
|
|
||||||
*
|
|
||||||
* Due to problems with focus management (see #339) it's possible to start a new cell editing
|
|
||||||
* without properly cleaning up previous one (lose focus in workbook, regain focus NOT in
|
|
||||||
* the input and then use the keyboard.
|
|
||||||
*/
|
|
||||||
id: number;
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
text: string;
|
|
||||||
base: string;
|
|
||||||
mode: CellEditMode;
|
|
||||||
focus: FocusType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NavigationKey = 'ArrowRight' | 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';
|
|
||||||
|
|
||||||
export const isNavigationKey = (key: string): key is NavigationKey =>
|
|
||||||
['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key);
|
|
||||||
|
|
||||||
function nameNeedsQuoting(name: string): boolean {
|
|
||||||
const chars = [' ', '(', ')', "'", '$', ',', ';', '-', '+', '{', '}'];
|
|
||||||
const l = chars.length;
|
|
||||||
for (let index = 0; index < l; index += 1) {
|
|
||||||
if (name.includes(chars[index])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: We should use the function of a similar name in the rust code.
|
|
||||||
export const quoteSheetName = (name: string): string => {
|
|
||||||
if (nameNeedsQuoting(name)) {
|
|
||||||
return `'${name.replace("'", "''")}'`;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function cellReprToRowColumn(cellRepr: string): { row: number; column: number } {
|
|
||||||
let row = 0;
|
|
||||||
let column = 0;
|
|
||||||
for (const character of cellRepr) {
|
|
||||||
if (Number.isNaN(Number.parseInt(character, 10))) {
|
|
||||||
column *= 26;
|
|
||||||
const characterCode = character.codePointAt(0);
|
|
||||||
const ACharacterCode = 'A'.codePointAt(0);
|
|
||||||
if (typeof characterCode === 'undefined' || typeof ACharacterCode === 'undefined') {
|
|
||||||
throw new TypeError('Failed to find character code');
|
|
||||||
}
|
|
||||||
const deltaCodes = characterCode - ACharacterCode;
|
|
||||||
if (deltaCodes < 0) {
|
|
||||||
throw new Error('Incorrect character');
|
|
||||||
}
|
|
||||||
column += deltaCodes + 1;
|
|
||||||
} else {
|
|
||||||
row *= 10;
|
|
||||||
row += Number.parseInt(character, 10);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { row, column };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMessageCellText = (
|
|
||||||
cell: string,
|
|
||||||
getMessageSheetNumber: (sheet: string) => number | undefined,
|
|
||||||
getCellText?: (sheet: number, row: number, column: number) => string | undefined,
|
|
||||||
) => {
|
|
||||||
const messageMatch = /^=?(?<sheet>\w+)!(?<cell>\w+)/.exec(cell);
|
|
||||||
if (messageMatch && messageMatch.groups) {
|
|
||||||
const messageSheet = getMessageSheetNumber(messageMatch.groups.sheet);
|
|
||||||
const dynamicIconCell = cellReprToRowColumn(messageMatch.groups.cell);
|
|
||||||
if (messageSheet !== undefined && getCellText) {
|
|
||||||
return getCellText(messageSheet, dynamicIconCell.row, dynamicIconCell.column) || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCellAddress = (selectedArea: Area, selectedCell?: Cell) => {
|
|
||||||
const isSingleCell =
|
|
||||||
selectedArea.rowStart === selectedArea.rowEnd &&
|
|
||||||
selectedArea.columnEnd === selectedArea.columnStart;
|
|
||||||
|
|
||||||
return isSingleCell && selectedCell
|
|
||||||
? `${columnNameFromNumber(selectedCell.column)}${selectedCell.row}`
|
|
||||||
: `${columnNameFromNumber(selectedArea.columnStart)}${
|
|
||||||
selectedArea.rowStart
|
|
||||||
}:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export enum Border {
|
|
||||||
Top = 'top',
|
|
||||||
Bottom = 'bottom',
|
|
||||||
Right = 'right',
|
|
||||||
Left = 'left',
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,566 +0,0 @@
|
|||||||
import React, { useRef, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
BorderBottomIcon,
|
|
||||||
BorderCenterHIcon,
|
|
||||||
BorderCenterVIcon,
|
|
||||||
BorderInnerIcon,
|
|
||||||
BorderLeftIcon,
|
|
||||||
BorderOuterIcon,
|
|
||||||
BorderRightIcon,
|
|
||||||
BorderTopIcon,
|
|
||||||
BorderNoneIcon,
|
|
||||||
BorderStyleIcon,
|
|
||||||
} from "../icons";
|
|
||||||
import ColorPicker from "./colorPicker";
|
|
||||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
|
||||||
import {
|
|
||||||
Check,
|
|
||||||
ChevronDown,
|
|
||||||
Grid2X2 as BorderAllIcon,
|
|
||||||
PencilLine,
|
|
||||||
ChevronRight,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import { theme } from "../theme";
|
|
||||||
import { BorderOptions, BorderStyle, BorderType } from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
type BorderPickerProps = {
|
|
||||||
className?: string;
|
|
||||||
onChange: (border: BorderOptions) => void;
|
|
||||||
anchorEl: React.RefObject<HTMLElement>;
|
|
||||||
anchorOrigin?: PopoverOrigin;
|
|
||||||
transformOrigin?: PopoverOrigin;
|
|
||||||
open: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BorderPicker = (properties: BorderPickerProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const [borderSelected, setBorderSelected] = useState(BorderType.None);
|
|
||||||
const [borderColor, setBorderColor] = useState("#000000");
|
|
||||||
const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin);
|
|
||||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
|
||||||
const [stylePickerOpen, setStylePickerOpen] = useState(false);
|
|
||||||
const closePicker = (): void => {
|
|
||||||
properties.onChange({
|
|
||||||
color: borderColor,
|
|
||||||
style: borderStyle,
|
|
||||||
border: borderSelected,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const borderColorButton = useRef(null);
|
|
||||||
const borderStyleButton = useRef(null);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StyledPopover
|
|
||||||
open={properties.open}
|
|
||||||
onClose={(): void => closePicker()}
|
|
||||||
anchorEl={properties.anchorEl.current}
|
|
||||||
anchorOrigin={
|
|
||||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
|
||||||
}
|
|
||||||
transformOrigin={
|
|
||||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BorderPickerDialog>
|
|
||||||
<Borders>
|
|
||||||
<Line>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderAll}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderAll) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderAll);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderAllIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderInner}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderInner) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderInner);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderInnerIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderCenterH}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderCenterH) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderCenterH);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderCenterHIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderCenterV}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderCenterV) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderCenterV);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderCenterVIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderOuter}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderOuter) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderOuter);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderOuterIcon />
|
|
||||||
</Button>
|
|
||||||
</Line>
|
|
||||||
<Line>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderNone}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderNone) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderNone);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderNoneIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderTop}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderTop) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderTop);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderTopIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderRight}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderRight) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderRight);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderRightIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderBottom}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderBottom) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderBottom);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderBottomIcon />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={borderSelected === BorderType.BorderLeft}
|
|
||||||
onClick={() => {
|
|
||||||
if (borderSelected === BorderType.BorderLeft) {
|
|
||||||
setBorderSelected(BorderType.None);
|
|
||||||
} else {
|
|
||||||
setBorderSelected(BorderType.BorderLeft);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderLeftIcon />
|
|
||||||
</Button>
|
|
||||||
</Line>
|
|
||||||
</Borders>
|
|
||||||
<Divider />
|
|
||||||
<Styles>
|
|
||||||
<ButtonWrapper onClick={() => setColorPickerOpen(true)}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
disabled={false}
|
|
||||||
ref={borderColorButton}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<PencilLine />
|
|
||||||
</Button>
|
|
||||||
<div style={{flexGrow:2}}>Border color</div>
|
|
||||||
<ChevronRightStyled />
|
|
||||||
</ButtonWrapper>
|
|
||||||
<ButtonWrapper onClick={() => setStylePickerOpen(true)} ref={borderStyleButton}>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
disabled={false}
|
|
||||||
title={t("workbook.toolbar.borders_button_title")}
|
|
||||||
>
|
|
||||||
<BorderStyleIcon />
|
|
||||||
</Button>
|
|
||||||
<div style={{flexGrow:2}}>Border style</div>
|
|
||||||
<ChevronRightStyled />
|
|
||||||
</ButtonWrapper>
|
|
||||||
</Styles>
|
|
||||||
</BorderPickerDialog>
|
|
||||||
<ColorPicker
|
|
||||||
color={borderColor}
|
|
||||||
onChange={(color): void => {
|
|
||||||
setBorderColor(color);
|
|
||||||
setColorPickerOpen(false);
|
|
||||||
}}
|
|
||||||
anchorEl={borderColorButton}
|
|
||||||
open={colorPickerOpen}
|
|
||||||
/>
|
|
||||||
<StyledPopover
|
|
||||||
open={stylePickerOpen}
|
|
||||||
onClose={(): void => {
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
anchorEl={borderStyleButton.current}
|
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
|
||||||
transformOrigin={{ vertical: 38, horizontal: -6 }}
|
|
||||||
>
|
|
||||||
<BorderStyleDialog>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dashed);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.None}
|
|
||||||
>
|
|
||||||
<BorderDescription>None</BorderDescription>
|
|
||||||
<NoneLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Thin);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Thin}
|
|
||||||
>
|
|
||||||
<BorderDescription>Thin</BorderDescription>
|
|
||||||
<SolidLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Medium);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Medium}
|
|
||||||
>
|
|
||||||
<BorderDescription>Medium</BorderDescription>
|
|
||||||
<MediumLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Thick);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Thick}
|
|
||||||
>
|
|
||||||
<BorderDescription>Thick</BorderDescription>
|
|
||||||
<ThickLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dotted);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Dotted}
|
|
||||||
>
|
|
||||||
<BorderDescription>Dotted</BorderDescription>
|
|
||||||
<DottedLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dashed);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Dashed}
|
|
||||||
>
|
|
||||||
<BorderDescription>Dashed</BorderDescription>
|
|
||||||
<DashedLine />
|
|
||||||
</LineWrapper>
|
|
||||||
<LineWrapper
|
|
||||||
onClick={() => {
|
|
||||||
setBorderStyle(BorderStyle.Dashed);
|
|
||||||
setStylePickerOpen(false);
|
|
||||||
}}
|
|
||||||
$checked={borderStyle === BorderStyle.Double}
|
|
||||||
>
|
|
||||||
<BorderDescription>Double</BorderDescription>
|
|
||||||
<DoubleLine />
|
|
||||||
</LineWrapper>
|
|
||||||
</BorderStyleDialog>
|
|
||||||
</StyledPopover>
|
|
||||||
</StyledPopover>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type LineWrapperProperties = { $checked: boolean };
|
|
||||||
const LineWrapper = styled("div")<LineWrapperProperties>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
background-color: ${({ $checked }): string => {
|
|
||||||
if ($checked) {
|
|
||||||
return '#EEEEEE;';
|
|
||||||
} else {
|
|
||||||
return 'inherit;';
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
&:hover {
|
|
||||||
border: 1px solid #EEEEEE;
|
|
||||||
}
|
|
||||||
padding:8px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid white;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CheckIconWrapper = styled("div")`
|
|
||||||
width: 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type CheckIconProperties = { $checked: boolean };
|
|
||||||
const CheckIcon = styled("div")<CheckIconProperties>`
|
|
||||||
width: 2px;
|
|
||||||
background-color: #EEE;
|
|
||||||
height: 28px;
|
|
||||||
visibility: ${({ $checked }): string => {
|
|
||||||
if ($checked) {
|
|
||||||
return "visible";
|
|
||||||
}
|
|
||||||
return "hidden";
|
|
||||||
}};
|
|
||||||
`;
|
|
||||||
const NoneLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px solid #E0E0E0;
|
|
||||||
`;
|
|
||||||
const SolidLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px solid #333333;
|
|
||||||
`;
|
|
||||||
const MediumLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 2px solid #333333;
|
|
||||||
`;
|
|
||||||
const ThickLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 3px solid #333333;
|
|
||||||
`;
|
|
||||||
const DashedLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px dashed #333333;
|
|
||||||
`;
|
|
||||||
const DottedLine = styled("div")`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 1px dotted #333333;
|
|
||||||
`;
|
|
||||||
const DoubleLine = styled('div')`
|
|
||||||
width: 68px;
|
|
||||||
border-top: 3px double #333333;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Divider = styled("div")`
|
|
||||||
display: inline-flex;
|
|
||||||
heigh: 1px;
|
|
||||||
border-bottom: 1px solid #EEE;
|
|
||||||
margin-left: 0px;
|
|
||||||
margin-right: 0px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Borders = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Styles = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Line = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ButtonWrapper = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
&:hover {
|
|
||||||
background-color: #EEE;
|
|
||||||
border-top-color: ${(): string => theme.palette.grey["400"]};
|
|
||||||
}
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BorderStyleDialog = styled("div")`
|
|
||||||
background: ${({ theme }): string => theme.palette.background.default};
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledPopover = styled(Popover)`
|
|
||||||
.MuiPopover-paper {
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 0px solid ${({ theme }): string => theme.palette.background.default};
|
|
||||||
box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
|
||||||
}
|
|
||||||
.MuiPopover-padding {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
.MuiList-padding {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
|
||||||
font-size: 13px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BorderPickerDialog = styled("div")`
|
|
||||||
background: ${({ theme }): string => theme.palette.background.default};
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const BorderDescription = styled("div")`
|
|
||||||
width: 70px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
|
||||||
// const Button = styled.button<TypeButtonProperties>`
|
|
||||||
// width: 23px;
|
|
||||||
// height: 23px;
|
|
||||||
// display: inline-flex;
|
|
||||||
// align-items: center;
|
|
||||||
// justify-content: center;
|
|
||||||
// font-size: 14px;
|
|
||||||
// border-radius: 2px;
|
|
||||||
// margin-right: 5px;
|
|
||||||
// transition: all 0.2s;
|
|
||||||
|
|
||||||
// ${({ theme, disabled, $pressed, $underlinedColor }): string => {
|
|
||||||
// if (disabled) {
|
|
||||||
// return `
|
|
||||||
// color: ${theme.palette.grey['600']};
|
|
||||||
// cursor: default;
|
|
||||||
// `;
|
|
||||||
// }
|
|
||||||
// return `
|
|
||||||
// border-top: ${$underlinedColor ? '3px solid #FFF' : 'none'};
|
|
||||||
// border-bottom: ${$underlinedColor ? `3px solid ${$underlinedColor}` : 'none'};
|
|
||||||
// color: ${theme.palette.text.primary};
|
|
||||||
// background-color: ${$pressed ? theme.palette.grey['600'] : '#FFF'};
|
|
||||||
// &:hover {
|
|
||||||
// background-color: ${theme.palette.grey['400']};
|
|
||||||
// border-top-color: ${theme.palette.grey['400']};
|
|
||||||
// }
|
|
||||||
// `;
|
|
||||||
// }}
|
|
||||||
// `;
|
|
||||||
|
|
||||||
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
|
||||||
const Button = styled("button")<TypeButtonProperties>(
|
|
||||||
({ disabled, $pressed, $underlinedColor }) => {
|
|
||||||
let result: Record<string, any> = {
|
|
||||||
width: "24px",
|
|
||||||
height: "24px",
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
// fontSize: "26px",
|
|
||||||
border: "0px solid #fff",
|
|
||||||
borderRadius: "2px",
|
|
||||||
marginRight: "5px",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "0px",
|
|
||||||
};
|
|
||||||
if (disabled) {
|
|
||||||
result.color = theme.palette.grey["600"];
|
|
||||||
result.cursor = "default";
|
|
||||||
} else {
|
|
||||||
result.borderTop = $underlinedColor ? "3px solid #FFF" : "none";
|
|
||||||
result.borderBottom = $underlinedColor
|
|
||||||
? `3px solid ${$underlinedColor}`
|
|
||||||
: "none";
|
|
||||||
(result.color = "#21243A"),
|
|
||||||
(result.backgroundColor = $pressed
|
|
||||||
? theme.palette.grey["600"]
|
|
||||||
: "inherit");
|
|
||||||
result["&:hover"] = {
|
|
||||||
backgroundColor: "#F1F2F8",
|
|
||||||
borderTopColor: "#F1F2F8",
|
|
||||||
};
|
|
||||||
result["svg"] = {
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const ChevronRightStyled = styled(ChevronRight)`
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default BorderPicker;
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
import styled from "@emotion/styled";
|
|
||||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
|
||||||
import { theme } from "../theme";
|
|
||||||
|
|
||||||
type ColorPickerProps = {
|
|
||||||
className?: string;
|
|
||||||
color: string;
|
|
||||||
onChange: (color: string) => void;
|
|
||||||
anchorEl: React.RefObject<HTMLElement>;
|
|
||||||
anchorOrigin?: PopoverOrigin;
|
|
||||||
transformOrigin?: PopoverOrigin;
|
|
||||||
open: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorPickerWidth = 240;
|
|
||||||
const colorPickerPadding = 15;
|
|
||||||
const colorfulHeight = 185; // 150 + 15 + 20
|
|
||||||
|
|
||||||
const ColorPicker = (properties: ColorPickerProps) => {
|
|
||||||
const [color, setColor] = useState<string>(properties.color);
|
|
||||||
const recentColors = useRef<string[]>([]);
|
|
||||||
|
|
||||||
const closePicker = (newColor: string): void => {
|
|
||||||
const maxRecentColors = 14;
|
|
||||||
properties.onChange(newColor);
|
|
||||||
const colors = recentColors.current.filter((c) => c !== newColor);
|
|
||||||
recentColors.current = [newColor, ...colors].slice(0, maxRecentColors);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setColor(properties.color);
|
|
||||||
}, [properties.color]);
|
|
||||||
|
|
||||||
const presetColors = [
|
|
||||||
"#FFFFFF",
|
|
||||||
"#1B717E",
|
|
||||||
"#59B9BC",
|
|
||||||
"#3BB68A",
|
|
||||||
"#8CB354",
|
|
||||||
"#F8CD3C",
|
|
||||||
"#EC5753",
|
|
||||||
"#A23C52",
|
|
||||||
"#D03627",
|
|
||||||
"#523E93",
|
|
||||||
"#3358B7",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
open={properties.open}
|
|
||||||
onClose={(): void => closePicker(color)}
|
|
||||||
anchorEl={properties.anchorEl.current}
|
|
||||||
anchorOrigin={
|
|
||||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
|
||||||
}
|
|
||||||
transformOrigin={
|
|
||||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ColorPickerDialog>
|
|
||||||
<HexColorPicker
|
|
||||||
color={color}
|
|
||||||
onChange={(newColor): void => {
|
|
||||||
setColor(newColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ColorPickerInput>
|
|
||||||
<HexWrapper>
|
|
||||||
<HexLabel>{"Hex"}</HexLabel>
|
|
||||||
<HexColorInputBox>
|
|
||||||
<HashLabel>{"#"}</HashLabel>
|
|
||||||
<HexColorInput
|
|
||||||
color={color}
|
|
||||||
onChange={(newColor): void => {
|
|
||||||
setColor(newColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</HexColorInputBox>
|
|
||||||
</HexWrapper>
|
|
||||||
<Swatch $color={color} />
|
|
||||||
</ColorPickerInput>
|
|
||||||
<HorizontalDivider />
|
|
||||||
<ColorList>
|
|
||||||
{presetColors.map((presetColor) => (
|
|
||||||
<Button
|
|
||||||
key={presetColor}
|
|
||||||
$color={presetColor}
|
|
||||||
onClick={(): void => {
|
|
||||||
closePicker(presetColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ColorList>
|
|
||||||
<HorizontalDivider />
|
|
||||||
<RecentLabel>{"Recent"}</RecentLabel>
|
|
||||||
<ColorList>
|
|
||||||
{recentColors.current.map((recentColor) => (
|
|
||||||
<Button
|
|
||||||
key={recentColor}
|
|
||||||
$color={recentColor}
|
|
||||||
onClick={(): void => {
|
|
||||||
closePicker(recentColor);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ColorList>
|
|
||||||
</ColorPickerDialog>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RecentLabel = styled.div`
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${theme.palette.text.secondary};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColorList = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
flex-direction: row;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Button = styled.button<{ $color: string }>`
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
${({ $color }): string => {
|
|
||||||
if ($color.toUpperCase() === "#FFFFFF") {
|
|
||||||
return `border: 1px solid ${theme.palette.grey['600']};`;
|
|
||||||
}
|
|
||||||
return `border: 1px solid ${$color};`;
|
|
||||||
}}
|
|
||||||
background-color: ${({ $color }): string => {
|
|
||||||
return $color;
|
|
||||||
}};
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HorizontalDivider = styled.div`
|
|
||||||
height: 0px;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px solid ${theme.palette.grey['400']};
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// const StyledPopover = styled(Popover)`
|
|
||||||
// .MuiPopover-paper {
|
|
||||||
// border-radius: 10px;
|
|
||||||
// border: 0px solid ${theme.palette.background.default};
|
|
||||||
// box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
|
||||||
// }
|
|
||||||
// .MuiPopover-padding {
|
|
||||||
// padding: 0px;
|
|
||||||
// }
|
|
||||||
// .MuiList-padding {
|
|
||||||
// padding: 0px;
|
|
||||||
// }
|
|
||||||
// `;
|
|
||||||
|
|
||||||
const ColorPickerDialog = styled.div`
|
|
||||||
background: ${theme.palette.background.default};
|
|
||||||
width: ${colorPickerWidth}px;
|
|
||||||
padding: 15px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
& .react-colorful {
|
|
||||||
height: ${colorfulHeight}px;
|
|
||||||
width: ${colorPickerWidth - colorPickerPadding * 2}px;
|
|
||||||
}
|
|
||||||
& .react-colorful__saturation {
|
|
||||||
border-bottom: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
& .react-colorful__hue {
|
|
||||||
height: 20px;
|
|
||||||
margin-top: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
& .react-colorful__saturation-pointer {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
& .react-colorful__hue-pointer {
|
|
||||||
width: 7px;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HashLabel = styled.div`
|
|
||||||
margin: auto 0px auto 10px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #7d8ec2;
|
|
||||||
font-family: ${theme.typography.button.fontFamily};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HexLabel = styled.div`
|
|
||||||
margin: auto 10px auto 0px;
|
|
||||||
font-size: 12px;
|
|
||||||
display: inline-flex;
|
|
||||||
font-family: ${theme.typography.button.fontFamily};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HexColorInputBox = styled.div`
|
|
||||||
display: inline-flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 140px;
|
|
||||||
height: 28px;
|
|
||||||
border: 1px solid ${theme.palette.grey['600']};
|
|
||||||
border-radius: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const HexWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
& input {
|
|
||||||
min-width: 0px;
|
|
||||||
border: 0px;
|
|
||||||
background: ${theme.palette.background.default};
|
|
||||||
outline: none;
|
|
||||||
font-family: ${theme.typography.button.fontFamily};
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& input:focus {
|
|
||||||
border-color: #4298ef;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Swatch = styled.div<{ $color: string }>`
|
|
||||||
display: inline-flex;
|
|
||||||
${({ $color }): string => {
|
|
||||||
if ($color.toUpperCase() === "#FFFFFF") {
|
|
||||||
return `border: 1px solid ${theme.palette.grey['600']};`;
|
|
||||||
}
|
|
||||||
return `border: 1px solid ${$color};`;
|
|
||||||
}}
|
|
||||||
background-color: ${({ $color }): string => $color};
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 5px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ColorPickerInput = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default ColorPicker;
|
|
||||||
@@ -1,420 +0,0 @@
|
|||||||
import {
|
|
||||||
CSSProperties,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
KeyboardEvent,
|
|
||||||
useContext,
|
|
||||||
} from "react";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import EditorContext, { Area } from "./editorContext";
|
|
||||||
import { getStringRange } from "./util";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the Cell Editor for IronCalc
|
|
||||||
* I uses a transparent textarea and a styled mask. What you see is the HTML styled content of the mask
|
|
||||||
* and the caret from the textarea. The alternative would be to have a 'contenteditable' div.
|
|
||||||
* That turns out to be a much more difficult implementation.
|
|
||||||
*
|
|
||||||
* The editor grows horizontally with text if it fits in the screen.
|
|
||||||
* If it doesn't fit, it wraps and grows vertically. If it doesn't fit vertically it scrolls.
|
|
||||||
*
|
|
||||||
* Many keyboard and mouse events are handled gracefully by the textarea in full or in part.
|
|
||||||
* For example letter key strokes like 'q' or '1' are handled full by the textarea.
|
|
||||||
* Some keyboard events like "RightArrow" might need to be handled separately and let them bubble up,
|
|
||||||
* or might be handled by the textarea, depending on the "editor mode".
|
|
||||||
* Some other like "Enter" we need to intercept and change the normal behaviour.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const commonCSS: CSSProperties = {
|
|
||||||
fontWeight: "inherit",
|
|
||||||
fontFamily: "inherit",
|
|
||||||
fontSize: "inherit",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
whiteSpace: "pre",
|
|
||||||
width: "100%",
|
|
||||||
padding: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Cell {
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EditorOptions {
|
|
||||||
minimalWidth: number;
|
|
||||||
minimalHeight: number;
|
|
||||||
textColor: string;
|
|
||||||
originalText: string;
|
|
||||||
getStyledText: (
|
|
||||||
text: string,
|
|
||||||
insertRangeText: string
|
|
||||||
) => {
|
|
||||||
html: JSX.Element[];
|
|
||||||
isInReferenceMode: boolean;
|
|
||||||
};
|
|
||||||
onEditEnd: (text: string) => void;
|
|
||||||
display: boolean;
|
|
||||||
cell: Cell;
|
|
||||||
sheetNames: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can either be editing a formula or content.
|
|
||||||
// When editing content (behaviour is common to Excel and Google Sheets):
|
|
||||||
// * If you start editing by typing you are in *accept* mode
|
|
||||||
// * If you start editing by F2 you are in *cruise* mode
|
|
||||||
// * If you start editing by double click you are in *cruise* mode
|
|
||||||
// In Google Sheets "Enter" starts editing and puts you in *cruise* mode. We do not do that
|
|
||||||
// Once you are in cruise mode it is not possible to switch to accept mode
|
|
||||||
// The only way to go from accept mode to cruise mode is clicking in the content somewhere
|
|
||||||
|
|
||||||
// When editing a formula.
|
|
||||||
// In Google Sheets you are either in insert mode or cruise mode.
|
|
||||||
// You can get back to accept mode if you delete the whole formula
|
|
||||||
// In Excel you can be either in insert or accept but if you click in the formula body
|
|
||||||
// you switch to cruise mode. Once in cruise mode you can go to insert mode by selecting a range.
|
|
||||||
// Then you are back in accept/insert modes
|
|
||||||
|
|
||||||
const Editor = (options: EditorOptions) => {
|
|
||||||
const {
|
|
||||||
minimalWidth,
|
|
||||||
minimalHeight,
|
|
||||||
textColor,
|
|
||||||
onEditEnd,
|
|
||||||
originalText,
|
|
||||||
display,
|
|
||||||
cell,
|
|
||||||
sheetNames,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const [width, setWidth] = useState(minimalWidth);
|
|
||||||
const [height, setHeight] = useState(minimalHeight);
|
|
||||||
|
|
||||||
const { editorContext, setEditorContext } = useContext(EditorContext);
|
|
||||||
|
|
||||||
const setBaseText = (newText: string) => {
|
|
||||||
console.log('Calling setBaseText');
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
baseText: newText,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const insertRangeText = editorContext.insertRange
|
|
||||||
? getStringRange(editorContext.insertRange, sheetNames)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const baseText = editorContext.baseText;
|
|
||||||
const text = baseText + insertRangeText;
|
|
||||||
console.log('baseText', baseText, 'insertRange:', insertRangeText);
|
|
||||||
|
|
||||||
const formulaRef = useRef<HTMLDivElement>(null);
|
|
||||||
const maskRef = useRef<HTMLDivElement>(null);
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// setBaseText(originalText);
|
|
||||||
// }, [cell]);
|
|
||||||
|
|
||||||
const { html: styledFormula, isInReferenceMode } = options.getStyledText(
|
|
||||||
baseText,
|
|
||||||
insertRangeText
|
|
||||||
);
|
|
||||||
|
|
||||||
if (display && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formulaRef.current) {
|
|
||||||
const scrollWidth = formulaRef.current.scrollWidth;
|
|
||||||
if (scrollWidth > width) {
|
|
||||||
setWidth(scrollWidth);
|
|
||||||
} else if (scrollWidth <= minimalWidth) {
|
|
||||||
setWidth(minimalWidth);
|
|
||||||
}
|
|
||||||
const scrollHeight = formulaRef.current.scrollHeight;
|
|
||||||
if (scrollHeight > height) {
|
|
||||||
setHeight(scrollHeight);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [text]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInReferenceMode) {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
mode: "insert",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
mode: "cruise",
|
|
||||||
insertRange: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [isInReferenceMode]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (display && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [display]);
|
|
||||||
|
|
||||||
console.log("Ok, this is running", text, editorContext.id);
|
|
||||||
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
const { key, shiftKey, altKey } = event;
|
|
||||||
const textarea = textareaRef.current;
|
|
||||||
const mode = editorContext.mode;
|
|
||||||
if (!textarea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (key) {
|
|
||||||
case "Enter": {
|
|
||||||
if (altKey) {
|
|
||||||
// new line
|
|
||||||
const start = textarea.selectionStart;
|
|
||||||
const end = textarea.selectionEnd;
|
|
||||||
const newText = text.slice(0, start) + "\n" + text.slice(end);
|
|
||||||
setBaseText(newText);
|
|
||||||
setTimeout(() => {
|
|
||||||
textarea.setSelectionRange(start + 1, start + 1);
|
|
||||||
}, 1);
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// end edit
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
// event bubbles up
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Escape": {
|
|
||||||
setBaseText(originalText);
|
|
||||||
textarea.blur();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowLeft": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
// event bubbles up
|
|
||||||
return;
|
|
||||||
} else if (mode == "insert") {
|
|
||||||
if (shiftKey) {
|
|
||||||
// increase the inserted range to the left
|
|
||||||
if (!editorContext.insertRange) {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
insertRange: {
|
|
||||||
absoluteColumnEnd: false,
|
|
||||||
absoluteColumnStart: false,
|
|
||||||
absoluteRowEnd: false,
|
|
||||||
absoluteRowStart: false,
|
|
||||||
sheet: cell.sheet,
|
|
||||||
rowStart: cell.row,
|
|
||||||
rowEnd: cell.row,
|
|
||||||
columnStart: cell.column,
|
|
||||||
columnEnd: cell.column,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// const r = insertRage;
|
|
||||||
// r.columnStart = Math.max(r.columnStart - 1, 1);
|
|
||||||
// setInsertRange(r);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// move inserted cell to the left
|
|
||||||
if (!editorContext.insertRange) {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
insertRange: {
|
|
||||||
absoluteColumnEnd: false,
|
|
||||||
absoluteColumnStart: false,
|
|
||||||
absoluteRowEnd: false,
|
|
||||||
absoluteRowStart: false,
|
|
||||||
sheet: cell.sheet,
|
|
||||||
rowStart: cell.row,
|
|
||||||
rowEnd: cell.row,
|
|
||||||
columnStart: cell.column,
|
|
||||||
columnEnd: cell.column,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setEditorContext((c) => {
|
|
||||||
const range = c.insertRange as Area;
|
|
||||||
const row = range.rowStart;
|
|
||||||
let column = range.columnStart - 1;
|
|
||||||
if (column < 1) {
|
|
||||||
column = 1;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
insertRange: {
|
|
||||||
absoluteColumnEnd: false,
|
|
||||||
absoluteColumnStart: false,
|
|
||||||
absoluteRowEnd: false,
|
|
||||||
absoluteRowStart: false,
|
|
||||||
sheet: range.sheet,
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: column,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// We don't do anything in "cruise mode" and rely on the textarea default behaviour
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowDown": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowRight": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ArrowUp": {
|
|
||||||
if (mode === "accept") {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Tab": {
|
|
||||||
onEditEnd(text);
|
|
||||||
textarea.blur();
|
|
||||||
// event bubbles up
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (editorContext.mode === "insert") {
|
|
||||||
setBaseText(text);
|
|
||||||
setEditorContext((context) => {
|
|
||||||
return {
|
|
||||||
...context,
|
|
||||||
mode: "cruise",
|
|
||||||
insertRange: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[text, editorContext]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "relative",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
overflow: "hidden",
|
|
||||||
background: "#FFF",
|
|
||||||
display: display ? "block" : "none",
|
|
||||||
}}
|
|
||||||
onClick={(_event) => {
|
|
||||||
console.log("Click on wrapper");
|
|
||||||
}}
|
|
||||||
onPointerDown={() => {
|
|
||||||
console.log("On pointer down wrapper");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={maskRef}
|
|
||||||
style={{
|
|
||||||
...commonCSS,
|
|
||||||
textAlign: "left",
|
|
||||||
pointerEvents: "none",
|
|
||||||
height,
|
|
||||||
}}
|
|
||||||
onClick={(_event) => {
|
|
||||||
console.log("Click on mask");
|
|
||||||
}}
|
|
||||||
onPointerDown={() => {
|
|
||||||
console.log("On pointer down mask");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div ref={formulaRef}>{styledFormula}</div>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
rows={1}
|
|
||||||
style={{
|
|
||||||
...commonCSS,
|
|
||||||
color: "transparent",
|
|
||||||
backgroundColor: "transparent",
|
|
||||||
caretColor: textColor,
|
|
||||||
outline: "none",
|
|
||||||
resize: "none",
|
|
||||||
border: "none",
|
|
||||||
height,
|
|
||||||
}}
|
|
||||||
spellCheck="false"
|
|
||||||
value={text}
|
|
||||||
onChange={(event) => {
|
|
||||||
console.log("onChange", event.target.value);
|
|
||||||
setBaseText(event.target.value);
|
|
||||||
}}
|
|
||||||
onScroll={() => {
|
|
||||||
if (maskRef.current && textareaRef.current) {
|
|
||||||
maskRef.current.style.left = `-${textareaRef.current.scrollLeft}px`;
|
|
||||||
maskRef.current.style.top = `-${textareaRef.current.scrollTop}px`;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onClick={(event) => {
|
|
||||||
console.log("Setting mode");
|
|
||||||
setEditorContext((c) => {
|
|
||||||
return {
|
|
||||||
...c,
|
|
||||||
mode: "cruise",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
console.log("here");
|
|
||||||
// if (display) {
|
|
||||||
event.stopPropagation();
|
|
||||||
// }
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
// on blur
|
|
||||||
}}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Editor;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Dispatch, SetStateAction, createContext } from "react";
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
sheet: number | null;
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
absoluteRowStart: boolean;
|
|
||||||
absoluteRowEnd: boolean;
|
|
||||||
absoluteColumnStart: boolean;
|
|
||||||
absoluteColumnEnd: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arrow keys behave in different ways depending on the "edit mode":
|
|
||||||
// * In _cruise_ mode arrowy keys navigate within the editor
|
|
||||||
// * In _accept_ mode pressing an arrow key will end editing
|
|
||||||
// * In _insert_ mode arrow keys will change the selected range
|
|
||||||
export type EditorMode = "cruise" | "accept" | "insert";
|
|
||||||
|
|
||||||
export interface EditorState {
|
|
||||||
mode: EditorMode;
|
|
||||||
insertRange: null | Area;
|
|
||||||
baseText: string;
|
|
||||||
id: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EditorContextType {
|
|
||||||
editorContext: EditorState;
|
|
||||||
setEditorContext: Dispatch<
|
|
||||||
SetStateAction<{ mode: EditorMode; insertRange: null | Area }>
|
|
||||||
>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EditorContext = createContext<EditorContextType>({
|
|
||||||
editorContext: {
|
|
||||||
mode: "accept",
|
|
||||||
insertRange: null,
|
|
||||||
baseText: '',
|
|
||||||
id: Math.floor(Math.random()*1000),
|
|
||||||
},
|
|
||||||
setEditorContext: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default EditorContext;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export { default } from './editor';
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
type ErrorType =
|
|
||||||
| 'REF'
|
|
||||||
| 'NAME'
|
|
||||||
| 'VALUE'
|
|
||||||
| 'DIV'
|
|
||||||
| 'NA'
|
|
||||||
| 'NUM'
|
|
||||||
| 'ERROR'
|
|
||||||
| 'NIMPL'
|
|
||||||
| 'SPILL'
|
|
||||||
| 'CALC'
|
|
||||||
| 'CIRC';
|
|
||||||
|
|
||||||
type OpCompareType =
|
|
||||||
| 'LessThan'
|
|
||||||
| 'GreaterThan'
|
|
||||||
| 'Equal'
|
|
||||||
| 'LessOrEqualThan'
|
|
||||||
| 'GreaterOrEqualThan'
|
|
||||||
| 'NonEqual';
|
|
||||||
|
|
||||||
type OpSumType = 'Add' | 'Minus';
|
|
||||||
|
|
||||||
type OpProductType = 'Times' | 'Divide';
|
|
||||||
|
|
||||||
interface ReferenceType {
|
|
||||||
sheet: string | null;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
absolute_column: boolean;
|
|
||||||
absolute_row: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParsedReferenceType {
|
|
||||||
column: number;
|
|
||||||
row: number;
|
|
||||||
absolute_column: boolean;
|
|
||||||
absolute_row: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Reference {
|
|
||||||
Reference: ReferenceType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Range {
|
|
||||||
Range: {
|
|
||||||
sheet: string | null;
|
|
||||||
left: ParsedReferenceType;
|
|
||||||
right: ParsedReferenceType;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TokenType =
|
|
||||||
| 'Illegal'
|
|
||||||
| 'Eof'
|
|
||||||
| { Ident: string }
|
|
||||||
| { String: string }
|
|
||||||
| { Boolean: boolean }
|
|
||||||
| { Number: number }
|
|
||||||
| { ERROR: ErrorType }
|
|
||||||
| { COMPARE: OpCompareType }
|
|
||||||
| { SUM: OpSumType }
|
|
||||||
| { PRODUCT: OpProductType }
|
|
||||||
| 'POWER'
|
|
||||||
| 'LPAREN'
|
|
||||||
| 'RPAREN'
|
|
||||||
| 'COLON'
|
|
||||||
| 'SEMICOLON'
|
|
||||||
| 'LBRACKET'
|
|
||||||
| 'RBRACKET'
|
|
||||||
| 'LBRACE'
|
|
||||||
| 'RBRACE'
|
|
||||||
| 'COMMA'
|
|
||||||
| 'BANG'
|
|
||||||
| 'PERCENT'
|
|
||||||
| 'AND'
|
|
||||||
| Reference
|
|
||||||
| Range;
|
|
||||||
|
|
||||||
export interface MarkedToken {
|
|
||||||
token: TokenType;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tokenIsReferenceType(token: TokenType): token is Reference {
|
|
||||||
return typeof token === 'object' && 'Reference' in token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tokenIsRangeType(token: TokenType): token is Range {
|
|
||||||
return typeof token === 'object' && 'Range' in token;
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { useCallback, KeyboardEvent } from "react";
|
|
||||||
import { WorkbookState } from "../workbookState";
|
|
||||||
import { Model } from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
// onMoveCaretToStart: () => void;
|
|
||||||
// onMoveCaretToEnd: () => void;
|
|
||||||
// onEditEnd: (delta: { deltaRow: number; deltaColumn: number }) => void;
|
|
||||||
// onEditEscape: () => void;
|
|
||||||
// onReferenceCycle: () => void;
|
|
||||||
// text: string;
|
|
||||||
// setText: (text: string) => void;
|
|
||||||
model: Model;
|
|
||||||
state: WorkbookState;
|
|
||||||
refresh: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useEditorKeydown = (
|
|
||||||
options: Options
|
|
||||||
): {
|
|
||||||
onKeyDown: (event: KeyboardEvent) => void;
|
|
||||||
} => {
|
|
||||||
const { state, model } = options;
|
|
||||||
const onKeyDown = useCallback((event: KeyboardEvent) => {
|
|
||||||
const { key, shiftKey } = event;
|
|
||||||
const { mode, text } = state.getEditor() ?? { mode: "init", text: "" };
|
|
||||||
switch (key) {
|
|
||||||
case "Enter":
|
|
||||||
// options.onEditEnd({ deltaRow: 1, deltaColumn: 0 });
|
|
||||||
const { row, column } = state.getSelectedCell();
|
|
||||||
const sheet = state.getSelectedSheet();
|
|
||||||
model.setUserInput(sheet, row, column, text);
|
|
||||||
state.selectCell({ row: row + 1, column });
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
options.refresh();
|
|
||||||
break;
|
|
||||||
// case 'ArrowUp': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: -1, deltaColumn: 0 });
|
|
||||||
// } else {
|
|
||||||
// options.onMoveCaretToStart();
|
|
||||||
// }
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'ArrowDown': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: 1, deltaColumn: 0 });
|
|
||||||
// } else {
|
|
||||||
// options.onMoveCaretToEnd();
|
|
||||||
// }
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'Tab': {
|
|
||||||
// if (event.shiftKey) {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: -1 });
|
|
||||||
// } else {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: 1 });
|
|
||||||
// }
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'Escape': {
|
|
||||||
// options.onEditEscape();
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'ArrowLeft': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: -1 });
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'ArrowRight': {
|
|
||||||
// if (mode === 'init') {
|
|
||||||
// options.onEditEnd({ deltaRow: 0, deltaColumn: 1 });
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// }
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// case 'F4': {
|
|
||||||
// options.onReferenceCycle();
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, [model, state]);
|
|
||||||
return { onKeyDown };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useEditorKeydown;
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
import { getTokens } from "@ironcalc/wasm";
|
|
||||||
import { tokenIsRangeType, tokenIsReferenceType } from "./tokenTypes";
|
|
||||||
import { Area } from "./editorContext";
|
|
||||||
|
|
||||||
const letters = [
|
|
||||||
"A",
|
|
||||||
"B",
|
|
||||||
"C",
|
|
||||||
"D",
|
|
||||||
"E",
|
|
||||||
"F",
|
|
||||||
"G",
|
|
||||||
"H",
|
|
||||||
"I",
|
|
||||||
"J",
|
|
||||||
"K",
|
|
||||||
"L",
|
|
||||||
"M",
|
|
||||||
"N",
|
|
||||||
"O",
|
|
||||||
"P",
|
|
||||||
"Q",
|
|
||||||
"R",
|
|
||||||
"S",
|
|
||||||
"T",
|
|
||||||
"U",
|
|
||||||
"V",
|
|
||||||
"W",
|
|
||||||
"X",
|
|
||||||
"Y",
|
|
||||||
"Z",
|
|
||||||
];
|
|
||||||
interface Reference {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
absoluteRow: boolean;
|
|
||||||
absoluteColumn: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function referenceToString(rf: Reference): string {
|
|
||||||
const absC = rf.absoluteColumn ? "$" : "";
|
|
||||||
const absR = rf.absoluteRow ? "$" : "";
|
|
||||||
return absC + columnNameFromNumber(rf.column) + absR + rf.row;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNameFromNumber(column: number): string {
|
|
||||||
let columnName = "";
|
|
||||||
let index = column;
|
|
||||||
while (index > 0) {
|
|
||||||
columnName = `${letters[(index - 1) % 26]}${columnName}`;
|
|
||||||
index = Math.floor((index - 1) / 26);
|
|
||||||
}
|
|
||||||
return columnName;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function columnNumberFromName(columnName: string): number {
|
|
||||||
let column = 0;
|
|
||||||
for (const character of columnName) {
|
|
||||||
const index = (character.codePointAt(0) ?? 0) - 64;
|
|
||||||
column = column * 26 + index;
|
|
||||||
}
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
interface Range {
|
|
||||||
sheet: number | null;
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
absoluteRowStart: boolean;
|
|
||||||
absoluteRowEnd: boolean;
|
|
||||||
absoluteColumnStart: boolean;
|
|
||||||
absoluteColumnEnd: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getStringRange(range: Range, sheetNames: string[]) {
|
|
||||||
const name = range.sheet ? `${sheetNames[range.sheet]}!` : "";
|
|
||||||
const left = referenceToString({
|
|
||||||
row: range.rowStart,
|
|
||||||
column: range.columnStart,
|
|
||||||
absoluteRow: range.absoluteRowStart,
|
|
||||||
absoluteColumn: range.absoluteColumnStart,
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
range.rowStart === range.rowEnd &&
|
|
||||||
range.columnStart === range.columnEnd
|
|
||||||
) {
|
|
||||||
return `${name}${left}`;
|
|
||||||
}
|
|
||||||
const right = referenceToString({
|
|
||||||
row: range.rowEnd,
|
|
||||||
column: range.columnEnd,
|
|
||||||
absoluteRow: range.absoluteRowEnd,
|
|
||||||
absoluteColumn: range.absoluteColumnEnd,
|
|
||||||
});
|
|
||||||
return `${name}${left}:${right}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActiveRange {
|
|
||||||
sheet: number;
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IronCalc Color Palette
|
|
||||||
export function getColor(index: number, alpha = 1): string {
|
|
||||||
const colors = [
|
|
||||||
{
|
|
||||||
name: "Cyan",
|
|
||||||
rgba: [89, 185, 188, 1],
|
|
||||||
hex: "#59B9BC",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Flamingo",
|
|
||||||
rgba: [236, 87, 83, 1],
|
|
||||||
hex: "#EC5753",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#3358B7",
|
|
||||||
rgba: [51, 88, 183, 1],
|
|
||||||
name: "Blue",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#F8CD3C",
|
|
||||||
rgba: [248, 205, 60, 1],
|
|
||||||
name: "Yellow",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#3BB68A",
|
|
||||||
rgba: [59, 182, 138, 1],
|
|
||||||
name: "Emerald",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#523E93",
|
|
||||||
rgba: [82, 62, 147, 1],
|
|
||||||
name: "Violet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#A23C52",
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: "Burgundy",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#8CB354",
|
|
||||||
rgba: [162, 60, 82, 1],
|
|
||||||
name: "Wasabi",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#D03627",
|
|
||||||
rgba: [208, 54, 39, 1],
|
|
||||||
name: "Red",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
hex: "#1B717E",
|
|
||||||
rgba: [27, 113, 126, 1],
|
|
||||||
name: "Teal",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (alpha === 1) {
|
|
||||||
return colors[index % 10].hex;
|
|
||||||
}
|
|
||||||
const { rgba } = colors[index % 10];
|
|
||||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* This function get a formula like `=A1*SUM(B5:C6)` and transforms it to:
|
|
||||||
*
|
|
||||||
* `<span>=</span><span>A1</span><span>SUM</span><span>(</span><span>B5:C6</span><span>)</span>`
|
|
||||||
*
|
|
||||||
* While also returning the set of ranges [A1, B5:C6] with specific color assignments for each range
|
|
||||||
*/
|
|
||||||
export function getFormulaHTML(
|
|
||||||
text: string,
|
|
||||||
sheet: number,
|
|
||||||
sheetList: string[],
|
|
||||||
insertRage: Area | null,
|
|
||||||
insertRangeText: string
|
|
||||||
): {
|
|
||||||
html: JSX.Element[];
|
|
||||||
activeRanges: ActiveRange[];
|
|
||||||
isInReferenceMode: boolean;
|
|
||||||
} {
|
|
||||||
let html = [];
|
|
||||||
const activeRanges: ActiveRange[] = [];
|
|
||||||
let colorCount = 0;
|
|
||||||
if (text.startsWith("=")) {
|
|
||||||
const formula = text.slice(1);
|
|
||||||
|
|
||||||
const tokens = getTokens(formula);
|
|
||||||
const tokenCount = tokens.length;
|
|
||||||
const usedColors: Record<string, string> = {};
|
|
||||||
for (let index = 0; index < tokenCount; index += 1) {
|
|
||||||
const { token, start, end } = tokens[index];
|
|
||||||
if (tokenIsReferenceType(token)) {
|
|
||||||
const { sheet: refSheet, row, column } = token.Reference;
|
|
||||||
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
|
||||||
const key = `${sheetIndex}-${row}-${column}`;
|
|
||||||
let color = usedColors[key];
|
|
||||||
if (!color) {
|
|
||||||
color = getColor(colorCount);
|
|
||||||
usedColors[key] = color;
|
|
||||||
colorCount += 1;
|
|
||||||
}
|
|
||||||
html.push(
|
|
||||||
<span key={index} style={{ color }}>
|
|
||||||
{formula.slice(start, end)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
activeRanges.push({
|
|
||||||
sheet: sheetIndex,
|
|
||||||
rowStart: row,
|
|
||||||
columnStart: column,
|
|
||||||
rowEnd: row,
|
|
||||||
columnEnd: column,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
} else if (tokenIsRangeType(token)) {
|
|
||||||
let {
|
|
||||||
sheet: refSheet,
|
|
||||||
left: { row: rowStart, column: columnStart },
|
|
||||||
right: { row: rowEnd, column: columnEnd },
|
|
||||||
} = token.Range;
|
|
||||||
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
|
|
||||||
|
|
||||||
const key = `${sheetIndex}-${rowStart}-${columnStart}:${rowEnd}-${columnEnd}`;
|
|
||||||
let color = usedColors[key];
|
|
||||||
if (!color) {
|
|
||||||
color = getColor(colorCount);
|
|
||||||
usedColors[key] = color;
|
|
||||||
colorCount += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowStart > rowEnd) {
|
|
||||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
|
||||||
}
|
|
||||||
if (columnStart > columnEnd) {
|
|
||||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
|
||||||
}
|
|
||||||
html.push(
|
|
||||||
<span key={index} style={{ color }}>
|
|
||||||
{formula.slice(start, end)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
colorCount += 1;
|
|
||||||
|
|
||||||
activeRanges.push({
|
|
||||||
sheet: sheetIndex,
|
|
||||||
rowStart,
|
|
||||||
columnStart,
|
|
||||||
rowEnd,
|
|
||||||
columnEnd,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
html.push(<span key={index}>{formula.slice(start, end)}</span>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tokenCount > 0) {
|
|
||||||
const lastToken = tokens[tokens.length - 1];
|
|
||||||
if (lastToken.end < text.length - 1) {
|
|
||||||
html.push(
|
|
||||||
<span key="rest">{text.slice(lastToken.end + 1, text.length)}</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html = [<span key="equals">=</span>].concat(html);
|
|
||||||
} else {
|
|
||||||
html = [<span key="single">{text}</span>];
|
|
||||||
}
|
|
||||||
const isRefMode = isInReferenceMode(text, text.length);
|
|
||||||
if (isRefMode) {
|
|
||||||
if (insertRage) {
|
|
||||||
const color = getColor(colorCount);
|
|
||||||
activeRanges.push({
|
|
||||||
sheet: insertRage.sheet || sheet,
|
|
||||||
rowStart: insertRage.rowStart,
|
|
||||||
rowEnd: insertRage.rowEnd,
|
|
||||||
columnStart: insertRage.columnStart,
|
|
||||||
columnEnd: insertRage.columnEnd,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
colorCount += 1;
|
|
||||||
html.push(
|
|
||||||
<span key="insert-range" style={{ color, textDecoration: "underline" }}>
|
|
||||||
{insertRangeText}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
html.push(
|
|
||||||
<span
|
|
||||||
key="insert-cue"
|
|
||||||
style={{
|
|
||||||
border: "1px solid #d5d5d5",
|
|
||||||
height: "2px",
|
|
||||||
width: "7px",
|
|
||||||
borderTop: 0,
|
|
||||||
display: "inline-block",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// We add a clickable element that spans the rest of the available space
|
|
||||||
html.push(<span key="spacer" style={{ flexGrow: 1 }}></span>);
|
|
||||||
return { html, activeRanges, isInReferenceMode: isRefMode };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isInReferenceMode(text: string, cursor: number): boolean {
|
|
||||||
// FIXME
|
|
||||||
// This is a gross oversimplification
|
|
||||||
// Returns true if both are true:
|
|
||||||
// 1. Cursor is at the end
|
|
||||||
// 2. Last char is one of [',', '(', '+', '*', '-', '/', '<', '>', '=', '&']
|
|
||||||
// This has many false positives like '="1+' and also likely some false negatives
|
|
||||||
// The right way of doing this is to have a partial parse of the formula tree
|
|
||||||
// and check if the next token could be a reference
|
|
||||||
if (!text.startsWith("=")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (text === "=") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const l = text.length;
|
|
||||||
const chars = [",", "(", "+", "*", "-", "/", "<", ">", "=", "&"];
|
|
||||||
if (cursor === l && chars.includes(text[l - 1])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { useState, useRef, ComponentProps } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { NumberFormats } from './formatUtil';
|
|
||||||
import { Menu, MenuItem, styled } from '@mui/material';
|
|
||||||
import FormatPicker from './formatPicker';
|
|
||||||
|
|
||||||
type FormatMenuProps = {
|
|
||||||
children: any; //ReactI18NextChild | Iterable<ReactI18NextChild>;
|
|
||||||
numFmt: string;
|
|
||||||
onChange: (numberFmt: string) => void;
|
|
||||||
onExited?: () => void;
|
|
||||||
anchorOrigin?: ComponentProps<typeof Menu>['anchorOrigin'];
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormatMenu = (properties: FormatMenuProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { onChange } = properties;
|
|
||||||
const [isMenuOpen, setMenuOpen] = useState(false);
|
|
||||||
const [isPickerOpen, setPickerOpen] = useState(false);
|
|
||||||
const anchorElement = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ChildrenWrapper onClick={(): void => setMenuOpen(true)} ref={anchorElement}>
|
|
||||||
{properties.children}
|
|
||||||
</ChildrenWrapper>
|
|
||||||
<Menu
|
|
||||||
open={isMenuOpen}
|
|
||||||
onClose={(): void => setMenuOpen(false)}
|
|
||||||
// onExited={properties.onExited}
|
|
||||||
anchorEl={anchorElement.current}
|
|
||||||
anchorOrigin={properties.anchorOrigin}
|
|
||||||
>
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.AUTO)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.auto')}</MenuItemText>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
{/** TODO: Text option that transforms into plain text */}
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.NUMBER)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.number')}</MenuItemText>
|
|
||||||
<MenuItemExample>{t('toolbar.format_menu.number_example')}</MenuItemExample>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.PERCENTAGE)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.percentage')}</MenuItemText>
|
|
||||||
<MenuItemExample>{t('toolbar.format_menu.percentage_example')}</MenuItemExample>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.CURRENCY_EUR)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.currency_eur')}</MenuItemText>
|
|
||||||
<MenuItemExample>{t('toolbar.format_menu.currency_eur_example')}</MenuItemExample>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.CURRENCY_USD)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.currency_usd')}</MenuItemText>
|
|
||||||
<MenuItemExample>{t('toolbar.format_menu.currency_usd_example')}</MenuItemExample>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.CURRENCY_GBP)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.currency_gbp')}</MenuItemText>
|
|
||||||
<MenuItemExample>{t('toolbar.format_menu.currency_gbp_example')}</MenuItemExample>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.DATE_SHORT)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.date_short')}</MenuItemText>
|
|
||||||
<MenuItemExample>{t('toolbar.format_menu.date_short_example')}</MenuItemExample>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
<MenuItemWrapper onClick={(): void => onChange(NumberFormats.DATE_LONG)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.date_long')}</MenuItemText>
|
|
||||||
<MenuItemExample>{t('toolbar.format_menu.date_long_example')}</MenuItemExample>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper onClick={(): void => setPickerOpen(true)}>
|
|
||||||
<MenuItemText>{t('toolbar.format_menu.custom')}</MenuItemText>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
</Menu>
|
|
||||||
<FormatPicker
|
|
||||||
numFmt={properties.numFmt}
|
|
||||||
onChange={properties.onChange}
|
|
||||||
open={isPickerOpen}
|
|
||||||
onClose={(): void => setPickerOpen(false)}
|
|
||||||
onExited={properties.onExited}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuItemWrapper = styled(MenuItem)`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ChildrenWrapper = styled('div')`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuDivider = styled('div')``;
|
|
||||||
|
|
||||||
const MenuItemText = styled('div')`
|
|
||||||
color: #000;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuItemExample = styled('div')`
|
|
||||||
margin-left: 20px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default FormatMenu;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from '@mui/material';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
type FormatPickerProps = {
|
|
||||||
className?: string;
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
onExited?: () => void;
|
|
||||||
numFmt: string;
|
|
||||||
onChange: (numberFmt: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FormatPicker = (properties: FormatPickerProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [formatCode, setFormatCode] = useState(properties.numFmt);
|
|
||||||
|
|
||||||
const onSubmit = (format_code: string): void => {
|
|
||||||
properties.onChange(format_code);
|
|
||||||
properties.onClose();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={properties.open}
|
|
||||||
onClose={properties.onClose}
|
|
||||||
>
|
|
||||||
<DialogTitle>{t('num_fmt.title')}</DialogTitle>
|
|
||||||
<DialogContent dividers>
|
|
||||||
<TextField
|
|
||||||
defaultValue={properties.numFmt}
|
|
||||||
label={t('num_fmt.label')}
|
|
||||||
name="format_code"
|
|
||||||
onChange={(event) => setFormatCode(event.target.value)}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => onSubmit(formatCode)}>
|
|
||||||
{t('num_fmt.save')}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default FormatPicker;
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
export function increaseDecimalPlaces(numberFormat: string): string {
|
|
||||||
// FIXME: Should it be done in the Rust? How should it work?
|
|
||||||
// Increase decimal places for existing numbers with decimals
|
|
||||||
const newNumberFormat = numberFormat.replace(/\.0/g, '.00');
|
|
||||||
// If no decimal places declared, add 0.0
|
|
||||||
if (!newNumberFormat.includes('.')) {
|
|
||||||
if (newNumberFormat.includes('0')) {
|
|
||||||
return newNumberFormat.replace(/0/g, '0.0');
|
|
||||||
}
|
|
||||||
if (newNumberFormat.includes('#')) {
|
|
||||||
return newNumberFormat.replace(/#([^#,]|$)/g, '0.0$1');
|
|
||||||
}
|
|
||||||
return '0.0';
|
|
||||||
}
|
|
||||||
return newNumberFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function decreaseDecimalPlaces(numberFormat: string): string {
|
|
||||||
// FIXME: Should it be done in the Rust? How should it work?
|
|
||||||
// Decrease decimal places for existing numbers with decimals
|
|
||||||
let newNumberFormat = numberFormat.replace(/\.0/g, '.');
|
|
||||||
// Fix leftover dots
|
|
||||||
newNumberFormat = newNumberFormat.replace(/0\.([^0]|$)/, '0$1');
|
|
||||||
return newNumberFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum NumberFormats {
|
|
||||||
AUTO = 'general',
|
|
||||||
CURRENCY_EUR = '"€"#,##0.00',
|
|
||||||
CURRENCY_USD = '"$"#,##0.00',
|
|
||||||
CURRENCY_GBP = '"£"#,##0.00',
|
|
||||||
DATE_SHORT = 'dd"/"mm"/"yyyy',
|
|
||||||
DATE_LONG = 'dddd"," mmmm dd"," yyyy',
|
|
||||||
PERCENTAGE = '0.00%',
|
|
||||||
NUMBER = '#,##0.00',
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { Button, styled } from "@mui/material";
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
import { Fx } from "../icons";
|
|
||||||
|
|
||||||
type FormulaBarProps = {
|
|
||||||
cellAddress: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formulaBarHeight = 30;
|
|
||||||
const headerColumnWidth = 30;
|
|
||||||
|
|
||||||
function FormulaBar(properties: FormulaBarProps) {
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<AddressContainer>
|
|
||||||
<CellBarAddress>{properties.cellAddress}</CellBarAddress>
|
|
||||||
<StyledButton>
|
|
||||||
<ChevronDown />
|
|
||||||
</StyledButton>
|
|
||||||
</AddressContainer>
|
|
||||||
<Divider />
|
|
||||||
<FormulaContainer>
|
|
||||||
<FormulaSymbolButton><Fx /></FormulaSymbolButton>
|
|
||||||
<Editor contentEditable="true" spellCheck="false" />
|
|
||||||
</FormulaContainer>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const StyledButton = styled(Button)`
|
|
||||||
width: 15px;
|
|
||||||
min-width: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
color: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
svg {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FormulaSymbolButton = styled(StyledButton)`
|
|
||||||
margin-right: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Divider = styled("div")`
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
width: 1px;
|
|
||||||
height: 20px;
|
|
||||||
margin-left: 16px;
|
|
||||||
margin-right: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FormulaContainer = styled("div")`
|
|
||||||
margin-left: 10px;
|
|
||||||
line-height: 22px;
|
|
||||||
font-weight: normal;
|
|
||||||
width: 100%;
|
|
||||||
height: 22px;
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Container = styled("div")`
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
background: ${(properties): string =>
|
|
||||||
properties.theme.palette.background.default};
|
|
||||||
height: ${formulaBarHeight}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AddressContainer = styled("div")`
|
|
||||||
padding-left: 16px;
|
|
||||||
color: #333;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 11px;
|
|
||||||
display: flex;
|
|
||||||
font-weight: 600;
|
|
||||||
flex-grow: row;
|
|
||||||
min-width: ${headerColumnWidth}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CellBarAddress = styled("div")`
|
|
||||||
width: 100%;
|
|
||||||
text-align: "center";
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Editor = styled("div")`
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0px;
|
|
||||||
border-width: 0px;
|
|
||||||
outline: none;
|
|
||||||
resize: none;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
vertical-align: bottom;
|
|
||||||
overflow: hidden;
|
|
||||||
text-align: left;
|
|
||||||
span {
|
|
||||||
min-width: 1px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default FormulaBar;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { default } from './navigation';
|
|
||||||
export type { NavigationProps } from './navigation';
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { styled } from "@mui/material";
|
|
||||||
import { SheetOptions } from "./types";
|
|
||||||
import Menu from "@mui/material/Menu";
|
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
|
||||||
|
|
||||||
interface SheetListMenuProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
close: () => void;
|
|
||||||
anchorEl: HTMLButtonElement | null;
|
|
||||||
onSheetSelected: (index: number) => void;
|
|
||||||
sheetOptionsList: SheetOptions[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const SheetListMenu = (properties: SheetListMenuProps) => {
|
|
||||||
const { isOpen, close, anchorEl, onSheetSelected, sheetOptionsList } =
|
|
||||||
properties;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledMenu
|
|
||||||
open={isOpen}
|
|
||||||
onClose={close}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sheetOptionsList.map((tab, index) => (
|
|
||||||
<StyledMenuItem
|
|
||||||
key={tab.sheetId}
|
|
||||||
onClick={(): void => onSheetSelected(index)}
|
|
||||||
>
|
|
||||||
<ItemColor style={{ backgroundColor: tab.color }} />
|
|
||||||
<ItemName>{tab.name}</ItemName>
|
|
||||||
</StyledMenuItem>
|
|
||||||
))}
|
|
||||||
</StyledMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledMenu = styled(Menu)({
|
|
||||||
"& .MuiPaper-root": {
|
|
||||||
borderRadius: 8,
|
|
||||||
padding: 4
|
|
||||||
},
|
|
||||||
"& .MuiList-padding": {
|
|
||||||
padding: 0,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledMenuItem = styled(MenuItem)({
|
|
||||||
padding: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
})
|
|
||||||
|
|
||||||
const ItemColor = styled("div")`
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-right: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ItemName = styled("div")`
|
|
||||||
font-size: 13px;
|
|
||||||
color: #333;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default SheetListMenu;
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import { styled } from "@mui/material";
|
|
||||||
import { ChevronLeft, ChevronRight, Menu, Plus } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { SheetOptions } from "./types";
|
|
||||||
import SheetListMenu from "./menus";
|
|
||||||
import Sheet from "./sheet";
|
|
||||||
import { StyledButton } from "../toolbar";
|
|
||||||
|
|
||||||
export interface NavigationProps {
|
|
||||||
sheets: SheetOptions[];
|
|
||||||
selectedIndex: number;
|
|
||||||
onSheetSelected: (index: number) => void;
|
|
||||||
onAddBlankSheet: () => void;
|
|
||||||
onSheetColorChanged: (hex: string) => void;
|
|
||||||
onSheetRenamed: (name: string) => void;
|
|
||||||
onSheetDeleted: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Navigation(props: NavigationProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { onSheetSelected, sheets, selectedIndex } = props;
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<StyledButton title={t("navigation.add_sheet")} $pressed={false}>
|
|
||||||
<Plus />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton onClick={handleClick} title={t("navigation.sheet_list")} $pressed={false}>
|
|
||||||
<Menu />
|
|
||||||
</StyledButton>
|
|
||||||
<Sheets>
|
|
||||||
<SheetInner>
|
|
||||||
{sheets.map((tab, index) => (
|
|
||||||
<Sheet
|
|
||||||
key={tab.sheetId}
|
|
||||||
name={tab.name}
|
|
||||||
color={tab.color}
|
|
||||||
selected={index === selectedIndex}
|
|
||||||
onSelected={() => onSheetSelected(index)}
|
|
||||||
onColorChanged={function (hex: string): void {
|
|
||||||
console.log("Picked:", hex);
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
}}
|
|
||||||
onRenamed={function (name: string): void {
|
|
||||||
console.log("Renamed:", name);
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
}}
|
|
||||||
onDeleted={function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SheetInner>
|
|
||||||
</Sheets>
|
|
||||||
<LeftDivider />
|
|
||||||
<ChevronLeftStyled />
|
|
||||||
<ChevronRightStyled />
|
|
||||||
<RightDivider />
|
|
||||||
<Advert>ironcalc.com</Advert>
|
|
||||||
<SheetListMenu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
isOpen={open}
|
|
||||||
close={handleClose}
|
|
||||||
sheetOptionsList={sheets}
|
|
||||||
onSheetSelected={onSheetSelected}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const ChevronLeftStyled = styled(ChevronLeft)`
|
|
||||||
color: #333333;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ChevronRightStyled = styled(ChevronRight)`
|
|
||||||
color: #333333;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Note I have to specify the font-family in every component that can be considered stand-alone
|
|
||||||
const Container = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
display: flex;
|
|
||||||
height: 40px;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: 12px;
|
|
||||||
font-family: Inter;
|
|
||||||
background-color: #fff;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Sheets = styled("div")`
|
|
||||||
flex-grow: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const SheetInner = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LeftDivider = styled("div")`
|
|
||||||
height: 10px;
|
|
||||||
width: 1px;
|
|
||||||
background-color: #eee;
|
|
||||||
margin: 0px 10px 0px 0px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RightDivider = styled("div")`
|
|
||||||
height: 10px;
|
|
||||||
width: 1px;
|
|
||||||
background-color: #eee;
|
|
||||||
margin: 0px 20px 0px 10px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Advert = styled("div")`
|
|
||||||
color: #f2994a;
|
|
||||||
margin-right: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Navigation;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { Button, Menu, MenuItem, styled } from "@mui/material";
|
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
interface SheetProps {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
selected: boolean;
|
|
||||||
onSelected: () => void;
|
|
||||||
onColorChanged: (hex: string) => void;
|
|
||||||
onRenamed: (name: string) => void;
|
|
||||||
onDeleted: () => void;
|
|
||||||
}
|
|
||||||
function Sheet(props: SheetProps) {
|
|
||||||
const { name, color, selected, onSelected } = props;
|
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
const handleOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Wrapper
|
|
||||||
style={{ borderBottomColor: color, fontWeight: selected ? 600 : 400 }}
|
|
||||||
onClick={onSelected}
|
|
||||||
>
|
|
||||||
<Name>{name}</Name>
|
|
||||||
<StyledButton onClick={handleOpen}>
|
|
||||||
<ChevronDown />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledMenu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={open}
|
|
||||||
onClose={handleClose}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: 6,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem>Rename</MenuItem>
|
|
||||||
<MenuItem>Change Color</MenuItem>
|
|
||||||
<MenuItem>Delete</MenuItem>
|
|
||||||
</StyledMenu>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const StyledMenu = styled(Menu)``;
|
|
||||||
|
|
||||||
const StyledButton = styled(Button)`
|
|
||||||
width: 15px;
|
|
||||||
height: 24px;
|
|
||||||
min-width: 0px;
|
|
||||||
padding: 0px;
|
|
||||||
color: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
svg {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Wrapper = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
margin-left: 20px;
|
|
||||||
border-bottom: 3px solid;
|
|
||||||
border-top: 3px solid white;
|
|
||||||
line-height: 34px;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Name = styled("div")`
|
|
||||||
font-size: 12px;
|
|
||||||
margin-right: 5px;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Sheet;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export interface SheetOptions {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
sheetId: number;
|
|
||||||
}
|
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
import {
|
|
||||||
AlignCenter,
|
|
||||||
AlignLeft,
|
|
||||||
AlignRight,
|
|
||||||
Bold,
|
|
||||||
ChevronDown,
|
|
||||||
Euro,
|
|
||||||
Italic,
|
|
||||||
PaintBucket,
|
|
||||||
Paintbrush2,
|
|
||||||
Percent,
|
|
||||||
Redo2,
|
|
||||||
Strikethrough,
|
|
||||||
Underline,
|
|
||||||
Undo2,
|
|
||||||
Grid2X2,
|
|
||||||
Type,
|
|
||||||
ArrowDownToLine,
|
|
||||||
ArrowUpToLine,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import ColorPicker from "./colorPicker";
|
|
||||||
import BorderPicker from "./borderPicker";
|
|
||||||
import {
|
|
||||||
ArrowMiddleFromLine,
|
|
||||||
DecimalPlacesDecreaseIcon,
|
|
||||||
DecimalPlacesIncreaseIcon,
|
|
||||||
} from "../icons";
|
|
||||||
import {
|
|
||||||
NumberFormats,
|
|
||||||
decreaseDecimalPlaces,
|
|
||||||
increaseDecimalPlaces,
|
|
||||||
} from "./formatUtil";
|
|
||||||
import FormatMenu from "./formatMenu";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import { theme } from "../theme";
|
|
||||||
import {
|
|
||||||
BorderOptions,
|
|
||||||
HorizontalAlignment,
|
|
||||||
VerticalAlignment,
|
|
||||||
} from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
type ToolbarProperties = {
|
|
||||||
canUndo: boolean;
|
|
||||||
canRedo: boolean;
|
|
||||||
onRedo: () => void;
|
|
||||||
onUndo: () => void;
|
|
||||||
onToggleUnderline: (u: boolean) => void;
|
|
||||||
onToggleBold: (v: boolean) => void;
|
|
||||||
onToggleItalic: (v: boolean) => void;
|
|
||||||
onToggleStrike: (v: boolean) => void;
|
|
||||||
onToggleHorizontalAlign: (v: string) => void;
|
|
||||||
onToggleVerticalAlign: (v: string) => void;
|
|
||||||
onCopyStyles: () => void;
|
|
||||||
onTextColorPicked: (hex: string) => void;
|
|
||||||
onFillColorPicked: (hex: string) => void;
|
|
||||||
onNumberFormatPicked: (numberFmt: string) => void;
|
|
||||||
onBorderChanged: (border: BorderOptions) => void;
|
|
||||||
fillColor: string;
|
|
||||||
fontColor: string;
|
|
||||||
bold: boolean;
|
|
||||||
underline: boolean;
|
|
||||||
italic: boolean;
|
|
||||||
strike: boolean;
|
|
||||||
horizontalAlign: HorizontalAlignment;
|
|
||||||
verticalAlign: VerticalAlignment;
|
|
||||||
canEdit: boolean;
|
|
||||||
numFmt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function Toolbar(properties: ToolbarProperties) {
|
|
||||||
const [fontColorPickerOpen, setFontColorPickerOpen] = useState(false);
|
|
||||||
const [fillColorPickerOpen, setFillColorPickerOpen] = useState(false);
|
|
||||||
const [borderPickerOpen, setBorderPickerOpen] = useState(false);
|
|
||||||
|
|
||||||
const fontColorButton = useRef(null);
|
|
||||||
const fillColorButton = useRef(null);
|
|
||||||
const borderButton = useRef(null);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { canEdit } = properties;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToolbarContainer>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={properties.onUndo}
|
|
||||||
disabled={!properties.canUndo}
|
|
||||||
title={t("toolbar.undo")}
|
|
||||||
>
|
|
||||||
<Undo2 />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={properties.onRedo}
|
|
||||||
disabled={!properties.canRedo}
|
|
||||||
title={t("toolbar.redo")}
|
|
||||||
>
|
|
||||||
<Redo2 />
|
|
||||||
</StyledButton>
|
|
||||||
<Divider />
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={properties.onCopyStyles}
|
|
||||||
title={t("toolbar.copy_styles")}
|
|
||||||
>
|
|
||||||
<Paintbrush2 />
|
|
||||||
</StyledButton>
|
|
||||||
<Divider />
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={(): void => {
|
|
||||||
properties.onNumberFormatPicked(NumberFormats.CURRENCY_EUR);
|
|
||||||
}}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.euro")}
|
|
||||||
>
|
|
||||||
<Euro />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={(): void => {
|
|
||||||
properties.onNumberFormatPicked(NumberFormats.PERCENTAGE);
|
|
||||||
}}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.percentage")}
|
|
||||||
>
|
|
||||||
<Percent />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={(): void => {
|
|
||||||
properties.onNumberFormatPicked(
|
|
||||||
decreaseDecimalPlaces(properties.numFmt)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.decimal_places_decrease")}
|
|
||||||
>
|
|
||||||
<DecimalPlacesDecreaseIcon />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={(): void => {
|
|
||||||
properties.onNumberFormatPicked(
|
|
||||||
increaseDecimalPlaces(properties.numFmt)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.decimal_places_increase")}
|
|
||||||
>
|
|
||||||
<DecimalPlacesIncreaseIcon />
|
|
||||||
</StyledButton>
|
|
||||||
<FormatMenu
|
|
||||||
numFmt={properties.numFmt}
|
|
||||||
onChange={(numberFmt): void => {
|
|
||||||
properties.onNumberFormatPicked(numberFmt);
|
|
||||||
}}
|
|
||||||
onExited={(): void => {}}
|
|
||||||
anchorOrigin={{
|
|
||||||
horizontal: 20, // Aligning the menu to the middle of FormatButton
|
|
||||||
vertical: "bottom",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.format_number")}
|
|
||||||
sx={{
|
|
||||||
width: "40px", // Keep in sync with anchorOrigin in FormatMenu above
|
|
||||||
fontSize: "13px",
|
|
||||||
fontWeight: 400,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{"123"}
|
|
||||||
<ChevronDown />
|
|
||||||
</StyledButton>
|
|
||||||
</FormatMenu>
|
|
||||||
<Divider />
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.bold}
|
|
||||||
onClick={() => properties.onToggleBold(!properties.bold)}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.bold")}
|
|
||||||
>
|
|
||||||
<Bold />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.italic}
|
|
||||||
onClick={() => properties.onToggleItalic(!properties.italic)}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.italic")}
|
|
||||||
>
|
|
||||||
<Italic />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.underline}
|
|
||||||
onClick={() => properties.onToggleUnderline(!properties.underline)}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.underline")}
|
|
||||||
>
|
|
||||||
<Underline />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.strike}
|
|
||||||
onClick={() => properties.onToggleStrike(!properties.strike)}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.strike_trough")}
|
|
||||||
>
|
|
||||||
<Strikethrough />
|
|
||||||
</StyledButton>
|
|
||||||
<Divider />
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.font_color")}
|
|
||||||
ref={fontColorButton}
|
|
||||||
$underlinedColor={properties.fontColor}
|
|
||||||
onClick={() => setFontColorPickerOpen(true)}
|
|
||||||
>
|
|
||||||
<Type />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.fill_color")}
|
|
||||||
ref={fillColorButton}
|
|
||||||
$underlinedColor={properties.fillColor}
|
|
||||||
onClick={() => setFillColorPickerOpen(true)}
|
|
||||||
>
|
|
||||||
<PaintBucket />
|
|
||||||
</StyledButton>
|
|
||||||
<Divider />
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.horizontalAlign === "left"}
|
|
||||||
onClick={() =>
|
|
||||||
properties.onToggleHorizontalAlign(
|
|
||||||
properties.horizontalAlign === "left" ? "general" : "left"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.align_left")}
|
|
||||||
>
|
|
||||||
<AlignLeft />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.horizontalAlign === "center"}
|
|
||||||
onClick={() =>
|
|
||||||
properties.onToggleHorizontalAlign(
|
|
||||||
properties.horizontalAlign === "center" ? "general" : "center"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.align_center")}
|
|
||||||
>
|
|
||||||
<AlignCenter />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.horizontalAlign === "right"}
|
|
||||||
onClick={() =>
|
|
||||||
properties.onToggleHorizontalAlign(
|
|
||||||
properties.horizontalAlign === "right" ? "general" : "right"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.align_right")}
|
|
||||||
>
|
|
||||||
<AlignRight />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.verticalAlign === "top"}
|
|
||||||
onClick={() =>
|
|
||||||
properties.onToggleVerticalAlign(
|
|
||||||
properties.verticalAlign === "top" ? "bottom" : "top"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.vertical_align_top")}
|
|
||||||
>
|
|
||||||
<ArrowUpToLine />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.verticalAlign === "center"}
|
|
||||||
onClick={() =>
|
|
||||||
properties.onToggleVerticalAlign(
|
|
||||||
properties.verticalAlign === "center" ? "bottom" : "center"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.vertical_align_center")}
|
|
||||||
>
|
|
||||||
<ArrowMiddleFromLine />
|
|
||||||
</StyledButton>
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={properties.verticalAlign === "bottom"}
|
|
||||||
onClick={() => properties.onToggleVerticalAlign("bottom")}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.vertical_align_bottom")}
|
|
||||||
>
|
|
||||||
<ArrowDownToLine />
|
|
||||||
</StyledButton>
|
|
||||||
<Divider />
|
|
||||||
<StyledButton
|
|
||||||
type="button"
|
|
||||||
$pressed={false}
|
|
||||||
onClick={() => setBorderPickerOpen(true)}
|
|
||||||
ref={borderButton}
|
|
||||||
disabled={!canEdit}
|
|
||||||
title={t("toolbar.borders")}
|
|
||||||
>
|
|
||||||
<Grid2X2 />
|
|
||||||
</StyledButton>
|
|
||||||
<ColorPicker
|
|
||||||
color={properties.fontColor}
|
|
||||||
onChange={(color): void => {
|
|
||||||
properties.onTextColorPicked(color);
|
|
||||||
setFontColorPickerOpen(false);
|
|
||||||
}}
|
|
||||||
anchorEl={fontColorButton}
|
|
||||||
open={fontColorPickerOpen}
|
|
||||||
/>
|
|
||||||
<ColorPicker
|
|
||||||
color={properties.fillColor}
|
|
||||||
onChange={(color): void => {
|
|
||||||
properties.onFillColorPicked(color);
|
|
||||||
setFillColorPickerOpen(false);
|
|
||||||
}}
|
|
||||||
anchorEl={fillColorButton}
|
|
||||||
open={fillColorPickerOpen}
|
|
||||||
/>
|
|
||||||
<BorderPicker
|
|
||||||
onChange={(border): void => {
|
|
||||||
properties.onBorderChanged(border);
|
|
||||||
setBorderPickerOpen(false);
|
|
||||||
}}
|
|
||||||
anchorEl={borderButton}
|
|
||||||
open={borderPickerOpen}
|
|
||||||
/>
|
|
||||||
</ToolbarContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const toolbarHeight = 40;
|
|
||||||
|
|
||||||
const ToolbarContainer = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
background: ${({ theme }) => theme.palette.background.paper};
|
|
||||||
height: ${toolbarHeight}px;
|
|
||||||
line-height: ${toolbarHeight}px;
|
|
||||||
border-bottom: 1px solid ${({}) => theme.palette.grey["600"]};
|
|
||||||
font-family: Inter;
|
|
||||||
border-radius: 4px 4px 0px 0px;
|
|
||||||
overflow-x: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
|
||||||
export const StyledButton = styled("button")<TypeButtonProperties>(
|
|
||||||
({ disabled, $pressed, $underlinedColor }) => {
|
|
||||||
let result: Record<string, any> = {
|
|
||||||
width: "24px",
|
|
||||||
height: "24px",
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
fontSize: "26px",
|
|
||||||
border: "0px solid #fff",
|
|
||||||
borderRadius: "2px",
|
|
||||||
marginRight: "5px",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
cursor: "pointer",
|
|
||||||
backgroundColor: "white",
|
|
||||||
padding: "0px",
|
|
||||||
};
|
|
||||||
if (disabled) {
|
|
||||||
result.color = theme.palette.grey["600"];
|
|
||||||
result.cursor = "default";
|
|
||||||
} else {
|
|
||||||
result.borderTop = $underlinedColor ? "3px solid #FFF" : "none";
|
|
||||||
result.borderBottom = $underlinedColor
|
|
||||||
? `3px solid ${$underlinedColor}`
|
|
||||||
: "none";
|
|
||||||
(result.color = "#21243A"), //theme.palette.text.primary;
|
|
||||||
(result.backgroundColor = $pressed
|
|
||||||
? theme.palette.grey["600"]
|
|
||||||
: "#FFF");
|
|
||||||
result["&:hover"] = {
|
|
||||||
backgroundColor: "#F1F2F8",
|
|
||||||
borderTopColor: "#F1F2F8",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
result["svg"] = {
|
|
||||||
width: "16px",
|
|
||||||
height: "16px",
|
|
||||||
};
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const Divider = styled("div")({
|
|
||||||
width: "0px",
|
|
||||||
height: "10px",
|
|
||||||
borderLeft: "1px solid #D3D6E9",
|
|
||||||
marginLeft: "5px",
|
|
||||||
marginRight: "10px",
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Toolbar;
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import { useCallback, KeyboardEvent, RefObject } from 'react';
|
|
||||||
import { isEditingKey, isNavigationKey, NavigationKey } from './WorksheetCanvas/util';
|
|
||||||
|
|
||||||
export enum Border {
|
|
||||||
Top = 'top',
|
|
||||||
Bottom = 'bottom',
|
|
||||||
Right = 'right',
|
|
||||||
Left = 'left',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
onCellsDeleted: () => void;
|
|
||||||
onExpandAreaSelectedKeyboard: (key: 'ArrowRight' | 'ArrowLeft' | 'ArrowUp' | 'ArrowDown') => void;
|
|
||||||
onEditKeyPressStart: (initText: string) => void;
|
|
||||||
onCellEditStart: () => void;
|
|
||||||
onBold: () => void;
|
|
||||||
onItalic: () => void;
|
|
||||||
onUnderline: () => void;
|
|
||||||
onNavigationToEdge: (direction: NavigationKey) => void;
|
|
||||||
onPageDown: () => void;
|
|
||||||
onPageUp: () => void;
|
|
||||||
onArrowDown: () => void;
|
|
||||||
onArrowUp: () => void;
|
|
||||||
onArrowLeft: () => void;
|
|
||||||
onArrowRight: () => void;
|
|
||||||
onKeyHome: () => void;
|
|
||||||
onKeyEnd: () => void;
|
|
||||||
onUndo: () => void;
|
|
||||||
onRedo: () => void;
|
|
||||||
root: RefObject<HTMLDivElement>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useKeyboardNavigation = (options: Options): { onKeyDown: (event: KeyboardEvent) => void } => {
|
|
||||||
const onKeyDown = useCallback(
|
|
||||||
(event: KeyboardEvent) => {
|
|
||||||
const { key } = event;
|
|
||||||
const { root } = options;
|
|
||||||
// Silence the linter
|
|
||||||
if (!root.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.target !== root.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.metaKey || event.ctrlKey) {
|
|
||||||
switch (key) {
|
|
||||||
case 'z': {
|
|
||||||
options.onUndo();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'y': {
|
|
||||||
options.onRedo();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'b': {
|
|
||||||
options.onBold();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'i': {
|
|
||||||
options.onItalic();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'u': {
|
|
||||||
options.onUnderline();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
if (isNavigationKey(key)) {
|
|
||||||
options.onNavigationToEdge(key);
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (key === 'F2') {
|
|
||||||
options.onCellEditStart();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isEditingKey(key) || key === 'Backspace') {
|
|
||||||
const initText = key === 'Backspace' ? '' : key;
|
|
||||||
options.onEditKeyPressStart(initText);
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Worksheet Navigation
|
|
||||||
if (event.shiftKey) {
|
|
||||||
if (
|
|
||||||
key === 'ArrowRight' ||
|
|
||||||
key === 'ArrowLeft' ||
|
|
||||||
key === 'ArrowUp' ||
|
|
||||||
key === 'ArrowDown'
|
|
||||||
) {
|
|
||||||
options.onExpandAreaSelectedKeyboard(key);
|
|
||||||
} else if (key === 'Tab') {
|
|
||||||
options.onArrowLeft();
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (key) {
|
|
||||||
case 'ArrowRight':
|
|
||||||
case 'Tab': {
|
|
||||||
options.onArrowRight();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ArrowLeft': {
|
|
||||||
options.onArrowLeft();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ArrowDown':
|
|
||||||
case 'Enter': {
|
|
||||||
options.onArrowDown();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'ArrowUp': {
|
|
||||||
options.onArrowUp();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'End': {
|
|
||||||
options.onKeyEnd();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Home': {
|
|
||||||
options.onKeyHome();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'Delete': {
|
|
||||||
options.onCellsDeleted();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'PageDown': {
|
|
||||||
options.onPageDown();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'PageUp': {
|
|
||||||
options.onPageUp();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// No default
|
|
||||||
}
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
},
|
|
||||||
[options],
|
|
||||||
);
|
|
||||||
return { onKeyDown };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useKeyboardNavigation;
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
import { useCallback, RefObject, PointerEvent, useRef } from 'react';
|
|
||||||
import WorksheetCanvas, { headerColumnWidth, headerRowHeight } from './WorksheetCanvas/worksheetCanvas';
|
|
||||||
import { Cell } from './WorksheetCanvas/util';
|
|
||||||
|
|
||||||
interface PointerSettings {
|
|
||||||
canvasElement: RefObject<HTMLCanvasElement>;
|
|
||||||
worksheetCanvas: RefObject<WorksheetCanvas | null>;
|
|
||||||
worksheetElement: RefObject<HTMLDivElement>;
|
|
||||||
// rowContextMenuAnchorElement: RefObject<HTMLDivElement>;
|
|
||||||
// columnContextMenuAnchorElement: RefObject<HTMLDivElement>;
|
|
||||||
onCellSelected: (cell: Cell, event: React.MouseEvent) => void;
|
|
||||||
onAreaSelecting: (cell: Cell) => void;
|
|
||||||
onExtendToCell: (cell: Cell) => void;
|
|
||||||
onExtendToEnd: () => void;
|
|
||||||
// onRowContextMenu: (row: number) => void;
|
|
||||||
// onColumnContextMenu: (column: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PointerEvents {
|
|
||||||
onPointerDown: (event: PointerEvent) => void;
|
|
||||||
onPointerMove: (event: PointerEvent) => void;
|
|
||||||
onPointerUp: (event: PointerEvent) => void;
|
|
||||||
onPointerHandleDown: (event: PointerEvent) => void;
|
|
||||||
// onContextMenu: (event: React.MouseEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const usePointer = (options: PointerSettings): PointerEvents => {
|
|
||||||
const isSelecting = useRef(false);
|
|
||||||
const isExtending = useRef(false);
|
|
||||||
|
|
||||||
// const onContextMenu = useCallback(
|
|
||||||
// (event: React.MouseEvent): void => {
|
|
||||||
// let x = event.clientX;
|
|
||||||
// let y = event.clientY;
|
|
||||||
// const {
|
|
||||||
// canvasElement,
|
|
||||||
// worksheetElement,
|
|
||||||
// worksheetCanvas,
|
|
||||||
// onRowContextMenu,
|
|
||||||
// rowContextMenuAnchorElement,
|
|
||||||
// onColumnContextMenu,
|
|
||||||
// columnContextMenuAnchorElement,
|
|
||||||
// } = options;
|
|
||||||
// const worksheet = worksheetCanvas.current;
|
|
||||||
// const canvas = canvasElement.current;
|
|
||||||
// const worksheetWrapper = worksheetElement.current;
|
|
||||||
// // Silence the linter
|
|
||||||
// if (!canvas || !worksheet || !worksheetWrapper) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// const canvasRect = canvas.getBoundingClientRect();
|
|
||||||
// x -= canvasRect.x;
|
|
||||||
// y -= canvasRect.y;
|
|
||||||
// const menuAnchorOffsetY = 10;
|
|
||||||
// if (x > 0 && x < headerColumnWidth && y > headerRowHeight && y < canvasRect.height) {
|
|
||||||
// // Click on a row number
|
|
||||||
// const cell = worksheet.getCellByCoordinates(headerColumnWidth, y);
|
|
||||||
// if (cell) {
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// if (rowContextMenuAnchorElement.current) {
|
|
||||||
// const scrollPosition = worksheet.getScrollPosition();
|
|
||||||
// rowContextMenuAnchorElement.current.style.left = `${x + scrollPosition.left}px`;
|
|
||||||
// rowContextMenuAnchorElement.current.style.top = `${
|
|
||||||
// y + scrollPosition.top + menuAnchorOffsetY
|
|
||||||
// }px`;
|
|
||||||
// }
|
|
||||||
// options.onPointerDownAtCell(cell, event);
|
|
||||||
// onRowContextMenu(cell.row);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if (x > headerColumnWidth && x < canvas.width && y > 0 && y < headerRowHeight) {
|
|
||||||
// // Click on a column number
|
|
||||||
// const cell = worksheet.getCellByCoordinates(x, headerRowHeight);
|
|
||||||
// if (cell) {
|
|
||||||
// event.preventDefault();
|
|
||||||
// event.stopPropagation();
|
|
||||||
// if (columnContextMenuAnchorElement.current) {
|
|
||||||
// const scrollPosition = worksheet.getScrollPosition();
|
|
||||||
// columnContextMenuAnchorElement.current.style.left = `${x + scrollPosition.left}px`;
|
|
||||||
// columnContextMenuAnchorElement.current.style.top = `${
|
|
||||||
// y + scrollPosition.top + menuAnchorOffsetY
|
|
||||||
// }px`;
|
|
||||||
// }
|
|
||||||
// options.onPointerDownAtCell(cell, event);
|
|
||||||
// onColumnContextMenu(cell.column);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// [options],
|
|
||||||
// );
|
|
||||||
|
|
||||||
const onPointerMove = useCallback(
|
|
||||||
(event: PointerEvent): void => {
|
|
||||||
// Range selections are disabled on non-mouse devices. Use touch move only
|
|
||||||
// to scroll for now.
|
|
||||||
if (event.pointerType !== 'mouse') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelecting.current) {
|
|
||||||
const { canvasElement, worksheetCanvas } = options;
|
|
||||||
const canvas = canvasElement.current;
|
|
||||||
const worksheet = worksheetCanvas.current;
|
|
||||||
// Silence the linter
|
|
||||||
if (!worksheet || !canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let x = event.clientX;
|
|
||||||
let y = event.clientY;
|
|
||||||
const canvasRect = canvas.getBoundingClientRect();
|
|
||||||
x -= canvasRect.x;
|
|
||||||
y -= canvasRect.y;
|
|
||||||
const cell = worksheet.getCellByCoordinates(x, y);
|
|
||||||
if (cell) {
|
|
||||||
options.onAreaSelecting(cell);
|
|
||||||
}
|
|
||||||
} else if (isExtending.current) {
|
|
||||||
const { canvasElement, worksheetCanvas } = options;
|
|
||||||
const canvas = canvasElement.current;
|
|
||||||
const worksheet = worksheetCanvas.current;
|
|
||||||
// Silence the linter
|
|
||||||
if (!worksheet || !canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let x = event.clientX;
|
|
||||||
let y = event.clientY;
|
|
||||||
const canvasRect = canvas.getBoundingClientRect();
|
|
||||||
x -= canvasRect.x;
|
|
||||||
y -= canvasRect.y;
|
|
||||||
const cell = worksheet.getCellByCoordinates(x, y);
|
|
||||||
if (!cell) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
options.onExtendToCell(cell);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[options],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerUp = useCallback(
|
|
||||||
(event: PointerEvent): void => {
|
|
||||||
if (isSelecting.current) {
|
|
||||||
const { worksheetElement } = options;
|
|
||||||
isSelecting.current = false;
|
|
||||||
worksheetElement.current?.releasePointerCapture(event.pointerId);
|
|
||||||
} else if (isExtending.current) {
|
|
||||||
const { worksheetElement } = options;
|
|
||||||
isExtending.current = false;
|
|
||||||
worksheetElement.current?.releasePointerCapture(event.pointerId);
|
|
||||||
options.onExtendToEnd();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[options],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerDown = useCallback(
|
|
||||||
(event: PointerEvent) => {
|
|
||||||
let x = event.clientX;
|
|
||||||
let y = event.clientY;
|
|
||||||
const { canvasElement, worksheetElement, worksheetCanvas } = options;
|
|
||||||
const worksheet = worksheetCanvas.current;
|
|
||||||
const canvas = canvasElement.current;
|
|
||||||
const worksheetWrapper = worksheetElement.current;
|
|
||||||
// Silence the linter
|
|
||||||
if (!canvas || !worksheet || !worksheetWrapper) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const canvasRect = canvas.getBoundingClientRect();
|
|
||||||
x -= canvasRect.x;
|
|
||||||
y -= canvasRect.y;
|
|
||||||
// Makes sure is in the sheet area
|
|
||||||
if (
|
|
||||||
x > canvasRect.width ||
|
|
||||||
x < headerColumnWidth ||
|
|
||||||
y < headerRowHeight ||
|
|
||||||
y > canvasRect.height
|
|
||||||
) {
|
|
||||||
if (x > 0 && x < headerColumnWidth && y > headerRowHeight && y < canvasRect.height) {
|
|
||||||
// Click on a row number
|
|
||||||
const cell = worksheet.getCellByCoordinates(headerColumnWidth, y);
|
|
||||||
if (cell) {
|
|
||||||
// TODO
|
|
||||||
// Row selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const cell = worksheet.getCellByCoordinates(x, y);
|
|
||||||
if (cell) {
|
|
||||||
options.onCellSelected(cell, event);
|
|
||||||
isSelecting.current = true;
|
|
||||||
worksheetWrapper.setPointerCapture(event.pointerId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[options],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onPointerHandleDown = useCallback(
|
|
||||||
(event: PointerEvent) => {
|
|
||||||
const worksheetWrapper = options.worksheetElement.current;
|
|
||||||
// Silence the linter
|
|
||||||
if (!worksheetWrapper) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isExtending.current = true;
|
|
||||||
worksheetWrapper.setPointerCapture(event.pointerId);
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
},
|
|
||||||
[options],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
onPointerDown,
|
|
||||||
onPointerMove,
|
|
||||||
onPointerUp,
|
|
||||||
onPointerHandleDown,
|
|
||||||
// onContextMenu,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default usePointer;
|
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
import Toolbar from "./toolbar";
|
|
||||||
import FormulaBar from "./formulabar";
|
|
||||||
import Navigation from "./navigation/navigation";
|
|
||||||
import Worksheet from "./worksheet";
|
|
||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import useKeyboardNavigation from "./useKeyboardNavigation";
|
|
||||||
import { NavigationKey, getCellAddress } from "./WorksheetCanvas/util";
|
|
||||||
import { LAST_COLUMN, LAST_ROW } from "./WorksheetCanvas/constants";
|
|
||||||
import { WorkbookState } from "./workbookState";
|
|
||||||
import { BorderOptions, Model, WorksheetProperties } from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|
||||||
const { model, workbookState } = props;
|
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [_redrawId, setRedrawId] = useState(0);
|
|
||||||
const info = model
|
|
||||||
.getWorksheetsProperties()
|
|
||||||
.map(({ name, color, sheet_id }: WorksheetProperties) => {
|
|
||||||
return { name, color: color ? color : "#FFF", sheetId: sheet_id };
|
|
||||||
});
|
|
||||||
|
|
||||||
const onRedo = () => {
|
|
||||||
model.redo();
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUndo = () => {
|
|
||||||
model.undo();
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateRangeStyle = (stylePath: string, value: string) => {
|
|
||||||
const area = {
|
|
||||||
sheet: workbookState.getSelectedSheet(),
|
|
||||||
...workbookState.getSelectedArea(),
|
|
||||||
};
|
|
||||||
const range = {
|
|
||||||
sheet: area.sheet,
|
|
||||||
row: area.rowStart,
|
|
||||||
column: area.columnStart,
|
|
||||||
width: area.columnEnd - area.columnStart + 1,
|
|
||||||
height: area.rowEnd - area.rowStart + 1,
|
|
||||||
};
|
|
||||||
model.updateRangeStyle(range, stylePath, value);
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleUnderline = (value: boolean) => {
|
|
||||||
updateRangeStyle("font.u", `${value}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleItalic = (value: boolean) => {
|
|
||||||
updateRangeStyle("font.i", `${value}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleBold = (value: boolean) => {
|
|
||||||
updateRangeStyle("font.b", `${value}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleStrike = (value: boolean) => {
|
|
||||||
updateRangeStyle("font.strike", `${value}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleHorizontalAlign = (value: string) => {
|
|
||||||
updateRangeStyle("alignment.horizontal", value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleVerticalAlign = (value: string) => {
|
|
||||||
updateRangeStyle("alignment.vertical", value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onTextColorPicked = (hex: string) => {
|
|
||||||
updateRangeStyle("font.color", hex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFillColorPicked = (hex: string) => {
|
|
||||||
updateRangeStyle("fill.fg_color", hex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onNumberFormatPicked = (numberFmt: string) => {
|
|
||||||
updateRangeStyle("num_fmt", numberFmt);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCopyStyles = () => {
|
|
||||||
const area = {
|
|
||||||
sheet: workbookState.getSelectedSheet(),
|
|
||||||
...workbookState.getSelectedArea(),
|
|
||||||
};
|
|
||||||
const styles = [];
|
|
||||||
for (let row = area.rowStart; row < area.rowEnd; row++) {
|
|
||||||
const styleRow = [];
|
|
||||||
for (let column = area.columnStart; column < area.columnEnd; column++) {
|
|
||||||
styleRow.push(model.getCellStyle(area.sheet, row, column));
|
|
||||||
}
|
|
||||||
styles.push(styleRow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const { onKeyDown } = useKeyboardNavigation({
|
|
||||||
onCellsDeleted: function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onExpandAreaSelectedKeyboard: function (
|
|
||||||
key: "ArrowRight" | "ArrowLeft" | "ArrowUp" | "ArrowDown"
|
|
||||||
): void {
|
|
||||||
console.log(key);
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onEditKeyPressStart: function (initText: string): void {
|
|
||||||
console.log(initText);
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onCellEditStart: function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onBold: () => {
|
|
||||||
let sheet = workbookState.getSelectedSheet();
|
|
||||||
let { row, column } = workbookState.getSelectedCell();
|
|
||||||
let value = !model.getCellStyle(sheet, row, column).font.b;
|
|
||||||
onToggleBold(!value);
|
|
||||||
},
|
|
||||||
onItalic: () => {
|
|
||||||
let sheet = workbookState.getSelectedSheet();
|
|
||||||
let { row, column } = workbookState.getSelectedCell();
|
|
||||||
let value = !model.getCellStyle(sheet, row, column).font.i;
|
|
||||||
onToggleItalic(!value);
|
|
||||||
},
|
|
||||||
onUnderline: () => {
|
|
||||||
let sheet = workbookState.getSelectedSheet();
|
|
||||||
let { row, column } = workbookState.getSelectedCell();
|
|
||||||
let value = !model.getCellStyle(sheet, row, column).font.u;
|
|
||||||
onToggleUnderline(!value);
|
|
||||||
},
|
|
||||||
onNavigationToEdge: function (direction: NavigationKey): void {
|
|
||||||
console.log(direction);
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onPageDown: function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onPageUp: function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onArrowDown: function (): void {
|
|
||||||
const cell = workbookState.getSelectedCell();
|
|
||||||
const row = cell.row + 1;
|
|
||||||
if (row > LAST_ROW) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
workbookState.selectCell({ row, column: cell.column });
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
},
|
|
||||||
onArrowUp: function (): void {
|
|
||||||
const cell = workbookState.getSelectedCell();
|
|
||||||
const row = cell.row - 1;
|
|
||||||
if (row < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
workbookState.selectCell({ row, column: cell.column });
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
},
|
|
||||||
onArrowLeft: function (): void {
|
|
||||||
const cell = workbookState.getSelectedCell();
|
|
||||||
const column = cell.column - 1;
|
|
||||||
if (column < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
workbookState.selectCell({ row: cell.row, column });
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
},
|
|
||||||
onArrowRight: function (): void {
|
|
||||||
const cell = workbookState.getSelectedCell();
|
|
||||||
const column = cell.column + 1;
|
|
||||||
if (column > LAST_COLUMN) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
workbookState.selectCell({ row: cell.row, column });
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
},
|
|
||||||
onKeyHome: function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onKeyEnd: function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
onUndo: function (): void {
|
|
||||||
model.undo();
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
},
|
|
||||||
onRedo: function (): void {
|
|
||||||
model.redo();
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
},
|
|
||||||
root: rootRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!rootRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rootRef.current.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
const cellAddress = getCellAddress(
|
|
||||||
workbookState.getSelectedArea(),
|
|
||||||
workbookState.getSelectedCell()
|
|
||||||
);
|
|
||||||
|
|
||||||
const sheet = workbookState.getSelectedSheet();
|
|
||||||
const { row, column } = workbookState.getSelectedCell();
|
|
||||||
|
|
||||||
const style = model.getCellStyle(sheet, row, column);
|
|
||||||
console.log("data", sheet, row, column, style);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container ref={rootRef} onKeyDown={onKeyDown} tabIndex={0}>
|
|
||||||
<Toolbar
|
|
||||||
canUndo={model.canUndo()}
|
|
||||||
canRedo={model.canRedo()}
|
|
||||||
onRedo={onRedo}
|
|
||||||
onUndo={onUndo}
|
|
||||||
onToggleUnderline={onToggleUnderline}
|
|
||||||
onToggleBold={onToggleBold}
|
|
||||||
onToggleItalic={onToggleItalic}
|
|
||||||
onToggleStrike={onToggleStrike}
|
|
||||||
onToggleHorizontalAlign={onToggleHorizontalAlign}
|
|
||||||
onToggleVerticalAlign={onToggleVerticalAlign}
|
|
||||||
onCopyStyles={onCopyStyles}
|
|
||||||
onTextColorPicked={onTextColorPicked}
|
|
||||||
onFillColorPicked={onFillColorPicked}
|
|
||||||
onNumberFormatPicked={onNumberFormatPicked}
|
|
||||||
onBorderChanged={function (_border: BorderOptions): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
}}
|
|
||||||
fillColor={style.fill.fg_color || "#FFF"}
|
|
||||||
fontColor={style.font.color}
|
|
||||||
bold={style.font.b}
|
|
||||||
underline={style.font.u}
|
|
||||||
italic={style.font.i}
|
|
||||||
strike={style.font.strike}
|
|
||||||
horizontalAlign={
|
|
||||||
style.alignment ? style.alignment.horizontal : "general"
|
|
||||||
}
|
|
||||||
verticalAlign={style.alignment ? style.alignment.vertical : "center"}
|
|
||||||
canEdit={true}
|
|
||||||
numFmt={""}
|
|
||||||
/>
|
|
||||||
<FormulaBar cellAddress={cellAddress} />
|
|
||||||
<Worksheet
|
|
||||||
model={model}
|
|
||||||
workbookState={workbookState}
|
|
||||||
refresh={(): void => {
|
|
||||||
setRedrawId((id) => id + 1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Navigation
|
|
||||||
sheets={info}
|
|
||||||
selectedIndex={workbookState.getSelectedSheet()}
|
|
||||||
onSheetSelected={function (sheet: number): void {
|
|
||||||
workbookState.setSelectedSheet(sheet);
|
|
||||||
setRedrawId((value) => value + 1);
|
|
||||||
}}
|
|
||||||
onAddBlankSheet={function (): void {
|
|
||||||
model.newSheet();
|
|
||||||
}}
|
|
||||||
onSheetColorChanged={function (hex: string): void {
|
|
||||||
console.log(hex);
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
}}
|
|
||||||
onSheetRenamed={function (name: string): void {
|
|
||||||
console.log(name);
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
}}
|
|
||||||
onSheetDeleted={function (): void {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Container = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Workbook;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
export interface Cell {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Scroll {
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FocusType = "cell" | "formula-bar";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In Excel there are two "modes" of editing
|
|
||||||
* * `init`: When you start typing in a cell. In this mode arrow keys will move away from the cell
|
|
||||||
* * `edit`: If you double click on a cell or click in the cell while editing.
|
|
||||||
* In this mode arrow keys will move within the cell.
|
|
||||||
*
|
|
||||||
* In a formula bar mode is always `edit`.
|
|
||||||
*/
|
|
||||||
type CellEditMode = "init" | "edit";
|
|
||||||
|
|
||||||
const WorkbookContext = createContext<{
|
|
||||||
selectedSheet: number;
|
|
||||||
selectedCell: Cell;
|
|
||||||
selectedArea: Area;
|
|
||||||
scroll: Scroll;
|
|
||||||
extendToArea: Area | null;
|
|
||||||
editor: Editor | null;
|
|
||||||
}>({
|
|
||||||
selectedSheet: 0,
|
|
||||||
selectedCell: {row: 1, column: 1},
|
|
||||||
selectedArea: {rowStart:1, rowEnd: 1, columnStart:1, columnEnd: 1},
|
|
||||||
scroll: {top: 0, left: 0},
|
|
||||||
extendToArea: null,
|
|
||||||
editor: null
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface Editor {
|
|
||||||
id: number;
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
text: string;
|
|
||||||
base: string;
|
|
||||||
mode: CellEditMode;
|
|
||||||
focus: FocusType;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default WorkbookContext;
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
export interface Cell {
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Area {
|
|
||||||
rowStart: number;
|
|
||||||
rowEnd: number;
|
|
||||||
columnStart: number;
|
|
||||||
columnEnd: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Scroll {
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FocusType = 'cell' | 'formula-bar';
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In Excel there are two "modes" of editing
|
|
||||||
* * `init`: When you start typing in a cell. In this mode arrow keys will move away from the cell
|
|
||||||
* * `edit`: If you double click on a cell or click in the cell while editing.
|
|
||||||
* In this mode arrow keys will move within the cell.
|
|
||||||
*
|
|
||||||
* In a formula bar mode is always `edit`.
|
|
||||||
*/
|
|
||||||
type CellEditMode = 'init' | 'edit';
|
|
||||||
|
|
||||||
interface Editor {
|
|
||||||
id: number;
|
|
||||||
sheet: number;
|
|
||||||
row: number;
|
|
||||||
column: number;
|
|
||||||
text: string;
|
|
||||||
base: string;
|
|
||||||
mode: CellEditMode;
|
|
||||||
focus: FocusType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkbookState {
|
|
||||||
private selectedSheet: number;
|
|
||||||
private selectedCell: Cell;
|
|
||||||
private selectedArea: Area;
|
|
||||||
private scroll: Scroll;
|
|
||||||
private extendToArea: Area | null;
|
|
||||||
private editor: Editor | null;
|
|
||||||
private id;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const row = 1;
|
|
||||||
const column = 1;
|
|
||||||
const sheet = 0;
|
|
||||||
this.selectedSheet = sheet;
|
|
||||||
this.selectedCell = { row, column };
|
|
||||||
this.selectedArea = {
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: column,
|
|
||||||
};
|
|
||||||
this.extendToArea = null;
|
|
||||||
this.scroll = {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
};
|
|
||||||
this.editor = null;
|
|
||||||
this.id = Math.floor(Math.random()*1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
startEditing(focus: FocusType, text: string) {
|
|
||||||
const {row, column} = this.selectedCell;
|
|
||||||
this.editor = {
|
|
||||||
id: 0,
|
|
||||||
sheet: this.selectedSheet,
|
|
||||||
row,
|
|
||||||
column,
|
|
||||||
base: '',
|
|
||||||
text,
|
|
||||||
mode: 'init',
|
|
||||||
focus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditorText(text: string) {
|
|
||||||
if (!this.editor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.editor.text = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
endEditing() {
|
|
||||||
this.editor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getEditor(): Editor | null {
|
|
||||||
console.log('getEditor', this.id);
|
|
||||||
return this.editor;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedSheet(): number {
|
|
||||||
return this.selectedSheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedSheet(sheet: number): void {
|
|
||||||
this.selectedSheet = sheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedCell(): Cell {
|
|
||||||
return this.selectedCell;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedCell(cell: Cell): void {
|
|
||||||
this.selectedCell = cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedArea(): Area {
|
|
||||||
return this.selectedArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedArea(area: Area): void {
|
|
||||||
this.selectedArea = area;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectCell(cell: { row: number; column: number }): void {
|
|
||||||
console.log('selectCell: ', this.id)
|
|
||||||
const { row, column } = cell;
|
|
||||||
this.selectedArea = {
|
|
||||||
rowStart: row,
|
|
||||||
rowEnd: row,
|
|
||||||
columnStart: column,
|
|
||||||
columnEnd: column,
|
|
||||||
};
|
|
||||||
this.selectedCell = { row, column };
|
|
||||||
this.editor = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getScroll(): Scroll {
|
|
||||||
return this.scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
setScroll(scroll: Scroll): void {
|
|
||||||
this.scroll = scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
getExtendToArea(): Area | null {
|
|
||||||
return this.extendToArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearExtendToArea(): void {
|
|
||||||
this.extendToArea = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setExtendToArea(area: Area): void {
|
|
||||||
this.extendToArea = area;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
import { styled } from "@mui/material/styles";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import WorksheetCanvas from "./WorksheetCanvas/worksheetCanvas";
|
|
||||||
import {
|
|
||||||
outlineBackgroundColor,
|
|
||||||
outlineColor,
|
|
||||||
} from "./WorksheetCanvas/constants";
|
|
||||||
import usePointer from "./usePointer";
|
|
||||||
import { WorkbookState } from "./workbookState";
|
|
||||||
import { Cell } from "./WorksheetCanvas/types";
|
|
||||||
import Editor from "./editor";
|
|
||||||
import EditorContext, { EditorState } from "./editor/editorContext";
|
|
||||||
import { getFormulaHTML } from "./editor/util";
|
|
||||||
import { Model } from "@ironcalc/wasm";
|
|
||||||
|
|
||||||
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 rootElement = 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 contextMenuAnchorElement = useRef<HTMLDivElement>(null);
|
|
||||||
const columnHeaders = useRef<HTMLDivElement>(null);
|
|
||||||
const worksheetCanvas = useRef<WorksheetCanvas | null>(null);
|
|
||||||
|
|
||||||
const [isEditing, setEditing] = useState(false);
|
|
||||||
|
|
||||||
const [editorContext, setEditorContext] = useState<EditorState>({
|
|
||||||
mode: "accept",
|
|
||||||
insertRange: null,
|
|
||||||
baseText: '',
|
|
||||||
id: Math.floor(Math.random()*1000),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('worksheet', editorContext.id);
|
|
||||||
|
|
||||||
const { model, workbookState, refresh } = props;
|
|
||||||
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;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!canvasRef ||
|
|
||||||
!columnGuideRef ||
|
|
||||||
!rowGuideRef ||
|
|
||||||
!columnHeadersRef ||
|
|
||||||
!worksheetRef ||
|
|
||||||
!outline ||
|
|
||||||
!handle ||
|
|
||||||
!area ||
|
|
||||||
!extendTo
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
onColumnWidthChanges(sheet, column, width) {
|
|
||||||
model.setColumnWidth(sheet, column, width);
|
|
||||||
worksheetCanvas.current?.renderSheet();
|
|
||||||
},
|
|
||||||
onRowHeightChanges(sheet, row, height) {
|
|
||||||
model.setRowHeight(sheet, row, height);
|
|
||||||
worksheetCanvas.current?.renderSheet();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const [sheetWidth, sheetHeight] = canvas.getSheetDimensions();
|
|
||||||
if (spacerElement.current) {
|
|
||||||
spacerElement.current.style.height = `${sheetHeight}px`;
|
|
||||||
spacerElement.current.style.width = `${sheetWidth}px`;
|
|
||||||
}
|
|
||||||
canvas.renderSheet();
|
|
||||||
worksheetCanvas.current = canvas;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sheetNames = model.getWorksheetsProperties().map((s: { name: string; }) => s.name);
|
|
||||||
|
|
||||||
const {
|
|
||||||
onPointerMove,
|
|
||||||
onPointerDown,
|
|
||||||
onPointerHandleDown,
|
|
||||||
onPointerUp,
|
|
||||||
// onContextMenu,
|
|
||||||
} = usePointer({
|
|
||||||
onCellSelected: (cell: Cell, event: React.MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
workbookState.selectCell(cell);
|
|
||||||
// worksheetCanvas.current?.renderSheet();
|
|
||||||
refresh();
|
|
||||||
},
|
|
||||||
onAreaSelecting: (cell: Cell) => {
|
|
||||||
const canvas = worksheetCanvas.current;
|
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { row, column } = cell;
|
|
||||||
// const { width, height } = worksheet.getBoundingClientRect();
|
|
||||||
// const [x, y] = canvas.getCoordinatesByCell(row, column);
|
|
||||||
// const [x1, y1] = canvas.getCoordinatesByCell(row + 1, column + 1);
|
|
||||||
// const { left: canvasLeft, top: canvasTop } = canvas.getScrollPosition();
|
|
||||||
// // let border = Border.Right;
|
|
||||||
// // let { left, top } = state.scrollPosition;
|
|
||||||
// // if (x < headerColumnWidth) {
|
|
||||||
// // border = Border.Left;
|
|
||||||
// // left = canvasLeft - headerColumnWidth + x;
|
|
||||||
// // } else if (x1 > width - 20) {
|
|
||||||
// // border = Border.Right;
|
|
||||||
// // }
|
|
||||||
// // if (y < headerRowHeight) {
|
|
||||||
// // border = Border.Top;
|
|
||||||
// // top = canvasTop - headerRowHeight + y;
|
|
||||||
// // } else if (y1 > height - 20) {
|
|
||||||
// // border = Border.Bottom;
|
|
||||||
// // }
|
|
||||||
const selectedCell = workbookState.getSelectedCell();
|
|
||||||
const area = {
|
|
||||||
rowStart: Math.min(selectedCell.row, row),
|
|
||||||
rowEnd: Math.max(selectedCell.row, row),
|
|
||||||
columnStart: Math.min(selectedCell.column, column),
|
|
||||||
columnEnd: Math.max(selectedCell.column, column),
|
|
||||||
};
|
|
||||||
workbookState.setSelectedArea(area);
|
|
||||||
canvas.renderSheet();
|
|
||||||
// // If there are frozen rows or columns snap to origin if we cross boundaries
|
|
||||||
// const frozenRows = canvas.workbook.getFrozenRowsCount();
|
|
||||||
// const frozenColumns = canvas.workbook.getFrozenColumnsCount();
|
|
||||||
// if (area.rowStart <= frozenRows && area.rowEnd > frozenRows) {
|
|
||||||
// top = 0;
|
|
||||||
// }
|
|
||||||
// if (area.columnStart <= frozenColumns && area.columnEnd > frozenColumns) {
|
|
||||||
// left = 0;
|
|
||||||
// }
|
|
||||||
}, // editorActions.onPointerMoveToCell,
|
|
||||||
onExtendToCell: (cell) => {
|
|
||||||
const canvas = worksheetCanvas.current;
|
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { row, column } = cell;
|
|
||||||
const selectedCell = workbookState.getSelectedCell();
|
|
||||||
const area = {
|
|
||||||
rowStart: Math.min(selectedCell.row, row),
|
|
||||||
rowEnd: Math.max(selectedCell.row, row),
|
|
||||||
columnStart: Math.min(selectedCell.column, column),
|
|
||||||
columnEnd: Math.max(selectedCell.column, column),
|
|
||||||
};
|
|
||||||
workbookState.setExtendToArea(area);
|
|
||||||
canvas.renderSheet();
|
|
||||||
}, // editorActions.onExtendToCell,
|
|
||||||
onExtendToEnd: () => {
|
|
||||||
const canvas = worksheetCanvas.current;
|
|
||||||
if (!canvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const sheet = workbookState.getSelectedSheet();
|
|
||||||
const initialArea = workbookState.getSelectedArea();
|
|
||||||
const extendedArea = workbookState.getExtendToArea();
|
|
||||||
if (!extendedArea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// model.extendTo(sheet, initialArea, extendedArea);
|
|
||||||
workbookState.clearExtendToArea();
|
|
||||||
canvas.renderSheet();
|
|
||||||
}, // editorActions.onExtendToEnd,
|
|
||||||
canvasElement,
|
|
||||||
worksheetElement,
|
|
||||||
worksheetCanvas,
|
|
||||||
// rowContextMenuAnchorElement,
|
|
||||||
// columnContextMenuAnchorElement,
|
|
||||||
// onRowContextMenu,
|
|
||||||
// onColumnContextMenu,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onScroll = (): void => {
|
|
||||||
if (!scrollElement.current || !worksheetCanvas.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const left = scrollElement.current.scrollLeft;
|
|
||||||
const top = scrollElement.current.scrollTop;
|
|
||||||
|
|
||||||
worksheetCanvas.current.setScrollPosition({ left, top });
|
|
||||||
worksheetCanvas.current.renderSheet();
|
|
||||||
};
|
|
||||||
|
|
||||||
const {row, column} = workbookState.getSelectedCell();
|
|
||||||
const selectedSheet = workbookState.getSelectedSheet();
|
|
||||||
|
|
||||||
return (
|
|
||||||
// <EditorContext.Provider value={{editorContext}}>
|
|
||||||
<Wrapper ref={scrollElement} onScroll={onScroll}>
|
|
||||||
<Spacer ref={spacerElement} />
|
|
||||||
<SheetContainer
|
|
||||||
ref={worksheetElement}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
if (isEditing === true && editorContext.mode !== 'insert') {
|
|
||||||
setEditing(false);
|
|
||||||
model.setUserInput(selectedSheet, row, column, editorContext.baseText);
|
|
||||||
}
|
|
||||||
onPointerDown(event);
|
|
||||||
}}
|
|
||||||
onPointerMove={onPointerMove}
|
|
||||||
onPointerUp={onPointerUp}
|
|
||||||
onDoubleClick={(event) => {
|
|
||||||
const sheet = workbookState.getSelectedSheet();
|
|
||||||
const {row, column} = workbookState.getSelectedCell();
|
|
||||||
const text = model.getCellContent(sheet, row, column) || '';
|
|
||||||
console.log('dbclick', text);
|
|
||||||
|
|
||||||
workbookState.startEditing("cell", `${text}`);
|
|
||||||
setEditorContext ((c: EditorState) => {
|
|
||||||
console.log('text', text, c.id);
|
|
||||||
return {
|
|
||||||
mode: c.mode,
|
|
||||||
insertRange: c.insertRange,
|
|
||||||
baseText: text,
|
|
||||||
dontChange: true,
|
|
||||||
id: c.id,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
setEditing(true);
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
// refresh();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SheetCanvas ref={canvasElement} />
|
|
||||||
<CellOutline ref={cellOutline}>
|
|
||||||
{
|
|
||||||
<Editor
|
|
||||||
minimalWidth={200}
|
|
||||||
minimalHeight={90}
|
|
||||||
textColor="#333"
|
|
||||||
getStyledText={(text: string, insertRangeText: string) => {
|
|
||||||
return getFormulaHTML(
|
|
||||||
text,
|
|
||||||
0,
|
|
||||||
sheetNames,
|
|
||||||
editorContext.insertRange,
|
|
||||||
insertRangeText
|
|
||||||
);
|
|
||||||
} }
|
|
||||||
onEditEnd={(text: string) => {
|
|
||||||
console.log(text);
|
|
||||||
setEditing(false);
|
|
||||||
model.setUserInput(selectedSheet, row, column, text);
|
|
||||||
} }
|
|
||||||
originalText={model.getCellContent(selectedSheet, row, column) || ''}
|
|
||||||
display={isEditing}
|
|
||||||
cell={{ sheet: selectedSheet, row, column }}
|
|
||||||
sheetNames={sheetNames}
|
|
||||||
/>
|
|
||||||
/* <Editor
|
|
||||||
data-testid={WorkbookTestId.WorkbookCellEditor}
|
|
||||||
onEditChange={onEditChange}
|
|
||||||
onEditEnd={onEditEnd}
|
|
||||||
onEditEscape={onEditEscape}
|
|
||||||
onReferenceCycle={onReferenceCycle}
|
|
||||||
display={!!cellEditing}
|
|
||||||
focus={cellEditing?.focus === FocusType.Cell}
|
|
||||||
html={cellEditing?.html ?? ''}
|
|
||||||
cursorStart={cellEditing?.cursorStart ?? 0}
|
|
||||||
cursorEnd={cellEditing?.cursorEnd ?? 0}
|
|
||||||
mode={cellEditing?.mode ?? 'init'}
|
|
||||||
/> */
|
|
||||||
}
|
|
||||||
</CellOutline>
|
|
||||||
<AreaOutline ref={areaOutline} />
|
|
||||||
<ExtendToOutline ref={extendToOutline} />
|
|
||||||
<CellOutlineHandle
|
|
||||||
ref={cellOutlineHandle}
|
|
||||||
onPointerDown={onPointerHandleDown}
|
|
||||||
/>
|
|
||||||
<ColumnResizeGuide ref={columnResizeGuide} />
|
|
||||||
<RowResizeGuide ref={rowResizeGuide} />
|
|
||||||
<ColumnHeaders ref={columnHeaders} />
|
|
||||||
</SheetContainer>
|
|
||||||
</Wrapper>
|
|
||||||
// </EditorContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 71,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 41,
|
|
||||||
});
|
|
||||||
|
|
||||||
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: 1px solid white;
|
|
||||||
border-radius: 1px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const ExtendToOutline = styled("div")`
|
|
||||||
position: absolute;
|
|
||||||
border: 1px dashed ${outlineColor};
|
|
||||||
border-radius: 3px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Worksheet;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user