@eeacms/volto-eea-website-theme 4.3.8 → 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 +12 -26
- 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/src/customizations/volto/helpers/Html/Html.jsx +7 -9
- package/src/customizations/volto/server.jsx +0 -4
- package/src/index.js +0 -4
- package/src/index.test.js +3 -1
|
@@ -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
|
+
});
|