@eeacms/volto-eea-website-theme 4.3.9 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -28
- package/package.json +1 -1
- package/src/customizations/@plone/volto-slate/blocks/Text/keyboard/joinBlocks.js +228 -0
- package/src/customizations/@plone/volto-slate/blocks/Text/keyboard/joinBlocks.js.diff +30 -0
- package/src/customizations/@plone/volto-slate/blocks/Text/keyboard/joinBlocks.js.md +10 -0
- package/src/customizations/@plone/volto-slate/utils/selection.js +271 -0
- package/src/customizations/@plone/volto-slate/utils/selection.js.diff +63 -0
- package/src/customizations/@plone/volto-slate/utils/selection.js.md +7 -0
- package/src/customizations/@plone/volto-slate/utils/selection.test.js +116 -0
- package/src/customizations/@plone/volto-slate/utils/volto-blocks.js +350 -0
- package/src/customizations/@plone/volto-slate/utils/volto-blocks.js.diff +61 -0
- package/src/customizations/@plone/volto-slate/utils/volto-blocks.js.md +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,16 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
-
### [4.
|
|
7
|
+
### [4.4.0](https://github.com/eea/volto-eea-website-theme/compare/4.3.9...4.4.0) - 24 June 2026
|
|
8
8
|
|
|
9
9
|
#### :bug: Bug Fixes
|
|
10
10
|
|
|
11
|
-
- fix:
|
|
11
|
+
- fix: Backspace behaving erratically near a styled or link text - refs #304369 [Alin Voinea - [`7f9fc05`](https://github.com/eea/volto-eea-website-theme/commit/7f9fc05814a0008a115596c5ef1875ef65269fac)]
|
|
12
|
+
|
|
13
|
+
#### :hammer_and_wrench: Others
|
|
14
|
+
|
|
15
|
+
- Release 4.4.0 [Alin Voinea - [`3ae92a0`](https://github.com/eea/volto-eea-website-theme/commit/3ae92a071a6929611c5cac1b8f152be863a84eb4)]
|
|
16
|
+
### [4.3.9](https://github.com/eea/volto-eea-website-theme/compare/4.3.8...4.3.9) - 23 June 2026
|
|
12
17
|
|
|
13
18
|
### [4.3.8](https://github.com/eea/volto-eea-website-theme/compare/4.3.7...4.3.8) - 9 June 2026
|
|
14
19
|
|
|
@@ -126,8 +131,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
126
131
|
|
|
127
132
|
#### :house: Internal changes
|
|
128
133
|
|
|
129
|
-
- chore: [JENKINSFILE] add package version in sonarqube [valentinab25 - [`b15b302`](https://github.com/eea/volto-eea-website-theme/commit/b15b302ff88ec6afa6901ee7e1726cc4e04a8739)]
|
|
130
|
-
- chore: [JENKINSFILE] use sonarqube branches [EEA Jenkins - [`36f2e1e`](https://github.com/eea/volto-eea-website-theme/commit/36f2e1e176471b3753d77ab8aaf0cba577b43b36)]
|
|
131
134
|
|
|
132
135
|
### [3.19.0](https://github.com/eea/volto-eea-website-theme/compare/3.18.1...3.19.0) - 11 February 2026
|
|
133
136
|
|
|
@@ -190,7 +193,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
190
193
|
#### :hammer_and_wrench: Others
|
|
191
194
|
|
|
192
195
|
- Release 3.12.0 [Alin Voinea - [`ea1e961`](https://github.com/eea/volto-eea-website-theme/commit/ea1e96144684b2a8238ae476fd8d47de82ff42b4)]
|
|
193
|
-
- Add Sonarqube tag using bise-frontend addons list [EEA Jenkins - [`2cac56e`](https://github.com/eea/volto-eea-website-theme/commit/2cac56e22e580e042d368b69254172539ed33b65)]
|
|
194
196
|
### [3.11.0](https://github.com/eea/volto-eea-website-theme/compare/3.10.1...3.11.0) - 29 September 2025
|
|
195
197
|
|
|
196
198
|
#### :bug: Bug Fixes
|
|
@@ -284,8 +286,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
284
286
|
|
|
285
287
|
#### :hammer_and_wrench: Others
|
|
286
288
|
|
|
287
|
-
- Add Sonarqube tag using fise-frontend addons list [EEA Jenkins - [`3d50874`](https://github.com/eea/volto-eea-website-theme/commit/3d50874281ebcb91a1ae3727248f656a6f26c603)]
|
|
288
|
-
- Add Sonarqube tag using ied-frontend addons list [EEA Jenkins - [`a3f7676`](https://github.com/eea/volto-eea-website-theme/commit/a3f76767d99cb6dd93cc7fa0eafdea57e9000b2c)]
|
|
289
289
|
### [3.5.4](https://github.com/eea/volto-eea-website-theme/compare/3.5.3...3.5.4) - 30 January 2025
|
|
290
290
|
|
|
291
291
|
#### :bug: Bug Fixes
|
|
@@ -386,7 +386,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
386
386
|
|
|
387
387
|
#### :hammer_and_wrench: Others
|
|
388
388
|
|
|
389
|
-
- Add Sonarqube tag using bise-frontend addons list [EEA Jenkins - [`e417f83`](https://github.com/eea/volto-eea-website-theme/commit/e417f839312045c56b67ab8134fe5b0622b3e2c3)]
|
|
390
389
|
## [3.0.0](https://github.com/eea/volto-eea-website-theme/compare/2.4.0...3.0.0) - 21 October 2024
|
|
391
390
|
|
|
392
391
|
#### :nail_care: Enhancements
|
|
@@ -472,7 +471,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
472
471
|
- Refs #269520 - other_organisation test [Tripon Eugen - [`7558b27`](https://github.com/eea/volto-eea-website-theme/commit/7558b27a9d1cd6d3480a32b296c7325c796006c9)]
|
|
473
472
|
- Refs #269520 - other_organisation token widget edit mode [Tripon Eugen - [`b6df127`](https://github.com/eea/volto-eea-website-theme/commit/b6df127bd91b6f55dda8f468e1107037d43752ff)]
|
|
474
473
|
- Refs #269520 - other_organisation token widget [Tripon Eugen - [`6fd7543`](https://github.com/eea/volto-eea-website-theme/commit/6fd7543894bd2c593a1729116befd95c82ea3c8f)]
|
|
475
|
-
- Add Sonarqube tag using marine-frontend addons list [EEA Jenkins - [`cbc36c7`](https://github.com/eea/volto-eea-website-theme/commit/cbc36c74dfcd5f7cb1013a4c673d9c02e0023766)]
|
|
476
474
|
### [2.1.1](https://github.com/eea/volto-eea-website-theme/compare/2.1.0...2.1.1) - 28 May 2024
|
|
477
475
|
|
|
478
476
|
#### :bug: Bug Fixes
|
|
@@ -557,7 +555,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
557
555
|
|
|
558
556
|
#### :hammer_and_wrench: Others
|
|
559
557
|
|
|
560
|
-
- Add Sonarqube tag using insitu-frontend addons list [EEA Jenkins - [`adc6730`](https://github.com/eea/volto-eea-website-theme/commit/adc6730e21a37afb865b842182624401de6a29f5)]
|
|
561
558
|
### [1.33.1](https://github.com/eea/volto-eea-website-theme/compare/1.33.0...1.33.1) - 4 April 2024
|
|
562
559
|
|
|
563
560
|
#### :bug: Bug Fixes
|
|
@@ -691,8 +688,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
691
688
|
|
|
692
689
|
- bump version [Razvan - [`721e939`](https://github.com/eea/volto-eea-website-theme/commit/721e939d12e324b459ebfa78a2e656ee7142a3d6)]
|
|
693
690
|
- merge master into this branch [Razvan - [`586c8f9`](https://github.com/eea/volto-eea-website-theme/commit/586c8f910bac55a043bd8dda60e9444bd2ae1663)]
|
|
694
|
-
- Add Sonarqube tag using freshwater-frontend addons list [EEA Jenkins - [`fd90044`](https://github.com/eea/volto-eea-website-theme/commit/fd9004442a9d1d465f7601ecdefe3e23c61e6a9c)]
|
|
695
|
-
- Add Sonarqube tag using insitu-frontend addons list [EEA Jenkins - [`4bc3dd3`](https://github.com/eea/volto-eea-website-theme/commit/4bc3dd3ae412a66befd04b5b80fab3716c929240)]
|
|
696
691
|
- test: Update jest,Jenkinsfile,lint to volto-addons-template PR30 [valentinab25 - [`c4dbd28`](https://github.com/eea/volto-eea-website-theme/commit/c4dbd289358205bc2d849aab7edb11ccf3b89cee)]
|
|
697
692
|
- fix tests [Razvan - [`042330b`](https://github.com/eea/volto-eea-website-theme/commit/042330bc97d32ffe7ba769b4f2453f71cffed706)]
|
|
698
693
|
- remove RemoveSchema logic [Razvan - [`08d10f8`](https://github.com/eea/volto-eea-website-theme/commit/08d10f8bf6f75478260e4e4c66d7316ba87b907a)]
|
|
@@ -787,11 +782,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
787
782
|
- test: Add real image to cypress test [Alin Voinea - [`4ff591a`](https://github.com/eea/volto-eea-website-theme/commit/4ff591ae3318c9588b4e2114582c0fa6cfdf31ae)]
|
|
788
783
|
- test: Add cypress tests for Image block styling position and align [Alin Voinea - [`7341ef7`](https://github.com/eea/volto-eea-website-theme/commit/7341ef7b92714fc0cc3ab0c31c39033e7b3e19e7)]
|
|
789
784
|
- Revert "change(tests): commented out rss test since title block config is missing" [Alin Voinea - [`fb61191`](https://github.com/eea/volto-eea-website-theme/commit/fb611918d6ca380b89b594f283dcf9f685a4b294)]
|
|
790
|
-
- test: [JENKINS] Use java17 for sonarqube scanner [valentinab25 - [`6a3be30`](https://github.com/eea/volto-eea-website-theme/commit/6a3be3092589411af7808a235f76de5222fd3868)]
|
|
791
|
-
- test: [JENKINS] Run cypress in started frontend container [valentinab25 - [`c3978f2`](https://github.com/eea/volto-eea-website-theme/commit/c3978f23375ef066e9fd6f6c2e34ba6c1c058f69)]
|
|
792
|
-
- test: [JENKINS] Add cpu limit on cypress docker [valentinab25 - [`f672779`](https://github.com/eea/volto-eea-website-theme/commit/f672779e845bec9240ccc901e9f53ec80c5a1819)]
|
|
793
|
-
- test: [JENKINS] Increase shm-size to cypress docker [valentinab25 - [`ae5d8e3`](https://github.com/eea/volto-eea-website-theme/commit/ae5d8e3f4e04dc2808d47ce2ee886e1b23b528da)]
|
|
794
|
-
- test: [JENKINS] Improve cypress time [valentinab25 - [`170ff0c`](https://github.com/eea/volto-eea-website-theme/commit/170ff0c8e3b30e69479bdf1117e811fea94f1027)]
|
|
795
785
|
### [1.23.0](https://github.com/eea/volto-eea-website-theme/compare/1.22.1...1.23.0) - 2 November 2023
|
|
796
786
|
|
|
797
787
|
#### :rocket: New Features
|
|
@@ -804,7 +794,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
804
794
|
|
|
805
795
|
#### :house: Internal changes
|
|
806
796
|
|
|
807
|
-
- chore: [JENKINS] Refactor automated testing [valentinab25 - [`f28fce3`](https://github.com/eea/volto-eea-website-theme/commit/f28fce3d1eb815f95fb9aa40de42b10b7e8e30c5)]
|
|
808
797
|
- chore: husky, lint-staged use fixed versions [valentinab25 - [`6d15088`](https://github.com/eea/volto-eea-website-theme/commit/6d150886c5aeb2ca0b569270486e60f7cc274e2c)]
|
|
809
798
|
- chore:volto 16 in tests, update docs, fix stylelint overrides [valentinab25 - [`20c0323`](https://github.com/eea/volto-eea-website-theme/commit/20c032380b33c0077c869a05136f93e2fb68e5d4)]
|
|
810
799
|
|
|
@@ -990,7 +979,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
990
979
|
|
|
991
980
|
#### :house: Internal changes
|
|
992
981
|
|
|
993
|
-
- chore: [JENKINS] Deprecate circularity website [valentinab25 - [`370dcbf`](https://github.com/eea/volto-eea-website-theme/commit/370dcbfbf1a8135ce7b1b3b271b004552a631837)]
|
|
994
982
|
|
|
995
983
|
#### :hammer_and_wrench: Others
|
|
996
984
|
|
|
@@ -1146,7 +1134,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1146
1134
|
|
|
1147
1135
|
#### :hammer_and_wrench: Others
|
|
1148
1136
|
|
|
1149
|
-
- Add Sonarqube tag using eea-website-frontend addons list [EEA Jenkins - [`6c5e2f8`](https://github.com/eea/volto-eea-website-theme/commit/6c5e2f80456e2061d9e9c15fd0a0b91b9ac70568)]
|
|
1150
1137
|
### [1.9.1](https://github.com/eea/volto-eea-website-theme/compare/1.9.0...1.9.1) - 28 February 2023
|
|
1151
1138
|
|
|
1152
1139
|
#### :bug: Bug Fixes
|
|
@@ -1293,7 +1280,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1293
1280
|
|
|
1294
1281
|
- For some reasons types is a string [Alin Voinea - [`3769a09`](https://github.com/eea/volto-eea-website-theme/commit/3769a0981181d5b633f3498daebbe96be8b4b833)]
|
|
1295
1282
|
- Fix(redirect): o.filter - refs #157627 [Alin Voinea - [`deb23da`](https://github.com/eea/volto-eea-website-theme/commit/deb23da846444cc96539697fd798429ae0abe89e)]
|
|
1296
|
-
- Add Sonarqube tag using advisory-board-frontend addons list [EEA Jenkins - [`f1fffc5`](https://github.com/eea/volto-eea-website-theme/commit/f1fffc5db96725440863d545580b4e76cce4b796)]
|
|
1297
1283
|
### [1.5.0](https://github.com/eea/volto-eea-website-theme/compare/1.4.2...1.5.0) - 9 January 2023
|
|
1298
1284
|
|
|
1299
1285
|
#### :hammer_and_wrench: Others
|
|
@@ -1327,7 +1313,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1327
1313
|
|
|
1328
1314
|
- Release 1.4.0 [Alin Voinea - [`bd42a0d`](https://github.com/eea/volto-eea-website-theme/commit/bd42a0d26e928cac5d99933194755da3db06b341)]
|
|
1329
1315
|
- bump version to use as volto-eea-design-system [David Ichim - [`f4be047`](https://github.com/eea/volto-eea-website-theme/commit/f4be047328b46399b03b612d378b18aaf82e7dc1)]
|
|
1330
|
-
- Add Sonarqube tag using advisory-board-frontend addons list [EEA Jenkins - [`9b7cfef`](https://github.com/eea/volto-eea-website-theme/commit/9b7cfefb4d34fc1c948015e491feb370f9795bd8)]
|
|
1331
1316
|
- test(Jenkins): Run tests and cypress with latest canary @plone/volto [Alin Voinea - [`df252a9`](https://github.com/eea/volto-eea-website-theme/commit/df252a9bfed0bb86cadf53c59dd1603b1e2cd822)]
|
|
1332
1317
|
### [1.3.2](https://github.com/eea/volto-eea-website-theme/compare/1.3.1...1.3.2) - 16 December 2022
|
|
1333
1318
|
|
|
@@ -1337,7 +1322,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1337
1322
|
|
|
1338
1323
|
#### :hammer_and_wrench: Others
|
|
1339
1324
|
|
|
1340
|
-
- Add Sonarqube tag using cca-frontend addons list [EEA Jenkins - [`a43c658`](https://github.com/eea/volto-eea-website-theme/commit/a43c658a7920c8df95e763b9a637f38ce77eba2c)]
|
|
1341
1325
|
- Better razzle.config [Tiberiu Ichim - [`81dbf48`](https://github.com/eea/volto-eea-website-theme/commit/81dbf48815fb27facb4f82c9b764540fdf188b2e)]
|
|
1342
1326
|
- Better razzle.config [Tiberiu Ichim - [`7bc9da2`](https://github.com/eea/volto-eea-website-theme/commit/7bc9da2cd837ab62a95cd29979cdd9b0055b7d67)]
|
|
1343
1327
|
### [1.3.1](https://github.com/eea/volto-eea-website-theme/compare/1.3.0...1.3.1) - 28 November 2022
|
|
@@ -1348,7 +1332,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1348
1332
|
|
|
1349
1333
|
#### :hammer_and_wrench: Others
|
|
1350
1334
|
|
|
1351
|
-
- yarn 3 [Alin Voinea - [`ea7a709`](https://github.com/eea/volto-eea-website-theme/commit/ea7a7094945312776e9b6f44e371178603e92139)]
|
|
1352
1335
|
### [1.3.0](https://github.com/eea/volto-eea-website-theme/compare/1.2.0...1.3.0) - 22 November 2022
|
|
1353
1336
|
|
|
1354
1337
|
#### :rocket: New Features
|
|
@@ -1389,7 +1372,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1389
1372
|
- Add subsite class to body [Tiberiu Ichim - [`74d700f`](https://github.com/eea/volto-eea-website-theme/commit/74d700fbfd6249a8604762a7e4e49cce857db0f3)]
|
|
1390
1373
|
- Add subsite info to header [Tiberiu Ichim - [`47daf8b`](https://github.com/eea/volto-eea-website-theme/commit/47daf8bb6374a1222040626b19d4154df7ba1b83)]
|
|
1391
1374
|
- fix eslint [Miu Razvan - [`eb8d0a7`](https://github.com/eea/volto-eea-website-theme/commit/eb8d0a790bc70c0aae256c6ff35f63c4885f338e)]
|
|
1392
|
-
- Add Sonarqube tag using circularity-frontend addons list [EEA Jenkins - [`cc578a4`](https://github.com/eea/volto-eea-website-theme/commit/cc578a413b205a8e61e091fab3a88f94cedefc89)]
|
|
1393
1375
|
### [1.1.0](https://github.com/eea/volto-eea-website-theme/compare/1.0.0...1.1.0) - 28 October 2022
|
|
1394
1376
|
|
|
1395
1377
|
#### :nail_care: Enhancements
|
|
@@ -1437,7 +1419,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1437
1419
|
|
|
1438
1420
|
#### :hammer_and_wrench: Others
|
|
1439
1421
|
|
|
1440
|
-
- Add Sonarqube tag using eea-website-frontend addons list [EEA Jenkins - [`33b56ac`](https://github.com/eea/volto-eea-website-theme/commit/33b56acb13fbaf0c5b79e8fc6e13c4b699c79c90)]
|
|
1441
1422
|
### [0.7.3](https://github.com/eea/volto-eea-website-theme/compare/0.7.2...0.7.3) - 22 September 2022
|
|
1442
1423
|
|
|
1443
1424
|
#### :hammer_and_wrench: Others
|
|
@@ -1705,7 +1686,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1705
1686
|
- Header refactor, add custom logo #5 [ichim-david - [`4950235`](https://github.com/eea/volto-eea-website-theme/commit/49502358105437cfeac3b144e6d301cb59aa2346)]
|
|
1706
1687
|
- Update footer.config with new publication card component [ichim-david - [`2e38e9a`](https://github.com/eea/volto-eea-website-theme/commit/2e38e9a417f835009d60c80d4eb4b30229f55e45)]
|
|
1707
1688
|
- feature(breadcrumbs): implement eea-design-system breadcrumb as Volto component #32 #7 [ichim-david - [`181af41`](https://github.com/eea/volto-eea-website-theme/commit/181af4125ce2b9ddac56dab4723cb11c26633221)]
|
|
1708
|
-
- Add Sonarqube tag using eea-website-frontend addons list [EEA Jenkins - [`da8ceb6`](https://github.com/eea/volto-eea-website-theme/commit/da8ceb68ea68bfbc9504e48ccd4d68277f11ab9a)]
|
|
1709
1689
|
- use breadcrumbs from eea-design-system [nileshgulia1 - [`db2f9e9`](https://github.com/eea/volto-eea-website-theme/commit/db2f9e9a4327420a3cce9a9903cd88549b129eab)]
|
|
1710
1690
|
- Update theme.config [ichim-david - [`8eca4f4`](https://github.com/eea/volto-eea-website-theme/commit/8eca4f40397a4aeca6d39029c92db78968d37064)]
|
|
1711
1691
|
- Added keyContent component to theme.config [ichim-david - [`d86f202`](https://github.com/eea/volto-eea-website-theme/commit/d86f202d0274d839487a88b51cae9a0e899beb23)]
|
|
@@ -1747,5 +1727,4 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
|
1747
1727
|
|
|
1748
1728
|
#### :hammer_and_wrench: Others
|
|
1749
1729
|
|
|
1750
|
-
- yarn bootstrap [Alin Voinea - [`6995e9e`](https://github.com/eea/volto-eea-website-theme/commit/6995e9e091f21fdbbdffa8a44fc0e2c626f6d46a)]
|
|
1751
1730
|
- Initial commit [Alin Voinea - [`6a9c03a`](https://github.com/eea/volto-eea-website-theme/commit/6a9c03a7cebe71ca87e82cf58c42904063e9d8d3)]
|
package/package.json
CHANGED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import ReactDOM from 'react-dom';
|
|
2
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
3
|
+
import { serializeNodesToText } from '@plone/volto-slate/editor/render';
|
|
4
|
+
import { Editor } from 'slate';
|
|
5
|
+
import {
|
|
6
|
+
getPreviousVoltoBlock,
|
|
7
|
+
getNextVoltoBlock,
|
|
8
|
+
mergeSlateWithBlockForward,
|
|
9
|
+
mergeSlateWithBlockBackward,
|
|
10
|
+
} from '@plone/volto-slate/utils/volto-blocks';
|
|
11
|
+
import {
|
|
12
|
+
isCursorAtBlockStart,
|
|
13
|
+
isCursorAtBlockEnd,
|
|
14
|
+
} from '@plone/volto-slate/utils/selection';
|
|
15
|
+
import { makeEditor } from '@plone/volto-slate/utils/editor';
|
|
16
|
+
import {
|
|
17
|
+
changeBlock,
|
|
18
|
+
deleteBlock,
|
|
19
|
+
getBlocksFieldname,
|
|
20
|
+
getBlocksLayoutFieldname,
|
|
21
|
+
} from '@plone/volto/helpers/Blocks/Blocks';
|
|
22
|
+
/**
|
|
23
|
+
* Joins the current block (which has an active Slate Editor)
|
|
24
|
+
* with the previous block, to make a single block.
|
|
25
|
+
*
|
|
26
|
+
* @param {Editor} editor
|
|
27
|
+
* @param {KeyboardEvent} event
|
|
28
|
+
*/
|
|
29
|
+
export function joinWithPreviousBlock({ editor, event }, intl) {
|
|
30
|
+
if (!isCursorAtBlockStart(editor)) return;
|
|
31
|
+
|
|
32
|
+
const blockProps = editor.getBlockProps();
|
|
33
|
+
const {
|
|
34
|
+
block,
|
|
35
|
+
index,
|
|
36
|
+
saveSlateBlockSelection,
|
|
37
|
+
onSelectBlock,
|
|
38
|
+
data,
|
|
39
|
+
properties,
|
|
40
|
+
onChangeField,
|
|
41
|
+
} = blockProps;
|
|
42
|
+
|
|
43
|
+
const blocksFieldname = getBlocksFieldname(properties);
|
|
44
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
45
|
+
|
|
46
|
+
const prev = getPreviousVoltoBlock(index, properties);
|
|
47
|
+
if (!prev) return;
|
|
48
|
+
const [otherBlock = {}, otherBlockId] = prev;
|
|
49
|
+
|
|
50
|
+
// Don't join with required blocks
|
|
51
|
+
if (data?.required || otherBlock?.required) return;
|
|
52
|
+
|
|
53
|
+
// If the previous block is not a slate block (e.g., image), we can only
|
|
54
|
+
// handle the empty-block case (delete the current empty block and select
|
|
55
|
+
// the previous block). If the current block has text, we cannot merge
|
|
56
|
+
// text into a non-slate block, so let the default Slate behavior run.
|
|
57
|
+
// Without this guard, getBlockEndAsRange and mergeSlateWithBlockBackward
|
|
58
|
+
// crash because non-slate blocks have no `value` property. (#8347)
|
|
59
|
+
if (otherBlock['@type'] !== 'slate') {
|
|
60
|
+
const text = Editor.string(editor, []);
|
|
61
|
+
if (text) return; // let default behavior handle it
|
|
62
|
+
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
const newFormData = deleteBlock(properties, block, intl);
|
|
66
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
67
|
+
onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
68
|
+
onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
69
|
+
onSelectBlock(otherBlockId);
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
event.stopPropagation();
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
|
|
77
|
+
// If the Editor contains no characters TODO: clarify if this special case
|
|
78
|
+
// really needs to be handled or not. In `joinWithNextBlock` it is not
|
|
79
|
+
// handled.
|
|
80
|
+
const text = Editor.string(editor, []);
|
|
81
|
+
if (!text) {
|
|
82
|
+
const cursor = getBlockEndAsRange(otherBlock);
|
|
83
|
+
const newFormData = deleteBlock(properties, block, intl);
|
|
84
|
+
|
|
85
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
86
|
+
saveSlateBlockSelection(otherBlockId, cursor);
|
|
87
|
+
|
|
88
|
+
onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
89
|
+
onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
90
|
+
|
|
91
|
+
onSelectBlock(otherBlockId);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Else the editor contains characters, so we merge the current block's
|
|
98
|
+
// `editor` with the block before, `otherBlock`.
|
|
99
|
+
const cursor = mergeSlateWithBlockBackward(editor, otherBlock, event);
|
|
100
|
+
|
|
101
|
+
// Get the merged content from the editor
|
|
102
|
+
const merged = JSON.parse(JSON.stringify(editor.children));
|
|
103
|
+
|
|
104
|
+
// // TODO: don't remove undo history, etc Should probably save both undo
|
|
105
|
+
// // histories, so that the blocks are split, the undos can be restored??
|
|
106
|
+
|
|
107
|
+
// const cursor = getBlockEndAsRange(otherBlock);
|
|
108
|
+
|
|
109
|
+
const formData = changeBlock(properties, otherBlockId, {
|
|
110
|
+
'@type': 'slate', // TODO: use a constant specified in src/constants.js instead of 'slate'
|
|
111
|
+
value: merged,
|
|
112
|
+
plaintext: serializeNodesToText(merged || []),
|
|
113
|
+
});
|
|
114
|
+
const newFormData = deleteBlock(formData, block, intl);
|
|
115
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
116
|
+
saveSlateBlockSelection(otherBlockId, cursor);
|
|
117
|
+
onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
118
|
+
onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
119
|
+
onSelectBlock(otherBlockId);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Joins the current block (which has the cursor) with the next block to make a
|
|
127
|
+
* single block.
|
|
128
|
+
* @param {Editor} editor
|
|
129
|
+
* @param {KeyboardEvent} event
|
|
130
|
+
*/
|
|
131
|
+
export function joinWithNextBlock({ editor, event }, intl) {
|
|
132
|
+
if (!isCursorAtBlockEnd(editor)) return;
|
|
133
|
+
|
|
134
|
+
const blockProps = editor.getBlockProps();
|
|
135
|
+
const {
|
|
136
|
+
block,
|
|
137
|
+
index,
|
|
138
|
+
// saveSlateBlockSelection,
|
|
139
|
+
onSelectBlock,
|
|
140
|
+
data,
|
|
141
|
+
} = blockProps;
|
|
142
|
+
|
|
143
|
+
const { properties, onChangeField } = editor.getBlockProps();
|
|
144
|
+
const [otherBlock = {}, otherBlockId] = getNextVoltoBlock(index, properties);
|
|
145
|
+
|
|
146
|
+
if (!otherBlockId) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Don't join with required blocks
|
|
151
|
+
if (data?.required || otherBlock?.required || otherBlock['@type'] !== 'slate')
|
|
152
|
+
return;
|
|
153
|
+
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
|
|
157
|
+
const blocksFieldname = getBlocksFieldname(properties);
|
|
158
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
159
|
+
|
|
160
|
+
// If next block is not a slate text block, do nothing
|
|
161
|
+
if (otherBlock['@type'] !== 'slate') {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const nextValue = otherBlock?.value;
|
|
166
|
+
const nextPlaintext =
|
|
167
|
+
otherBlock?.plaintext ?? serializeNodesToText(nextValue || []);
|
|
168
|
+
// Treat the next block as empty if both its structured value and plaintext representation
|
|
169
|
+
// indicate no content. In that case we can delete it instead of attempting a merge.
|
|
170
|
+
const isEmptySlateBlock =
|
|
171
|
+
!Array.isArray(nextValue) ||
|
|
172
|
+
nextValue.length === 0 ||
|
|
173
|
+
!nextPlaintext ||
|
|
174
|
+
nextPlaintext.trim().length === 0;
|
|
175
|
+
|
|
176
|
+
if (isEmptySlateBlock) {
|
|
177
|
+
const newFormData = deleteBlock(properties, otherBlockId, intl);
|
|
178
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
179
|
+
onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
180
|
+
onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
181
|
+
onSelectBlock(block);
|
|
182
|
+
});
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
// Merge next text block into current one and delete the next block
|
|
186
|
+
mergeSlateWithBlockForward(editor, otherBlock);
|
|
187
|
+
|
|
188
|
+
const combined = JSON.parse(JSON.stringify(editor.children));
|
|
189
|
+
|
|
190
|
+
const formData = changeBlock(properties, block, {
|
|
191
|
+
'@type': 'slate',
|
|
192
|
+
value: combined,
|
|
193
|
+
plaintext: serializeNodesToText(combined || []),
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const newFormData = deleteBlock(formData, otherBlockId, intl);
|
|
197
|
+
|
|
198
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
199
|
+
onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
200
|
+
onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
201
|
+
onSelectBlock(block);
|
|
202
|
+
});
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {object} block The Volto object representing the configuration and
|
|
208
|
+
* contents of a Volto Block of type Slate Text.
|
|
209
|
+
* @returns {Range} The collapsed Slate Range that represents the last position
|
|
210
|
+
* the text cursor can take inside the given block.
|
|
211
|
+
*/
|
|
212
|
+
function getBlockEndAsRange(block) {
|
|
213
|
+
const { value } = block;
|
|
214
|
+
const location = [value.length - 1]; // adress of root node
|
|
215
|
+
const editor = { children: value };
|
|
216
|
+
const newEditor = makeEditor();
|
|
217
|
+
newEditor.children = cloneDeep(editor.children);
|
|
218
|
+
const path = Editor.last(newEditor, location)[1]; // last Node in the block
|
|
219
|
+
// The last Text node (leaf node) entry inside the path computed just above.
|
|
220
|
+
const [leaf, leafpath] = Editor.leaf(newEditor, path);
|
|
221
|
+
// The offset of the Points in the collapsed Range computed below:
|
|
222
|
+
const offset = (leaf.text || '').length;
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
anchor: { path: leafpath, offset },
|
|
226
|
+
focus: { path: leafpath, offset },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
--- node_modules/@plone/volto-slate/src/blocks/Text/keyboard/joinBlocks.js 2026-06-19 16:56:50.025195065 +0300
|
|
2
|
+
+++ src/addons/volto-eea-website-theme/src/customizations/@plone/volto-slate/blocks/Text/keyboard/joinBlocks.js 2026-06-24 18:24:23.816712188 +0300
|
|
3
|
+
@@ -50,6 +50,27 @@
|
|
4
|
+
// Don't join with required blocks
|
|
5
|
+
if (data?.required || otherBlock?.required) return;
|
|
6
|
+
|
|
7
|
+
+ // If the previous block is not a slate block (e.g., image), we can only
|
|
8
|
+
+ // handle the empty-block case (delete the current empty block and select
|
|
9
|
+
+ // the previous block). If the current block has text, we cannot merge
|
|
10
|
+
+ // text into a non-slate block, so let the default Slate behavior run.
|
|
11
|
+
+ // Without this guard, getBlockEndAsRange and mergeSlateWithBlockBackward
|
|
12
|
+
+ // crash because non-slate blocks have no `value` property. (#8347)
|
|
13
|
+
+ if (otherBlock['@type'] !== 'slate') {
|
|
14
|
+
+ const text = Editor.string(editor, []);
|
|
15
|
+
+ if (text) return; // let default behavior handle it
|
|
16
|
+
+
|
|
17
|
+
+ event.stopPropagation();
|
|
18
|
+
+ event.preventDefault();
|
|
19
|
+
+ const newFormData = deleteBlock(properties, block, intl);
|
|
20
|
+
+ ReactDOM.unstable_batchedUpdates(() => {
|
|
21
|
+
+ onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
22
|
+
+ onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
23
|
+
+ onSelectBlock(otherBlockId);
|
|
24
|
+
+ });
|
|
25
|
+
+ return true;
|
|
26
|
+
+ }
|
|
27
|
+
+
|
|
28
|
+
event.stopPropagation();
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Shadow patch for #8347: Guard joinWithPreviousBlock against non-slate
|
|
2
|
+
previous blocks (e.g., image). Without this, getBlockEndAsRange and
|
|
3
|
+
mergeSlateWithBlockBackward crash because non-slate blocks have no
|
|
4
|
+
`value` property. See:
|
|
5
|
+
|
|
6
|
+
- https://github.com/plone/volto/issues/8347
|
|
7
|
+
- https://github.com/plone/volto/pull/8355
|
|
8
|
+
|
|
9
|
+
Remove this shadow once a @plone/volto-slate release containing the fix
|
|
10
|
+
is published and the dependency is bumped in frontend/package.json.
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import castArray from 'lodash/castArray';
|
|
2
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
3
|
+
import { Editor, Transforms, Range, Node, Text } from 'slate';
|
|
4
|
+
import { ReactEditor } from 'slate-react';
|
|
5
|
+
import { isCursorInList } from '@plone/volto-slate/utils/lists';
|
|
6
|
+
import { makeEditor } from '@plone/volto-slate/utils/editor';
|
|
7
|
+
import { LI } from '@plone/volto-slate/constants';
|
|
8
|
+
import config from '@plone/volto/registry';
|
|
9
|
+
|
|
10
|
+
// Shadow patch for #8347: Backspace behaving erratically near a styled or
|
|
11
|
+
// link text. This file replaces @plone/volto-slate/src/utils/selection.js
|
|
12
|
+
// (v18.10.0) with the fixed isCursorAtBlockStart that only returns true when
|
|
13
|
+
// the caret is at the very first leaf of the block, not at offset 0 of any
|
|
14
|
+
// leaf. Remove this shadow once a volto-slate release containing the fix
|
|
15
|
+
// is published and the dependency is bumped in frontend/package.json.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* firstLeafPath.
|
|
19
|
+
*
|
|
20
|
+
* @param {} editor
|
|
21
|
+
*/
|
|
22
|
+
function firstLeafPath(editor) {
|
|
23
|
+
if (!editor.children?.length) return null;
|
|
24
|
+
let [node, path] = Node.first(editor, []);
|
|
25
|
+
while (!Text.isText(node)) {
|
|
26
|
+
if (!node.children?.length) return null;
|
|
27
|
+
[node, path] = Node.first(editor, path);
|
|
28
|
+
}
|
|
29
|
+
return path;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* isAtFirstLeaf.
|
|
34
|
+
*
|
|
35
|
+
* @param {} editor
|
|
36
|
+
* @param {} path
|
|
37
|
+
*/
|
|
38
|
+
function isAtFirstLeaf(editor, path) {
|
|
39
|
+
const first = firstLeafPath(editor);
|
|
40
|
+
if (!first) return false;
|
|
41
|
+
return path.length === first.length && path.every((n, i) => n === first[i]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the nodes with a type included in `types` in the selection (from root to leaf).
|
|
46
|
+
*
|
|
47
|
+
* @param {} editor
|
|
48
|
+
* @param {} types
|
|
49
|
+
* @param {} options
|
|
50
|
+
*/
|
|
51
|
+
export function getSelectionNodesByType(editor, types, options = {}) {
|
|
52
|
+
types = castArray(types);
|
|
53
|
+
|
|
54
|
+
return Editor.nodes(editor, {
|
|
55
|
+
match: (n) => {
|
|
56
|
+
return types.includes(n.type);
|
|
57
|
+
},
|
|
58
|
+
...options,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Is there a node with a type included in `types` in the selection (from root to leaf).
|
|
64
|
+
*/
|
|
65
|
+
export function isNodeInSelection(editor, types, options = {}) {
|
|
66
|
+
const [match] = getSelectionNodesByType(editor, types, options);
|
|
67
|
+
return !!match;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* getSelectionNodesArrayByType.
|
|
72
|
+
*
|
|
73
|
+
* @param {} editor
|
|
74
|
+
* @param {} types
|
|
75
|
+
* @param {} options
|
|
76
|
+
*/
|
|
77
|
+
export function getSelectionNodesArrayByType(editor, types, options = {}) {
|
|
78
|
+
return Array.from(getSelectionNodesByType(editor, types, options));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* getMaxRange.
|
|
83
|
+
*
|
|
84
|
+
* @param {} editor
|
|
85
|
+
*
|
|
86
|
+
* TODO: is [0] ok as a path?
|
|
87
|
+
*/
|
|
88
|
+
export function getMaxRange(editor) {
|
|
89
|
+
const maxRange = {
|
|
90
|
+
anchor: Editor.start(editor, [0]),
|
|
91
|
+
focus: Editor.end(editor, [0]),
|
|
92
|
+
};
|
|
93
|
+
return maxRange;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* selectAll.
|
|
98
|
+
*
|
|
99
|
+
* @param {} editor
|
|
100
|
+
*/
|
|
101
|
+
export function selectAll(editor) {
|
|
102
|
+
Transforms.select(editor, getMaxRange(editor));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// In the isCursorAtBlockStart/End functions maybe use a part of these pieces of code:
|
|
106
|
+
// Range.isCollapsed(editor.selection) &&
|
|
107
|
+
// Point.equals(editor.selection.anchor, Editor.start(editor, []))
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* isCursorAtBlockStart.
|
|
111
|
+
*
|
|
112
|
+
* @param {} editor
|
|
113
|
+
*/
|
|
114
|
+
export function isCursorAtBlockStart(editor) {
|
|
115
|
+
// It does not work properly with lists
|
|
116
|
+
|
|
117
|
+
if (editor.selection && Range.isCollapsed(editor.selection)) {
|
|
118
|
+
const { anchor } = editor.selection;
|
|
119
|
+
if (anchor.offset !== 0) return false;
|
|
120
|
+
if (!editor.children?.length) return false;
|
|
121
|
+
return isAtFirstLeaf(editor, anchor.path);
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* isCursorAtBlockEnd.
|
|
128
|
+
*
|
|
129
|
+
* @param {} editor
|
|
130
|
+
*/
|
|
131
|
+
export function isCursorAtBlockEnd(editor) {
|
|
132
|
+
// fixSelection(editor);
|
|
133
|
+
|
|
134
|
+
// if the selection is collapsed
|
|
135
|
+
if (editor.selection && Range.isCollapsed(editor.selection)) {
|
|
136
|
+
const anchor = editor.selection?.anchor || {};
|
|
137
|
+
|
|
138
|
+
// the last block node in the editor
|
|
139
|
+
const [node] = Node.last(editor, []);
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
// if the node with the selection is the last block node
|
|
143
|
+
Node.get(editor, anchor.path) === node &&
|
|
144
|
+
// if the collapsed selection is at the end of the last block node
|
|
145
|
+
anchor.offset === node.text.length
|
|
146
|
+
) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const defaultListItemValue = () => {
|
|
154
|
+
const { slate } = config.settings;
|
|
155
|
+
const dv = slate.defaultValue();
|
|
156
|
+
dv[0].type = LI;
|
|
157
|
+
return dv;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* getFragmentFromStartOfSelectionToEndOfEditor.
|
|
162
|
+
*
|
|
163
|
+
* @param {} editor
|
|
164
|
+
*/
|
|
165
|
+
export function getFragmentFromStartOfSelectionToEndOfEditor(
|
|
166
|
+
editor,
|
|
167
|
+
initialSelection,
|
|
168
|
+
) {
|
|
169
|
+
if (typeof initialSelection === 'undefined') {
|
|
170
|
+
initialSelection = editor.selection;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { slate } = config.settings;
|
|
174
|
+
const range = Editor.range(
|
|
175
|
+
editor,
|
|
176
|
+
Range.isBackward(initialSelection)
|
|
177
|
+
? initialSelection.focus
|
|
178
|
+
: initialSelection.anchor,
|
|
179
|
+
Editor.end(editor, []),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// this is the case when the fragment is empty, and we must return
|
|
183
|
+
// empty fragment but without formatting
|
|
184
|
+
if (Range.isCollapsed(range)) {
|
|
185
|
+
if (isCursorInList(editor)) {
|
|
186
|
+
return defaultListItemValue();
|
|
187
|
+
} else {
|
|
188
|
+
return slate.defaultValue();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// immer doesn't like editor.savedSelection
|
|
193
|
+
const newEditor = makeEditor();
|
|
194
|
+
newEditor.children = cloneDeep(editor.children);
|
|
195
|
+
return Editor.fragment(newEditor, range);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* getFragmentFromBeginningOfEditorToStartOfSelection.
|
|
200
|
+
*
|
|
201
|
+
* @param {} editor
|
|
202
|
+
*/
|
|
203
|
+
export function getFragmentFromBeginningOfEditorToStartOfSelection(
|
|
204
|
+
editor,
|
|
205
|
+
initialSelection,
|
|
206
|
+
) {
|
|
207
|
+
if (typeof initialSelection === 'undefined') {
|
|
208
|
+
initialSelection = editor.selection;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// immer doesn't like editor.savedSelection
|
|
212
|
+
// TODO: there's a bug here related to splitting lists
|
|
213
|
+
const newEditor = makeEditor();
|
|
214
|
+
newEditor.children = cloneDeep(editor.children);
|
|
215
|
+
|
|
216
|
+
return Editor.fragment(
|
|
217
|
+
newEditor,
|
|
218
|
+
Editor.range(
|
|
219
|
+
newEditor,
|
|
220
|
+
[],
|
|
221
|
+
Range.isBackward(initialSelection)
|
|
222
|
+
? initialSelection.focus
|
|
223
|
+
: initialSelection.anchor,
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @returns {boolean} true if editor contains a range selection (active
|
|
230
|
+
* selection or at least a saved selection)
|
|
231
|
+
* @param {Editor} editor
|
|
232
|
+
*/
|
|
233
|
+
export function hasRangeSelection(editor, useSavedSelection = true) {
|
|
234
|
+
const { selection } = editor;
|
|
235
|
+
const savedSelection = editor.getSavedSelection();
|
|
236
|
+
|
|
237
|
+
const range = ReactEditor.isFocused(editor)
|
|
238
|
+
? selection || (useSavedSelection ? savedSelection : null)
|
|
239
|
+
: savedSelection;
|
|
240
|
+
|
|
241
|
+
if (!range) {
|
|
242
|
+
// console.log('no range', editor);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const res = Range.isExpanded(range);
|
|
247
|
+
// console.log('call hasRange', res);
|
|
248
|
+
return res;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function parseDefaultSelection(editor, defaultSelection) {
|
|
252
|
+
if (defaultSelection) {
|
|
253
|
+
if (defaultSelection === 'start') {
|
|
254
|
+
const [, path] = Node.first(editor, []);
|
|
255
|
+
const newSel = {
|
|
256
|
+
anchor: { path, offset: 0 },
|
|
257
|
+
focus: { path, offset: 0 },
|
|
258
|
+
};
|
|
259
|
+
return newSel;
|
|
260
|
+
}
|
|
261
|
+
if (defaultSelection === 'end') {
|
|
262
|
+
const [leaf, path] = Node.last(editor, []);
|
|
263
|
+
const newSel = {
|
|
264
|
+
anchor: { path, offset: (leaf.text || '').length },
|
|
265
|
+
focus: { path, offset: (leaf.text || '').length },
|
|
266
|
+
};
|
|
267
|
+
return newSel;
|
|
268
|
+
}
|
|
269
|
+
return defaultSelection;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- a/selection.js
|
|
2
|
+
+++ b/selection.js
|
|
3
|
+
@@ -1,12 +1,46 @@
|
|
4
|
+
import castArray from 'lodash/castArray';
|
|
5
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
6
|
+
-import { Editor, Transforms, Range, Node } from 'slate';
|
|
7
|
+
+import { Editor, Transforms, Range, Node, Text } from 'slate';
|
|
8
|
+
import { ReactEditor } from 'slate-react';
|
|
9
|
+
import { isCursorInList } from '@plone/volto-slate/utils/lists';
|
|
10
|
+
import { makeEditor } from '@plone/volto-slate/utils/editor';
|
|
11
|
+
import { LI } from '@plone/volto-slate/constants';
|
|
12
|
+
import config from '@plone/volto/registry';
|
|
13
|
+
|
|
14
|
+
+// Shadow patch for #8347: Backspace behaving erratically near a styled or
|
|
15
|
+
+// link text. This file replaces @plone/volto-slate/src/utils/selection.js
|
|
16
|
+
+// (v18.10.0) with the fixed isCursorAtBlockStart that only returns true when
|
|
17
|
+
+// the caret is at the very first leaf of the block, not at offset 0 of any
|
|
18
|
+
+// leaf. Remove this shadow once a volto-slate release containing the fix
|
|
19
|
+
+// is published and the dependency is bumped in frontend/package.json.
|
|
20
|
+
+
|
|
21
|
+
+/**
|
|
22
|
+
+ * firstLeafPath.
|
|
23
|
+
+ *
|
|
24
|
+
+ * @param {} editor
|
|
25
|
+
+ */
|
|
26
|
+
+function firstLeafPath(editor) {
|
|
27
|
+
+ if (!editor.children?.length) return null;
|
|
28
|
+
+ let [node, path] = Node.first(editor, []);
|
|
29
|
+
+ while (!Text.isText(node)) {
|
|
30
|
+
+ if (!node.children?.length) return null;
|
|
31
|
+
+ [node, path] = Node.first(editor, path);
|
|
32
|
+
+ }
|
|
33
|
+
+ return path;
|
|
34
|
+
+}
|
|
35
|
+
+
|
|
36
|
+
+/**
|
|
37
|
+
+ * isAtFirstLeaf.
|
|
38
|
+
+ *
|
|
39
|
+
+ * @param {} editor
|
|
40
|
+
+ * @param {} path
|
|
41
|
+
+ */
|
|
42
|
+
+function isAtFirstLeaf(editor, path) {
|
|
43
|
+
+ const first = firstLeafPath(editor);
|
|
44
|
+
+ if (!first) return false;
|
|
45
|
+
+ return path.length === first.length && path.every((n, i) => n === first[i]);
|
|
46
|
+
+}
|
|
47
|
+
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the nodes with a type included in `types` in the selection (from root to leaf).
|
|
50
|
+
*
|
|
51
|
+
@@ -82,8 +116,9 @@
|
|
52
|
+
|
|
53
|
+
if (editor.selection && Range.isCollapsed(editor.selection)) {
|
|
54
|
+
const { anchor } = editor.selection;
|
|
55
|
+
- // Check if cursor is at offset 0 of any leaf node (not just the first one)
|
|
56
|
+
- return anchor.offset === 0;
|
|
57
|
+
+ if (anchor.offset !== 0) return false;
|
|
58
|
+
+ if (!editor.children?.length) return false;
|
|
59
|
+
+ return isAtFirstLeaf(editor, anchor.path);
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Shadow patch for #8347: Backspace behaving erratically near a styled or link text. See:
|
|
2
|
+
|
|
3
|
+
- https://github.com/plone/volto/issues/8347
|
|
4
|
+
- https://github.com/plone/volto/pull/8355
|
|
5
|
+
|
|
6
|
+
Remove this shadow once a @plone/volto-slate release containing the fix
|
|
7
|
+
is published and the dependency is bumped in frontend/package.json.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { isCursorAtBlockStart } from './selection';
|
|
2
|
+
|
|
3
|
+
// `isCursorAtBlockStart` only inspects `editor.selection` and `editor.children`,
|
|
4
|
+
// so plain editor-shaped objects are enough (no full Volto config registry).
|
|
5
|
+
const makeEditor = (children, selection) => ({ children, selection });
|
|
6
|
+
const at = (path, offset) => ({ path, offset });
|
|
7
|
+
const collapsed = (path, offset) => ({
|
|
8
|
+
anchor: at(path, offset),
|
|
9
|
+
focus: at(path, offset),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('isCursorAtBlockStart (shadow patch for #8347)', () => {
|
|
13
|
+
it('returns true when the caret is at the first leaf of the block', () => {
|
|
14
|
+
const editor = makeEditor(
|
|
15
|
+
[{ type: 'p', children: [{ text: 'Hello' }] }],
|
|
16
|
+
collapsed([0, 0], 0),
|
|
17
|
+
);
|
|
18
|
+
expect(isCursorAtBlockStart(editor)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns true for the first leaf of a nested list item (path [0,0,0])', () => {
|
|
22
|
+
const editor = makeEditor(
|
|
23
|
+
[
|
|
24
|
+
{
|
|
25
|
+
type: 'ul',
|
|
26
|
+
children: [
|
|
27
|
+
{
|
|
28
|
+
type: 'li',
|
|
29
|
+
children: [{ text: 'first item' }],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
collapsed([0, 0, 0], 0),
|
|
35
|
+
);
|
|
36
|
+
expect(isCursorAtBlockStart(editor)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns false at offset 0 of a non-first leaf inside a list item (#8347)', () => {
|
|
40
|
+
// A list item: {text:""} + link + {text:""} — the empty text leaf after
|
|
41
|
+
// the link is at path [0,0,2]. Backspace there must NOT count as block start.
|
|
42
|
+
const editor = makeEditor(
|
|
43
|
+
[
|
|
44
|
+
{
|
|
45
|
+
type: 'ul',
|
|
46
|
+
children: [
|
|
47
|
+
{
|
|
48
|
+
type: 'li',
|
|
49
|
+
children: [
|
|
50
|
+
{ text: '' },
|
|
51
|
+
{
|
|
52
|
+
type: 'link',
|
|
53
|
+
data: { url: 'https://demo.plone.org/' },
|
|
54
|
+
children: [{ text: 'Plone 6 (this site)' }],
|
|
55
|
+
},
|
|
56
|
+
{ text: '' },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
collapsed([0, 0, 2], 0),
|
|
63
|
+
);
|
|
64
|
+
expect(isCursorAtBlockStart(editor)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns false at offset 0 of a non-first leaf of a plain paragraph', () => {
|
|
68
|
+
const editor = makeEditor(
|
|
69
|
+
[
|
|
70
|
+
{
|
|
71
|
+
type: 'p',
|
|
72
|
+
children: [
|
|
73
|
+
{ text: 'before' },
|
|
74
|
+
{
|
|
75
|
+
type: 'link',
|
|
76
|
+
data: { url: 'https://example.com' },
|
|
77
|
+
children: [{ text: 'link' }],
|
|
78
|
+
},
|
|
79
|
+
{ text: 'after' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
collapsed([0, 2], 0),
|
|
84
|
+
);
|
|
85
|
+
expect(isCursorAtBlockStart(editor)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns false when the offset is greater than 0', () => {
|
|
89
|
+
const editor = makeEditor(
|
|
90
|
+
[{ type: 'p', children: [{ text: 'Hello' }] }],
|
|
91
|
+
collapsed([0, 0], 2),
|
|
92
|
+
);
|
|
93
|
+
expect(isCursorAtBlockStart(editor)).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns false when there is no selection', () => {
|
|
97
|
+
const editor = makeEditor(
|
|
98
|
+
[{ type: 'p', children: [{ text: 'Hi' }] }],
|
|
99
|
+
null,
|
|
100
|
+
);
|
|
101
|
+
expect(isCursorAtBlockStart(editor)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns false when the selection is expanded', () => {
|
|
105
|
+
const editor = makeEditor([{ type: 'p', children: [{ text: 'Hello' }] }], {
|
|
106
|
+
anchor: at([0, 0], 0),
|
|
107
|
+
focus: at([0, 0], 3),
|
|
108
|
+
});
|
|
109
|
+
expect(isCursorAtBlockStart(editor)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns false when the editor has no children', () => {
|
|
113
|
+
const editor = makeEditor([], collapsed([0, 0], 0));
|
|
114
|
+
expect(isCursorAtBlockStart(editor)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import ReactDOM from 'react-dom';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
import {
|
|
4
|
+
addBlock,
|
|
5
|
+
changeBlock,
|
|
6
|
+
insertBlock,
|
|
7
|
+
blockHasValue,
|
|
8
|
+
getBlocksFieldname,
|
|
9
|
+
getBlocksLayoutFieldname,
|
|
10
|
+
} from '@plone/volto/helpers/Blocks/Blocks';
|
|
11
|
+
import { Transforms, Editor, Node, Text } from 'slate';
|
|
12
|
+
import { serializeNodesToText } from '@plone/volto-slate/editor/render';
|
|
13
|
+
import omit from 'lodash/omit';
|
|
14
|
+
import config from '@plone/volto/registry';
|
|
15
|
+
|
|
16
|
+
function fromEntries(pairs) {
|
|
17
|
+
const res = {};
|
|
18
|
+
pairs.forEach((p) => {
|
|
19
|
+
res[p[0]] = p[1];
|
|
20
|
+
});
|
|
21
|
+
return res;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mergeSlateWithBlockBackward(editor, prevBlock, event) {
|
|
25
|
+
// Merge the current block content into the previous block
|
|
26
|
+
// The current block content should be appended to the previous block
|
|
27
|
+
// and the cursor should be positioned at the start of what was the current block content
|
|
28
|
+
|
|
29
|
+
const prevValue = [...prevBlock.value];
|
|
30
|
+
const currentValue = [...editor.children];
|
|
31
|
+
|
|
32
|
+
const lastNode = prevValue[prevValue.length - 1];
|
|
33
|
+
const firstNode = currentValue[0];
|
|
34
|
+
let merged;
|
|
35
|
+
let cursor;
|
|
36
|
+
|
|
37
|
+
if (lastNode && firstNode && lastNode.type === firstNode.type) {
|
|
38
|
+
// If the last node of previous block and first node of current block have the same type,
|
|
39
|
+
// merge their children together
|
|
40
|
+
const mergedFirstNode = {
|
|
41
|
+
...lastNode,
|
|
42
|
+
children: [...lastNode.children, ...firstNode.children],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
merged = [
|
|
46
|
+
...prevValue.slice(0, -1),
|
|
47
|
+
mergedFirstNode,
|
|
48
|
+
...currentValue.slice(1),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// If the last child of the previous block's last node is a text node,
|
|
52
|
+
// Slate's normalization will merge it with the first child of the
|
|
53
|
+
// current block's first node (if it's also a text node). Point the
|
|
54
|
+
// cursor into the surviving (pre-merge) text node at the offset equal
|
|
55
|
+
// to its text length, so that after normalization the cursor lands at
|
|
56
|
+
// the correct position — the start of what was the current block's
|
|
57
|
+
// content. Without this, the cursor points to a child index that no
|
|
58
|
+
// longer exists after normalization, causing
|
|
59
|
+
// "Cannot find a descendant at path [..]" crashes. (#8347)
|
|
60
|
+
const lastChild = lastNode.children[lastNode.children.length - 1];
|
|
61
|
+
if (Text.isText(lastChild)) {
|
|
62
|
+
cursor = {
|
|
63
|
+
anchor: {
|
|
64
|
+
path: [prevValue.length - 1, lastNode.children.length - 1],
|
|
65
|
+
offset: lastChild.text.length,
|
|
66
|
+
},
|
|
67
|
+
focus: {
|
|
68
|
+
path: [prevValue.length - 1, lastNode.children.length - 1],
|
|
69
|
+
offset: lastChild.text.length,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
cursor = {
|
|
74
|
+
anchor: {
|
|
75
|
+
path: [prevValue.length - 1, lastNode.children.length],
|
|
76
|
+
offset: 0,
|
|
77
|
+
},
|
|
78
|
+
focus: {
|
|
79
|
+
path: [prevValue.length - 1, lastNode.children.length],
|
|
80
|
+
offset: 0,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Otherwise, just append the current block content to the previous block
|
|
86
|
+
merged = [...prevValue, ...currentValue];
|
|
87
|
+
// Position cursor at the start of the merged content (where current block was inserted)
|
|
88
|
+
// The current block content starts at index prevValue.length in the merged array
|
|
89
|
+
cursor = {
|
|
90
|
+
anchor: {
|
|
91
|
+
path: [prevValue.length, 0],
|
|
92
|
+
offset: 0,
|
|
93
|
+
},
|
|
94
|
+
focus: {
|
|
95
|
+
path: [prevValue.length, 0],
|
|
96
|
+
offset: 0,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Update the editor with the merged content
|
|
102
|
+
editor.children = merged;
|
|
103
|
+
|
|
104
|
+
return cursor;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function mergeSlateWithBlockForward(editor, nextBlock, event) {
|
|
108
|
+
// To work around current architecture limitations, read the value from next
|
|
109
|
+
// block. Replace it in the current editor (over which we have control), join
|
|
110
|
+
// with current block value, then use this result for next block, delete
|
|
111
|
+
// current block
|
|
112
|
+
|
|
113
|
+
const next = nextBlock?.value;
|
|
114
|
+
// Safeguard: if next block has no value or an empty value, there is nothing to merge
|
|
115
|
+
if (!Array.isArray(next) || next.length === 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// collapse the selection to its start point
|
|
120
|
+
Transforms.collapse(editor, { edge: 'end' });
|
|
121
|
+
Transforms.insertNodes(editor, next, {
|
|
122
|
+
at: Editor.end(editor, []),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
Editor.deleteForward(editor, { unit: 'character' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function syncCreateSlateBlock(value) {
|
|
129
|
+
const id = uuid();
|
|
130
|
+
const block = {
|
|
131
|
+
'@type': 'slate',
|
|
132
|
+
value: JSON.parse(JSON.stringify(value)),
|
|
133
|
+
plaintext: serializeNodesToText(value),
|
|
134
|
+
};
|
|
135
|
+
return [id, block];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createImageBlock(url, index, props, intl) {
|
|
139
|
+
const { properties, onChangeField, onSelectBlock } = props;
|
|
140
|
+
const blocksFieldname = getBlocksFieldname(properties);
|
|
141
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
142
|
+
|
|
143
|
+
const currBlockId = properties.blocks_layout.items[index];
|
|
144
|
+
const currBlockHasValue = blockHasValue(properties.blocks[currBlockId]);
|
|
145
|
+
let id, newFormData;
|
|
146
|
+
|
|
147
|
+
if (currBlockHasValue) {
|
|
148
|
+
[id, newFormData] = addBlock(properties, 'image', index + 1, null, intl);
|
|
149
|
+
newFormData = changeBlock(newFormData, id, { '@type': 'image', url });
|
|
150
|
+
} else {
|
|
151
|
+
[id, newFormData] = insertBlock(properties, currBlockId, {
|
|
152
|
+
'@type': 'image',
|
|
153
|
+
url,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
158
|
+
onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
159
|
+
onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
160
|
+
onSelectBlock(id);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const createAndSelectNewBlockAfter = (editor, blockValue, intl) => {
|
|
165
|
+
const blockProps = editor.getBlockProps();
|
|
166
|
+
|
|
167
|
+
const { onSelectBlock, properties, index, onChangeField } = blockProps;
|
|
168
|
+
|
|
169
|
+
const [blockId, formData] = addBlock(
|
|
170
|
+
properties,
|
|
171
|
+
'slate',
|
|
172
|
+
index + 1,
|
|
173
|
+
null,
|
|
174
|
+
intl,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const options = {
|
|
178
|
+
'@type': 'slate',
|
|
179
|
+
value: JSON.parse(JSON.stringify(blockValue)),
|
|
180
|
+
plaintext: serializeNodesToText(blockValue),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const newFormData = changeBlock(formData, blockId, options);
|
|
184
|
+
|
|
185
|
+
const blocksFieldname = getBlocksFieldname(properties);
|
|
186
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
187
|
+
// console.log('layout', blocksLayoutFieldname, newFormData);
|
|
188
|
+
|
|
189
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
190
|
+
blockProps.saveSlateBlockSelection(blockId, 'start');
|
|
191
|
+
onChangeField(blocksFieldname, newFormData[blocksFieldname]);
|
|
192
|
+
onChangeField(blocksLayoutFieldname, newFormData[blocksLayoutFieldname]);
|
|
193
|
+
onSelectBlock(blockId);
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export function getNextVoltoBlock(index, properties) {
|
|
198
|
+
// TODO: look for any next slate block
|
|
199
|
+
// join this block with previous block, if previous block is slate
|
|
200
|
+
const blocksFieldname = getBlocksFieldname(properties);
|
|
201
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
202
|
+
|
|
203
|
+
const blocks_layout = properties[blocksLayoutFieldname];
|
|
204
|
+
|
|
205
|
+
if (index === blocks_layout.items.length) return;
|
|
206
|
+
|
|
207
|
+
const nextBlockId = blocks_layout.items[index + 1];
|
|
208
|
+
const nextBlock = properties[blocksFieldname][nextBlockId];
|
|
209
|
+
|
|
210
|
+
return [nextBlock, nextBlockId];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function getPreviousVoltoBlock(index, properties) {
|
|
214
|
+
// TODO: look for any prev slate block
|
|
215
|
+
if (index === 0) return;
|
|
216
|
+
|
|
217
|
+
const blocksFieldname = getBlocksFieldname(properties);
|
|
218
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
219
|
+
|
|
220
|
+
const blocks_layout = properties[blocksLayoutFieldname];
|
|
221
|
+
const prevBlockId = blocks_layout.items[index - 1];
|
|
222
|
+
const prevBlock = properties[blocksFieldname][prevBlockId];
|
|
223
|
+
|
|
224
|
+
return [prevBlock, prevBlockId];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// //check for existing img children
|
|
228
|
+
// const checkContainImg = (elements) => {
|
|
229
|
+
// var check = false;
|
|
230
|
+
// elements.forEach((e) =>
|
|
231
|
+
// e.children.forEach((c) => {
|
|
232
|
+
// if (c && c.type && c.type === 'img') {
|
|
233
|
+
// check = true;
|
|
234
|
+
// }
|
|
235
|
+
// }),
|
|
236
|
+
// );
|
|
237
|
+
// return check;
|
|
238
|
+
// };
|
|
239
|
+
|
|
240
|
+
// //check for existing table children
|
|
241
|
+
// const checkContainTable = (elements) => {
|
|
242
|
+
// var check = false;
|
|
243
|
+
// elements.forEach((e) => {
|
|
244
|
+
// if (e && e.type && e.type === 'table') {
|
|
245
|
+
// check = true;
|
|
246
|
+
// }
|
|
247
|
+
// });
|
|
248
|
+
// return check;
|
|
249
|
+
// };
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The editor has the properties `dataTransferHandlers` (object) and
|
|
253
|
+
* `dataTransferFormatsOrder` and in `dataTransferHandlers` are functions which
|
|
254
|
+
* sometimes must call this function. Some types of data storeable in Slate
|
|
255
|
+
* documents can be and should be put into separate Volto blocks. The
|
|
256
|
+
* `deconstructToVoltoBlocks` function scans the contents of the Slate document
|
|
257
|
+
* and, through configured Volto block emitters, it outputs separate Volto
|
|
258
|
+
* blocks into the same Volto page form. The `deconstructToVoltoBlocks` function
|
|
259
|
+
* should be called only in key places where it is necessary.
|
|
260
|
+
*
|
|
261
|
+
* @example See the `src/editor/extensions/insertData.js` file.
|
|
262
|
+
*
|
|
263
|
+
* @param {Editor} editor The Slate editor object which should be deconstructed
|
|
264
|
+
* if possible.
|
|
265
|
+
*
|
|
266
|
+
* @returns {Promise}
|
|
267
|
+
*/
|
|
268
|
+
export function deconstructToVoltoBlocks(editor) {
|
|
269
|
+
// Explodes editor content into separate blocks
|
|
270
|
+
// If the editor has multiple top-level children, split the current block
|
|
271
|
+
// into multiple slate blocks. This will delete and replace the current
|
|
272
|
+
// block.
|
|
273
|
+
//
|
|
274
|
+
// It returns a promise that, when resolved, will pass a list of Volto block
|
|
275
|
+
// ids that were affected
|
|
276
|
+
//
|
|
277
|
+
// For the Volto blocks manipulation we do low-level changes to the context
|
|
278
|
+
// form state, as that ensures a better performance (no un-needed UI updates)
|
|
279
|
+
|
|
280
|
+
if (!editor.getBlockProps) return;
|
|
281
|
+
|
|
282
|
+
const blockProps = editor.getBlockProps();
|
|
283
|
+
const { slate } = config.settings;
|
|
284
|
+
const { voltoBlockEmiters } = slate;
|
|
285
|
+
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
if (!editor?.children) return;
|
|
288
|
+
|
|
289
|
+
if (editor.children.length === 1) {
|
|
290
|
+
return resolve([blockProps.block]);
|
|
291
|
+
}
|
|
292
|
+
const { properties, onChangeField, onSelectBlock } = editor.getBlockProps();
|
|
293
|
+
const blocksFieldname = getBlocksFieldname(properties);
|
|
294
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(properties);
|
|
295
|
+
|
|
296
|
+
const { index } = blockProps;
|
|
297
|
+
let blocks = [];
|
|
298
|
+
|
|
299
|
+
// TODO: should use Editor.levels() instead of Node.children
|
|
300
|
+
const pathRefs = Array.from(Node.children(editor, [])).map(([, path]) =>
|
|
301
|
+
Editor.pathRef(editor, path),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
for (const pathRef of pathRefs) {
|
|
305
|
+
// extra nodes are always extracted after the text node
|
|
306
|
+
let extras = voltoBlockEmiters
|
|
307
|
+
.map((emit) => emit(editor, pathRef))
|
|
308
|
+
.flat(1);
|
|
309
|
+
|
|
310
|
+
// The node might have been replaced with a Volto block
|
|
311
|
+
if (pathRef.current) {
|
|
312
|
+
const [childNode] = Editor.node(editor, pathRef.current);
|
|
313
|
+
if (childNode && !Editor.isEmpty(editor, childNode))
|
|
314
|
+
blocks.push(syncCreateSlateBlock([childNode]));
|
|
315
|
+
}
|
|
316
|
+
blocks = [...blocks, ...extras];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const blockids = blocks.map((b) => b[0]);
|
|
320
|
+
|
|
321
|
+
// TODO: add the placeholder block, because we remove it
|
|
322
|
+
// (when we remove the current block)
|
|
323
|
+
|
|
324
|
+
const blocksData = omit(
|
|
325
|
+
{
|
|
326
|
+
...properties[blocksFieldname],
|
|
327
|
+
...fromEntries(blocks),
|
|
328
|
+
},
|
|
329
|
+
blockProps.block,
|
|
330
|
+
);
|
|
331
|
+
const layoutData = {
|
|
332
|
+
...properties[blocksLayoutFieldname],
|
|
333
|
+
items: [
|
|
334
|
+
...properties[blocksLayoutFieldname].items.slice(0, index),
|
|
335
|
+
...blockids,
|
|
336
|
+
...properties[blocksLayoutFieldname].items.slice(index),
|
|
337
|
+
].filter((id) => id !== blockProps.block),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// TODO: use onChangeFormData instead of this API style
|
|
341
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
342
|
+
onChangeField(blocksFieldname, blocksData);
|
|
343
|
+
onChangeField(blocksLayoutFieldname, layoutData);
|
|
344
|
+
onSelectBlock(blockids[blockids.length - 1]);
|
|
345
|
+
// resolve(blockids);
|
|
346
|
+
// or rather this?
|
|
347
|
+
Promise.resolve().then(resolve(blockids));
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--- node_modules/@plone/volto-slate/src/utils/volto-blocks.js 2026-06-19 16:56:50.131820782 +0300
|
|
2
|
+
+++ src/addons/volto-eea-website-theme/src/customizations/@plone/volto-slate/utils/volto-blocks.js 2026-06-24 18:09:06.099715266 +0300
|
|
3
|
+
@@ -8,7 +8,7 @@
|
|
4
|
+
getBlocksFieldname,
|
|
5
|
+
getBlocksLayoutFieldname,
|
|
6
|
+
} from '@plone/volto/helpers/Blocks/Blocks';
|
|
7
|
+
-import { Transforms, Editor, Node } from 'slate';
|
|
8
|
+
+import { Transforms, Editor, Node, Text } from 'slate';
|
|
9
|
+
import { serializeNodesToText } from '@plone/volto-slate/editor/render';
|
|
10
|
+
import omit from 'lodash/omit';
|
|
11
|
+
import config from '@plone/volto/registry';
|
|
12
|
+
@@ -48,16 +48,39 @@
|
|
13
|
+
...currentValue.slice(1),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
- cursor = {
|
|
17
|
+
- anchor: {
|
|
18
|
+
- path: [prevValue.length - 1, lastNode.children.length],
|
|
19
|
+
- offset: 0,
|
|
20
|
+
- },
|
|
21
|
+
- focus: {
|
|
22
|
+
- path: [prevValue.length - 1, lastNode.children.length],
|
|
23
|
+
- offset: 0,
|
|
24
|
+
- },
|
|
25
|
+
- };
|
|
26
|
+
+ // If the last child of the previous block's last node is a text node,
|
|
27
|
+
+ // Slate's normalization will merge it with the first child of the
|
|
28
|
+
+ // current block's first node (if it's also a text node). Point the
|
|
29
|
+
+ // cursor into the surviving (pre-merge) text node at the offset equal
|
|
30
|
+
+ // to its text length, so that after normalization the cursor lands at
|
|
31
|
+
+ // the correct position — the start of what was the current block's
|
|
32
|
+
+ // content. Without this, the cursor points to a child index that no
|
|
33
|
+
+ // longer exists after normalization, causing
|
|
34
|
+
+ // "Cannot find a descendant at path [..]" crashes. (#8347)
|
|
35
|
+
+ const lastChild = lastNode.children[lastNode.children.length - 1];
|
|
36
|
+
+ if (Text.isText(lastChild)) {
|
|
37
|
+
+ cursor = {
|
|
38
|
+
+ anchor: {
|
|
39
|
+
+ path: [prevValue.length - 1, lastNode.children.length - 1],
|
|
40
|
+
+ offset: lastChild.text.length,
|
|
41
|
+
+ },
|
|
42
|
+
+ focus: {
|
|
43
|
+
+ path: [prevValue.length - 1, lastNode.children.length - 1],
|
|
44
|
+
+ offset: lastChild.text.length,
|
|
45
|
+
+ },
|
|
46
|
+
+ };
|
|
47
|
+
+ } else {
|
|
48
|
+
+ cursor = {
|
|
49
|
+
+ anchor: {
|
|
50
|
+
+ path: [prevValue.length - 1, lastNode.children.length],
|
|
51
|
+
+ offset: 0,
|
|
52
|
+
+ },
|
|
53
|
+
+ focus: {
|
|
54
|
+
+ path: [prevValue.length - 1, lastNode.children.length],
|
|
55
|
+
+ offset: 0,
|
|
56
|
+
+ },
|
|
57
|
+
+ };
|
|
58
|
+
+ }
|
|
59
|
+
} else {
|
|
60
|
+
// Otherwise, just append the current block content to the previous block
|
|
61
|
+
merged = [...prevValue, ...currentValue];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Shadow patch for #8347: Fix cursor position after mergeSlateWithBlockBackward
|
|
2
|
+
when merging two same-type blocks. Slate normalization merges adjacent text
|
|
3
|
+
nodes, making the original cursor path invalid. See:
|
|
4
|
+
|
|
5
|
+
- https://github.com/plone/volto/issues/8347
|
|
6
|
+
- https://github.com/plone/volto/pull/8355
|
|
7
|
+
|
|
8
|
+
Remove this shadow once a @plone/volto-slate release containing the fix
|
|
9
|
+
is published and the dependency is bumped in frontend/package.json.
|