@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,50 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl'; // , defineMessages
|
|
2
|
+
import linkSVG from '@plone/volto/icons/link.svg';
|
|
3
|
+
import { makeInlineElementPlugin } from '@plone/volto-slate/elementEditor';
|
|
4
|
+
|
|
5
|
+
import { LINK } from '@plone/volto-slate/constants';
|
|
6
|
+
import { LinkElement } from './render';
|
|
7
|
+
import { withLink } from './extensions';
|
|
8
|
+
import { linkDeserializer } from './deserialize';
|
|
9
|
+
import LinkEditSchema from './schema';
|
|
10
|
+
|
|
11
|
+
const messages = defineMessages({
|
|
12
|
+
edit: {
|
|
13
|
+
id: 'Edit link',
|
|
14
|
+
defaultMessage: 'Edit link',
|
|
15
|
+
},
|
|
16
|
+
delete: {
|
|
17
|
+
id: 'Remove link',
|
|
18
|
+
defaultMessage: 'Remove link',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export default function advancedLink(config) {
|
|
23
|
+
const { slate } = config.settings;
|
|
24
|
+
|
|
25
|
+
slate.toolbarButtons = [...(slate.toolbarButtons || []), LINK];
|
|
26
|
+
slate.expandedToolbarButtons = [
|
|
27
|
+
...(slate.expandedToolbarButtons || []),
|
|
28
|
+
LINK,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
slate.htmlTagsToSlate.A = linkDeserializer;
|
|
32
|
+
|
|
33
|
+
const opts = {
|
|
34
|
+
title: 'Link',
|
|
35
|
+
pluginId: LINK,
|
|
36
|
+
elementType: LINK,
|
|
37
|
+
element: LinkElement,
|
|
38
|
+
isInlineElement: true,
|
|
39
|
+
editSchema: LinkEditSchema,
|
|
40
|
+
extensions: [withLink],
|
|
41
|
+
hasValue: (formData) => !!formData.link,
|
|
42
|
+
toolbarButtonIcon: linkSVG,
|
|
43
|
+
messages,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const [installLinkEditor] = makeInlineElementPlugin(opts);
|
|
47
|
+
config = installLinkEditor(config);
|
|
48
|
+
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { UniversalLink } from '@plone/volto/components';
|
|
3
|
+
import './styles.less';
|
|
4
|
+
|
|
5
|
+
export const LinkElement = ({ attributes, children, element, mode }) => {
|
|
6
|
+
// TODO: handle title on internal links
|
|
7
|
+
let url = element.url;
|
|
8
|
+
const { link } = element.data || {};
|
|
9
|
+
|
|
10
|
+
const internal_link = link?.internal?.internal_link?.[0]?.['@id'];
|
|
11
|
+
const external_link = link?.external?.external_link;
|
|
12
|
+
const email = link?.email;
|
|
13
|
+
|
|
14
|
+
const href = email
|
|
15
|
+
? `mailto:${email.email_address}${
|
|
16
|
+
email.email_subject ? `?subject=${email.email_subject}` : ''
|
|
17
|
+
}`
|
|
18
|
+
: external_link || internal_link || url;
|
|
19
|
+
|
|
20
|
+
const { title } = element?.data || {};
|
|
21
|
+
|
|
22
|
+
return mode === 'view' ? (
|
|
23
|
+
<>
|
|
24
|
+
<UniversalLink
|
|
25
|
+
href={href || '#'}
|
|
26
|
+
openLinkInNewTab={link?.external?.target === '_blank'}
|
|
27
|
+
title={title}
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</UniversalLink>
|
|
31
|
+
</>
|
|
32
|
+
) : (
|
|
33
|
+
<span {...attributes} className="slate-editor-link">
|
|
34
|
+
{children}
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import externalSVG from '@plone/volto/icons/link.svg';
|
|
2
|
+
import internalSVG from '@plone/volto/icons/nav.svg';
|
|
3
|
+
import emailSVG from '@plone/volto/icons/email.svg';
|
|
4
|
+
// import pageLinkSVG from '@plone/volto/icons/show-blocks.svg';
|
|
5
|
+
|
|
6
|
+
export const EmailLinkSchema = {
|
|
7
|
+
title: 'Email address',
|
|
8
|
+
fieldsets: [
|
|
9
|
+
{
|
|
10
|
+
id: 'email',
|
|
11
|
+
title: 'Email',
|
|
12
|
+
fields: ['email_address', 'email_subject'],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
properties: {
|
|
16
|
+
email_address: {
|
|
17
|
+
title: 'Email address',
|
|
18
|
+
},
|
|
19
|
+
email_subject: {
|
|
20
|
+
title: 'Email subject',
|
|
21
|
+
description: 'Optional',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
required: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const InternalLinkSchema = {
|
|
28
|
+
title: 'Internal link',
|
|
29
|
+
fieldsets: [
|
|
30
|
+
{
|
|
31
|
+
id: 'internal',
|
|
32
|
+
title: 'Internal',
|
|
33
|
+
fields: ['internal_link'],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
properties: {
|
|
37
|
+
internal_link: {
|
|
38
|
+
widget: 'object_browser',
|
|
39
|
+
title: 'Internal link',
|
|
40
|
+
multiple: false,
|
|
41
|
+
mode: 'link',
|
|
42
|
+
selectedItemAttrs: [],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const ExternalLinkSchema = {
|
|
49
|
+
title: 'External link',
|
|
50
|
+
fieldsets: [
|
|
51
|
+
{
|
|
52
|
+
id: 'external',
|
|
53
|
+
title: 'External',
|
|
54
|
+
fields: ['external_link', 'target'],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
properties: {
|
|
58
|
+
external_link: {
|
|
59
|
+
title: 'External URL',
|
|
60
|
+
description:
|
|
61
|
+
'URL can be relative within this site or absolute if it starts with http:// or https://',
|
|
62
|
+
},
|
|
63
|
+
target: {
|
|
64
|
+
title: 'Target',
|
|
65
|
+
choices: [
|
|
66
|
+
['_self', 'Open in this window / frame'],
|
|
67
|
+
['_blank', 'Open in new window'],
|
|
68
|
+
['_parent', 'Open in parent window / frame'],
|
|
69
|
+
['_top', 'Open in top frame (replaces all frames)'],
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const LinkEditSchema = {
|
|
77
|
+
title: 'Insert link',
|
|
78
|
+
fieldsets: [
|
|
79
|
+
{
|
|
80
|
+
id: 'default',
|
|
81
|
+
title: 'Internal link',
|
|
82
|
+
fields: ['link', 'title'],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
properties: {
|
|
86
|
+
title: {
|
|
87
|
+
title: 'Link Title',
|
|
88
|
+
},
|
|
89
|
+
link: {
|
|
90
|
+
title: 'Link',
|
|
91
|
+
widget: 'object_by_type',
|
|
92
|
+
schemas: [
|
|
93
|
+
{
|
|
94
|
+
id: 'internal',
|
|
95
|
+
icon: internalSVG,
|
|
96
|
+
schema: InternalLinkSchema,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'external',
|
|
100
|
+
icon: externalSVG,
|
|
101
|
+
schema: ExternalLinkSchema,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'email',
|
|
105
|
+
icon: emailSVG,
|
|
106
|
+
schema: EmailLinkSchema,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: [],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default LinkEditSchema;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BlockButton } from '@plone/volto-slate/editor/ui';
|
|
3
|
+
import quoteIcon from '@plone/volto/icons/quote.svg';
|
|
4
|
+
|
|
5
|
+
// TODO: this needs to use constants for el type
|
|
6
|
+
|
|
7
|
+
export const BlockquoteElement = ({ attributes, children }) => {
|
|
8
|
+
// the 'callout' class is defined in file 'blocks.less'
|
|
9
|
+
// TODO: move the style out of it into a `blockquote` tag name selector
|
|
10
|
+
return <blockquote {...attributes}>{children}</blockquote>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function install(config) {
|
|
14
|
+
const { slate } = config.settings;
|
|
15
|
+
|
|
16
|
+
slate.buttons['blockquote'] = (props) => (
|
|
17
|
+
<BlockButton
|
|
18
|
+
format="blockquote"
|
|
19
|
+
icon={quoteIcon}
|
|
20
|
+
title="Blockquote"
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
slate.elements['blockquote'] = BlockquoteElement;
|
|
25
|
+
|
|
26
|
+
slate.toolbarButtons.push('blockquote');
|
|
27
|
+
slate.expandedToolbarButtons.push('blockquote');
|
|
28
|
+
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BlockButton } from '@plone/volto-slate/editor/ui';
|
|
3
|
+
import calloutSVG from '@plone/volto/icons/megaphone.svg';
|
|
4
|
+
|
|
5
|
+
// TODO: this needs to use constants for el type
|
|
6
|
+
|
|
7
|
+
export const CalloutElement = ({ attributes, children, element }) => {
|
|
8
|
+
// the 'callout' class is defined in file 'blocks.less'
|
|
9
|
+
// TODO: move the style out of it into a `blockquote` tag name selector
|
|
10
|
+
return (
|
|
11
|
+
<p {...attributes} className="callout">
|
|
12
|
+
{children}
|
|
13
|
+
</p>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default function install(config) {
|
|
18
|
+
const { slate } = config.settings;
|
|
19
|
+
|
|
20
|
+
slate.buttons['callout'] = (props) => (
|
|
21
|
+
<BlockButton
|
|
22
|
+
format="callout"
|
|
23
|
+
icon={calloutSVG}
|
|
24
|
+
title="Callout"
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
slate.elements['callout'] = CalloutElement;
|
|
29
|
+
|
|
30
|
+
slate.toolbarButtons.push('callout');
|
|
31
|
+
slate.expandedToolbarButtons.push('callout');
|
|
32
|
+
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid';
|
|
2
|
+
import { Editor, Transforms } from 'slate';
|
|
3
|
+
import { IMAGE } from '@plone/volto-slate/constants';
|
|
4
|
+
|
|
5
|
+
export function syncCreateImageBlock(url) {
|
|
6
|
+
const id = uuid();
|
|
7
|
+
const block = {
|
|
8
|
+
'@type': 'image',
|
|
9
|
+
url,
|
|
10
|
+
};
|
|
11
|
+
return [id, block];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// This function is used by deconstructToVoltoBlocks, so not directly by the
|
|
15
|
+
// <SlateEditor>. File exists here because there's no "blocks/Image" folder
|
|
16
|
+
export const extractImages = (editor, pathRef) => {
|
|
17
|
+
const imageNodes = Array.from(
|
|
18
|
+
Editor.nodes(editor, {
|
|
19
|
+
at: pathRef.current,
|
|
20
|
+
match: (node) => node.type === IMAGE,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
const images = imageNodes.map(([el, path]) => el);
|
|
24
|
+
Transforms.removeNodes(editor, {
|
|
25
|
+
at: pathRef.current,
|
|
26
|
+
match: (node) => node.type === IMAGE,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return images.map((el) => syncCreateImageBlock(el.url));
|
|
30
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The default behavior is to allow images to be copy/pasted inside the editor
|
|
2
|
+
// The TextBlockEdit extensions will come and then split the images into
|
|
3
|
+
// separate dedicated Volto image blocks.
|
|
4
|
+
|
|
5
|
+
import { IMAGE } from '@plone/volto-slate/constants';
|
|
6
|
+
import { jsx } from 'slate-hyperscript';
|
|
7
|
+
|
|
8
|
+
export const deserializeImageTag = (editor, el) => {
|
|
9
|
+
const attrs = { type: IMAGE };
|
|
10
|
+
|
|
11
|
+
// TODO: not all of these attributes should be stored in the DB
|
|
12
|
+
for (const name of el.getAttributeNames()) {
|
|
13
|
+
attrs[name] = el.getAttribute(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// TODO: recognize more unsupported protocols
|
|
17
|
+
if (typeof attrs.src === 'undefined' || attrs.src.startsWith('file:///')) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attrs.url = attrs.src;
|
|
22
|
+
delete attrs.src;
|
|
23
|
+
|
|
24
|
+
return [jsx('element', attrs, [{ text: '' }])];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Allows for pasting images from clipboard.
|
|
29
|
+
* Not yet: dragging and dropping images, selecting them through a file system dialog.
|
|
30
|
+
* @param typeImg
|
|
31
|
+
*/
|
|
32
|
+
export const withImage = (editor) => {
|
|
33
|
+
const { isVoid, isInline } = editor;
|
|
34
|
+
|
|
35
|
+
editor.isVoid = (element) => {
|
|
36
|
+
return element.type === IMAGE ? true : isVoid(element);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// If it's not marked as inline, Slate will strip the {type:'img"} nodes when
|
|
40
|
+
// it finds them next to {text: ''} nodes
|
|
41
|
+
editor.isInline = (element) => {
|
|
42
|
+
return element && element.type === IMAGE ? true : isInline(element);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
editor.htmlTagsToSlate = {
|
|
46
|
+
...editor.htmlTagsToSlate,
|
|
47
|
+
IMG: deserializeImageTag,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return editor;
|
|
51
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { withImage } from './extensions';
|
|
2
|
+
import { ImageElement } from './render';
|
|
3
|
+
|
|
4
|
+
export default function install(config) {
|
|
5
|
+
const { slate } = config.settings;
|
|
6
|
+
|
|
7
|
+
slate.extensions = [...(slate.extensions || []), withImage];
|
|
8
|
+
slate.elements.img = ImageElement;
|
|
9
|
+
|
|
10
|
+
return config;
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSelected, useFocused } from 'slate-react';
|
|
3
|
+
|
|
4
|
+
export const ImageElement = (props) => {
|
|
5
|
+
const { attributes, children, element } = props;
|
|
6
|
+
const selected = useSelected();
|
|
7
|
+
const focused = useFocused();
|
|
8
|
+
|
|
9
|
+
const style = {
|
|
10
|
+
display: 'block',
|
|
11
|
+
maxWidth: '100%',
|
|
12
|
+
maxHeight: '20em',
|
|
13
|
+
boxShadow: selected && focused ? '0 0 0 2px blue' : 'none',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<span {...attributes} style={{ display: 'inline-block' }}>
|
|
18
|
+
{children}
|
|
19
|
+
<img alt="" src={element.url} style={style} />
|
|
20
|
+
</span>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Text, Transforms, Element } from 'slate'; // Editor,
|
|
2
|
+
import { SIMPLELINK } from '@plone/volto-slate/constants';
|
|
3
|
+
import { jsx } from 'slate-hyperscript';
|
|
4
|
+
import { deserialize } from '@plone/volto-slate/editor/deserialize';
|
|
5
|
+
|
|
6
|
+
const nodeToText = (node) => {
|
|
7
|
+
if (Text.isText(node)) {
|
|
8
|
+
return node.text.trim();
|
|
9
|
+
} else {
|
|
10
|
+
return node.children.map(nodeToText).join('');
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const withSimpleLink = (editor) => {
|
|
15
|
+
const { isInline, normalizeNode } = editor;
|
|
16
|
+
|
|
17
|
+
editor.isInline = (element) => {
|
|
18
|
+
return element && element.type === SIMPLELINK ? true : isInline(element);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
editor.normalizeNode = (entry) => {
|
|
22
|
+
const [node, path] = entry;
|
|
23
|
+
const isTextNode = Text.isText(node);
|
|
24
|
+
const isElementNode = Element.isElement(node);
|
|
25
|
+
const isLinkTypeNode = node.type === SIMPLELINK;
|
|
26
|
+
|
|
27
|
+
// delete childless link nodes
|
|
28
|
+
if (!isTextNode && isElementNode && isLinkTypeNode && !nodeToText(node)) {
|
|
29
|
+
Transforms.removeNodes(editor, { at: path });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return normalizeNode(entry);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return editor;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const simpleLinkDeserializer = (editor, el) => {
|
|
40
|
+
let parent = el;
|
|
41
|
+
|
|
42
|
+
let children = Array.from(parent.childNodes)
|
|
43
|
+
.map((el) => deserialize(editor, el))
|
|
44
|
+
.flat();
|
|
45
|
+
|
|
46
|
+
if (!children.length) children = [{ text: '' }];
|
|
47
|
+
|
|
48
|
+
const attrs = {
|
|
49
|
+
type: SIMPLELINK,
|
|
50
|
+
data: {
|
|
51
|
+
url: el.getAttribute('href'),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return jsx('element', attrs, children);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
simpleLinkDeserializer.id = 'simpleLinkDeserializer';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { defineMessages } from 'react-intl'; // , defineMessages
|
|
3
|
+
import { ReactEditor, useSlate } from 'slate-react';
|
|
4
|
+
import { useSelector, useDispatch } from 'react-redux';
|
|
5
|
+
import AddLinkForm from '@plone/volto/components/manage/AnchorPlugin/components/LinkButton/AddLinkForm';
|
|
6
|
+
import {
|
|
7
|
+
_insertElement,
|
|
8
|
+
_unwrapElement,
|
|
9
|
+
_isActiveElement,
|
|
10
|
+
_getActiveElement,
|
|
11
|
+
} from '@plone/volto-slate/elementEditor/utils';
|
|
12
|
+
import { SIMPLELINK, LINK } from '@plone/volto-slate/constants';
|
|
13
|
+
import { LinkElement } from './render';
|
|
14
|
+
import { simpleLinkDeserializer, withSimpleLink } from './extensions';
|
|
15
|
+
import { setPluginOptions } from '@plone/volto-slate/actions';
|
|
16
|
+
import {
|
|
17
|
+
ToolbarButton as UIToolbarButton,
|
|
18
|
+
PositionedToolbar,
|
|
19
|
+
} from '@plone/volto-slate/editor/ui';
|
|
20
|
+
import { useSelectionPosition } from '@plone/volto-slate/hooks';
|
|
21
|
+
|
|
22
|
+
import linkSVG from '@plone/volto/icons/link.svg';
|
|
23
|
+
import unlinkSVG from '@plone/volto/icons/unlink.svg';
|
|
24
|
+
|
|
25
|
+
const messages = defineMessages({
|
|
26
|
+
add: {
|
|
27
|
+
id: 'Add link',
|
|
28
|
+
defaultMessage: 'Add link',
|
|
29
|
+
},
|
|
30
|
+
edit: {
|
|
31
|
+
id: 'Edit link',
|
|
32
|
+
defaultMessage: 'Edit link',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
function getPositionStyle(rect) {
|
|
37
|
+
return {
|
|
38
|
+
style: {
|
|
39
|
+
opacity: 1,
|
|
40
|
+
top: rect.top + window.pageYOffset - 6,
|
|
41
|
+
left: rect.left + window.pageXOffset + rect.width / 2,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const LinkEditor = (props) => {
|
|
47
|
+
const { editor, pluginId, getActiveElement, unwrapElement, insertElement } =
|
|
48
|
+
props;
|
|
49
|
+
const pid = `${editor.uid}-${pluginId}`;
|
|
50
|
+
const showEditor = useSelector((state) => {
|
|
51
|
+
return state['slate_plugins']?.[pid]?.show_sidebar_editor;
|
|
52
|
+
});
|
|
53
|
+
const savedPosition = React.useRef();
|
|
54
|
+
const rect = useSelectionPosition();
|
|
55
|
+
|
|
56
|
+
const dispatch = useDispatch();
|
|
57
|
+
|
|
58
|
+
const active = getActiveElement(editor);
|
|
59
|
+
// console.log('active', active);
|
|
60
|
+
const [node] = active || [];
|
|
61
|
+
|
|
62
|
+
if (showEditor && !savedPosition.current) {
|
|
63
|
+
savedPosition.current = getPositionStyle(rect);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return showEditor ? (
|
|
67
|
+
<PositionedToolbar className="add-link" position={savedPosition.current}>
|
|
68
|
+
<AddLinkForm
|
|
69
|
+
block="draft-js"
|
|
70
|
+
placeholder={'Add link'}
|
|
71
|
+
data={{ url: node?.data?.url || '' }}
|
|
72
|
+
theme={{}}
|
|
73
|
+
onChangeValue={(url) => {
|
|
74
|
+
if (!active) {
|
|
75
|
+
if (!editor.selection) editor.selection = editor.savedSelection;
|
|
76
|
+
insertElement(editor, { url });
|
|
77
|
+
} else {
|
|
78
|
+
const selection = unwrapElement(editor);
|
|
79
|
+
editor.selection = selection;
|
|
80
|
+
insertElement(editor, { url });
|
|
81
|
+
}
|
|
82
|
+
ReactEditor.focus(editor);
|
|
83
|
+
dispatch(setPluginOptions(pid, { show_sidebar_editor: false }));
|
|
84
|
+
savedPosition.current = null;
|
|
85
|
+
}}
|
|
86
|
+
onClear={() => {
|
|
87
|
+
// clear button was pressed in the link edit popup
|
|
88
|
+
const newSelection = JSON.parse(
|
|
89
|
+
JSON.stringify(unwrapElement(editor)),
|
|
90
|
+
);
|
|
91
|
+
editor.selection = newSelection;
|
|
92
|
+
editor.savedSelection = newSelection;
|
|
93
|
+
}}
|
|
94
|
+
onOverrideContent={(c) => {
|
|
95
|
+
dispatch(setPluginOptions(pid, { show_sidebar_editor: false }));
|
|
96
|
+
savedPosition.current = null;
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</PositionedToolbar>
|
|
100
|
+
) : null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const applyConfig = (config) => {
|
|
104
|
+
const { slate } = config.settings;
|
|
105
|
+
|
|
106
|
+
const PLUGINID = SIMPLELINK;
|
|
107
|
+
|
|
108
|
+
const linkBtnIndex = slate.toolbarButtons.findIndex((b) => b === LINK);
|
|
109
|
+
slate.expandedToolbarButtons = slate.expandedToolbarButtons.filter(
|
|
110
|
+
(b) => b !== LINK,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const insertElement = _insertElement(PLUGINID);
|
|
114
|
+
const getActiveElement = _getActiveElement(PLUGINID);
|
|
115
|
+
const isActiveElement = _isActiveElement(PLUGINID);
|
|
116
|
+
const unwrapElement = _unwrapElement(PLUGINID);
|
|
117
|
+
|
|
118
|
+
const ToolbarButton = (props) => {
|
|
119
|
+
const dispatch = useDispatch();
|
|
120
|
+
const editor = useSlate();
|
|
121
|
+
const isElement = isActiveElement(editor);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<UIToolbarButton
|
|
125
|
+
title={isElement ? messages.edit : messages.add}
|
|
126
|
+
icon={isElement ? unlinkSVG : linkSVG}
|
|
127
|
+
active={isElement}
|
|
128
|
+
onMouseDown={(e) => {
|
|
129
|
+
e.stopPropagation();
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
const pid = `${editor.uid}-${PLUGINID}`;
|
|
132
|
+
editor.savedSelection = JSON.parse(JSON.stringify(editor.selection));
|
|
133
|
+
dispatch(setPluginOptions(pid, { show_sidebar_editor: true }));
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const pluginOptions = {
|
|
140
|
+
insertElement,
|
|
141
|
+
getActiveElement,
|
|
142
|
+
isActiveElement,
|
|
143
|
+
unwrapElement,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
slate.buttons[PLUGINID] = ToolbarButton;
|
|
147
|
+
slate.toolbarButtons[linkBtnIndex] = PLUGINID;
|
|
148
|
+
slate.htmlTagsToSlate.A = simpleLinkDeserializer;
|
|
149
|
+
slate.extensions.push(withSimpleLink);
|
|
150
|
+
slate.elements[PLUGINID] = LinkElement;
|
|
151
|
+
slate.nodeTypesToHighlight.push(PLUGINID);
|
|
152
|
+
slate.persistentHelpers.push((props) => (
|
|
153
|
+
<LinkEditor {...props} pluginId={PLUGINID} {...pluginOptions} />
|
|
154
|
+
));
|
|
155
|
+
|
|
156
|
+
return config;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export default applyConfig;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import { UniversalLink } from '@plone/volto/components';
|
|
4
|
+
import config from '@plone/volto/registry';
|
|
5
|
+
import { isInternalURL, flattenToAppURL } from '@plone/volto/helpers';
|
|
6
|
+
|
|
7
|
+
const ViewLink = ({ url, target, download, children }) => {
|
|
8
|
+
const { openExternalLinkInNewTab } = config.settings;
|
|
9
|
+
return (
|
|
10
|
+
<UniversalLink
|
|
11
|
+
href={url}
|
|
12
|
+
openLinkInNewTab={
|
|
13
|
+
(openExternalLinkInNewTab && !isInternalURL(url)) || target === '_blank'
|
|
14
|
+
}
|
|
15
|
+
download={download}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</UniversalLink>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const LinkElement = (props) => {
|
|
23
|
+
const { attributes, children, element, mode = 'edit' } = props;
|
|
24
|
+
const isInternalUrl = isInternalURL(element.data?.url);
|
|
25
|
+
const linkUrl = element.data?.url;
|
|
26
|
+
|
|
27
|
+
return mode === 'view' ? (
|
|
28
|
+
<ViewLink {...(element.data || {})}>{children}</ViewLink>
|
|
29
|
+
) : (
|
|
30
|
+
<a
|
|
31
|
+
{...attributes}
|
|
32
|
+
className={cx('slate-editor-link', { external: !isInternalUrl })}
|
|
33
|
+
href={isInternalUrl ? flattenToAppURL(linkUrl) : linkUrl}
|
|
34
|
+
onClick={(e) => e.preventDefault()}
|
|
35
|
+
>
|
|
36
|
+
{Array.isArray(children)
|
|
37
|
+
? children.map((child, i) => {
|
|
38
|
+
if (child?.props?.decorations) {
|
|
39
|
+
const isSelection =
|
|
40
|
+
child.props.decorations.findIndex((deco) => deco.isSelection) >
|
|
41
|
+
-1;
|
|
42
|
+
if (isSelection)
|
|
43
|
+
return (
|
|
44
|
+
<span className="highlight-selection" key={`${i}-sel`}>
|
|
45
|
+
{child}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return child;
|
|
50
|
+
})
|
|
51
|
+
: children}
|
|
52
|
+
</a>
|
|
53
|
+
);
|
|
54
|
+
};
|