@plone/volto 18.0.0-alpha.35 → 18.0.0-alpha.36
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/.release-it.json +1 -1
- package/CHANGELOG.md +32 -0
- package/locales/ca/LC_MESSAGES/volto.po +41 -4
- package/locales/ca.json +1 -1
- package/locales/de/LC_MESSAGES/volto.po +41 -4
- package/locales/de.json +1 -1
- package/locales/en/LC_MESSAGES/volto.po +41 -4
- package/locales/en.json +1 -1
- package/locales/es/LC_MESSAGES/volto.po +41 -4
- package/locales/es.json +1 -1
- package/locales/eu/LC_MESSAGES/volto.po +41 -4
- package/locales/eu.json +1 -1
- package/locales/fi/LC_MESSAGES/volto.po +44 -7
- package/locales/fi.json +1 -1
- package/locales/fr/LC_MESSAGES/volto.po +41 -4
- package/locales/fr.json +1 -1
- package/locales/hi/LC_MESSAGES/volto.po +41 -4
- package/locales/hi.json +1 -1
- package/locales/it/LC_MESSAGES/volto.po +41 -4
- package/locales/it.json +1 -1
- package/locales/ja/LC_MESSAGES/volto.po +41 -4
- package/locales/ja.json +1 -1
- package/locales/nl/LC_MESSAGES/volto.po +41 -4
- package/locales/nl.json +1 -1
- package/locales/pt/LC_MESSAGES/volto.po +41 -4
- package/locales/pt.json +1 -1
- package/locales/pt_BR/LC_MESSAGES/volto.po +41 -4
- package/locales/pt_BR.json +1 -1
- package/locales/ro/LC_MESSAGES/volto.po +41 -4
- package/locales/ro.json +1 -1
- package/locales/volto.pot +42 -5
- package/locales/zh_CN/LC_MESSAGES/volto.po +41 -4
- package/locales/zh_CN.json +1 -1
- package/package.json +3 -3
- package/src/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm.jsx +78 -72
- package/src/components/manage/AnchorPlugin/useLinkEditor.js +79 -0
- package/src/components/manage/Blocks/Image/Edit.jsx +45 -347
- package/src/components/manage/Blocks/Teaser/Data.jsx +69 -1
- package/src/components/manage/Blocks/Teaser/schema.js +24 -3
- package/src/components/manage/Form/BlocksToolbar.jsx +8 -7
- package/src/components/manage/Form/Form.jsx +8 -1
- package/src/components/manage/Form/InlineForm.jsx +4 -0
- package/src/components/manage/Widgets/ImageWidget.jsx +311 -0
- package/src/components/manage/Widgets/ImageWidget.stories.jsx +46 -0
- package/src/config/NonContentRoutes.jsx +2 -1
- package/src/config/Widgets.jsx +2 -0
- package/src/helpers/Blocks/cloneBlocks.test.js +54 -0
- package/src/helpers/Blocks/cloneBlocks.ts +43 -0
- package/src/icons/external-link.svg +4 -0
- package/theme/themes/pastanaga/collections/form.overrides +4 -0
- package/theme/themes/pastanaga/elements/button.overrides +2 -1
- package/theme/themes/pastanaga/extras/blocks.less +40 -4
- package/theme/themes/pastanaga/extras/grid.less +3 -3
- package/theme/themes/pastanaga/extras/teaser.less +53 -0
- package/theme/themes/pastanaga/extras/widgets.less +108 -0
- package/types/components/manage/AnchorPlugin/useLinkEditor.d.ts +7 -0
- package/types/components/manage/Blocks/Teaser/schema.d.ts +9 -2
- package/types/components/manage/Widgets/ImageWidget.d.ts +10 -0
- package/types/components/manage/Widgets/ImageWidget.stories.d.ts +11 -0
- package/types/config/Widgets.d.ts +2 -0
- package/types/helpers/Blocks/cloneBlocks.d.ts +1 -0
- package/types/helpers/Blocks/cloneBlocks.test.d.ts +1 -0
- /package/src/components/manage/Blocks/Image/{schema.js → schema.jsx} +0 -0
- /package/src/components/theme/LanguageSelector/{LanguageSelector.js → LanguageSelector.jsx} +0 -0
- /package/src/helpers/Extensions/{withBlockExtensions.js → withBlockExtensions.jsx} +0 -0
- /package/src/helpers/Extensions/{withBlockSchemaEnhancer.js → withBlockSchemaEnhancer.jsx} +0 -0
- /package/src/helpers/FormValidation/{FormValidation.js → FormValidation.jsx} +0 -0
- /package/src/helpers/Helmet/{Helmet.js → Helmet.jsx} +0 -0
- /package/src/helpers/Loadable/{Loadable.js → Loadable.jsx} +0 -0
- /package/src/helpers/Loadable/__mocks__/{Loadable.js → Loadable.jsx} +0 -0
- /package/src/helpers/Utils/{Utils.js → Utils.jsx} +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Button, Dimmer, Loader, Message } from 'semantic-ui-react';
|
|
3
|
+
import { useIntl, defineMessages } from 'react-intl';
|
|
4
|
+
import { useDispatch } from 'react-redux';
|
|
5
|
+
import { useLocation } from 'react-router-dom';
|
|
6
|
+
import loadable from '@loadable/component';
|
|
7
|
+
import { connect } from 'react-redux';
|
|
8
|
+
import { compose } from 'redux';
|
|
9
|
+
import useLinkEditor from '@plone/volto/components/manage/AnchorPlugin/useLinkEditor';
|
|
10
|
+
import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
flattenToAppURL,
|
|
14
|
+
getBaseUrl,
|
|
15
|
+
isInternalURL,
|
|
16
|
+
validateFileUploadSize,
|
|
17
|
+
} from '@plone/volto/helpers';
|
|
18
|
+
import { createContent } from '@plone/volto/actions';
|
|
19
|
+
import { readAsDataURL } from 'promise-file-reader';
|
|
20
|
+
import { FormFieldWrapper, Icon } from '@plone/volto/components';
|
|
21
|
+
|
|
22
|
+
import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
|
|
23
|
+
import clearSVG from '@plone/volto/icons/clear.svg';
|
|
24
|
+
import navTreeSVG from '@plone/volto/icons/nav.svg';
|
|
25
|
+
import linkSVG from '@plone/volto/icons/link.svg';
|
|
26
|
+
import uploadSVG from '@plone/volto/icons/upload.svg';
|
|
27
|
+
|
|
28
|
+
const Dropzone = loadable(() => import('react-dropzone'));
|
|
29
|
+
|
|
30
|
+
export const ImageToolbar = ({ className, data, id, onChange, selected }) => (
|
|
31
|
+
<div className="image-upload-widget-toolbar">
|
|
32
|
+
<Button.Group>
|
|
33
|
+
<Button icon basic onClick={() => onChange(id, null)}>
|
|
34
|
+
<Icon className="circled" name={clearSVG} size="24px" color="#e40166" />
|
|
35
|
+
</Button>
|
|
36
|
+
</Button.Group>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const messages = defineMessages({
|
|
41
|
+
addImage: {
|
|
42
|
+
id: 'Browse the site, drop an image, or type a URL',
|
|
43
|
+
defaultMessage: 'Browse the site, drop an image, or use a URL',
|
|
44
|
+
},
|
|
45
|
+
pickAnImage: {
|
|
46
|
+
id: 'pickAnImage',
|
|
47
|
+
defaultMessage: 'Pick an existing image',
|
|
48
|
+
},
|
|
49
|
+
uploadAnImage: {
|
|
50
|
+
id: 'uploadAnImage',
|
|
51
|
+
defaultMessage: 'Upload an image from your computer',
|
|
52
|
+
},
|
|
53
|
+
linkAnImage: {
|
|
54
|
+
id: 'linkAnImage',
|
|
55
|
+
defaultMessage: 'Enter a URL to an image',
|
|
56
|
+
},
|
|
57
|
+
uploadingImage: {
|
|
58
|
+
id: 'Uploading image',
|
|
59
|
+
defaultMessage: 'Uploading image',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const UnconnectedImageInput = (props) => {
|
|
64
|
+
const {
|
|
65
|
+
id,
|
|
66
|
+
onChange,
|
|
67
|
+
onFocus,
|
|
68
|
+
openObjectBrowser,
|
|
69
|
+
value,
|
|
70
|
+
imageSize = 'teaser',
|
|
71
|
+
selected = true,
|
|
72
|
+
hideLinkPicker = false,
|
|
73
|
+
hideObjectBrowserPicker = false,
|
|
74
|
+
restrictFileUpload = false,
|
|
75
|
+
objectBrowserPickerType = 'image',
|
|
76
|
+
description,
|
|
77
|
+
placeholderLinkInput = '',
|
|
78
|
+
} = props;
|
|
79
|
+
|
|
80
|
+
const intl = useIntl();
|
|
81
|
+
const linkEditor = useLinkEditor();
|
|
82
|
+
const location = useLocation();
|
|
83
|
+
const dispatch = useDispatch();
|
|
84
|
+
const contextUrl = location.pathname;
|
|
85
|
+
|
|
86
|
+
const [uploading, setUploading] = React.useState(false);
|
|
87
|
+
const [dragging, setDragging] = React.useState(false);
|
|
88
|
+
|
|
89
|
+
const imageUploadInputRef = useRef(null);
|
|
90
|
+
|
|
91
|
+
const requestId = `image-upload-${id}`;
|
|
92
|
+
|
|
93
|
+
const { loading, loaded } = props.request;
|
|
94
|
+
const { content } = props;
|
|
95
|
+
const imageId = content?.['@id'];
|
|
96
|
+
const image = content?.image;
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (uploading && !loading && loaded) {
|
|
100
|
+
setUploading(false);
|
|
101
|
+
onChange(id, imageId, {
|
|
102
|
+
image_field: 'image',
|
|
103
|
+
image_scales: { image: [image] },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}, [loading, loaded, uploading, imageId, image, id, onChange]); // Explicitly list all dependencies
|
|
107
|
+
|
|
108
|
+
const handleUpload = React.useCallback(
|
|
109
|
+
(eventOrFile) => {
|
|
110
|
+
if (restrictFileUpload === true) return;
|
|
111
|
+
eventOrFile.target && eventOrFile.stopPropagation();
|
|
112
|
+
|
|
113
|
+
setUploading(true);
|
|
114
|
+
const file = eventOrFile.target
|
|
115
|
+
? eventOrFile.target.files[0]
|
|
116
|
+
: eventOrFile[0];
|
|
117
|
+
if (!validateFileUploadSize(file, intl.formatMessage)) return;
|
|
118
|
+
readAsDataURL(file).then((fileData) => {
|
|
119
|
+
const fields = fileData.match(/^data:(.*);(.*),(.*)$/);
|
|
120
|
+
dispatch(
|
|
121
|
+
createContent(
|
|
122
|
+
getBaseUrl(contextUrl),
|
|
123
|
+
{
|
|
124
|
+
'@type': 'Image',
|
|
125
|
+
title: file.name,
|
|
126
|
+
image: {
|
|
127
|
+
data: fields[3],
|
|
128
|
+
encoding: fields[2],
|
|
129
|
+
'content-type': fields[1],
|
|
130
|
+
filename: file.name,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
props.block || requestId,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
[
|
|
139
|
+
restrictFileUpload,
|
|
140
|
+
intl.formatMessage,
|
|
141
|
+
dispatch,
|
|
142
|
+
props,
|
|
143
|
+
contextUrl,
|
|
144
|
+
requestId,
|
|
145
|
+
],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const onDragEnter = React.useCallback(() => {
|
|
149
|
+
if (restrictFileUpload === false) setDragging(true);
|
|
150
|
+
}, [restrictFileUpload]);
|
|
151
|
+
const onDragLeave = React.useCallback(() => setDragging(false), []);
|
|
152
|
+
|
|
153
|
+
return value ? (
|
|
154
|
+
<div
|
|
155
|
+
className="image-upload-widget-image"
|
|
156
|
+
onClick={onFocus}
|
|
157
|
+
onKeyDown={onFocus}
|
|
158
|
+
role="toolbar"
|
|
159
|
+
>
|
|
160
|
+
{selected && <ImageToolbar {...props} />}
|
|
161
|
+
<img
|
|
162
|
+
className={props.className}
|
|
163
|
+
src={`${flattenToAppURL(value)}/@@images/image/${imageSize}`}
|
|
164
|
+
alt=""
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
) : (
|
|
168
|
+
<div
|
|
169
|
+
className="image-upload-widget"
|
|
170
|
+
onClick={onFocus}
|
|
171
|
+
onKeyDown={onFocus}
|
|
172
|
+
role="toolbar"
|
|
173
|
+
>
|
|
174
|
+
<Dropzone
|
|
175
|
+
noClick
|
|
176
|
+
onDrop={handleUpload}
|
|
177
|
+
onDragEnter={onDragEnter}
|
|
178
|
+
onDragLeave={onDragLeave}
|
|
179
|
+
className="dropzone"
|
|
180
|
+
>
|
|
181
|
+
{({ getRootProps, getInputProps }) => (
|
|
182
|
+
<div {...getRootProps()}>
|
|
183
|
+
<Message>
|
|
184
|
+
{dragging && <Dimmer active></Dimmer>}
|
|
185
|
+
{uploading && (
|
|
186
|
+
<Dimmer active>
|
|
187
|
+
<Loader indeterminate>
|
|
188
|
+
{intl.formatMessage(messages.uploadingImage)}
|
|
189
|
+
</Loader>
|
|
190
|
+
</Dimmer>
|
|
191
|
+
)}
|
|
192
|
+
<img src={imageBlockSVG} alt="" className="placeholder" />
|
|
193
|
+
<p>{description || intl.formatMessage(messages.addImage)}</p>
|
|
194
|
+
<div className="toolbar-wrapper">
|
|
195
|
+
<div className="toolbar-inner" ref={linkEditor.anchorNode}>
|
|
196
|
+
{hideObjectBrowserPicker === false && (
|
|
197
|
+
<Button.Group>
|
|
198
|
+
<Button
|
|
199
|
+
aria-label={intl.formatMessage(messages.pickAnImage)}
|
|
200
|
+
icon
|
|
201
|
+
basic
|
|
202
|
+
onClick={(e) => {
|
|
203
|
+
onFocus && onFocus();
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
openObjectBrowser({
|
|
206
|
+
mode: objectBrowserPickerType,
|
|
207
|
+
onSelectItem: (
|
|
208
|
+
url,
|
|
209
|
+
{ title, image_field, image_scales },
|
|
210
|
+
) => {
|
|
211
|
+
onChange(props.id, flattenToAppURL(url), {
|
|
212
|
+
title,
|
|
213
|
+
image_field,
|
|
214
|
+
image_scales,
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
currentPath: contextUrl,
|
|
218
|
+
});
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
<Icon name={navTreeSVG} size="24px" />
|
|
222
|
+
</Button>
|
|
223
|
+
</Button.Group>
|
|
224
|
+
)}
|
|
225
|
+
{restrictFileUpload === false && (
|
|
226
|
+
<Button.Group>
|
|
227
|
+
<Button
|
|
228
|
+
aria-label={intl.formatMessage(messages.uploadAnImage)}
|
|
229
|
+
icon
|
|
230
|
+
basic
|
|
231
|
+
compact
|
|
232
|
+
onClick={() => {
|
|
233
|
+
imageUploadInputRef.current.click();
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
<Icon name={uploadSVG} size="24px" />
|
|
237
|
+
</Button>
|
|
238
|
+
<input
|
|
239
|
+
{...getInputProps({
|
|
240
|
+
type: 'file',
|
|
241
|
+
ref: imageUploadInputRef,
|
|
242
|
+
onChange: handleUpload,
|
|
243
|
+
style: { display: 'none' },
|
|
244
|
+
})}
|
|
245
|
+
/>
|
|
246
|
+
</Button.Group>
|
|
247
|
+
)}
|
|
248
|
+
|
|
249
|
+
{hideLinkPicker === false && (
|
|
250
|
+
<Button.Group>
|
|
251
|
+
<Button
|
|
252
|
+
icon
|
|
253
|
+
basic
|
|
254
|
+
aria-label={intl.formatMessage(messages.linkAnImage)}
|
|
255
|
+
onClick={(e) => {
|
|
256
|
+
!props.selected && onFocus && onFocus();
|
|
257
|
+
linkEditor.show();
|
|
258
|
+
}}
|
|
259
|
+
>
|
|
260
|
+
<Icon name={linkSVG} circled size="24px" />
|
|
261
|
+
</Button>
|
|
262
|
+
</Button.Group>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
{linkEditor.anchorNode && (
|
|
266
|
+
<linkEditor.LinkEditor
|
|
267
|
+
value={value}
|
|
268
|
+
placeholder={
|
|
269
|
+
placeholderLinkInput ||
|
|
270
|
+
intl.formatMessage(messages.linkAnImage)
|
|
271
|
+
}
|
|
272
|
+
objectBrowserPickerType={objectBrowserPickerType}
|
|
273
|
+
onChange={(_, e) =>
|
|
274
|
+
onChange(
|
|
275
|
+
props.id,
|
|
276
|
+
isInternalURL(e) ? flattenToAppURL(e) : e,
|
|
277
|
+
{},
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
id={id}
|
|
281
|
+
/>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</Message>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</Dropzone>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export const ImageInput = compose(
|
|
293
|
+
connect(
|
|
294
|
+
(state, ownProps) => {
|
|
295
|
+
const requestId = `image-upload-${ownProps.id}`;
|
|
296
|
+
return {
|
|
297
|
+
request: state.content.subrequests[ownProps.block || requestId] || {},
|
|
298
|
+
content: state.content.subrequests[ownProps.block || requestId]?.data,
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
{ createContent },
|
|
302
|
+
),
|
|
303
|
+
)(withObjectBrowser(UnconnectedImageInput));
|
|
304
|
+
|
|
305
|
+
const ImageUploadWidget = (props) => (
|
|
306
|
+
<FormFieldWrapper {...props} className="image-upload-widget">
|
|
307
|
+
<ImageInput {...props} />
|
|
308
|
+
</FormFieldWrapper>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
export default ImageUploadWidget;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ImageWidget from './ImageWidget';
|
|
3
|
+
import WidgetStory from './story';
|
|
4
|
+
|
|
5
|
+
export const Default = WidgetStory.bind({
|
|
6
|
+
props: { id: 'image_widget', title: 'Image', data: {}, properties: {} },
|
|
7
|
+
widget: ImageWidget,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const Filled = WidgetStory.bind({
|
|
11
|
+
props: {
|
|
12
|
+
id: 'image_widget',
|
|
13
|
+
title: 'Image',
|
|
14
|
+
data: {},
|
|
15
|
+
properties: {},
|
|
16
|
+
value: 'halfdome2022.jpeg',
|
|
17
|
+
},
|
|
18
|
+
widget: ImageWidget,
|
|
19
|
+
});
|
|
20
|
+
Filled.args = {
|
|
21
|
+
value: 'halfdome2022.jpeg',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Inline = WidgetStory.bind({
|
|
25
|
+
props: {
|
|
26
|
+
id: 'image_widget',
|
|
27
|
+
title: 'Image',
|
|
28
|
+
data: {},
|
|
29
|
+
properties: {},
|
|
30
|
+
inline: true,
|
|
31
|
+
},
|
|
32
|
+
widget: ImageWidget,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
title: 'Edit Widgets/Image',
|
|
37
|
+
component: ImageWidget,
|
|
38
|
+
decorators: [
|
|
39
|
+
(Story) => (
|
|
40
|
+
<div className="ui segment form attached" style={{ width: '400px' }}>
|
|
41
|
+
<Story />
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
],
|
|
45
|
+
argTypes: {},
|
|
46
|
+
};
|
package/src/config/Widgets.jsx
CHANGED
|
@@ -50,6 +50,7 @@ import TextViewWidget from '@plone/volto/components/theme/Widgets/TextWidget';
|
|
|
50
50
|
import TitleViewWidget from '@plone/volto/components/theme/Widgets/TitleWidget';
|
|
51
51
|
import TokenViewWidget from '@plone/volto/components/theme/Widgets/TokenWidget';
|
|
52
52
|
import UrlViewWidget from '@plone/volto/components/theme/Widgets/UrlWidget';
|
|
53
|
+
import ImageWidget from '@plone/volto/components/manage/Widgets/ImageWidget';
|
|
53
54
|
|
|
54
55
|
// Widgets mapping
|
|
55
56
|
export const widgetMapping = {
|
|
@@ -68,6 +69,7 @@ export const widgetMapping = {
|
|
|
68
69
|
date: DatetimeWidget,
|
|
69
70
|
password: PasswordWidget,
|
|
70
71
|
file: FileWidget,
|
|
72
|
+
image: ImageWidget,
|
|
71
73
|
align: AlignWidget,
|
|
72
74
|
buttons: ButtonsWidget,
|
|
73
75
|
url: UrlWidget,
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { cloneDeep } from 'lodash';
|
|
2
|
+
import { cloneBlocks } from './cloneBlocks';
|
|
3
|
+
|
|
4
|
+
describe('cloneBlocks', () => {
|
|
5
|
+
it('cloneBlocks - basic', () => {
|
|
6
|
+
const blocksData = {
|
|
7
|
+
blocks: {
|
|
8
|
+
a: { '@type': 'slate', value: 1 },
|
|
9
|
+
b: { '@type': 'slate', value: 2 },
|
|
10
|
+
},
|
|
11
|
+
blocks_layout: { items: ['a', 'b'] },
|
|
12
|
+
};
|
|
13
|
+
const result = cloneBlocks(blocksData);
|
|
14
|
+
|
|
15
|
+
expect(result.blocks_layout.items[0]).not.toBe('a');
|
|
16
|
+
expect(result.blocks[result.blocks_layout.items[0]]).toStrictEqual({
|
|
17
|
+
'@type': 'slate',
|
|
18
|
+
value: 1,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('cloneBlocks - nested', () => {
|
|
23
|
+
const blocksData = {
|
|
24
|
+
blocks: {
|
|
25
|
+
a: { '@type': 'slate', value: 1 },
|
|
26
|
+
b: { '@type': 'slate', value: 2 },
|
|
27
|
+
},
|
|
28
|
+
blocks_layout: { items: ['a', 'b'] },
|
|
29
|
+
};
|
|
30
|
+
blocksData.blocks.c = cloneDeep(blocksData);
|
|
31
|
+
blocksData.blocks_layout.items.push('c');
|
|
32
|
+
|
|
33
|
+
const result = cloneBlocks(blocksData);
|
|
34
|
+
|
|
35
|
+
expect(result.blocks_layout.items[0]).not.toBe('a');
|
|
36
|
+
expect(result.blocks[result.blocks_layout.items[0]]).toStrictEqual({
|
|
37
|
+
'@type': 'slate',
|
|
38
|
+
value: 1,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
result.blocks[result.blocks_layout.items[2]].blocks_layout.items[0],
|
|
43
|
+
).not.toBe('c');
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
result.blocks[result.blocks_layout.items[2]].blocks[
|
|
47
|
+
result.blocks[result.blocks_layout.items[2]].blocks_layout.items[0]
|
|
48
|
+
],
|
|
49
|
+
).toStrictEqual({
|
|
50
|
+
'@type': 'slate',
|
|
51
|
+
value: 1,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
import {
|
|
4
|
+
getBlocksFieldname,
|
|
5
|
+
getBlocksLayoutFieldname,
|
|
6
|
+
hasBlocksData,
|
|
7
|
+
} from './Blocks';
|
|
8
|
+
import config from '@plone/registry';
|
|
9
|
+
|
|
10
|
+
export function cloneBlocks(blocksData) {
|
|
11
|
+
if (hasBlocksData(blocksData)) {
|
|
12
|
+
const blocksFieldname = getBlocksFieldname(blocksData);
|
|
13
|
+
const blocksLayoutFieldname = getBlocksLayoutFieldname(blocksData);
|
|
14
|
+
|
|
15
|
+
const cloneWithIds = Object.keys(blocksData.blocks)
|
|
16
|
+
.map((key) => {
|
|
17
|
+
const block = blocksData.blocks[key];
|
|
18
|
+
const blockConfig = config.blocks.blocksConfig[blocksData['@type']];
|
|
19
|
+
return blockConfig?.cloneData
|
|
20
|
+
? blockConfig.cloneData(block)
|
|
21
|
+
: [uuid(), cloneBlocks(block)];
|
|
22
|
+
})
|
|
23
|
+
.filter((info) => !!info); // some blocks may refuse to be copied
|
|
24
|
+
|
|
25
|
+
const newBlockData = {
|
|
26
|
+
...blocksData,
|
|
27
|
+
[blocksFieldname]: {
|
|
28
|
+
...Object.assign(
|
|
29
|
+
{},
|
|
30
|
+
...cloneWithIds.map(([id, data]) => ({ [id]: data })),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
[blocksLayoutFieldname]: {
|
|
34
|
+
...blocksData[blocksLayoutFieldname],
|
|
35
|
+
items: [...cloneWithIds.map(([id]) => id)],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return newBlockData;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return blocksData;
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z"/>
|
|
3
|
+
<path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z"/>
|
|
4
|
+
</svg>
|
|
@@ -401,7 +401,6 @@ body.has-toolbar.has-sidebar-collapsed .ui.wrapper > .ui.inner.block.full {
|
|
|
401
401
|
padding: 4px;
|
|
402
402
|
border-radius: 1px;
|
|
403
403
|
margin-left: 4px;
|
|
404
|
-
color: @brown !important;
|
|
405
404
|
|
|
406
405
|
&:hover {
|
|
407
406
|
background-color: #edf1f2 !important;
|
|
@@ -414,13 +413,50 @@ body.has-toolbar.has-sidebar-collapsed .ui.wrapper > .ui.inner.block.full {
|
|
|
414
413
|
}
|
|
415
414
|
}
|
|
416
415
|
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.toolbar-buttons {
|
|
419
|
+
// used by the ImageUploadWidget
|
|
420
|
+
display: flex;
|
|
421
|
+
justify-content: center;
|
|
422
|
+
gap: 4px;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.image-upload-widget-toolbar {
|
|
426
|
+
position: absolute;
|
|
427
|
+
top: 1em;
|
|
428
|
+
right: 1em;
|
|
429
|
+
|
|
430
|
+
.icon.circled {
|
|
431
|
+
border: 1px solid #e4e8ec;
|
|
432
|
+
background: #f3f5f7;
|
|
433
|
+
box-shadow: inset 0 0 0 0.14285714rem transparent !important;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
div.image-upload-widget-image {
|
|
438
|
+
position: relative;
|
|
439
|
+
|
|
440
|
+
img {
|
|
441
|
+
max-width: 100% !important;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.image-upload-widget {
|
|
446
|
+
text-align: center;
|
|
417
447
|
|
|
418
|
-
.
|
|
419
|
-
|
|
420
|
-
|
|
448
|
+
.toolbar-wrapper {
|
|
449
|
+
display: flex;
|
|
450
|
+
flex-direction: column;
|
|
451
|
+
justify-content: flex-end;
|
|
421
452
|
}
|
|
422
453
|
}
|
|
423
454
|
|
|
455
|
+
.ui.active.basic.icon.button {
|
|
456
|
+
border: 1px solid #2996da;
|
|
457
|
+
background: transparent !important;
|
|
458
|
+
}
|
|
459
|
+
|
|
424
460
|
// Hero block
|
|
425
461
|
.block.hero {
|
|
426
462
|
clear: both;
|
|
@@ -396,9 +396,9 @@ body.has-toolbar.has-sidebar .drag.gridBlock.handle.wrapper {
|
|
|
396
396
|
|
|
397
397
|
// Aspect ratio on all images
|
|
398
398
|
.grid-image-wrapper img,
|
|
399
|
-
.gridBlock .block.image img,
|
|
400
|
-
.block-editor-gridBlock .block-editor-image img,
|
|
401
|
-
.block-editor-image.contained img {
|
|
399
|
+
.gridBlock .block.image img:not(.placeholder),
|
|
400
|
+
.block-editor-gridBlock .block-editor-image img:not(.placeholder),
|
|
401
|
+
.block-editor-image.contained img:not(.placeholder) {
|
|
402
402
|
width: 100%;
|
|
403
403
|
aspect-ratio: @grid-images-aspect-ratio;
|
|
404
404
|
object-fit: cover;
|
|
@@ -164,3 +164,56 @@
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
#sidebar-properties .ui.form {
|
|
169
|
+
.teaser.block.overwrite.actions {
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
|
|
173
|
+
> div {
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
|
|
177
|
+
.ui.label {
|
|
178
|
+
padding-left: 0;
|
|
179
|
+
background: unset;
|
|
180
|
+
font-size: 15px;
|
|
181
|
+
text-transform: capitalize;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
h3 {
|
|
186
|
+
text-transform: capitalize;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.ui.button:hover {
|
|
190
|
+
background: none !important;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.ui.message.warning:has(.teaser.block.overwrite.actions) {
|
|
195
|
+
display: block;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#sidebar-properties .ui.segment.attached.actions {
|
|
200
|
+
display: flex;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
padding: 1.5em 1em;
|
|
203
|
+
border-top: none;
|
|
204
|
+
|
|
205
|
+
.ui.buttons.refresh.teaser button {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
padding: 0.5833em 0.833em;
|
|
209
|
+
background-color: #e8e8e8 !important;
|
|
210
|
+
color: rgba(0, 0, 0, 0.6) !important;
|
|
211
|
+
font-size: 0.85714286rem;
|
|
212
|
+
font-weight: bold;
|
|
213
|
+
|
|
214
|
+
svg {
|
|
215
|
+
padding: 0 0 0 0.5em;
|
|
216
|
+
margin-right: 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|