@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 +12 -0
- package/locales/eu/LC_MESSAGES/volto.po +7 -6
- package/package.json +1 -1
- package/src/blocks/Text/index.jsx +2 -0
- package/src/blocks/Text/keyboard/joinBlocks.js +7 -6
- package/src/blocks/Text/keyboard/splitAtSeam.js +44 -0
- package/src/utils/selection.js +2 -4
- package/src/utils/volto-blocks.js +53 -63
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:
|
|
8
|
-
"Language:
|
|
9
|
-
"Language
|
|
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:
|
|
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 "
|
|
51
|
+
msgstr "Zatitu lerro bakoitza zeldatan"
|
|
51
52
|
|
|
52
53
|
#: elementEditor/messages
|
|
53
54
|
msgid "Edit element"
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
90
|
-
plaintext: serializeNodesToText(
|
|
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
|
+
}
|
package/src/utils/selection.js
CHANGED
|
@@ -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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
+
merged = [
|
|
46
|
+
...prevValue.slice(0, -1),
|
|
47
|
+
mergedFirstNode,
|
|
48
|
+
...currentValue.slice(1),
|
|
49
|
+
];
|
|
80
50
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
81
|
+
return cursor;
|
|
92
82
|
}
|
|
93
83
|
|
|
94
84
|
export function mergeSlateWithBlockForward(editor, nextBlock, event) {
|