@plone/volto-slate 19.0.2 → 19.0.4

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
+ ## 19.0.4 (2026-07-16)
12
+
13
+ ### Bugfix
14
+
15
+ - Fix Backspace behaving erratically near a styled or link text and fix cursor position after merging two same-type slate blocks via Backspace. @avoinea [#8347](https://github.com/plone/volto/issues/8347)
16
+
17
+ ## 19.0.3 (2026-07-01)
18
+
19
+ ### Bugfix
20
+
21
+ - Fixed unintended form submission triggered by table block buttons. @ilariamassa96 [#8330](https://github.com/plone/volto/issues/8330)
22
+
11
23
  ## 19.0.2 (2026-05-28)
12
24
 
13
25
  ### Bugfix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plone/volto-slate",
3
- "version": "19.0.2",
3
+ "version": "19.0.4",
4
4
  "description": "Slate.js integration with Volto",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -355,6 +355,7 @@ const Edit = (props) => {
355
355
  <Button
356
356
  icon
357
357
  basic
358
+ type="button"
358
359
  onClick={onInsertRowBefore}
359
360
  title={intl.formatMessage(messages.insertRowBefore)}
360
361
  aria-label={intl.formatMessage(messages.insertRowBefore)}
@@ -366,6 +367,7 @@ const Edit = (props) => {
366
367
  <Button
367
368
  icon
368
369
  basic
370
+ type="button"
369
371
  onClick={onInsertRowAfter}
370
372
  title={intl.formatMessage(messages.insertRowAfter)}
371
373
  aria-label={intl.formatMessage(messages.insertRowAfter)}
@@ -377,6 +379,7 @@ const Edit = (props) => {
377
379
  <Button
378
380
  icon
379
381
  basic
382
+ type="button"
380
383
  onClick={onDeleteRow}
381
384
  disabled={data.table?.rows?.length === 1}
382
385
  title={intl.formatMessage(messages.deleteRow)}
@@ -389,6 +392,7 @@ const Edit = (props) => {
389
392
  <Button
390
393
  icon
391
394
  basic
395
+ type="button"
392
396
  onClick={onInsertColBefore}
393
397
  title={intl.formatMessage(messages.insertColBefore)}
394
398
  aria-label={intl.formatMessage(messages.insertColBefore)}
@@ -400,6 +404,7 @@ const Edit = (props) => {
400
404
  <Button
401
405
  icon
402
406
  basic
407
+ type="button"
403
408
  onClick={onInsertColAfter}
404
409
  title={intl.formatMessage(messages.insertColAfter)}
405
410
  aria-label={intl.formatMessage(messages.insertColAfter)}
@@ -411,6 +416,7 @@ const Edit = (props) => {
411
416
  <Button
412
417
  icon
413
418
  basic
419
+ type="button"
414
420
  onClick={onDeleteCol}
415
421
  disabled={data.table?.rows?.[0].cells.length === 1}
416
422
  title={intl.formatMessage(messages.deleteCol)}
@@ -1,12 +1,34 @@
1
1
  import castArray from 'lodash/castArray';
2
2
  import cloneDeep from 'lodash/cloneDeep';
3
- import { Editor, Transforms, Range, Node } from 'slate';
3
+ import { Editor, Transforms, Range, Node, Path } from 'slate';
4
4
  import { ReactEditor } from 'slate-react';
5
5
  import { isCursorInList } from '@plone/volto-slate/utils/lists';
6
6
  import { makeEditor } from '@plone/volto-slate/utils/editor';
7
7
  import { LI } from '@plone/volto-slate/constants';
8
8
  import config from '@plone/volto/registry';
9
9
 
10
+ /**
11
+ * firstLeafPath.
12
+ *
13
+ * @param {} editor
14
+ */
15
+ function firstLeafPath(editor) {
16
+ if (!editor.children?.length) return null;
17
+ return Node.first(editor, [])[1];
18
+ }
19
+
20
+ /**
21
+ * isAtFirstLeaf.
22
+ *
23
+ * @param {} editor
24
+ * @param {} path
25
+ */
26
+ function isAtFirstLeaf(editor, path) {
27
+ const first = firstLeafPath(editor);
28
+ if (!first) return false;
29
+ return Path.equals(path, first);
30
+ }
31
+
10
32
  /**
11
33
  * Get the nodes with a type included in `types` in the selection (from root to leaf).
12
34
  *
@@ -82,8 +104,9 @@ export function isCursorAtBlockStart(editor) {
82
104
 
83
105
  if (editor.selection && Range.isCollapsed(editor.selection)) {
84
106
  const { anchor } = editor.selection;
85
- // Check if cursor is at offset 0 of any leaf node (not just the first one)
86
- return anchor.offset === 0;
107
+ if (anchor.offset !== 0) return false;
108
+ if (!editor.children?.length) return false;
109
+ return isAtFirstLeaf(editor, anchor.path);
87
110
  }
88
111
  return false;
89
112
  }
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { isCursorAtBlockStart } from './selection';
3
+
4
+ // `isCursorAtBlockStart` only inspects `editor.selection` and `editor.children`,
5
+ // so plain editor-shaped objects are enough (no full Volto config registry).
6
+ const makeEditor = (children, selection) => ({ children, selection });
7
+ const at = (path, offset) => ({ path, offset });
8
+ const collapsed = (path, offset) => ({
9
+ anchor: at(path, offset),
10
+ focus: at(path, offset),
11
+ });
12
+
13
+ describe('isCursorAtBlockStart', () => {
14
+ it('returns true when the caret is at the first leaf of the block', () => {
15
+ const editor = makeEditor(
16
+ [{ type: 'p', children: [{ text: 'Hello' }] }],
17
+ collapsed([0, 0], 0),
18
+ );
19
+ expect(isCursorAtBlockStart(editor)).toBe(true);
20
+ });
21
+
22
+ it('returns true for the first leaf of a nested list item (path [0,0,0])', () => {
23
+ const editor = makeEditor(
24
+ [
25
+ {
26
+ type: 'ul',
27
+ children: [
28
+ {
29
+ type: 'li',
30
+ children: [{ text: 'first item' }],
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ collapsed([0, 0, 0], 0),
36
+ );
37
+ expect(isCursorAtBlockStart(editor)).toBe(true);
38
+ });
39
+
40
+ it('returns false at offset 0 of a non-first leaf inside a list item (#8347)', () => {
41
+ // A list item: {text:""} + link + {text:""} — the empty text leaf after
42
+ // the link is at path [0,0,2]. Backspace there must NOT count as block start.
43
+ const editor = makeEditor(
44
+ [
45
+ {
46
+ type: 'ul',
47
+ children: [
48
+ {
49
+ type: 'li',
50
+ children: [
51
+ { text: '' },
52
+ {
53
+ type: 'link',
54
+ data: { url: 'https://demo.plone.org/' },
55
+ children: [{ text: 'Plone 6 (this site)' }],
56
+ },
57
+ { text: '' },
58
+ ],
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ collapsed([0, 0, 2], 0),
64
+ );
65
+ expect(isCursorAtBlockStart(editor)).toBe(false);
66
+ });
67
+
68
+ it('returns false at offset 0 of a non-first leaf of a plain paragraph', () => {
69
+ const editor = makeEditor(
70
+ [
71
+ {
72
+ type: 'p',
73
+ children: [
74
+ { text: 'before' },
75
+ {
76
+ type: 'link',
77
+ data: { url: 'https://example.com' },
78
+ children: [{ text: 'link' }],
79
+ },
80
+ { text: 'after' },
81
+ ],
82
+ },
83
+ ],
84
+ collapsed([0, 2], 0),
85
+ );
86
+ expect(isCursorAtBlockStart(editor)).toBe(false);
87
+ });
88
+
89
+ it('returns false when the offset is greater than 0', () => {
90
+ const editor = makeEditor(
91
+ [{ type: 'p', children: [{ text: 'Hello' }] }],
92
+ collapsed([0, 0], 2),
93
+ );
94
+ expect(isCursorAtBlockStart(editor)).toBe(false);
95
+ });
96
+
97
+ it('returns false when there is no selection', () => {
98
+ const editor = makeEditor(
99
+ [{ type: 'p', children: [{ text: 'Hi' }] }],
100
+ null,
101
+ );
102
+ expect(isCursorAtBlockStart(editor)).toBe(false);
103
+ });
104
+
105
+ it('returns false when the selection is expanded', () => {
106
+ const editor = makeEditor([{ type: 'p', children: [{ text: 'Hello' }] }], {
107
+ anchor: at([0, 0], 0),
108
+ focus: at([0, 0], 3),
109
+ });
110
+ expect(isCursorAtBlockStart(editor)).toBe(false);
111
+ });
112
+
113
+ it('returns false when the editor has no children', () => {
114
+ const editor = makeEditor([], collapsed([0, 0], 0));
115
+ expect(isCursorAtBlockStart(editor)).toBe(false);
116
+ });
117
+ });
@@ -8,7 +8,7 @@ import {
8
8
  getBlocksFieldname,
9
9
  getBlocksLayoutFieldname,
10
10
  } from '@plone/volto/helpers/Blocks/Blocks';
11
- import { Transforms, Editor, Node } from 'slate';
11
+ import { Transforms, Editor, Node, Text } 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';
@@ -48,16 +48,30 @@ export function mergeSlateWithBlockBackward(editor, prevBlock, event) {
48
48
  ...currentValue.slice(1),
49
49
  ];
50
50
 
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
- };
51
+ const lastChild = lastNode.children[lastNode.children.length - 1];
52
+ if (Text.isText(lastChild)) {
53
+ cursor = {
54
+ anchor: {
55
+ path: [prevValue.length - 1, lastNode.children.length - 1],
56
+ offset: lastChild.text.length,
57
+ },
58
+ focus: {
59
+ path: [prevValue.length - 1, lastNode.children.length - 1],
60
+ offset: lastChild.text.length,
61
+ },
62
+ };
63
+ } else {
64
+ cursor = {
65
+ anchor: {
66
+ path: [prevValue.length - 1, lastNode.children.length],
67
+ offset: 0,
68
+ },
69
+ focus: {
70
+ path: [prevValue.length - 1, lastNode.children.length],
71
+ offset: 0,
72
+ },
73
+ };
74
+ }
61
75
  } else {
62
76
  // Otherwise, just append the current block content to the previous block
63
77
  merged = [...prevValue, ...currentValue];