@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,161 @@
|
|
|
1
|
+
import { Editor, Text, Transforms } from 'slate';
|
|
2
|
+
import { deserialize } from '@plone/volto-slate/editor/deserialize';
|
|
3
|
+
import {
|
|
4
|
+
createBlock,
|
|
5
|
+
createDefaultBlock,
|
|
6
|
+
MIMETypeName,
|
|
7
|
+
normalizeExternalData,
|
|
8
|
+
} from '@plone/volto-slate/utils';
|
|
9
|
+
import { isBlockActive } from '../../utils/blocks';
|
|
10
|
+
|
|
11
|
+
export const insertData = (editor) => {
|
|
12
|
+
editor.dataTransferHandlers = {
|
|
13
|
+
...editor.dataTransferHandlers,
|
|
14
|
+
'application/x-slate-fragment': (dt, fullMime) => {
|
|
15
|
+
const decoded = decodeURIComponent(window.atob(dt));
|
|
16
|
+
const parsed = JSON.parse(decoded);
|
|
17
|
+
editor.beforeInsertFragment && editor.beforeInsertFragment(parsed);
|
|
18
|
+
editor.insertFragment(parsed);
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
},
|
|
22
|
+
'text/html': (dt, fullMime) => {
|
|
23
|
+
const parsed = new DOMParser().parseFromString(dt, 'text/html');
|
|
24
|
+
|
|
25
|
+
const body =
|
|
26
|
+
parsed.getElementsByTagName('google-sheets-html-origin').length > 0
|
|
27
|
+
? parsed.querySelector('google-sheets-html-origin > table')
|
|
28
|
+
: parsed.body;
|
|
29
|
+
|
|
30
|
+
let fragment;
|
|
31
|
+
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.debug('clipboard operation', {
|
|
34
|
+
clipboard: dt,
|
|
35
|
+
parsedBody: body,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const val = deserialize(editor, body);
|
|
39
|
+
fragment = Array.isArray(val) ? val : [val];
|
|
40
|
+
fragment = editor.normalizeExternalData(fragment);
|
|
41
|
+
|
|
42
|
+
editor.insertFragment(fragment);
|
|
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
|
+
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
'text/plain': (dt, fullMime) => {
|
|
56
|
+
const text = dt;
|
|
57
|
+
if (!text) return;
|
|
58
|
+
|
|
59
|
+
const paras = text.split('\n');
|
|
60
|
+
|
|
61
|
+
// If just 1 line insert text
|
|
62
|
+
if (paras.length === 1) {
|
|
63
|
+
Transforms.insertText(editor, paras[0]);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if inside a list
|
|
68
|
+
const fragment =
|
|
69
|
+
isBlockActive(editor, 'ul') || isBlockActive(editor, 'ol')
|
|
70
|
+
? paras.map((p) => createBlock('li', [{ text: p }]))
|
|
71
|
+
: paras.map((p) => createDefaultBlock([{ text: p }]));
|
|
72
|
+
|
|
73
|
+
// check if fragment is p with text and insert as fragment if so
|
|
74
|
+
const fragmentContainsText = (f) => {
|
|
75
|
+
var trigger = false;
|
|
76
|
+
if (f && f[0]) {
|
|
77
|
+
f.forEach((frag) => {
|
|
78
|
+
if (frag.type === 'p') {
|
|
79
|
+
if (frag.children) {
|
|
80
|
+
frag.children.forEach((child) => {
|
|
81
|
+
if (child.text) {
|
|
82
|
+
trigger = true;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return trigger;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// When there's already text in the editor, insert a fragment, not nodes
|
|
93
|
+
const containsText = fragmentContainsText(fragment);
|
|
94
|
+
if (fragment && containsText) {
|
|
95
|
+
Transforms.insertFragment(editor, fragment);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (Editor.string(editor, [])) {
|
|
99
|
+
if (
|
|
100
|
+
Array.isArray(fragment) &&
|
|
101
|
+
fragment.findIndex(
|
|
102
|
+
(b) => Editor.isInline(editor, b) || Text.isText(b),
|
|
103
|
+
) > -1
|
|
104
|
+
) {
|
|
105
|
+
// console.log('insert fragment', fragment);
|
|
106
|
+
// TODO: we want normalization also when dealing with fragments
|
|
107
|
+
Transforms.insertFragment(editor, fragment);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// always normalize when dealing with plain text
|
|
113
|
+
const nodes = normalizeExternalData(editor, fragment);
|
|
114
|
+
if (!containsText) {
|
|
115
|
+
Transforms.insertNodes(editor, nodes);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// TODO: use the rtf data to get the embedded images.
|
|
123
|
+
// const text = data.getData('text/rtf');
|
|
124
|
+
|
|
125
|
+
const { insertData } = editor;
|
|
126
|
+
|
|
127
|
+
// TODO: move this to extensions/insertData
|
|
128
|
+
// TODO: update and improve comments & docs related to
|
|
129
|
+
// `dataTransferFormatsOrder` and `dataTransferHandlers` features
|
|
130
|
+
editor.insertData = (data) => {
|
|
131
|
+
if (editor.beforeInsertData) {
|
|
132
|
+
editor.beforeInsertData(data);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < editor.dataTransferFormatsOrder.length; ++i) {
|
|
136
|
+
const dt = editor.dataTransferFormatsOrder[i];
|
|
137
|
+
if (dt === 'files') {
|
|
138
|
+
const { files } = data;
|
|
139
|
+
if (files && files.length > 0) {
|
|
140
|
+
// or handled here
|
|
141
|
+
return editor.dataTransferHandlers['files'](files);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const satisfyingFormats = data.types.filter((y) =>
|
|
146
|
+
new MIMETypeName(dt).matches(new MIMETypeName(y)),
|
|
147
|
+
);
|
|
148
|
+
for (let j = 0; j < satisfyingFormats.length; ++j) {
|
|
149
|
+
const y = satisfyingFormats[j];
|
|
150
|
+
if (editor.dataTransferHandlers[dt](data.getData(y), y)) {
|
|
151
|
+
// handled here
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// not handled until this point
|
|
157
|
+
return insertData(data);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return editor;
|
|
161
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
|
|
3
|
+
export const isInline = (editor) => {
|
|
4
|
+
const { isInline } = editor;
|
|
5
|
+
const { slate } = config.settings;
|
|
6
|
+
|
|
7
|
+
editor.isInline = (element) => {
|
|
8
|
+
return element && slate.inlineElements.includes(element.type)
|
|
9
|
+
? true
|
|
10
|
+
: isInline(element);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
return editor;
|
|
14
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Text, Transforms, Element, Node } from 'slate'; // Editor,
|
|
2
|
+
import config from '@plone/volto/registry';
|
|
3
|
+
|
|
4
|
+
export const normalizeNode = (editor) => {
|
|
5
|
+
// enforce list rules (no block elements, only ol/ul/li as possible children
|
|
6
|
+
const { normalizeNode } = editor;
|
|
7
|
+
const { slate } = config.settings;
|
|
8
|
+
|
|
9
|
+
const validListElements = [...slate.listTypes, slate.listItemType];
|
|
10
|
+
|
|
11
|
+
editor.normalizeNode = (entry) => {
|
|
12
|
+
const [node, path] = entry;
|
|
13
|
+
|
|
14
|
+
const isTextNode = Text.isText(node);
|
|
15
|
+
const isInlineNode = editor.isInline(node);
|
|
16
|
+
const isElementNode = Element.isElement(node);
|
|
17
|
+
const isListTypeNode = slate.listTypes.includes(node.type);
|
|
18
|
+
|
|
19
|
+
// delete childless ul/ol nodes
|
|
20
|
+
if (!isTextNode && isElementNode && !isInlineNode && isListTypeNode) {
|
|
21
|
+
if ((node.children || []).length === 0) {
|
|
22
|
+
Transforms.removeNodes(editor, { at: path });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isElementNode && isListTypeNode) {
|
|
28
|
+
// lift all child nodes of ul/ol that are not ul/ol/li
|
|
29
|
+
for (const [child, childPath] of Node.children(editor, path)) {
|
|
30
|
+
if (
|
|
31
|
+
!validListElements.includes(child.type) &&
|
|
32
|
+
!validListElements.includes(node.type)
|
|
33
|
+
) {
|
|
34
|
+
Transforms.liftNodes(editor, { at: childPath, split: true });
|
|
35
|
+
|
|
36
|
+
// Alternate strategy, need to investigate
|
|
37
|
+
// const newParent = { type: slate.defaultBlockType, children: [] };
|
|
38
|
+
// Transforms.wrapNodes(editor, newParent, { at: childPath });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
normalizeNode(entry);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return editor;
|
|
48
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import config from '@plone/volto/registry';
|
|
2
|
+
|
|
3
|
+
export const withDeserializers = (editor) => {
|
|
4
|
+
// Saving a copy of these deserializers in the editor makes possible to have
|
|
5
|
+
// different deserializers per editor
|
|
6
|
+
//
|
|
7
|
+
// For example, for the TextBlock deserializer we exclude h1,h4,h5,h6 tags
|
|
8
|
+
// and handle lists very differently
|
|
9
|
+
|
|
10
|
+
const { slate } = config.settings;
|
|
11
|
+
|
|
12
|
+
editor.htmlTagsToSlate = slate.htmlTagsToSlate;
|
|
13
|
+
|
|
14
|
+
return editor;
|
|
15
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ReactEditor } from 'slate-react';
|
|
3
|
+
import { omit } from 'lodash';
|
|
4
|
+
|
|
5
|
+
const withTestingFeatures = (WrappedComponent) => {
|
|
6
|
+
return (props) => {
|
|
7
|
+
let ref = React.useRef();
|
|
8
|
+
|
|
9
|
+
// Source: https://stackoverflow.com/a/53623568/258462
|
|
10
|
+
const onTestSelectWord = (val) => {
|
|
11
|
+
let slateEditor =
|
|
12
|
+
val.detail.parentElement.parentElement.parentElement.parentElement;
|
|
13
|
+
|
|
14
|
+
// Events are special, can't use spread or Object.keys
|
|
15
|
+
let selectEvent = {};
|
|
16
|
+
for (let key in val) {
|
|
17
|
+
if (key === 'currentTarget') {
|
|
18
|
+
selectEvent['currentTarget'] = slateEditor;
|
|
19
|
+
} else if (key === 'type') {
|
|
20
|
+
selectEvent['type'] = 'select';
|
|
21
|
+
} else {
|
|
22
|
+
selectEvent[key] = val[key];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Make selection
|
|
27
|
+
let selection = window.getSelection();
|
|
28
|
+
let range = document.createRange();
|
|
29
|
+
range.selectNodeContents(val.detail);
|
|
30
|
+
selection.removeAllRanges();
|
|
31
|
+
selection.addRange(range);
|
|
32
|
+
|
|
33
|
+
// Slate monitors DOM selection changes automatically
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const onTestSelectRange = (val) => {
|
|
37
|
+
const newDomRange =
|
|
38
|
+
val && ReactEditor.toDOMRange(window.focusedSlateEditor, val.detail);
|
|
39
|
+
|
|
40
|
+
let selection = window.getSelection();
|
|
41
|
+
selection.removeAllRanges();
|
|
42
|
+
selection.addRange(newDomRange);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
document.addEventListener('Test_SelectWord', onTestSelectWord);
|
|
47
|
+
document.addEventListener('Test_SelectRange', onTestSelectRange);
|
|
48
|
+
return () => {
|
|
49
|
+
document.removeEventListener('Test_SelectWord', onTestSelectWord);
|
|
50
|
+
document.removeEventListener('Test_SelectRange', onTestSelectRange);
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const handleFocus = React.useCallback(() => {
|
|
55
|
+
window.focusedSlateEditor = ref?.current;
|
|
56
|
+
if (props.onFocus) {
|
|
57
|
+
props.onFocus();
|
|
58
|
+
}
|
|
59
|
+
}, [props]);
|
|
60
|
+
|
|
61
|
+
const managedProps = useMemo(() => {
|
|
62
|
+
return omit(props, 'onFocus');
|
|
63
|
+
}, [props]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<WrappedComponent
|
|
67
|
+
debug
|
|
68
|
+
debug-values={{
|
|
69
|
+
'data-slate-value': JSON.stringify(props.value, null, 2),
|
|
70
|
+
'data-slate-selection': JSON.stringify(
|
|
71
|
+
ref?.current?.selection,
|
|
72
|
+
null,
|
|
73
|
+
2,
|
|
74
|
+
),
|
|
75
|
+
}}
|
|
76
|
+
testingEditorRef={ref}
|
|
77
|
+
onFocus={handleFocus}
|
|
78
|
+
{...managedProps}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default withTestingFeatures;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as slateConfig from './config';
|
|
2
|
+
import installDefaultPlugins from './plugins';
|
|
3
|
+
export { default as SlateEditor } from './SlateEditor';
|
|
4
|
+
export { default as EditorReference } from './EditorReference';
|
|
5
|
+
|
|
6
|
+
export default function applyConfig(config) {
|
|
7
|
+
config.settings.slate = {
|
|
8
|
+
...slateConfig,
|
|
9
|
+
// showExpandedToolbar: false,
|
|
10
|
+
enableExpandedToolbar: false,
|
|
11
|
+
};
|
|
12
|
+
config = installDefaultPlugins(config);
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
@import 'globals.less';
|
|
2
|
+
|
|
3
|
+
@addon: '@plone/volto-slate';
|
|
4
|
+
@addontype: 'editor';
|
|
5
|
+
@addonelement: 'slate';
|
|
6
|
+
|
|
7
|
+
.loadAddonVariables();
|
|
8
|
+
|
|
9
|
+
// TODO: move these to less variables
|
|
10
|
+
|
|
11
|
+
& {
|
|
12
|
+
.slate-inline-toolbar {
|
|
13
|
+
position: absolute;
|
|
14
|
+
|
|
15
|
+
/* should be above admin panes (100), and below the Link modal form (1000) */
|
|
16
|
+
z-index: 1500;
|
|
17
|
+
|
|
18
|
+
top: -10000px;
|
|
19
|
+
left: -10000px;
|
|
20
|
+
|
|
21
|
+
max-width: 100vw;
|
|
22
|
+
opacity: 0;
|
|
23
|
+
// transition: opacity 0.5s;
|
|
24
|
+
|
|
25
|
+
&.upper {
|
|
26
|
+
transform: translateY(-100%);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.toolbar-wrapper.active {
|
|
31
|
+
margin-bottom: 0.6em;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.slate-editor p {
|
|
35
|
+
/* In editor the <p> are wrapped in weird markup */
|
|
36
|
+
margin-bottom: 0 !important;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.slate-toolbar {
|
|
40
|
+
display: flex;
|
|
41
|
+
box-sizing: border-box;
|
|
42
|
+
padding: 3px;
|
|
43
|
+
border: none;
|
|
44
|
+
background: #fff;
|
|
45
|
+
border-radius: 2px;
|
|
46
|
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
|
47
|
+
font-family: 'Poppins', 'Helvetica Neue', Arial, Helvetica, sans-serif;
|
|
48
|
+
font-size: 1rem;
|
|
49
|
+
font-weight: normal;
|
|
50
|
+
|
|
51
|
+
.expando {
|
|
52
|
+
flex-grow: 1;
|
|
53
|
+
background-color: #fff;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.toolbar-separator {
|
|
57
|
+
display: inline-block;
|
|
58
|
+
height: 32px;
|
|
59
|
+
border-right: 1px solid #ddd;
|
|
60
|
+
margin: 0 0.5rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.toolbar-separator + .button-wrapper {
|
|
64
|
+
margin-left: 0px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.button-wrapper {
|
|
68
|
+
display: inline-block;
|
|
69
|
+
margin-left: 3px;
|
|
70
|
+
|
|
71
|
+
.ui.tiny.compact.icon.toggle.button {
|
|
72
|
+
width: 32px;
|
|
73
|
+
height: 32px;
|
|
74
|
+
box-sizing: border-box;
|
|
75
|
+
padding: 4px !important;
|
|
76
|
+
border: 0;
|
|
77
|
+
background: rgba(255, 255, 255, 0.975);
|
|
78
|
+
border-radius: 1px;
|
|
79
|
+
color: @brown;
|
|
80
|
+
font-size: 18px;
|
|
81
|
+
vertical-align: bottom;
|
|
82
|
+
|
|
83
|
+
& > svg {
|
|
84
|
+
fill: #888;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
&:hover,
|
|
88
|
+
&:focus {
|
|
89
|
+
background: #f3f3f3;
|
|
90
|
+
outline: 0; /* reset for :focus */
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
&.active {
|
|
94
|
+
// TODO: stop using !important, use smth better
|
|
95
|
+
background: #efefef !important;
|
|
96
|
+
border-radius: 3px;
|
|
97
|
+
box-shadow: inset 0 0 0 1px @blue !important;
|
|
98
|
+
color: @blue !important;
|
|
99
|
+
|
|
100
|
+
& > svg {
|
|
101
|
+
fill: #444;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ui.buttons {
|
|
108
|
+
width: 100%;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ui.button:not(.icon) > .icon:not(.button):not(.dropdown) {
|
|
112
|
+
// TODO: is it possible to not use !important here and override button.less?
|
|
113
|
+
margin: auto auto auto auto !important;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.ui.button:last-child {
|
|
117
|
+
// TODO: is it possible to not use !important here and override button.less?
|
|
118
|
+
margin-right: 0 !important;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.highlight-selection {
|
|
123
|
+
background-color: #8080803d;
|
|
124
|
+
// color: white;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.ui.input.editor-link input {
|
|
128
|
+
padding: 0;
|
|
129
|
+
border: none;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.sidebar-container {
|
|
134
|
+
.slate-editor {
|
|
135
|
+
ul {
|
|
136
|
+
li {
|
|
137
|
+
display: list-item;
|
|
138
|
+
padding: 0;
|
|
139
|
+
|
|
140
|
+
span {
|
|
141
|
+
display: initial;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.power-user-menu {
|
|
149
|
+
position: absolute;
|
|
150
|
+
z-index: 10;
|
|
151
|
+
top: 29px;
|
|
152
|
+
left: -9px;
|
|
153
|
+
width: 210px;
|
|
154
|
+
background-color: rgba(255, 255, 255, 0.975);
|
|
155
|
+
border-radius: 2px;
|
|
156
|
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
157
|
+
|
|
158
|
+
.ui.menu {
|
|
159
|
+
border: 0;
|
|
160
|
+
border-radius: 2px;
|
|
161
|
+
|
|
162
|
+
.icon {
|
|
163
|
+
margin-right: 12px;
|
|
164
|
+
vertical-align: middle;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.item.active {
|
|
168
|
+
background: #efefef !important;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.loadAddonOverrides();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Needed by semantic.less */
|
|
2
|
+
@type: 'extra';
|
|
3
|
+
@element: 'custom';
|
|
4
|
+
|
|
5
|
+
@import (multiple, reference, optional) '../../theme.config';
|
|
6
|
+
|
|
7
|
+
/* Enables customization of addons */
|
|
8
|
+
.loadAddonOverrides() {
|
|
9
|
+
@import (optional)
|
|
10
|
+
'@{siteFolder}/../addons/@{addon}/@{addontype}s/@{addonelement}.overrides';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Helper to load variables */
|
|
14
|
+
.loadAddonVariables() {
|
|
15
|
+
@import (optional) '@{addonelement}.variables';
|
|
16
|
+
@import (optional)
|
|
17
|
+
'@{siteFolder}/../addons/@{addon}/@{addontype}s/@{addonelement}.variables';
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
h1,
|
|
2
|
+
h2,
|
|
3
|
+
h3,
|
|
4
|
+
h4 {
|
|
5
|
+
&:hover {
|
|
6
|
+
a.anchor {
|
|
7
|
+
svg {
|
|
8
|
+
opacity: 1;
|
|
9
|
+
transform: rotate(15deg);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
a.anchor {
|
|
15
|
+
position: absolute;
|
|
16
|
+
display: inline-block;
|
|
17
|
+
margin-left: 5px;
|
|
18
|
+
vertical-align: middle;
|
|
19
|
+
|
|
20
|
+
svg {
|
|
21
|
+
width: 1.6ch;
|
|
22
|
+
fill: #42526e;
|
|
23
|
+
opacity: 0;
|
|
24
|
+
transform: rotate(15deg) translate(-8px, 2px);
|
|
25
|
+
transition: opacity 0.2s ease 0s, transform 0.2s ease 0s;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx } from 'slate-hyperscript';
|
|
2
|
+
import { LINK } from '@plone/volto-slate/constants';
|
|
3
|
+
import { deserialize } from '@plone/volto-slate/editor/deserialize';
|
|
4
|
+
import { isEmpty } from 'lodash';
|
|
5
|
+
// import { Editor } from 'slate';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {HTMLAnchorElement} aEl
|
|
9
|
+
*/
|
|
10
|
+
const hasValidTarget = (aEl) => {
|
|
11
|
+
return (
|
|
12
|
+
aEl.hasAttribute('target') &&
|
|
13
|
+
['_blank', '_self', '_parent', '_top'].includes(aEl.getAttribute('target'))
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* This is almost the inverse function of LinkElement render function at
|
|
19
|
+
* src/editor/plugins/Link/render.jsx
|
|
20
|
+
* @param {Editor} editor
|
|
21
|
+
* @param {HTMLElement} el
|
|
22
|
+
*/
|
|
23
|
+
export const linkDeserializer = (editor, el) => {
|
|
24
|
+
let parent = el;
|
|
25
|
+
|
|
26
|
+
let children = Array.from(parent.childNodes)
|
|
27
|
+
.map((el) => deserialize(editor, el))
|
|
28
|
+
.flat();
|
|
29
|
+
|
|
30
|
+
if (isEmpty(children)) {
|
|
31
|
+
//nodes must contain at least one Text descendant
|
|
32
|
+
children = [{ text: '' }];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const attrs = {
|
|
36
|
+
type: LINK,
|
|
37
|
+
url: el.getAttribute('href'),
|
|
38
|
+
data: {},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (el.hasAttribute('title')) attrs.data.title = el.getAttribute('title');
|
|
42
|
+
|
|
43
|
+
const targetSet = hasValidTarget(el);
|
|
44
|
+
|
|
45
|
+
// We don't use this isExternalLink because links can come w/o a target from
|
|
46
|
+
// outside of Volto Slate blocks and still be external.
|
|
47
|
+
// let isExternalLink;
|
|
48
|
+
if (targetSet) {
|
|
49
|
+
attrs.data = attrs.data || {};
|
|
50
|
+
attrs.data.link = attrs.data.link || {};
|
|
51
|
+
attrs.data.link.external = { target: el.getAttribute('target') };
|
|
52
|
+
// isExternalLink = true;
|
|
53
|
+
} else {
|
|
54
|
+
// isExternalLink = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (attrs.url?.startsWith('mailto:')) {
|
|
58
|
+
// TODO: improve security because we are using regex-es
|
|
59
|
+
attrs.data = attrs.data || {};
|
|
60
|
+
attrs.data.link = attrs.data.link || {};
|
|
61
|
+
attrs.data.link.email = {
|
|
62
|
+
email_address: attrs.url
|
|
63
|
+
.replace(/^mailto:/g, '')
|
|
64
|
+
.replace(/\?subject=.+$/g, ''),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const subject = attrs.url.match(/\?subject=(.*)$/);
|
|
68
|
+
if (subject && subject[1]) {
|
|
69
|
+
attrs.data.link.email.email_subject = subject[1];
|
|
70
|
+
}
|
|
71
|
+
} else if (/* !isExternalLink && */ attrs.url?.startsWith('/')) {
|
|
72
|
+
// TODO: improve this condition if it is not very good
|
|
73
|
+
attrs.data = attrs.data || {};
|
|
74
|
+
attrs.data.link = attrs.data.link || {};
|
|
75
|
+
attrs.data.link.internal = { internal_link: [{ '@id': attrs.url }] };
|
|
76
|
+
} else {
|
|
77
|
+
// the general condition: if it is external link
|
|
78
|
+
attrs.data = attrs.data || {};
|
|
79
|
+
attrs.data.link = attrs.data.link || {};
|
|
80
|
+
attrs.data.link.external = attrs.data.link.external || {};
|
|
81
|
+
attrs.data.link.external.external_link = attrs.url;
|
|
82
|
+
if (!targetSet) {
|
|
83
|
+
attrs.data.link.external.target = '_self';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return jsx('element', attrs, children);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
linkDeserializer.id = 'linkDeserializer';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// import isUrl from 'is-url';
|
|
2
|
+
// import { wrapLink } from './utils';
|
|
3
|
+
import { LINK } from '@plone/volto-slate/constants';
|
|
4
|
+
|
|
5
|
+
export const withLink = (editor) => {
|
|
6
|
+
// const { insertData, insertText, isInline } = editor;
|
|
7
|
+
|
|
8
|
+
const { isInline } = editor;
|
|
9
|
+
|
|
10
|
+
editor.isInline = (element) => {
|
|
11
|
+
return element && element.type === LINK ? true : isInline(element);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// editor.insertText = (text) => {
|
|
15
|
+
// if (text && isUrl(text)) {
|
|
16
|
+
// wrapLink(editor, text);
|
|
17
|
+
// } else {
|
|
18
|
+
// insertText(text);
|
|
19
|
+
// }
|
|
20
|
+
// };
|
|
21
|
+
//
|
|
22
|
+
// editor.insertData = (data) => {
|
|
23
|
+
// const text = data.getData('text/plain');
|
|
24
|
+
//
|
|
25
|
+
// if (text && isUrl(text)) {
|
|
26
|
+
// wrapLink(editor, text);
|
|
27
|
+
// } else {
|
|
28
|
+
// insertData(data);
|
|
29
|
+
// }
|
|
30
|
+
// };
|
|
31
|
+
return editor;
|
|
32
|
+
};
|