@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.
@@ -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
+ });