@plone/volto 16.0.0 → 16.2.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 +30 -6
- package/cypress/support/commands.js +1 -1
- package/locales/ca/LC_MESSAGES/volto.po +22 -0
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +22 -0
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +22 -0
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +22 -0
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +22 -0
- package/locales/eu.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +22 -0
- package/locales/fr.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +22 -0
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +22 -0
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +22 -0
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +22 -0
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +22 -0
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +22 -0
- package/locales/ro.json +1 -1
- package/locales/volto.pot +23 -1
- package/package.json +5 -3
- package/packages/volto-slate/README.md +2 -233
- package/packages/volto-slate/src/blocks/Text/extensions/index.js +1 -0
- package/packages/volto-slate/src/blocks/Text/extensions/normalizeExternalData.js +7 -0
- package/packages/volto-slate/src/blocks/Text/index.js +2 -0
- package/packages/volto-slate/src/editor/config.jsx +2 -0
- package/packages/volto-slate/src/editor/deserialize.js +25 -55
- package/packages/volto-slate/src/editor/extensions/index.js +1 -0
- package/packages/volto-slate/src/editor/extensions/insertData.js +17 -4
- package/packages/volto-slate/src/editor/extensions/normalizeExternalData.js +8 -0
- package/packages/volto-slate/src/editor/ui/Toolbar.jsx +8 -3
- package/packages/volto-slate/src/editor/ui/ToolbarButton.test.js +2 -2
- package/packages/volto-slate/src/editor/utils.js +248 -0
- package/src/components/index.js +3 -0
- package/src/components/manage/Blocks/Block/DefaultEdit.jsx +44 -0
- package/src/components/manage/Blocks/Block/DefaultView.jsx +78 -0
- package/src/components/manage/Blocks/Block/Edit.jsx +3 -2
- package/src/components/manage/Blocks/HeroImageLeft/Data.jsx +2 -1
- package/src/components/manage/Blocks/Image/ImageSidebar.jsx +1 -0
- package/src/components/manage/Blocks/Listing/ListingData.jsx +1 -0
- package/src/components/manage/Blocks/Maps/MapsSidebar.jsx +1 -0
- package/src/components/manage/Blocks/Search/SearchBlockEdit.jsx +1 -0
- package/src/components/manage/Blocks/Search/components/SelectFacet.jsx +2 -1
- package/src/components/manage/Blocks/ToC/Edit.jsx +1 -0
- package/src/components/manage/Blocks/Video/VideoSidebar.jsx +1 -1
- package/src/components/manage/Controlpanels/Users/UsersControlpanel.jsx +14 -11
- package/src/components/manage/Widgets/NumberWidget.jsx +1 -1
- package/src/components/manage/Widgets/ObjectListWidget.jsx +19 -2
- package/src/components/theme/Error/ErrorBoundary.jsx +29 -0
- package/src/components/theme/Error/ErrorBoundary.test.jsx +34 -0
- package/src/components/theme/View/RenderBlocks.jsx +3 -1
- package/src/config/Style.jsx +9 -0
- package/src/config/index.js +2 -0
- package/src/helpers/Blocks/Blocks.js +37 -38
- package/src/helpers/Blocks/Blocks.test.js +64 -0
- package/src/helpers/MessageLabels/MessageLabels.js +18 -0
- package/test-setup-config.js +7 -0
|
@@ -1,70 +1,37 @@
|
|
|
1
1
|
import { jsx } from 'slate-hyperscript';
|
|
2
2
|
import { Text } from 'slate';
|
|
3
|
-
import { isWhitespace } from '@plone/volto-slate/utils';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
COMMENT,
|
|
8
|
-
ELEMENT_NODE,
|
|
9
|
-
INLINE_ELEMENTS,
|
|
10
|
-
TEXT_NODE,
|
|
11
|
-
} from '../constants';
|
|
12
|
-
|
|
13
|
-
const isInline = (node) =>
|
|
14
|
-
node &&
|
|
15
|
-
(node.nodeType === TEXT_NODE || INLINE_ELEMENTS.includes(node.nodeName));
|
|
3
|
+
// import { isWhitespace } from '@plone/volto-slate/utils';
|
|
4
|
+
import { TD, TH, COMMENT, ELEMENT_NODE, TEXT_NODE } from '../constants';
|
|
5
|
+
|
|
6
|
+
import { collapseInlineSpace } from './utils';
|
|
16
7
|
|
|
17
8
|
/**
|
|
18
|
-
* Deserialize to
|
|
9
|
+
* Deserialize to a Slate Node, an Array of Slate Nodes or null
|
|
10
|
+
*
|
|
11
|
+
* One particularity of this function is that it tries to do
|
|
12
|
+
* a "perception-based" conversion. For example, in html, multiple whitespaces
|
|
13
|
+
* display as a single space. A new line character in text is actually rendered
|
|
14
|
+
* as a space, etc. So we try to meet user's expectations that when they
|
|
15
|
+
* copy/paste content, we'll preserve the aspect of their text.
|
|
19
16
|
*
|
|
20
|
-
*
|
|
17
|
+
* See https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
|
|
21
18
|
*/
|
|
22
19
|
export const deserialize = (editor, el) => {
|
|
23
|
-
// console.log('deserialize el:', el);
|
|
24
20
|
const { htmlTagsToSlate } = editor;
|
|
25
21
|
|
|
26
|
-
// console.log('des:', el.nodeType, el);
|
|
27
22
|
if (el.nodeType === COMMENT) {
|
|
28
23
|
return null;
|
|
29
24
|
} else if (el.nodeType === TEXT_NODE) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// next: el.nextSibling,
|
|
37
|
-
// isPrev: isInline(el.previousSibling),
|
|
38
|
-
// isNext: isInline(el.nextSibling),
|
|
39
|
-
// prevName: el.previousSibling && el.previousSibling.nodeName,
|
|
40
|
-
// nextName: el.nextSibling && el.nextSibling.nodeName,
|
|
41
|
-
// });
|
|
42
|
-
|
|
43
|
-
if (isWhitespace(el.textContent)) {
|
|
44
|
-
// console.log({
|
|
45
|
-
// text: `-${el.textContent}-`,
|
|
46
|
-
// prev: el.previousSibling,
|
|
47
|
-
// next: el.nextSibling,
|
|
48
|
-
// isPrev: isInline(el.previousSibling),
|
|
49
|
-
// isNext: isInline(el.nextSibling),
|
|
50
|
-
// prevName: el.previousSibling && el.previousSibling.nodeName,
|
|
51
|
-
// nextName: el.nextSibling && el.nextSibling.nodeName,
|
|
52
|
-
// });
|
|
53
|
-
// if it's empty text between 2 tags, it should be ignored
|
|
54
|
-
return isInline(el.previousSibling) || isInline(el.nextSibling)
|
|
55
|
-
? { text: el.textContent } // perceptually multiple whitespace render as a single space
|
|
56
|
-
: null;
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
text: el.textContent
|
|
60
|
-
.replace(/\n$/g, ' ')
|
|
61
|
-
.replace(/\n/g, ' ')
|
|
62
|
-
.replace(/\t/g, ''),
|
|
63
|
-
};
|
|
25
|
+
const text = collapseInlineSpace(el);
|
|
26
|
+
return text
|
|
27
|
+
? {
|
|
28
|
+
text,
|
|
29
|
+
}
|
|
30
|
+
: null;
|
|
64
31
|
} else if (el.nodeType !== ELEMENT_NODE) {
|
|
65
32
|
return null;
|
|
66
33
|
} else if (el.nodeName === 'BR') {
|
|
67
|
-
//
|
|
34
|
+
// gets merged with sibling text nodes by Slate normalization in insertData
|
|
68
35
|
return { text: '\n' };
|
|
69
36
|
}
|
|
70
37
|
|
|
@@ -78,7 +45,8 @@ export const deserialize = (editor, el) => {
|
|
|
78
45
|
return htmlTagsToSlate[nodeName](editor, el);
|
|
79
46
|
}
|
|
80
47
|
|
|
81
|
-
|
|
48
|
+
// fallback deserializer, all unknown elements are "stripped"
|
|
49
|
+
return deserializeChildren(el, editor);
|
|
82
50
|
};
|
|
83
51
|
|
|
84
52
|
export const typeDeserialize = (editor, el) => {
|
|
@@ -138,10 +106,11 @@ export const inlineTagDeserializer = (attrs) => (editor, el) => {
|
|
|
138
106
|
export const spanTagDeserializer = (editor, el) => {
|
|
139
107
|
const style = el.getAttribute('style') || '';
|
|
140
108
|
let children = el.childNodes;
|
|
109
|
+
|
|
141
110
|
if (
|
|
142
111
|
// handle formatting from OpenOffice
|
|
143
112
|
children.length === 1 &&
|
|
144
|
-
children[0].nodeType ===
|
|
113
|
+
children[0].nodeType === TEXT_NODE &&
|
|
145
114
|
children[0].textContent === '\n'
|
|
146
115
|
) {
|
|
147
116
|
return jsx('text', {}, ' ');
|
|
@@ -149,7 +118,7 @@ export const spanTagDeserializer = (editor, el) => {
|
|
|
149
118
|
children = deserializeChildren(el, editor);
|
|
150
119
|
|
|
151
120
|
// whitespace is replaced by deserialize() with null;
|
|
152
|
-
children = children.map((c) => (c === null ? '
|
|
121
|
+
children = children.map((c) => (c === null ? '' : c));
|
|
153
122
|
|
|
154
123
|
// TODO: handle sub/sup as <sub> and <sup>
|
|
155
124
|
// Handle Google Docs' <sub> formatting
|
|
@@ -171,6 +140,7 @@ export const spanTagDeserializer = (editor, el) => {
|
|
|
171
140
|
const res = children.find((c) => typeof c !== 'string')
|
|
172
141
|
? children
|
|
173
142
|
: jsx('text', {}, children);
|
|
143
|
+
|
|
174
144
|
return res;
|
|
175
145
|
};
|
|
176
146
|
|
|
@@ -29,14 +29,27 @@ export const insertData = (editor) => {
|
|
|
29
29
|
|
|
30
30
|
let fragment;
|
|
31
31
|
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.debug('clipboard operation', {
|
|
34
|
+
clipboard: dt,
|
|
35
|
+
parsedBody: body,
|
|
36
|
+
});
|
|
37
|
+
|
|
32
38
|
const val = deserialize(editor, body);
|
|
33
39
|
fragment = Array.isArray(val) ? val : [val];
|
|
34
|
-
|
|
35
|
-
// external normalization
|
|
36
|
-
fragment = normalizeExternalData(editor, fragment);
|
|
40
|
+
fragment = editor.normalizeExternalData(fragment);
|
|
37
41
|
|
|
38
42
|
editor.insertFragment(fragment);
|
|
39
43
|
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.debug('result clipboard operation', {
|
|
46
|
+
clipboard: dt,
|
|
47
|
+
parsedBody: body,
|
|
48
|
+
deserializedValue: val,
|
|
49
|
+
normalizedFragment: fragment,
|
|
50
|
+
editorChildren: editor.children,
|
|
51
|
+
});
|
|
52
|
+
|
|
40
53
|
return true;
|
|
41
54
|
},
|
|
42
55
|
'text/plain': (dt, fullMime) => {
|
|
@@ -94,6 +107,7 @@ export const insertData = (editor) => {
|
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
|
|
110
|
+
// always normalize when dealing with plain text
|
|
97
111
|
const nodes = normalizeExternalData(editor, fragment);
|
|
98
112
|
if (!containsText) {
|
|
99
113
|
Transforms.insertNodes(editor, nodes);
|
|
@@ -116,7 +130,6 @@ export const insertData = (editor) => {
|
|
|
116
130
|
editor.beforeInsertData(data);
|
|
117
131
|
}
|
|
118
132
|
|
|
119
|
-
// debugger;
|
|
120
133
|
for (let i = 0; i < editor.dataTransferFormatsOrder.length; ++i) {
|
|
121
134
|
const dt = editor.dataTransferFormatsOrder[i];
|
|
122
135
|
if (dt === 'files') {
|
|
@@ -20,7 +20,7 @@ const Toolbar = ({
|
|
|
20
20
|
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
const domNode = ref.current;
|
|
23
|
-
let rect;
|
|
23
|
+
let rect = { width: 1, top: 0, left: 0 };
|
|
24
24
|
|
|
25
25
|
if ((children || []).length === 0) {
|
|
26
26
|
domNode.removeAttribute('style');
|
|
@@ -63,8 +63,13 @@ const Toolbar = ({
|
|
|
63
63
|
// TODO: should we fallback to editor.getSelection()?
|
|
64
64
|
// TODO: test with third party plugins
|
|
65
65
|
const slateNode = Node.get(editor, selection.anchor.path);
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
try {
|
|
67
|
+
const domEl = ReactEditor.toDOMNode(editor, slateNode);
|
|
68
|
+
rect = domEl.getBoundingClientRect();
|
|
69
|
+
} catch {
|
|
70
|
+
// ignoring error here is safe, editor is out of sync and the selection
|
|
71
|
+
// is actually none, so no toolbar should be shown
|
|
72
|
+
}
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
domNode.style.opacity = 1;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import configureStore from 'redux-mock-store';
|
|
3
3
|
import { Provider } from 'react-intl-redux';
|
|
4
|
-
import {
|
|
4
|
+
import { waitFor, render } from '@testing-library/react';
|
|
5
5
|
|
|
6
6
|
import ToolbarButton from './ToolbarButton';
|
|
7
7
|
|
|
@@ -20,6 +20,6 @@ describe('ToolbarButton', () => {
|
|
|
20
20
|
<ToolbarButton />
|
|
21
21
|
</Provider>,
|
|
22
22
|
);
|
|
23
|
-
await
|
|
23
|
+
await waitFor(() => expect(asFragment()).toMatchSnapshot());
|
|
24
24
|
});
|
|
25
25
|
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { INLINE_ELEMENTS, TEXT_NODE } from '../constants';
|
|
2
|
+
|
|
3
|
+
// Original at https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
|
|
4
|
+
/**
|
|
5
|
+
* Throughout, whitespace is defined as one of the characters
|
|
6
|
+
* "\t" TAB \u0009
|
|
7
|
+
* "\n" LF \u000A
|
|
8
|
+
* "\r" CR \u000D
|
|
9
|
+
* " " SPC \u0020
|
|
10
|
+
*
|
|
11
|
+
* This does not use JavaScript's "\s" because that includes non-breaking
|
|
12
|
+
* spaces (and also some other characters).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Determine whether a node's text content is entirely whitespace.
|
|
17
|
+
*
|
|
18
|
+
* @param nod A node implementing the |CharacterData| interface (i.e.,
|
|
19
|
+
* a |Text|, |Comment|, or |CDATASection| node
|
|
20
|
+
* @return True if all of the text content of |nod| is whitespace,
|
|
21
|
+
* otherwise false.
|
|
22
|
+
*/
|
|
23
|
+
export function is_all_ws(text) {
|
|
24
|
+
return !/[^\t\n\r ]/.test(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Version of |data| that doesn't include whitespace at the beginning
|
|
29
|
+
* and end and normalizes all whitespace to a single space. (Normally
|
|
30
|
+
* |data| is a property of text nodes that gives the text of the node.)
|
|
31
|
+
*
|
|
32
|
+
* @param txt The text node whose data should be returned
|
|
33
|
+
* @return A string giving the contents of the text node with
|
|
34
|
+
* whitespace collapsed.
|
|
35
|
+
*/
|
|
36
|
+
export function data_of(txt) {
|
|
37
|
+
let data = txt.textContent;
|
|
38
|
+
data = data.replace(/[\t\n\r ]+/g, ' ');
|
|
39
|
+
if (data[0] === ' ') {
|
|
40
|
+
data = data.substring(1, data.length);
|
|
41
|
+
}
|
|
42
|
+
if (data[data.length - 1] === ' ') {
|
|
43
|
+
data = data.substring(0, data.length - 1);
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Determine if a node should be ignored by the iterator functions.
|
|
50
|
+
*
|
|
51
|
+
* @param nod An object implementing the DOM1 |Node| interface.
|
|
52
|
+
* @return true if the node is:
|
|
53
|
+
* 1) A |Text| node that is all whitespace
|
|
54
|
+
* 2) A |Comment| node
|
|
55
|
+
* and otherwise false.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
export function is_ignorable(nod) {
|
|
59
|
+
return (
|
|
60
|
+
nod.nodeType === 8 || // A comment node
|
|
61
|
+
(nod.nodeType === 3 && is_all_ws(nod.textContent))
|
|
62
|
+
); // a text node, all ws
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Version of |previousSibling| that skips nodes that are entirely
|
|
67
|
+
* whitespace or comments. (Normally |previousSibling| is a property
|
|
68
|
+
* of all DOM nodes that gives the sibling node, the node that is
|
|
69
|
+
* a child of the same parent, that occurs immediately before the
|
|
70
|
+
* reference node.)
|
|
71
|
+
*
|
|
72
|
+
* @param sib The reference node.
|
|
73
|
+
* @return Either:
|
|
74
|
+
* 1) The closest previous sibling to |sib| that is not
|
|
75
|
+
* ignorable according to |is_ignorable|, or
|
|
76
|
+
* 2) null if no such node exists.
|
|
77
|
+
*/
|
|
78
|
+
export function node_before(sib) {
|
|
79
|
+
while ((sib = sib.previousSibling)) {
|
|
80
|
+
if (!is_ignorable(sib)) {
|
|
81
|
+
return sib;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Version of |nextSibling| that skips nodes that are entirely
|
|
89
|
+
* whitespace or comments.
|
|
90
|
+
*
|
|
91
|
+
* @param sib The reference node.
|
|
92
|
+
* @return Either:
|
|
93
|
+
* 1) The closest next sibling to |sib| that is not
|
|
94
|
+
* ignorable according to |is_ignorable|, or
|
|
95
|
+
* 2) null if no such node exists.
|
|
96
|
+
*/
|
|
97
|
+
export function node_after(sib) {
|
|
98
|
+
while ((sib = sib.nextSibling)) {
|
|
99
|
+
if (!is_ignorable(sib)) {
|
|
100
|
+
return sib;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Version of |lastChild| that skips nodes that are entirely
|
|
108
|
+
* whitespace or comments. (Normally |lastChild| is a property
|
|
109
|
+
* of all DOM nodes that gives the last of the nodes contained
|
|
110
|
+
* directly in the reference node.)
|
|
111
|
+
*
|
|
112
|
+
* @param sib The reference node.
|
|
113
|
+
* @return Either:
|
|
114
|
+
* 1) The last child of |sib| that is not
|
|
115
|
+
* ignorable according to |is_ignorable|, or
|
|
116
|
+
* 2) null if no such node exists.
|
|
117
|
+
*/
|
|
118
|
+
export function last_child(par) {
|
|
119
|
+
let res = par.lastChild;
|
|
120
|
+
while (res) {
|
|
121
|
+
if (!is_ignorable(res)) {
|
|
122
|
+
return res;
|
|
123
|
+
}
|
|
124
|
+
res = res.previousSibling;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Version of |firstChild| that skips nodes that are entirely
|
|
131
|
+
* whitespace and comments.
|
|
132
|
+
*
|
|
133
|
+
* @param sib The reference node.
|
|
134
|
+
* @return Either:
|
|
135
|
+
* 1) The first child of |sib| that is not
|
|
136
|
+
* ignorable according to |is_ignorable|, or
|
|
137
|
+
* 2) null if no such node exists.
|
|
138
|
+
*/
|
|
139
|
+
export function first_child(par) {
|
|
140
|
+
let res = par.firstChild;
|
|
141
|
+
while (res) {
|
|
142
|
+
if (!is_ignorable(res)) {
|
|
143
|
+
return res;
|
|
144
|
+
}
|
|
145
|
+
res = res.nextSibling;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const removeSpaceBeforeAfterEndLine = (text) => {
|
|
151
|
+
text = text.replace(/\s+\n/gm, '\n'); // space before endline
|
|
152
|
+
text = text.replace(/\n\s+/gm, '\n'); // space after endline
|
|
153
|
+
return text;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export const convertTabsToSpaces = (text) => text.replace(/\t/gm, ' ');
|
|
157
|
+
export const convertLineBreaksToSpaces = (text) => text.replace(/\n/gm, ' ');
|
|
158
|
+
|
|
159
|
+
export const isInline = (node) =>
|
|
160
|
+
node &&
|
|
161
|
+
(node.nodeType === TEXT_NODE || INLINE_ELEMENTS.includes(node.nodeName));
|
|
162
|
+
|
|
163
|
+
export const removeSpaceFollowSpace = (text, node) => {
|
|
164
|
+
// Any space immediately following another space (even across two separate
|
|
165
|
+
// inline elements) is ignored (rule 4)
|
|
166
|
+
text = text.replace(/ ( +)/gm, ' ');
|
|
167
|
+
if (!text.startsWith(' ')) return text;
|
|
168
|
+
|
|
169
|
+
if (node.previousSibling) {
|
|
170
|
+
if (node.previousSibling.nodeType === TEXT_NODE) {
|
|
171
|
+
if (node.previousSibling.textContent.endsWith(' ')) {
|
|
172
|
+
return text.replace(/^ /, '');
|
|
173
|
+
}
|
|
174
|
+
} else if (isInline(node.previousSibling)) {
|
|
175
|
+
const prevText = collapseInlineSpace(node.previousSibling);
|
|
176
|
+
if (prevText.endsWith(' ')) {
|
|
177
|
+
return text.replace(/^ /, '');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
const parent = node.parentNode;
|
|
182
|
+
if (parent.previousSibling) {
|
|
183
|
+
// && isInline(parent.previousSibling)
|
|
184
|
+
const prevText = collapseInlineSpace(parent.previousSibling);
|
|
185
|
+
if (prevText && prevText.endsWith(' ')) {
|
|
186
|
+
return text.replace(/^ /, '');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return text;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const removeElementEdges = (text, node) => {
|
|
195
|
+
if (
|
|
196
|
+
!isInline(node.parentNode) &&
|
|
197
|
+
!node.previousSibling &&
|
|
198
|
+
text.match(/^\s/)
|
|
199
|
+
) {
|
|
200
|
+
text = text.replace(/^\s+/, '');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (text.match(/\s$/) && !node.nextSibling && !isInline(node.parentNode)) {
|
|
204
|
+
text = text.replace(/\s$/, '');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return text;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const collapseInlineSpace = (node) => {
|
|
211
|
+
let text = node.textContent;
|
|
212
|
+
|
|
213
|
+
// See https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace
|
|
214
|
+
|
|
215
|
+
// 1. all spaces and tabs immediately before and after a line break are ignored
|
|
216
|
+
|
|
217
|
+
text = removeSpaceBeforeAfterEndLine(text);
|
|
218
|
+
|
|
219
|
+
// 2. Next, all tab characters are handled as space characters
|
|
220
|
+
text = convertTabsToSpaces(text);
|
|
221
|
+
|
|
222
|
+
// 3. Convert all line breaks to spaces
|
|
223
|
+
text = convertLineBreaksToSpaces(text);
|
|
224
|
+
|
|
225
|
+
// 4. Any space immediately following another space
|
|
226
|
+
// (even across two separate inline elements) is ignored
|
|
227
|
+
text = removeSpaceFollowSpace(text, node);
|
|
228
|
+
|
|
229
|
+
// 5. Sequences of spaces at the beginning and end of an element are removed
|
|
230
|
+
text = removeElementEdges(text, node);
|
|
231
|
+
|
|
232
|
+
// (volto) Return null if the element is not adjacent to an inline node
|
|
233
|
+
// This will cause the element to be ignored in the deserialization
|
|
234
|
+
// TODO: use the node traverse functions defined here
|
|
235
|
+
if (
|
|
236
|
+
is_all_ws(text) &&
|
|
237
|
+
!(
|
|
238
|
+
isInline(node.previousSibling) ||
|
|
239
|
+
isInline(node.nextSibling) ||
|
|
240
|
+
isInline(node.parentNode.nextSibling) ||
|
|
241
|
+
isInline(node.parentNode.previousSibling)
|
|
242
|
+
)
|
|
243
|
+
) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return text;
|
|
248
|
+
};
|
package/src/components/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export EventDetails from '@plone/volto/components/theme/EventDetails/EventDetail
|
|
|
30
30
|
export PreviewImage from '@plone/volto/components/theme/PreviewImage/PreviewImage';
|
|
31
31
|
|
|
32
32
|
export Error from '@plone/volto/components/theme/Error/Error';
|
|
33
|
+
export ErrorBoundary from '@plone/volto/components/theme/Error/ErrorBoundary';
|
|
33
34
|
export NotFound from '@plone/volto/components/theme/NotFound/NotFound';
|
|
34
35
|
export Forbidden from '@plone/volto/components/theme/Forbidden/Forbidden';
|
|
35
36
|
export Unauthorized from '@plone/volto/components/theme/Unauthorized/Unauthorized';
|
|
@@ -172,6 +173,7 @@ export ObjectBrowserWidgetMode from '@plone/volto/components/manage/Widgets/Obje
|
|
|
172
173
|
export ObjectWidget from '@plone/volto/components/manage/Widgets/ObjectWidget';
|
|
173
174
|
export ObjectListWidget from '@plone/volto/components/manage/Widgets/ObjectListWidget';
|
|
174
175
|
|
|
176
|
+
export EditDefaultBlock from '@plone/volto/components/manage/Blocks/Block/DefaultEdit';
|
|
175
177
|
export EditDescriptionBlock from '@plone/volto/components/manage/Blocks/Description/Edit';
|
|
176
178
|
export EditTitleBlock from '@plone/volto/components/manage/Blocks/Title/Edit';
|
|
177
179
|
export EditToCBlock from '@plone/volto/components/manage/Blocks/ToC/Edit';
|
|
@@ -185,6 +187,7 @@ export ViewHeroImageLeftBlock from '@plone/volto/components/manage/Blocks/HeroIm
|
|
|
185
187
|
export EditMapBlock from '@plone/volto/components/manage/Blocks/Maps/Edit';
|
|
186
188
|
export EditHTMLBlock from '@plone/volto/components/manage/Blocks/HTML/Edit';
|
|
187
189
|
|
|
190
|
+
export ViewDefaultBlock from '@plone/volto/components/manage/Blocks/Block/DefaultView';
|
|
188
191
|
export ViewDescriptionBlock from '@plone/volto/components/manage/Blocks/Description/View';
|
|
189
192
|
export ViewTitleBlock from '@plone/volto/components/manage/Blocks/Title/View';
|
|
190
193
|
export ViewToCBlock from '@plone/volto/components/manage/Blocks/ToC/View';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import config from '@plone/volto/registry';
|
|
3
|
+
import { useIntl } from 'react-intl';
|
|
4
|
+
import { SidebarPortal } from '@plone/volto/components';
|
|
5
|
+
import { BlockDataForm } from '@plone/volto/components';
|
|
6
|
+
import DefaultBlockView from './DefaultView';
|
|
7
|
+
|
|
8
|
+
const DefaultBlockEdit = (props) => {
|
|
9
|
+
const { blocksConfig = config.blocks.blocksConfig } = props;
|
|
10
|
+
const { data, onChangeBlock, block, selected } = props;
|
|
11
|
+
const intl = useIntl();
|
|
12
|
+
const blockSchema = blocksConfig?.[data['@type']]?.blockSchema;
|
|
13
|
+
const schema =
|
|
14
|
+
typeof blockSchema === 'function'
|
|
15
|
+
? blockSchema({ ...props, intl })
|
|
16
|
+
: blockSchema;
|
|
17
|
+
|
|
18
|
+
const BlockView = blocksConfig?.[data['@type']]?.['view'] || DefaultBlockView;
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
<BlockView {...props} />
|
|
22
|
+
{schema ? (
|
|
23
|
+
<SidebarPortal selected={selected}>
|
|
24
|
+
<BlockDataForm
|
|
25
|
+
block={block}
|
|
26
|
+
schema={schema}
|
|
27
|
+
title={schema.title}
|
|
28
|
+
onChangeField={(id, value) => {
|
|
29
|
+
onChangeBlock(block, {
|
|
30
|
+
...data,
|
|
31
|
+
[id]: value,
|
|
32
|
+
});
|
|
33
|
+
}}
|
|
34
|
+
formData={data}
|
|
35
|
+
/>
|
|
36
|
+
</SidebarPortal>
|
|
37
|
+
) : (
|
|
38
|
+
''
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default DefaultBlockEdit;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Container, Segment, Grid, Label } from 'semantic-ui-react';
|
|
4
|
+
import { ErrorBoundary } from '@plone/volto/components';
|
|
5
|
+
import { getWidget } from '@plone/volto/helpers/Widget/utils';
|
|
6
|
+
import config from '@plone/volto/registry';
|
|
7
|
+
|
|
8
|
+
const messages = defineMessages({
|
|
9
|
+
unknownBlock: {
|
|
10
|
+
id: 'Unknown Block',
|
|
11
|
+
defaultMessage: 'Unknown Block {block}',
|
|
12
|
+
},
|
|
13
|
+
invalidBlock: {
|
|
14
|
+
id: 'Invalid Block',
|
|
15
|
+
defaultMessage: 'Invalid block - Will be removed on saving',
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const DefaultBlockView = (props) => {
|
|
20
|
+
const { data, block } = props;
|
|
21
|
+
const intl = useIntl();
|
|
22
|
+
const { views } = config.widgets;
|
|
23
|
+
const { blocksConfig = config.blocks.blocksConfig } = props;
|
|
24
|
+
if (!data)
|
|
25
|
+
return <div key={block}>{intl.formatMessage(messages.invalidBlock)}</div>;
|
|
26
|
+
// Compatibility with RenderBlocks non-view
|
|
27
|
+
|
|
28
|
+
const blockSchema = blocksConfig?.[data['@type']]?.blockSchema;
|
|
29
|
+
const schema =
|
|
30
|
+
typeof blockSchema === 'function'
|
|
31
|
+
? blockSchema({ ...props, intl })
|
|
32
|
+
: blockSchema;
|
|
33
|
+
const fieldsets = schema.fieldsets || [];
|
|
34
|
+
|
|
35
|
+
return schema ? (
|
|
36
|
+
<Container className="page-block">
|
|
37
|
+
{fieldsets?.map((fs) => {
|
|
38
|
+
return (
|
|
39
|
+
<div className="fieldset" key={fs.id}>
|
|
40
|
+
{fs.id !== 'default' && <h2>{fs.title}</h2>}
|
|
41
|
+
{fs.fields?.map((f, key) => {
|
|
42
|
+
let field = {
|
|
43
|
+
...schema?.properties[f],
|
|
44
|
+
id: f,
|
|
45
|
+
widget: getWidget(f, schema?.properties[f]),
|
|
46
|
+
};
|
|
47
|
+
let Widget = views?.getWidget(field);
|
|
48
|
+
return f !== 'title' ? (
|
|
49
|
+
<Grid celled="internally" key={key}>
|
|
50
|
+
<Grid.Row>
|
|
51
|
+
<Label>{field.title}:</Label>
|
|
52
|
+
</Grid.Row>
|
|
53
|
+
<Grid.Row>
|
|
54
|
+
<Segment basic>
|
|
55
|
+
<ErrorBoundary name={f}>
|
|
56
|
+
<Widget value={data[f]} />
|
|
57
|
+
</ErrorBoundary>
|
|
58
|
+
</Segment>
|
|
59
|
+
</Grid.Row>
|
|
60
|
+
</Grid>
|
|
61
|
+
) : (
|
|
62
|
+
<Widget key={key} value={data[f]} />
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</Container>
|
|
69
|
+
) : (
|
|
70
|
+
<div key={block}>
|
|
71
|
+
{intl.formatMessage(messages.unknownBlock, {
|
|
72
|
+
block: data['@type'],
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export default DefaultBlockView;
|
|
@@ -13,6 +13,7 @@ import { setSidebarTab } from '@plone/volto/actions';
|
|
|
13
13
|
import config from '@plone/volto/registry';
|
|
14
14
|
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
|
|
15
15
|
import { applyBlockDefaults } from '@plone/volto/helpers';
|
|
16
|
+
import { ViewDefaultBlock, EditDefaultBlock } from '@plone/volto/components';
|
|
16
17
|
|
|
17
18
|
import {
|
|
18
19
|
SidebarPortal,
|
|
@@ -120,12 +121,12 @@ export class Edit extends Component {
|
|
|
120
121
|
|
|
121
122
|
const disableNewBlocks = this.props.data?.disableNewBlocks;
|
|
122
123
|
|
|
123
|
-
let Block = blocksConfig?.[type]?.['edit'] ||
|
|
124
|
+
let Block = blocksConfig?.[type]?.['edit'] || EditDefaultBlock;
|
|
124
125
|
if (
|
|
125
126
|
this.props.data?.readOnly ||
|
|
126
127
|
(!editable && !config.blocks.showEditBlocksInBabelView)
|
|
127
128
|
) {
|
|
128
|
-
Block = blocksConfig?.[type]?.['view'] ||
|
|
129
|
+
Block = blocksConfig?.[type]?.['view'] || ViewDefaultBlock;
|
|
129
130
|
}
|
|
130
131
|
const schema = blocksConfig?.[type]?.['schema'] || BlockSettingsSchema;
|
|
131
132
|
const blockHasOwnFocusManagement =
|
|
@@ -9,7 +9,6 @@ const HeroImageLeftBlockData = (props) => {
|
|
|
9
9
|
const schema = schemaHero({ ...props, intl });
|
|
10
10
|
return (
|
|
11
11
|
<BlockDataForm
|
|
12
|
-
block={block}
|
|
13
12
|
schema={schema}
|
|
14
13
|
title={schema.title}
|
|
15
14
|
onChangeField={(id, value) => {
|
|
@@ -18,7 +17,9 @@ const HeroImageLeftBlockData = (props) => {
|
|
|
18
17
|
[id]: value,
|
|
19
18
|
});
|
|
20
19
|
}}
|
|
20
|
+
onChangeBlock={onChangeBlock}
|
|
21
21
|
formData={data}
|
|
22
|
+
block={block}
|
|
22
23
|
/>
|
|
23
24
|
);
|
|
24
25
|
};
|