@plone/volto-slate 18.6.0 → 18.7.1

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 CHANGED
@@ -8,6 +8,18 @@
8
8
 
9
9
  <!-- towncrier release notes start -->
10
10
 
11
+ ## 18.7.1 (2025-10-10)
12
+
13
+ ### Bugfix
14
+
15
+ - Fix Backspace at start of a text block: merge current block into previous inline (no extra newline), delete the current block, and place the caret before the first character of the merged content. Also handle Enter immediately after such inline merge by splitting back into two blocks. @aryan7081 [#7373](https://github.com/plone/volto/issues/7373)
16
+
17
+ ## 18.7.0 (2025-10-07)
18
+
19
+ ### Feature
20
+
21
+ - Update translations: eu @erral [#7394](https://github.com/plone/volto/issues/7394)
22
+
11
23
  ## 18.6.0 (2025-09-26)
12
24
 
13
25
  ### Feature
@@ -3,13 +3,14 @@ msgstr ""
3
3
  "Project-Id-Version: \n"
4
4
  "Report-Msgid-Bugs-To: \n"
5
5
  "POT-Creation-Date: \n"
6
- "PO-Revision-Date: \n"
7
- "Last-Translator: \n"
8
- "Language: \n"
9
- "Language-Team: \n"
6
+ "PO-Revision-Date: 2025-09-23 17:01+0000\n"
7
+ "Last-Translator: Mikel Larreategi <mlarreategi@codesyntax.com>\n"
8
+ "Language-Team: Basque <https://hosted.weblate.org/projects/plone/volto-slate-18/eu/>\n"
9
+ "Language: eu\n"
10
10
  "Content-Type: \n"
11
11
  "Content-Transfer-Encoding: \n"
12
- "Plural-Forms: \n"
12
+ "Plural-Forms: nplurals=2; plural=n != 1;\n"
13
+ "X-Generator: Weblate 5.14-dev\n"
13
14
 
14
15
  #: editor/plugins/Link/index
15
16
  msgid "Add link"
@@ -47,7 +48,7 @@ msgstr "Ezabatu taula"
47
48
 
48
49
  #: blocks/Table/TableBlockEdit
49
50
  msgid "Divide each row into separate cells"
50
- msgstr "Banatu lerro bakoitza gelaxka desberdinetan"
51
+ msgstr "Zatitu lerro bakoitza zeldatan"
51
52
 
52
53
  #: elementEditor/messages
53
54
  msgid "Edit element"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "18.6.0",
3
+ "version": "18.7.1",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -19,6 +19,7 @@ import {
19
19
  slashMenu,
20
20
  cancelEsc,
21
21
  } from './keyboard';
22
+ import { splitAtSeam } from './keyboard/splitAtSeam';
22
23
  import { withDeleteSelectionOnEnter } from '@plone/volto-slate/editor/extensions';
23
24
  import {
24
25
  breakList,
@@ -66,6 +67,7 @@ export default function applyConfig(config) {
66
67
  Enter: [
67
68
  slashMenu,
68
69
  unwrapEmptyString,
70
+ splitAtSeam,
69
71
  softBreak, // Handles shift+Enter as a newline (<br/>)
70
72
  ],
71
73
  ArrowUp: [
@@ -5,8 +5,8 @@ import { Editor } from 'slate';
5
5
  import {
6
6
  getPreviousVoltoBlock,
7
7
  getNextVoltoBlock,
8
- mergeSlateWithBlockBackward,
9
8
  mergeSlateWithBlockForward,
9
+ mergeSlateWithBlockBackward,
10
10
  } from '@plone/volto-slate/utils/volto-blocks';
11
11
  import {
12
12
  isCursorAtBlockStart,
@@ -76,21 +76,22 @@ export function joinWithPreviousBlock({ editor, event }, intl) {
76
76
 
77
77
  // Else the editor contains characters, so we merge the current block's
78
78
  // `editor` with the block before, `otherBlock`.
79
- const cursor = mergeSlateWithBlockBackward(editor, otherBlock);
79
+ const cursor = mergeSlateWithBlockBackward(editor, otherBlock, event);
80
80
 
81
- const combined = JSON.parse(JSON.stringify(editor.children));
81
+ // Get the merged content from the editor
82
+ const merged = JSON.parse(JSON.stringify(editor.children));
82
83
 
83
84
  // // TODO: don't remove undo history, etc Should probably save both undo
84
85
  // // histories, so that the blocks are split, the undos can be restored??
85
86
 
86
87
  // const cursor = getBlockEndAsRange(otherBlock);
88
+
87
89
  const formData = changeBlock(properties, otherBlockId, {
88
90
  '@type': 'slate', // TODO: use a constant specified in src/constants.js instead of 'slate'
89
- value: combined,
90
- plaintext: serializeNodesToText(combined || []),
91
+ value: merged,
92
+ plaintext: serializeNodesToText(merged || []),
91
93
  });
92
94
  const newFormData = deleteBlock(formData, block, intl);
93
-
94
95
  ReactDOM.unstable_batchedUpdates(() => {
95
96
  saveSlateBlockSelection(otherBlockId, cursor);
96
97
  onChangeField(blocksFieldname, newFormData[blocksFieldname]);
@@ -0,0 +1,44 @@
1
+ import ReactDOM from 'react-dom';
2
+ import { Editor, Range } from 'slate';
3
+ import { splitEditorInTwoFragments } from '@plone/volto-slate/utils/ops';
4
+ import { setEditorContent } from '@plone/volto-slate/utils/editor';
5
+ import { createAndSelectNewBlockAfter } from '@plone/volto-slate/utils/volto-blocks';
6
+
7
+ /**
8
+ * Handle Enter after an inline Backspace-merge seam.
9
+ *
10
+ * Detects when the caret is at offset 0 of a leaf that is not the first
11
+ * leaf of a top-level node (e.g., after merging two blocks of same type),
12
+ * and splits the current Slate block into two Volto Slate blocks.
13
+ */
14
+ export function splitAtSeam({ editor, event }, intl) {
15
+ const { selection } = editor;
16
+ if (!selection || !Range.isCollapsed(selection)) return;
17
+
18
+ const point = selection.anchor;
19
+ if (point.offset !== 0) return;
20
+
21
+ // We are interested in positions like [paragraphIndex, leafIndex]
22
+ // where leafIndex > 0 (caret at the beginning of a subsequent leaf)
23
+ if (!point.path || point.path.length < 2) return;
24
+
25
+ const leafIndex = point.path[1];
26
+ if (leafIndex <= 0) return; // first leaf, let default handlers run
27
+
28
+ // Ensure parent is a top-level block (parent path length 1)
29
+ const parentEntry = Editor.parent(editor, point);
30
+ if (!parentEntry) return;
31
+ const parentPath = parentEntry[1];
32
+ if (!parentPath || parentPath.length !== 1) return;
33
+
34
+ event.preventDefault();
35
+ event.stopPropagation();
36
+
37
+ ReactDOM.unstable_batchedUpdates(() => {
38
+ const [top, bottom] = splitEditorInTwoFragments(editor);
39
+ createAndSelectNewBlockAfter(editor, bottom, intl);
40
+ setEditorContent(editor, top);
41
+ });
42
+
43
+ return true;
44
+ }
@@ -82,10 +82,8 @@ export function isCursorAtBlockStart(editor) {
82
82
 
83
83
  if (editor.selection && Range.isCollapsed(editor.selection)) {
84
84
  const { anchor } = editor.selection;
85
- return anchor.offset > 0
86
- ? false
87
- : anchor.path.reduce((acc, x) => acc + x, 0) === 0;
88
- // anchor.path.length === 2 &&
85
+ // Check if cursor is at offset 0 of any leaf node (not just the first one)
86
+ return anchor.offset === 0;
89
87
  }
90
88
  return false;
91
89
  }
@@ -8,7 +8,7 @@ import {
8
8
  getBlocksFieldname,
9
9
  getBlocksLayoutFieldname,
10
10
  } from '@plone/volto/helpers/Blocks/Blocks';
11
- import { Transforms, Editor, Node, Text, Path } from 'slate';
11
+ import { Transforms, Editor, Node } from 'slate';
12
12
  import { serializeNodesToText } from '@plone/volto-slate/editor/render';
13
13
  import omit from 'lodash/omit';
14
14
  import config from '@plone/volto/registry';
@@ -21,74 +21,64 @@ function fromEntries(pairs) {
21
21
  return res;
22
22
  }
23
23
 
24
- // TODO: should be made generic, no need for "prevBlock.value"
25
24
  export function mergeSlateWithBlockBackward(editor, prevBlock, event) {
26
- // To work around current architecture limitations, read the value from
27
- // previous block. Replace it in the current editor (over which we have
28
- // control), join with current block value, then use this result for previous
29
- // block, delete current block
30
-
31
- const prev = prevBlock.value;
32
-
33
- // collapse the selection to its start point
34
- Transforms.collapse(editor, { edge: 'start' });
35
-
36
- let rangeRef;
37
- let end;
38
-
39
- Editor.withoutNormalizing(editor, () => {
40
- // insert block #0 contents in block #1 contents, at the beginning
41
- Transforms.insertNodes(editor, prev, {
42
- at: Editor.start(editor, []),
43
- });
44
-
45
- // the contents that should be moved into the `ul`, as the last `li`
46
- rangeRef = Editor.rangeRef(editor, {
47
- anchor: Editor.start(editor, [1]),
48
- focus: Editor.end(editor, [1]),
49
- });
50
-
51
- const source = rangeRef.current;
52
-
53
- end = Editor.end(editor, [0]);
54
-
55
- let endPoint;
56
-
57
- Transforms.insertNodes(editor, { text: '' }, { at: end });
58
-
59
- end = Editor.end(editor, [0]);
60
-
61
- Transforms.splitNodes(editor, {
62
- at: end,
63
- always: true,
64
- height: 1,
65
- mode: 'highest',
66
- match: (n) => n.type === 'li' || Text.isText(n),
67
- });
68
-
69
- endPoint = Editor.end(editor, [0]);
70
-
71
- Transforms.moveNodes(editor, {
72
- at: source,
73
- to: endPoint.path,
74
- mode: 'all',
75
- match: (n, p) => p.length === 2,
76
- });
77
- });
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
+ };
78
44
 
79
- const [n] = Editor.node(editor, [1]);
45
+ merged = [
46
+ ...prevValue.slice(0, -1),
47
+ mergedFirstNode,
48
+ ...currentValue.slice(1),
49
+ ];
80
50
 
81
- if (Editor.isEmpty(editor, n)) {
82
- Transforms.removeNodes(editor, { at: [1] });
51
+ cursor = {
52
+ anchor: {
53
+ path: [prevValue.length - 1, lastNode.children.length],
54
+ offset: 0,
55
+ },
56
+ focus: {
57
+ path: [prevValue.length - 1, lastNode.children.length],
58
+ offset: 0,
59
+ },
60
+ };
61
+ } else {
62
+ // Otherwise, just append the current block content to the previous block
63
+ merged = [...prevValue, ...currentValue];
64
+ // Position cursor at the start of the merged content (where current block was inserted)
65
+ // The current block content starts at index prevValue.length in the merged array
66
+ cursor = {
67
+ anchor: {
68
+ path: [prevValue.length, 0],
69
+ offset: 0,
70
+ },
71
+ focus: {
72
+ path: [prevValue.length, 0],
73
+ offset: 0,
74
+ },
75
+ };
83
76
  }
84
77
 
85
- rangeRef.unref();
86
-
87
- const [, lastPath] = Editor.last(editor, [0]);
88
-
89
- end = Editor.start(editor, Path.parent(lastPath));
78
+ // Update the editor with the merged content
79
+ editor.children = merged;
90
80
 
91
- return end;
81
+ return cursor;
92
82
  }
93
83
 
94
84
  export function mergeSlateWithBlockForward(editor, nextBlock, event) {