@plone/volto-slate 18.0.0-alpha.4
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/.eslintrc.js +6 -0
- package/.release-it.json +25 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE.md +21 -0
- package/README.md +10 -0
- package/build/messages/src/blocks/Table/TableBlockEdit.json +90 -0
- package/build/messages/src/blocks/Text/DefaultTextBlockEditor.json +6 -0
- package/build/messages/src/blocks/Text/DetachedTextBlockEditor.json +6 -0
- package/build/messages/src/blocks/Text/SlashMenu.json +6 -0
- package/build/messages/src/editor/plugins/AdvancedLink/index.json +10 -0
- package/build/messages/src/editor/plugins/Link/index.json +10 -0
- package/build/messages/src/editor/plugins/Table/index.json +30 -0
- package/build/messages/src/elementEditor/messages.json +10 -0
- package/build/messages/src/widgets/HtmlSlateWidget.json +6 -0
- package/build/messages/src/widgets/RichTextWidgetView.json +6 -0
- package/locales/de/LC_MESSAGES/volto.po +148 -0
- package/locales/en/LC_MESSAGES/volto.po +148 -0
- package/locales/volto.pot +182 -0
- package/package.json +42 -0
- package/src/actions/content.js +30 -0
- package/src/actions/index.js +3 -0
- package/src/actions/plugins.js +9 -0
- package/src/actions/selection.js +22 -0
- package/src/blocks/Table/Cell.jsx +87 -0
- package/src/blocks/Table/Cell.test.js +54 -0
- package/src/blocks/Table/TableBlockEdit.jsx +694 -0
- package/src/blocks/Table/TableBlockEdit.test.js +40 -0
- package/src/blocks/Table/TableBlockView.jsx +150 -0
- package/src/blocks/Table/TableBlockView.test.js +49 -0
- package/src/blocks/Table/__snapshots__/Cell.test.js.snap +3 -0
- package/src/blocks/Table/__snapshots__/TableBlockEdit.test.js.snap +22 -0
- package/src/blocks/Table/__snapshots__/TableBlockView.test.js.snap +27 -0
- package/src/blocks/Table/deconstruct.js +113 -0
- package/src/blocks/Table/extensions/normalizeTable.js +5 -0
- package/src/blocks/Table/index.js +60 -0
- package/src/blocks/Table/schema.js +122 -0
- package/src/blocks/Text/DefaultTextBlockEditor.jsx +304 -0
- package/src/blocks/Text/DetachedTextBlockEditor.jsx +77 -0
- package/src/blocks/Text/MarkdownIntroduction.jsx +59 -0
- package/src/blocks/Text/PluginSidebar.jsx +18 -0
- package/src/blocks/Text/ShortcutListing.jsx +28 -0
- package/src/blocks/Text/SlashMenu.jsx +203 -0
- package/src/blocks/Text/TextBlockEdit.jsx +38 -0
- package/src/blocks/Text/TextBlockEdit.test.js +107 -0
- package/src/blocks/Text/TextBlockSchema.js +54 -0
- package/src/blocks/Text/TextBlockView.jsx +31 -0
- package/src/blocks/Text/__snapshots__/TextBlockEdit.test.js.snap +62 -0
- package/src/blocks/Text/css/editor.css +18 -0
- package/src/blocks/Text/extensions/Readme.md +49 -0
- package/src/blocks/Text/extensions/breakList.js +100 -0
- package/src/blocks/Text/extensions/index.js +6 -0
- package/src/blocks/Text/extensions/insertBreak.js +57 -0
- package/src/blocks/Text/extensions/isSelected.js +7 -0
- package/src/blocks/Text/extensions/normalizeExternalData.js +7 -0
- package/src/blocks/Text/extensions/withDeserializers.js +87 -0
- package/src/blocks/Text/extensions/withLists.js +5 -0
- package/src/blocks/Text/index.js +171 -0
- package/src/blocks/Text/keyboard/backspaceInList.js +58 -0
- package/src/blocks/Text/keyboard/breakBlocks.js +3 -0
- package/src/blocks/Text/keyboard/cancelEsc.js +7 -0
- package/src/blocks/Text/keyboard/indentListItems.js +240 -0
- package/src/blocks/Text/keyboard/index.js +52 -0
- package/src/blocks/Text/keyboard/joinBlocks.js +180 -0
- package/src/blocks/Text/keyboard/moveListItems.js +124 -0
- package/src/blocks/Text/keyboard/slashMenu.js +19 -0
- package/src/blocks/Text/keyboard/softBreak.js +7 -0
- package/src/blocks/Text/keyboard/traverseBlocks.js +81 -0
- package/src/blocks/Text/keyboard/unwrapEmptyString.js +26 -0
- package/src/blocks/Text/schema.js +39 -0
- package/src/constants.js +123 -0
- package/src/editor/EditorContext.jsx +5 -0
- package/src/editor/EditorReference.jsx +22 -0
- package/src/editor/SlateEditor.jsx +375 -0
- package/src/editor/config.jsx +344 -0
- package/src/editor/decorate.js +68 -0
- package/src/editor/deserialize.js +185 -0
- package/src/editor/extensions/index.js +6 -0
- package/src/editor/extensions/insertBreak.js +15 -0
- package/src/editor/extensions/insertData.js +161 -0
- package/src/editor/extensions/isInline.js +14 -0
- package/src/editor/extensions/normalizeExternalData.js +8 -0
- package/src/editor/extensions/normalizeNode.js +48 -0
- package/src/editor/extensions/withDeserializers.js +15 -0
- package/src/editor/extensions/withTestingFeatures.jsx +84 -0
- package/src/editor/index.js +14 -0
- package/src/editor/less/editor.less +173 -0
- package/src/editor/less/globals.less +18 -0
- package/src/editor/less/slate.less +28 -0
- package/src/editor/plugins/AdvancedLink/deserialize.js +90 -0
- package/src/editor/plugins/AdvancedLink/extensions.js +32 -0
- package/src/editor/plugins/AdvancedLink/index.js +50 -0
- package/src/editor/plugins/AdvancedLink/render.jsx +37 -0
- package/src/editor/plugins/AdvancedLink/schema.js +114 -0
- package/src/editor/plugins/AdvancedLink/styles.less +8 -0
- package/src/editor/plugins/Blockquote/index.js +30 -0
- package/src/editor/plugins/Callout/index.js +34 -0
- package/src/editor/plugins/Image/deconstruct.js +30 -0
- package/src/editor/plugins/Image/extensions.js +51 -0
- package/src/editor/plugins/Image/index.js +11 -0
- package/src/editor/plugins/Image/render.jsx +22 -0
- package/src/editor/plugins/Link/extensions.js +58 -0
- package/src/editor/plugins/Link/index.js +159 -0
- package/src/editor/plugins/Link/render.jsx +54 -0
- package/src/editor/plugins/Markdown/constants.js +81 -0
- package/src/editor/plugins/Markdown/extensions.js +336 -0
- package/src/editor/plugins/Markdown/index.js +28 -0
- package/src/editor/plugins/Markdown/utils.js +198 -0
- package/src/editor/plugins/StyleMenu/StyleMenu.jsx +153 -0
- package/src/editor/plugins/StyleMenu/index.js +19 -0
- package/src/editor/plugins/StyleMenu/style.less +29 -0
- package/src/editor/plugins/StyleMenu/utils.js +168 -0
- package/src/editor/plugins/Table/TableButton.jsx +142 -0
- package/src/editor/plugins/Table/TableCell.jsx +44 -0
- package/src/editor/plugins/Table/TableContainer.jsx +37 -0
- package/src/editor/plugins/Table/TableSizePicker.jsx +83 -0
- package/src/editor/plugins/Table/extensions.js +87 -0
- package/src/editor/plugins/Table/index.js +390 -0
- package/src/editor/plugins/Table/less/public.less +29 -0
- package/src/editor/plugins/Table/less/table.less +28 -0
- package/src/editor/plugins/Table/render.jsx +30 -0
- package/src/editor/plugins/index.js +19 -0
- package/src/editor/render.jsx +224 -0
- package/src/editor/ui/BasicToolbar.jsx +11 -0
- package/src/editor/ui/BlockButton.jsx +31 -0
- package/src/editor/ui/ClearFormattingButton.jsx +21 -0
- package/src/editor/ui/ExpandedToolbar.jsx +18 -0
- package/src/editor/ui/Expando.jsx +5 -0
- package/src/editor/ui/InlineToolbar.jsx +69 -0
- package/src/editor/ui/MarkButton.jsx +23 -0
- package/src/editor/ui/MarkElementButton.jsx +30 -0
- package/src/editor/ui/Menu.jsx +13 -0
- package/src/editor/ui/PositionedToolbar.jsx +32 -0
- package/src/editor/ui/Separator.jsx +7 -0
- package/src/editor/ui/SlateContextToolbar.jsx +13 -0
- package/src/editor/ui/SlateToolbar.jsx +96 -0
- package/src/editor/ui/Toolbar.jsx +103 -0
- package/src/editor/ui/ToolbarButton.jsx +33 -0
- package/src/editor/ui/ToolbarButton.test.js +25 -0
- package/src/editor/ui/__snapshots__/ToolbarButton.test.js.snap +16 -0
- package/src/editor/ui/index.js +15 -0
- package/src/editor/utils.js +248 -0
- package/src/elementEditor/ContextButtons.jsx +57 -0
- package/src/elementEditor/PluginEditor.jsx +124 -0
- package/src/elementEditor/Readme.md +6 -0
- package/src/elementEditor/SchemaProvider.jsx +4 -0
- package/src/elementEditor/SidebarEditor.jsx +46 -0
- package/src/elementEditor/ToolbarButton.jsx +44 -0
- package/src/elementEditor/index.js +5 -0
- package/src/elementEditor/makeInlineElementPlugin.js +100 -0
- package/src/elementEditor/messages.js +14 -0
- package/src/elementEditor/utils.js +227 -0
- package/src/hooks/index.js +3 -0
- package/src/hooks/useEditorContext.js +6 -0
- package/src/hooks/useIsomorphicLayoutEffect.js +7 -0
- package/src/hooks/useSelectionPosition.js +25 -0
- package/src/i18n.js +180 -0
- package/src/icons/hashlink.svg +57 -0
- package/src/index.js +61 -0
- package/src/reducers/content.js +74 -0
- package/src/reducers/index.js +3 -0
- package/src/reducers/plugins.js +17 -0
- package/src/reducers/selection.js +16 -0
- package/src/utils/blocks.js +379 -0
- package/src/utils/blocks.test.js +138 -0
- package/src/utils/editor.js +31 -0
- package/src/utils/image.js +25 -0
- package/src/utils/index.js +11 -0
- package/src/utils/internals.js +46 -0
- package/src/utils/lists.js +92 -0
- package/src/utils/marks.js +104 -0
- package/src/utils/mime-types.js +24 -0
- package/src/utils/nodes.js +4 -0
- package/src/utils/ops.js +20 -0
- package/src/utils/random.js +17 -0
- package/src/utils/selection.js +236 -0
- package/src/utils/slate-string-utils.js +409 -0
- package/src/utils/volto-blocks.js +314 -0
- package/src/widgets/ErrorBoundary.jsx +27 -0
- package/src/widgets/HtmlSlateWidget.jsx +138 -0
- package/src/widgets/ObjectByTypeWidget.jsx +49 -0
- package/src/widgets/RichTextWidget.jsx +72 -0
- package/src/widgets/RichTextWidgetView.jsx +36 -0
- package/src/widgets/style.css +21 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import ReactDOM from 'react-dom';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { readAsDataURL } from 'promise-file-reader';
|
|
4
|
+
import Dropzone from 'react-dropzone';
|
|
5
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
6
|
+
import { useInView } from 'react-intersection-observer';
|
|
7
|
+
import { Dimmer, Loader, Message, Segment } from 'semantic-ui-react';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
flattenToAppURL,
|
|
11
|
+
getBaseUrl,
|
|
12
|
+
validateFileUploadSize,
|
|
13
|
+
} from '@plone/volto/helpers';
|
|
14
|
+
import config from '@plone/volto/registry';
|
|
15
|
+
import {
|
|
16
|
+
BlockDataForm,
|
|
17
|
+
SidebarPortal,
|
|
18
|
+
BlockChooserButton,
|
|
19
|
+
} from '@plone/volto/components';
|
|
20
|
+
|
|
21
|
+
import { SlateEditor } from '@plone/volto-slate/editor';
|
|
22
|
+
import { serializeNodesToText } from '@plone/volto-slate/editor/render';
|
|
23
|
+
import {
|
|
24
|
+
createImageBlock,
|
|
25
|
+
parseDefaultSelection,
|
|
26
|
+
deconstructToVoltoBlocks,
|
|
27
|
+
} from '@plone/volto-slate/utils';
|
|
28
|
+
import { Transforms } from 'slate';
|
|
29
|
+
|
|
30
|
+
import PersistentSlashMenu from './SlashMenu';
|
|
31
|
+
import ShortcutListing from './ShortcutListing';
|
|
32
|
+
import MarkdownIntroduction from './MarkdownIntroduction';
|
|
33
|
+
import { handleKey } from './keyboard';
|
|
34
|
+
import TextBlockSchema from './schema';
|
|
35
|
+
|
|
36
|
+
import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg';
|
|
37
|
+
|
|
38
|
+
import './css/editor.css';
|
|
39
|
+
|
|
40
|
+
// TODO: refactor dropzone to separate component wrapper
|
|
41
|
+
|
|
42
|
+
const messages = defineMessages({
|
|
43
|
+
text: {
|
|
44
|
+
id: 'Type text…',
|
|
45
|
+
defaultMessage: 'Type text…',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const DEBUG = false;
|
|
50
|
+
|
|
51
|
+
export const DefaultTextBlockEditor = (props) => {
|
|
52
|
+
const {
|
|
53
|
+
block,
|
|
54
|
+
blocksConfig,
|
|
55
|
+
data,
|
|
56
|
+
detached = false,
|
|
57
|
+
index,
|
|
58
|
+
onChangeBlock,
|
|
59
|
+
onInsertBlock,
|
|
60
|
+
onMutateBlock,
|
|
61
|
+
onSelectBlock,
|
|
62
|
+
pathname,
|
|
63
|
+
properties,
|
|
64
|
+
selected,
|
|
65
|
+
uploadRequest,
|
|
66
|
+
uploadContent,
|
|
67
|
+
uploadedContent,
|
|
68
|
+
defaultSelection,
|
|
69
|
+
saveSlateBlockSelection,
|
|
70
|
+
allowedBlocks,
|
|
71
|
+
formTitle,
|
|
72
|
+
formDescription,
|
|
73
|
+
} = props;
|
|
74
|
+
|
|
75
|
+
const { slate } = config.settings;
|
|
76
|
+
const { textblockExtensions } = slate;
|
|
77
|
+
const { value } = data;
|
|
78
|
+
const intl = useIntl();
|
|
79
|
+
|
|
80
|
+
// const [addNewBlockOpened, setAddNewBlockOpened] = React.useState();
|
|
81
|
+
const [showDropzone, setShowDropzone] = React.useState(false);
|
|
82
|
+
const [uploading, setUploading] = React.useState(false);
|
|
83
|
+
const [newImageId, setNewImageId] = React.useState(null);
|
|
84
|
+
|
|
85
|
+
const prevReq = React.useRef(null);
|
|
86
|
+
|
|
87
|
+
const withBlockProperties = React.useCallback(
|
|
88
|
+
(editor) => {
|
|
89
|
+
editor.getBlockProps = () => props;
|
|
90
|
+
return editor;
|
|
91
|
+
},
|
|
92
|
+
[props],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const slateSettings = React.useMemo(
|
|
96
|
+
() => ({
|
|
97
|
+
...config.settings.slate,
|
|
98
|
+
persistentHelpers: [
|
|
99
|
+
...config.settings.slate.persistentHelpers,
|
|
100
|
+
PersistentSlashMenu,
|
|
101
|
+
],
|
|
102
|
+
}),
|
|
103
|
+
[],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const onDrop = React.useCallback(
|
|
107
|
+
(files) => {
|
|
108
|
+
// TODO: need to fix setUploading, treat uploading indicator
|
|
109
|
+
// inteligently, show progress report on uploading files
|
|
110
|
+
setUploading(true);
|
|
111
|
+
files.forEach((file) => {
|
|
112
|
+
const [mime] = file.type.split('/');
|
|
113
|
+
if (mime !== 'image') return;
|
|
114
|
+
if (!validateFileUploadSize(file, intl.formatMessage)) return;
|
|
115
|
+
|
|
116
|
+
readAsDataURL(file).then((data) => {
|
|
117
|
+
const fields = data.match(/^data:(.*);(.*),(.*)$/);
|
|
118
|
+
uploadContent(
|
|
119
|
+
getBaseUrl(pathname),
|
|
120
|
+
{
|
|
121
|
+
'@type': 'Image',
|
|
122
|
+
title: file.name,
|
|
123
|
+
image: {
|
|
124
|
+
data: fields[3],
|
|
125
|
+
encoding: fields[2],
|
|
126
|
+
'content-type': fields[1],
|
|
127
|
+
filename: file.name,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
block,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
setShowDropzone(false);
|
|
135
|
+
},
|
|
136
|
+
[pathname, uploadContent, block, intl.formatMessage],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const { loaded, loading } = uploadRequest;
|
|
140
|
+
const imageId = uploadedContent['@id'];
|
|
141
|
+
const prevLoaded = prevReq.current;
|
|
142
|
+
|
|
143
|
+
React.useEffect(() => {
|
|
144
|
+
if (loaded && !loading && !prevLoaded && newImageId !== imageId) {
|
|
145
|
+
const url = flattenToAppURL(imageId);
|
|
146
|
+
setNewImageId(imageId);
|
|
147
|
+
|
|
148
|
+
createImageBlock(url, index, props);
|
|
149
|
+
}
|
|
150
|
+
prevReq.current = loaded;
|
|
151
|
+
}, [props, loaded, loading, prevLoaded, imageId, newImageId, index]);
|
|
152
|
+
|
|
153
|
+
const handleUpdate = React.useCallback(
|
|
154
|
+
(editor) => {
|
|
155
|
+
// defaultSelection is used for things such as "restoring" the selection
|
|
156
|
+
// when joining blocks or moving the selection to block start on block
|
|
157
|
+
// split
|
|
158
|
+
if (defaultSelection) {
|
|
159
|
+
const selection = parseDefaultSelection(editor, defaultSelection);
|
|
160
|
+
if (selection) {
|
|
161
|
+
Transforms.select(editor, selection);
|
|
162
|
+
saveSlateBlockSelection(block, null);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
[defaultSelection, block, saveSlateBlockSelection],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const onEditorChange = (value, editor) => {
|
|
170
|
+
ReactDOM.unstable_batchedUpdates(() => {
|
|
171
|
+
onChangeBlock(block, {
|
|
172
|
+
...data,
|
|
173
|
+
value,
|
|
174
|
+
plaintext: serializeNodesToText(value || []),
|
|
175
|
+
// TODO: also add html serialized value
|
|
176
|
+
});
|
|
177
|
+
deconstructToVoltoBlocks(editor);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Get editing instructions from block settings or props
|
|
182
|
+
let instructions = data?.instructions?.data || data?.instructions;
|
|
183
|
+
if (!instructions || instructions === '<p><br/></p>') {
|
|
184
|
+
instructions = formDescription;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const placeholder =
|
|
188
|
+
data.placeholder || formTitle || intl.formatMessage(messages.text);
|
|
189
|
+
const schema = TextBlockSchema(data);
|
|
190
|
+
|
|
191
|
+
const disableNewBlocks = data?.disableNewBlocks || detached;
|
|
192
|
+
const { ref, inView } = useInView({
|
|
193
|
+
threshold: 0,
|
|
194
|
+
rootMargin: '0px 0px 200px 0px',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const handleFocus = React.useCallback(() => {
|
|
198
|
+
if (!selected) {
|
|
199
|
+
onSelectBlock(block);
|
|
200
|
+
}
|
|
201
|
+
}, [onSelectBlock, selected, block]);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="text-slate-editor-inner" ref={ref}>
|
|
205
|
+
<>
|
|
206
|
+
<Dropzone
|
|
207
|
+
disableClick
|
|
208
|
+
onDrop={onDrop}
|
|
209
|
+
className="dropzone"
|
|
210
|
+
onDragOver={() => setShowDropzone(true)}
|
|
211
|
+
onDragLeave={() => setShowDropzone(false)}
|
|
212
|
+
>
|
|
213
|
+
{({ getRootProps, getInputProps }) => {
|
|
214
|
+
return showDropzone ? (
|
|
215
|
+
<div className="drop-indicator">
|
|
216
|
+
{uploading ? (
|
|
217
|
+
<Dimmer active>
|
|
218
|
+
<Loader indeterminate>Uploading image</Loader>
|
|
219
|
+
</Dimmer>
|
|
220
|
+
) : (
|
|
221
|
+
<Message>
|
|
222
|
+
<center>
|
|
223
|
+
<img src={imageBlockSVG} alt="" />
|
|
224
|
+
</center>
|
|
225
|
+
</Message>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
) : (
|
|
229
|
+
<>
|
|
230
|
+
<SlateEditor
|
|
231
|
+
index={index}
|
|
232
|
+
readOnly={!inView}
|
|
233
|
+
properties={properties}
|
|
234
|
+
extensions={textblockExtensions}
|
|
235
|
+
renderExtensions={[withBlockProperties]}
|
|
236
|
+
value={value}
|
|
237
|
+
block={block /* is this needed? */}
|
|
238
|
+
defaultSelection={defaultSelection}
|
|
239
|
+
onUpdate={handleUpdate}
|
|
240
|
+
debug={DEBUG}
|
|
241
|
+
onFocus={handleFocus}
|
|
242
|
+
onChange={(value, editor) => onEditorChange(value, editor)}
|
|
243
|
+
onKeyDown={handleKey}
|
|
244
|
+
selected={selected}
|
|
245
|
+
placeholder={placeholder}
|
|
246
|
+
slateSettings={slateSettings}
|
|
247
|
+
editableProps={{ 'aria-multiline': 'false' }}
|
|
248
|
+
/>
|
|
249
|
+
{DEBUG ? <div>{block}</div> : ''}
|
|
250
|
+
</>
|
|
251
|
+
);
|
|
252
|
+
}}
|
|
253
|
+
</Dropzone>
|
|
254
|
+
|
|
255
|
+
{!config.experimental.addBlockButton.enabled &&
|
|
256
|
+
selected &&
|
|
257
|
+
!data.plaintext?.trim() &&
|
|
258
|
+
!disableNewBlocks && (
|
|
259
|
+
<BlockChooserButton
|
|
260
|
+
data={data}
|
|
261
|
+
block={block}
|
|
262
|
+
onInsertBlock={(id, value) => {
|
|
263
|
+
onSelectBlock(onInsertBlock(id, value));
|
|
264
|
+
}}
|
|
265
|
+
onMutateBlock={onMutateBlock}
|
|
266
|
+
allowedBlocks={allowedBlocks}
|
|
267
|
+
blocksConfig={blocksConfig}
|
|
268
|
+
size="24px"
|
|
269
|
+
properties={properties}
|
|
270
|
+
/>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
<SidebarPortal selected={selected}>
|
|
274
|
+
<div id="slate-plugin-sidebar"></div>
|
|
275
|
+
{instructions ? (
|
|
276
|
+
<Segment attached>
|
|
277
|
+
<div dangerouslySetInnerHTML={{ __html: instructions }} />
|
|
278
|
+
</Segment>
|
|
279
|
+
) : (
|
|
280
|
+
<>
|
|
281
|
+
<ShortcutListing />
|
|
282
|
+
<MarkdownIntroduction />
|
|
283
|
+
<BlockDataForm
|
|
284
|
+
block={block}
|
|
285
|
+
schema={schema}
|
|
286
|
+
title={schema.title}
|
|
287
|
+
onChangeBlock={onChangeBlock}
|
|
288
|
+
onChangeField={(id, value) => {
|
|
289
|
+
onChangeBlock(block, {
|
|
290
|
+
...data,
|
|
291
|
+
[id]: value,
|
|
292
|
+
});
|
|
293
|
+
}}
|
|
294
|
+
formData={data}
|
|
295
|
+
/>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
298
|
+
</SidebarPortal>
|
|
299
|
+
</>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export default DefaultTextBlockEditor;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { useInView } from 'react-intersection-observer';
|
|
4
|
+
import { SlateEditor } from '@plone/volto-slate/editor';
|
|
5
|
+
import { serializeNodesToText } from '@plone/volto-slate/editor/render';
|
|
6
|
+
import { handleKeyDetached } from './keyboard';
|
|
7
|
+
|
|
8
|
+
const DEBUG = false;
|
|
9
|
+
|
|
10
|
+
const messages = defineMessages({
|
|
11
|
+
text: {
|
|
12
|
+
id: 'Type text…',
|
|
13
|
+
defaultMessage: 'Type text…',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const DetachedTextBlockEditor = (props) => {
|
|
18
|
+
const {
|
|
19
|
+
data,
|
|
20
|
+
index,
|
|
21
|
+
properties,
|
|
22
|
+
onSelectBlock,
|
|
23
|
+
onChangeBlock,
|
|
24
|
+
block,
|
|
25
|
+
selected,
|
|
26
|
+
formTitle,
|
|
27
|
+
formDescription,
|
|
28
|
+
} = props;
|
|
29
|
+
const { value } = data;
|
|
30
|
+
|
|
31
|
+
const intl = useIntl();
|
|
32
|
+
const placeholder =
|
|
33
|
+
data.placeholder || formTitle || intl.formatMessage(messages.text);
|
|
34
|
+
let instructions = data?.instructions?.data || data?.instructions;
|
|
35
|
+
if (!instructions || instructions === '<p><br/></p>') {
|
|
36
|
+
instructions = formDescription;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { ref, inView } = useInView({
|
|
40
|
+
threshold: 0,
|
|
41
|
+
rootMargin: '0px 0px 200px 0px',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="text-slate-editor-inner detached-slate-editor" ref={ref}>
|
|
46
|
+
<SlateEditor
|
|
47
|
+
index={index}
|
|
48
|
+
readOnly={!inView}
|
|
49
|
+
properties={properties}
|
|
50
|
+
renderExtensions={[]}
|
|
51
|
+
value={value}
|
|
52
|
+
block={block /* is this needed? */}
|
|
53
|
+
debug={DEBUG}
|
|
54
|
+
slateSettings={props.slateSettings}
|
|
55
|
+
onFocus={() => {
|
|
56
|
+
if (!selected) {
|
|
57
|
+
onSelectBlock(block);
|
|
58
|
+
}
|
|
59
|
+
}}
|
|
60
|
+
onChange={(value, selection, editor) => {
|
|
61
|
+
onChangeBlock(block, {
|
|
62
|
+
...data,
|
|
63
|
+
value,
|
|
64
|
+
plaintext: serializeNodesToText(value || []),
|
|
65
|
+
// TODO: also add html serialized value
|
|
66
|
+
});
|
|
67
|
+
}}
|
|
68
|
+
selected={selected}
|
|
69
|
+
placeholder={placeholder}
|
|
70
|
+
onKeyDown={handleKeyDetached}
|
|
71
|
+
editableProps={{ 'aria-multiline': 'true' }}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default DetachedTextBlockEditor;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Segment, List } from 'semantic-ui-react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A component to be shown in the sidebar as a introduction to the Markdown support in the Slate-based Text block. It renders a header and a list and has no state.
|
|
6
|
+
* @param {object} props Can be an empty object (no props are used in this component).
|
|
7
|
+
*/
|
|
8
|
+
const MarkdownIntroduction = (props) => {
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<header className="header">
|
|
12
|
+
<h2>Markdown shortcuts</h2>
|
|
13
|
+
</header>
|
|
14
|
+
|
|
15
|
+
<Segment secondary attached style={{ fontFamily: 'monospace' }}>
|
|
16
|
+
<List>
|
|
17
|
+
<List.Item key={1} style={{ fontSize: 'xx-large' }}>
|
|
18
|
+
# Title
|
|
19
|
+
</List.Item>
|
|
20
|
+
<List.Item key={2} style={{ fontSize: 'x-large' }}>
|
|
21
|
+
## Subtitle
|
|
22
|
+
</List.Item>
|
|
23
|
+
|
|
24
|
+
<List.Item key={3} style={{ paddingTop: '1rem' }}>
|
|
25
|
+
* unordered list item
|
|
26
|
+
</List.Item>
|
|
27
|
+
<List.Item key={4}>+ unordered list item</List.Item>
|
|
28
|
+
<List.Item key={5}>- unordered list item</List.Item>
|
|
29
|
+
|
|
30
|
+
<List.Item key={6} style={{ paddingTop: '1rem' }}>
|
|
31
|
+
1. ordered list item
|
|
32
|
+
</List.Item>
|
|
33
|
+
<List.Item key={7}>1) ordered list item</List.Item>
|
|
34
|
+
|
|
35
|
+
<List.Item key={8} className="callout">
|
|
36
|
+
> block quote
|
|
37
|
+
</List.Item>
|
|
38
|
+
<List.Item key={9} style={{ fontWeight: 'bold' }}>
|
|
39
|
+
**bold text**
|
|
40
|
+
</List.Item>
|
|
41
|
+
<List.Item key={10} style={{ fontWeight: 'bold' }}>
|
|
42
|
+
__bold text__
|
|
43
|
+
</List.Item>
|
|
44
|
+
<List.Item key={11} style={{ fontStyle: 'italic' }}>
|
|
45
|
+
*italic text*
|
|
46
|
+
</List.Item>
|
|
47
|
+
<List.Item key={12} style={{ fontStyle: 'italic' }}>
|
|
48
|
+
_italic text_
|
|
49
|
+
</List.Item>
|
|
50
|
+
<List.Item key={13} style={{ textDecoration: 'line-through' }}>
|
|
51
|
+
~~strikethrough text~~
|
|
52
|
+
</List.Item>
|
|
53
|
+
</List>
|
|
54
|
+
</Segment>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default MarkdownIntroduction;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Portal } from 'react-portal';
|
|
3
|
+
|
|
4
|
+
const PluginSidebar = ({ children, selected }) => {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
{selected && (
|
|
8
|
+
<Portal
|
|
9
|
+
node={__CLIENT__ && document.getElementById('slate-plugin-sidebar')}
|
|
10
|
+
>
|
|
11
|
+
{children}
|
|
12
|
+
</Portal>
|
|
13
|
+
)}
|
|
14
|
+
</>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default PluginSidebar;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Segment, List } from 'semantic-ui-react';
|
|
2
|
+
import config from '@plone/volto/registry';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
const ShortcutListing = (props) => {
|
|
6
|
+
const hotkeys = config.settings?.slate?.hotkeys;
|
|
7
|
+
return (
|
|
8
|
+
<div>
|
|
9
|
+
<header className="header">
|
|
10
|
+
<h2>Editor shortcuts</h2>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<Segment secondary attached>
|
|
14
|
+
<List>
|
|
15
|
+
<List.Item>
|
|
16
|
+
Type a slash (<em>/</em>) to change block type
|
|
17
|
+
</List.Item>
|
|
18
|
+
{Object.entries(hotkeys || {}).map(([shortcut, { format, type }]) => (
|
|
19
|
+
<List.Item key={shortcut}>{`${shortcut}: ${format}`}</List.Item>
|
|
20
|
+
))}
|
|
21
|
+
</List>
|
|
22
|
+
<div>On Windows, the MOD key is Ctrl, on Mac OS X it's Cmd.</div>
|
|
23
|
+
</Segment>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default ShortcutListing;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { filter, isEmpty } from 'lodash';
|
|
4
|
+
import { Menu } from 'semantic-ui-react';
|
|
5
|
+
import { useIntl, FormattedMessage } from 'react-intl';
|
|
6
|
+
import { Icon } from '@plone/volto/components';
|
|
7
|
+
|
|
8
|
+
const emptySlateBlock = () => ({
|
|
9
|
+
value: [
|
|
10
|
+
{
|
|
11
|
+
children: [
|
|
12
|
+
{
|
|
13
|
+
text: '',
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
type: 'p',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
plaintext: '',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const useIsMounted = () => {
|
|
23
|
+
const ref = React.useRef();
|
|
24
|
+
React.useEffect(() => {
|
|
25
|
+
ref.current = true;
|
|
26
|
+
return () => (ref.current = false);
|
|
27
|
+
}, []);
|
|
28
|
+
return ref.current;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SlashMenu = ({
|
|
32
|
+
currentBlock,
|
|
33
|
+
onMutateBlock,
|
|
34
|
+
selected,
|
|
35
|
+
availableBlocks,
|
|
36
|
+
}) => {
|
|
37
|
+
const intl = useIntl();
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="power-user-menu">
|
|
41
|
+
<Menu vertical fluid borderless>
|
|
42
|
+
{availableBlocks.map((block, index) => (
|
|
43
|
+
<Menu.Item
|
|
44
|
+
key={block.id}
|
|
45
|
+
className={block.id}
|
|
46
|
+
active={index === selected}
|
|
47
|
+
onClick={(e) => {
|
|
48
|
+
// onInsertBlock(currentBlock, { '@type': block.id });
|
|
49
|
+
onMutateBlock(currentBlock, { '@type': block.id });
|
|
50
|
+
e.stopPropagation();
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Icon name={block.icon} size="24px" />
|
|
54
|
+
{intl.formatMessage({
|
|
55
|
+
id: block.title,
|
|
56
|
+
defaultMessage: block.title,
|
|
57
|
+
})}
|
|
58
|
+
</Menu.Item>
|
|
59
|
+
))}
|
|
60
|
+
{availableBlocks.length === 0 && (
|
|
61
|
+
<Menu.Item>
|
|
62
|
+
<FormattedMessage
|
|
63
|
+
id="No matching blocks"
|
|
64
|
+
defaultMessage="No matching blocks"
|
|
65
|
+
/>
|
|
66
|
+
</Menu.Item>
|
|
67
|
+
)}
|
|
68
|
+
</Menu>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
SlashMenu.propTypes = {
|
|
74
|
+
currentBlock: PropTypes.string.isRequired,
|
|
75
|
+
onInsertBlock: PropTypes.func,
|
|
76
|
+
selected: PropTypes.number,
|
|
77
|
+
blocksConfig: PropTypes.arrayOf(PropTypes.any),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const translateBlockTitle = (block, intl) =>
|
|
81
|
+
intl.formatMessage({
|
|
82
|
+
id: block.title,
|
|
83
|
+
defaultMessage: block.title,
|
|
84
|
+
});
|
|
85
|
+
const scoreBlock = (block, slashCommand, intl) => {
|
|
86
|
+
if (!slashCommand) return 0;
|
|
87
|
+
const title = translateBlockTitle(block, intl).toLowerCase();
|
|
88
|
+
// prefer initial title matches, then title substring matches
|
|
89
|
+
if (title.indexOf(slashCommand[1]) === 0) return 2;
|
|
90
|
+
if (title.indexOf(slashCommand[1]) !== -1) return 1;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A SlashMenu wrapper implemented as a volto-slate PersistentHelper.
|
|
95
|
+
*/
|
|
96
|
+
const PersistentSlashMenu = ({ editor }) => {
|
|
97
|
+
const props = editor.getBlockProps();
|
|
98
|
+
const intl = useIntl();
|
|
99
|
+
const {
|
|
100
|
+
block,
|
|
101
|
+
blocksConfig,
|
|
102
|
+
data,
|
|
103
|
+
onMutateBlock,
|
|
104
|
+
properties,
|
|
105
|
+
selected,
|
|
106
|
+
allowedBlocks,
|
|
107
|
+
detached,
|
|
108
|
+
} = props;
|
|
109
|
+
const disableNewBlocks = data?.disableNewBlocks || detached;
|
|
110
|
+
|
|
111
|
+
const [slashMenuSelected, setSlashMenuSelected] = React.useState(0);
|
|
112
|
+
|
|
113
|
+
const hasAllowedBlocks = !isEmpty(allowedBlocks);
|
|
114
|
+
const slashCommand = data.plaintext
|
|
115
|
+
?.toLowerCase()
|
|
116
|
+
.trim()
|
|
117
|
+
.match(/^\/([a-z]*)$/);
|
|
118
|
+
|
|
119
|
+
const availableBlocks = React.useMemo(
|
|
120
|
+
() =>
|
|
121
|
+
filter(blocksConfig, (item) =>
|
|
122
|
+
hasAllowedBlocks
|
|
123
|
+
? allowedBlocks.includes(item.id)
|
|
124
|
+
: typeof item.restricted === 'function'
|
|
125
|
+
? !item.restricted({ properties, block: item })
|
|
126
|
+
: !item.restricted,
|
|
127
|
+
)
|
|
128
|
+
.filter((block) => Boolean(block.title && block.id))
|
|
129
|
+
.filter((block) => {
|
|
130
|
+
// typed text is a substring of the title or id
|
|
131
|
+
const title = translateBlockTitle(block, intl).toLowerCase();
|
|
132
|
+
return (
|
|
133
|
+
block.id !== 'slate' &&
|
|
134
|
+
slashCommand &&
|
|
135
|
+
title.indexOf(slashCommand[1]) !== -1
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
.sort((a, b) => {
|
|
139
|
+
const scoreDiff =
|
|
140
|
+
scoreBlock(b, slashCommand, intl) -
|
|
141
|
+
scoreBlock(a, slashCommand, intl);
|
|
142
|
+
if (scoreDiff) return scoreDiff;
|
|
143
|
+
// sort equally scored blocks by title
|
|
144
|
+
return translateBlockTitle(a, intl).localeCompare(
|
|
145
|
+
translateBlockTitle(b, intl),
|
|
146
|
+
);
|
|
147
|
+
}),
|
|
148
|
+
[
|
|
149
|
+
allowedBlocks,
|
|
150
|
+
blocksConfig,
|
|
151
|
+
intl,
|
|
152
|
+
properties,
|
|
153
|
+
slashCommand,
|
|
154
|
+
hasAllowedBlocks,
|
|
155
|
+
],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const slashMenuSize = availableBlocks.length;
|
|
159
|
+
const show = selected && slashCommand && !disableNewBlocks;
|
|
160
|
+
|
|
161
|
+
const isMounted = useIsMounted();
|
|
162
|
+
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
if (isMounted && show && slashMenuSelected > slashMenuSize - 1) {
|
|
165
|
+
setSlashMenuSelected(slashMenuSize - 1);
|
|
166
|
+
}
|
|
167
|
+
}, [show, slashMenuSelected, isMounted, slashMenuSize]);
|
|
168
|
+
|
|
169
|
+
editor.showSlashMenu = show;
|
|
170
|
+
|
|
171
|
+
editor.slashEnter = () =>
|
|
172
|
+
slashMenuSize > 0 &&
|
|
173
|
+
onMutateBlock(
|
|
174
|
+
block,
|
|
175
|
+
{
|
|
176
|
+
'@type': availableBlocks[slashMenuSelected].id,
|
|
177
|
+
},
|
|
178
|
+
emptySlateBlock(),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
editor.slashArrowUp = () =>
|
|
182
|
+
setSlashMenuSelected(
|
|
183
|
+
slashMenuSelected === 0 ? slashMenuSize - 1 : slashMenuSelected - 1,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
editor.slashArrowDown = () =>
|
|
187
|
+
setSlashMenuSelected(
|
|
188
|
+
slashMenuSelected >= slashMenuSize - 1 ? 0 : slashMenuSelected + 1,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
return show ? (
|
|
192
|
+
<SlashMenu
|
|
193
|
+
currentBlock={block}
|
|
194
|
+
onMutateBlock={onMutateBlock}
|
|
195
|
+
availableBlocks={availableBlocks}
|
|
196
|
+
selected={slashMenuSelected}
|
|
197
|
+
/>
|
|
198
|
+
) : (
|
|
199
|
+
''
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
export default PersistentSlashMenu;
|