diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b0dac27..b81c0438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [5.1.2](https://github.com/eea/volto-slate/compare/5.1.1...5.1.2) + +- Backwards block merge 2 [`#195`](https://github.com/eea/volto-slate/pull/195) +- Upgrade slate and slate-react versions [`#196`](https://github.com/eea/volto-slate/pull/196) +- Remove useless commented line [`f02a9e3`](https://github.com/eea/volto-slate/commit/f02a9e3aca19b5f7f0d39ec6a2e3040fc65a3591) +- Multiple Description blocks on the same page update each other [`44ec992`](https://github.com/eea/volto-slate/commit/44ec9923717b99516c2899fd26f51e8a860e14e0) +- Make it work with new react-slate method of getting the value [`9256b59`](https://github.com/eea/volto-slate/commit/9256b5904617e700f78ccde2e92a1d042461f28c) +- Split text block editor components in separate modules [`0888e61`](https://github.com/eea/volto-slate/commit/0888e6177254562c501847c151e946b6c8f10537) +- Revert to older version of slate-react [`044fadf`](https://github.com/eea/volto-slate/commit/044fadf592dc71a5756e82a94a98df0ba0b8c7ac) +- Refs #142010 - Optimize Volto-addons gitflow pipelines [`33bc1ed`](https://github.com/eea/volto-slate/commit/33bc1edeb63ee0d0e6fb8fa6c170d11ed4922a3d) + #### [5.1.1](https://github.com/eea/volto-slate/compare/5.1.0...5.1.1) +> 18 November 2021 + +- Title description nested blocks [`#188`](https://github.com/eea/volto-slate/pull/188) - Title description nested blocks [`#187`](https://github.com/eea/volto-slate/pull/187) #### [5.1.0](https://github.com/eea/volto-slate/compare/5.0.0...5.1.0) diff --git a/Jenkinsfile b/Jenkinsfile index 1f197c7f..8db56c1e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,6 +13,13 @@ pipeline { stages { stage('Code') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + not { branch 'master' } + not { changelog '.*^Automated release [0-9\\.]+$' } + } + } steps { parallel( @@ -38,6 +45,13 @@ pipeline { } stage('Tests') { + when { + allOf { + environment name: 'CHANGE_ID', value: '' + not { branch 'master' } + not { changelog '.*^Automated release [0-9\\.]+$' } + } + } steps { parallel( @@ -77,10 +91,11 @@ pipeline { } stage('Integration tests') { - // Exclude Pull-Requests. Already running on branch when { allOf { environment name: 'CHANGE_ID', value: '' + not { branch 'master' } + not { changelog '.*^Automated release [0-9\\.]+$' } } } steps { @@ -130,11 +145,13 @@ pipeline { } stage('Report to SonarQube') { - // Exclude Pull-Requests when { - allOf { environment name: 'CHANGE_ID', value: '' - } + anyOf { + branch 'master' + branch 'develop' + } + not { changelog '.*^Automated release [0-9\\.]+$' } } steps { node(label: 'swarm') { @@ -164,8 +181,8 @@ pipeline { steps { node(label: 'docker') { script { - if ( env.CHANGE_BRANCH != "develop" && !( env.CHANGE_BRANCH.startsWith("hotfix")) ) { - error "Pipeline aborted due to PR not made from develop or hotfix branch" + if ( env.CHANGE_BRANCH != "develop" ) { + error "Pipeline aborted due to PR not made from develop branch" } withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) { sh '''docker pull eeacms/gitflow''' diff --git a/package.json b/package.json index 277d9aa3..b51ada3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "volto-slate", - "version": "5.1.1", + "version": "5.1.2", "description": "Slate.js integration with Volto", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", @@ -25,10 +25,10 @@ "jsdom": "^16.6.0", "react-intersection-observer": "^8.32.0", "react-visibility-sensor": "5.1.1", - "slate": "^0.70.0", + "slate": "^0.71.0", "slate-history": "^0.66.0", "slate-hyperscript": "^0.67.0", - "slate-react": "^0.70.0", + "slate-react": "^0.71.0", "weak-key": "^1.0.2" }, "peerDependencies": { diff --git a/src/blocks/Text/DefaultTextBlockEditor.jsx b/src/blocks/Text/DefaultTextBlockEditor.jsx new file mode 100644 index 00000000..872c6a19 --- /dev/null +++ b/src/blocks/Text/DefaultTextBlockEditor.jsx @@ -0,0 +1,286 @@ +import ReactDOM from 'react-dom'; +import React from 'react'; +import { readAsDataURL } from 'promise-file-reader'; +import Dropzone from 'react-dropzone'; +import { defineMessages, useIntl } from 'react-intl'; +import { useInView } from 'react-intersection-observer'; +import { Dimmer, Loader, Message, Segment } from 'semantic-ui-react'; + +import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers'; +import config from '@plone/volto/registry'; +import { + InlineForm, + SidebarPortal, + BlockChooserButton, +} from '@plone/volto/components'; + +import { SlateEditor } from 'volto-slate/editor'; +import { serializeNodesToText } from 'volto-slate/editor/render'; +import { + createImageBlock, + parseDefaultSelection, + deconstructToVoltoBlocks, +} from 'volto-slate/utils'; +import { Transforms } from 'slate'; + +import ShortcutListing from './ShortcutListing'; +import MarkdownIntroduction from './MarkdownIntroduction'; +import { handleKey } from './keyboard'; +import TextBlockSchema from './schema'; + +import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg'; + +import './css/editor.css'; + +// TODO: refactor dropzone to separate component wrapper + +const messages = defineMessages({ + text: { + id: 'Type text…', + defaultMessage: 'Type text…', + }, +}); + +const DEBUG = false; + +export const DefaultTextBlockEditor = (props) => { + const { + block, + blocksConfig, + data, + detached = false, + index, + onChangeBlock, + onInsertBlock, + onMutateBlock, + onSelectBlock, + pathname, + properties, + selected, + uploadRequest, + uploadContent, + uploadedContent, + defaultSelection, + saveSlateBlockSelection, + allowedBlocks, + formTitle, + formDescription, + } = props; + + const { slate } = config.settings; + const { textblockExtensions } = slate; + const { value } = data; + + // const [addNewBlockOpened, setAddNewBlockOpened] = React.useState(); + const [showDropzone, setShowDropzone] = React.useState(false); + const [uploading, setUploading] = React.useState(false); + const [newImageId, setNewImageId] = React.useState(null); + + const prevReq = React.useRef(null); + + const withBlockProperties = React.useCallback( + (editor) => { + editor.getBlockProps = () => props; + return editor; + }, + [props], + ); + + const onDrop = React.useCallback( + (files) => { + // TODO: need to fix setUploading, treat uploading indicator + // inteligently, show progress report on uploading files + setUploading(true); + files.forEach((file) => { + const [mime] = file.type.split('/'); + if (mime !== 'image') return; + + readAsDataURL(file).then((data) => { + const fields = data.match(/^data:(.*);(.*),(.*)$/); + uploadContent( + getBaseUrl(pathname), + { + '@type': 'Image', + title: file.name, + image: { + data: fields[3], + encoding: fields[2], + 'content-type': fields[1], + filename: file.name, + }, + }, + block, + ); + }); + }); + setShowDropzone(false); + }, + [pathname, uploadContent, block], + ); + + const { loaded, loading } = uploadRequest; + const imageId = uploadedContent['@id']; + const prevLoaded = prevReq.current; + + React.useEffect(() => { + if (loaded && !loading && !prevLoaded && newImageId !== imageId) { + const url = flattenToAppURL(imageId); + setNewImageId(imageId); + + createImageBlock(url, index, props); + } + prevReq.current = loaded; + }, [props, loaded, loading, prevLoaded, imageId, newImageId, index]); + + const handleUpdate = React.useCallback( + (editor) => { + // defaultSelection is used for things such as "restoring" the selection + // when joining blocks or moving the selection to block start on block + // split + if (defaultSelection) { + const selection = parseDefaultSelection(editor, defaultSelection); + if (selection) { + setTimeout(() => { + Transforms.select(editor, selection); + saveSlateBlockSelection(block, null); + }, 120); + // TODO: use React sync render API + // without setTimeout, the join is not correct. Slate uses internally + // a 100ms throttle, so setting to a bigger value seems to help + } + } + }, + [defaultSelection, block, saveSlateBlockSelection], + ); + + const onEditorChange = (value, editor) => { + ReactDOM.unstable_batchedUpdates(() => { + onChangeBlock(block, { + ...data, + value, + plaintext: serializeNodesToText(value || []), + // TODO: also add html serialized value + }); + deconstructToVoltoBlocks(editor); + }); + }; + + // Get editing instructions from block settings or props + let instructions = data?.instructions?.data || data?.instructions; + if (!instructions || instructions === '


') { + instructions = formDescription; + } + + const intl = useIntl(); + const placeholder = + data.placeholder || formTitle || intl.formatMessage(messages.text); + const schema = TextBlockSchema(data); + + const disableNewBlocks = data?.disableNewBlocks || detached; + const { ref, inView } = useInView({ + threshold: 0, + rootMargin: '0px 0px 200px 0px', + }); + + const handleFocus = React.useCallback(() => { + if (!selected) { + onSelectBlock(block); + } + }, [onSelectBlock, selected, block]); + + return ( +
+ <> + setShowDropzone(true)} + onDragLeave={() => setShowDropzone(false)} + > + {({ getRootProps, getInputProps }) => { + return showDropzone ? ( +
+ {uploading ? ( + + Uploading image + + ) : ( + +
+ +
+
+ )} +
+ ) : ( + <> + onEditorChange(value, editor)} + onKeyDown={handleKey} + selected={selected} + placeholder={placeholder} + /> + {DEBUG ?
{block}
: ''} + + ); + }} +
+ + {selected && !data.plaintext && !disableNewBlocks && ( + { + onSelectBlock(onInsertBlock(id, value)); + }} + onMutateBlock={onMutateBlock} + allowedBlocks={allowedBlocks} + blocksConfig={blocksConfig} + size="24px" + className="block-add-button" + properties={properties} + /> + )} + + +
+ {instructions ? ( + +
+ + ) : ( + <> + + + { + onChangeBlock(block, { + ...data, + [id]: value, + }); + }} + formData={data} + /> + + )} + + +
+ ); +}; + +export default DefaultTextBlockEditor; diff --git a/src/blocks/Text/DetachedTextBlockEditor.jsx b/src/blocks/Text/DetachedTextBlockEditor.jsx new file mode 100644 index 00000000..639a834f --- /dev/null +++ b/src/blocks/Text/DetachedTextBlockEditor.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useInView } from 'react-intersection-observer'; +import { SlateEditor } from 'volto-slate/editor'; +import { serializeNodesToText } from 'volto-slate/editor/render'; +import { handleKeyDetached } from './keyboard'; + +const DEBUG = false; + +const messages = defineMessages({ + text: { + id: 'Type text…', + defaultMessage: 'Type text…', + }, +}); + +export const DetachedTextBlockEditor = (props) => { + const { + data, + index, + properties, + onSelectBlock, + onChangeBlock, + block, + selected, + formTitle, + formDescription, + } = props; + const { value } = data; + + const intl = useIntl(); + const placeholder = + data.placeholder || formTitle || intl.formatMessage(messages.text); + let instructions = data?.instructions?.data || data?.instructions; + if (!instructions || instructions === '


') { + instructions = formDescription; + } + + const { ref, inView } = useInView({ + threshold: 0, + rootMargin: '0px 0px 200px 0px', + }); + + return ( +
+ { + if (!selected) { + onSelectBlock(block); + } + }} + onChange={(value, selection, editor) => { + onChangeBlock(block, { + ...data, + value, + plaintext: serializeNodesToText(value || []), + // TODO: also add html serialized value + }); + }} + selected={selected} + placeholder={placeholder} + onKeyDown={handleKeyDetached} + /> +
+ ); +}; + +export default DetachedTextBlockEditor; diff --git a/src/blocks/Text/TextBlockEdit.jsx b/src/blocks/Text/TextBlockEdit.jsx index 0ea642f5..8890c823 100644 --- a/src/blocks/Text/TextBlockEdit.jsx +++ b/src/blocks/Text/TextBlockEdit.jsx @@ -1,348 +1,14 @@ -import ReactDOM from 'react-dom'; import React from 'react'; -import { connect } from 'react-redux'; -import { readAsDataURL } from 'promise-file-reader'; -import Dropzone from 'react-dropzone'; -import { defineMessages, useIntl } from 'react-intl'; -import { useInView } from 'react-intersection-observer'; -import { Dimmer, Loader, Message, Segment } from 'semantic-ui-react'; - -import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers'; -import config from '@plone/volto/registry'; -import { - InlineForm, - SidebarPortal, - BlockChooserButton, -} from '@plone/volto/components'; -import { saveSlateBlockSelection } from 'volto-slate/actions'; -import { SlateEditor } from 'volto-slate/editor'; -import { serializeNodesToText } from 'volto-slate/editor/render'; -import { - createImageBlock, - parseDefaultSelection, - deconstructToVoltoBlocks, -} from 'volto-slate/utils'; -import { uploadContent } from 'volto-slate/actions'; -import { Transforms } from 'slate'; +import { connect } from 'react-redux'; -import ShortcutListing from './ShortcutListing'; -import MarkdownIntroduction from './MarkdownIntroduction'; -import { handleKey, handleKeyDetached } from './keyboard'; -import TextBlockSchema from './schema'; +import { uploadContent, saveSlateBlockSelection } from 'volto-slate/actions'; -import imageBlockSVG from '@plone/volto/components/manage/Blocks/Image/block-image.svg'; +import DefaultTextBlockEditor from './DefaultTextBlockEditor'; +import DetachedTextBlockEditor from './DetachedTextBlockEditor'; import './css/editor.css'; -// TODO: refactor dropzone to separate component wrapper - -const messages = defineMessages({ - text: { - id: 'Type text…', - defaultMessage: 'Type text…', - }, -}); - -const DEBUG = false; - -export const DefaultTextBlockEditor = (props) => { - const { - block, - blocksConfig, - data, - detached = false, - index, - onChangeBlock, - onInsertBlock, - onMutateBlock, - onSelectBlock, - pathname, - properties, - selected, - uploadRequest, - uploadContent, - uploadedContent, - defaultSelection, - saveSlateBlockSelection, - allowedBlocks, - formTitle, - formDescription, - } = props; - - const { slate } = config.settings; - const { textblockExtensions } = slate; - const { value } = data; - - // const [addNewBlockOpened, setAddNewBlockOpened] = React.useState(); - const [showDropzone, setShowDropzone] = React.useState(false); - const [uploading, setUploading] = React.useState(false); - const [newImageId, setNewImageId] = React.useState(null); - - const prevReq = React.useRef(null); - - const withBlockProperties = React.useCallback( - (editor) => { - editor.getBlockProps = () => props; - return editor; - }, - [props], - ); - - const onDrop = React.useCallback( - (files) => { - // TODO: need to fix setUploading, treat uploading indicator - // inteligently, show progress report on uploading files - setUploading(true); - files.forEach((file) => { - const [mime] = file.type.split('/'); - if (mime !== 'image') return; - - readAsDataURL(file).then((data) => { - const fields = data.match(/^data:(.*);(.*),(.*)$/); - uploadContent( - getBaseUrl(pathname), - { - '@type': 'Image', - title: file.name, - image: { - data: fields[3], - encoding: fields[2], - 'content-type': fields[1], - filename: file.name, - }, - }, - block, - ); - }); - }); - setShowDropzone(false); - }, - [pathname, uploadContent, block], - ); - - const { loaded, loading } = uploadRequest; - const imageId = uploadedContent['@id']; - const prevLoaded = prevReq.current; - - React.useEffect(() => { - if (loaded && !loading && !prevLoaded && newImageId !== imageId) { - const url = flattenToAppURL(imageId); - setNewImageId(imageId); - - createImageBlock(url, index, props); - } - prevReq.current = loaded; - }, [props, loaded, loading, prevLoaded, imageId, newImageId, index]); - - const handleUpdate = React.useCallback( - (editor) => { - // defaultSelection is used for things such as "restoring" the selection - // when joining blocks or moving the selection to block start on block - // split - if (defaultSelection) { - const selection = parseDefaultSelection(editor, defaultSelection); - if (selection) { - setTimeout(() => { - Transforms.select(editor, selection); - saveSlateBlockSelection(block, null); - }, 120); - // TODO: use React sync render API - // without setTimeout, the join is not correct. Slate uses internally - // a 100ms throttle, so setting to a bigger value seems to help - } - } - }, - [defaultSelection, block, saveSlateBlockSelection], - ); - - const onEditorChange = (value, editor) => { - ReactDOM.unstable_batchedUpdates(() => { - onChangeBlock(block, { - ...data, - value, - plaintext: serializeNodesToText(value || []), - // TODO: also add html serialized value - }); - deconstructToVoltoBlocks(editor); - }); - }; - - // Get editing instructions from block settings or props - let instructions = data?.instructions?.data || data?.instructions; - if (!instructions || instructions === '


') { - instructions = formDescription; - } - - const intl = useIntl(); - const placeholder = - data.placeholder || formTitle || intl.formatMessage(messages.text); - const schema = TextBlockSchema(data); - - const disableNewBlocks = data?.disableNewBlocks || detached; - const { ref, inView } = useInView({ - threshold: 0, - rootMargin: '0px 0px 200px 0px', - }); - - const handleFocus = React.useCallback(() => { - if (!selected) { - onSelectBlock(block); - } - }, [onSelectBlock, selected, block]); - - return ( -
- <> - setShowDropzone(true)} - onDragLeave={() => setShowDropzone(false)} - > - {({ getRootProps, getInputProps }) => { - return showDropzone ? ( -
- {uploading ? ( - - Uploading image - - ) : ( - -
- -
-
- )} -
- ) : ( - <> - onEditorChange(value, editor)} - onKeyDown={handleKey} - selected={selected} - placeholder={placeholder} - /> - {DEBUG ?
{block}
: ''} - - ); - }} -
- - {selected && !data.plaintext && !disableNewBlocks && ( - { - onSelectBlock(onInsertBlock(id, value)); - }} - onMutateBlock={onMutateBlock} - allowedBlocks={allowedBlocks} - blocksConfig={blocksConfig} - size="24px" - className="block-add-button" - properties={properties} - /> - )} - - -
- {instructions ? ( - -
- - ) : ( - <> - - - { - onChangeBlock(block, { - ...data, - [id]: value, - }); - }} - formData={data} - /> - - )} - - -
- ); -}; - -export const DetachedTextBlockEditor = (props) => { - const { - data, - index, - properties, - onSelectBlock, - onChangeBlock, - block, - selected, - formTitle, - formDescription, - } = props; - const { value } = data; - - const intl = useIntl(); - const placeholder = - data.placeholder || formTitle || intl.formatMessage(messages.text); - let instructions = data?.instructions?.data || data?.instructions; - if (!instructions || instructions === '


') { - instructions = formDescription; - } - - const { ref, inView } = useInView({ - threshold: 0, - rootMargin: '0px 0px 200px 0px', - }); - - return ( -
- { - if (!selected) { - onSelectBlock(block); - } - }} - onChange={(value, selection, editor) => { - onChangeBlock(block, { - ...data, - value, - plaintext: serializeNodesToText(value || []), - // TODO: also add html serialized value - }); - }} - selected={selected} - placeholder={placeholder} - onKeyDown={handleKeyDetached} - /> -
- ); -}; - const TextBlockEdit = (props) => { return props.detached ? ( // || props.disableNewBlocks diff --git a/src/blocks/Text/keyboard/joinBlocks.js b/src/blocks/Text/keyboard/joinBlocks.js index 66f0a68a..6ffd65a0 100644 --- a/src/blocks/Text/keyboard/joinBlocks.js +++ b/src/blocks/Text/keyboard/joinBlocks.js @@ -17,7 +17,9 @@ import { } from '@plone/volto/helpers'; /** - * Joins the current block with the previous block to make a single block. + * Joins the current block (which has an active Slate Editor) + * with the previous block, to make a single block. + * * @param {Editor} editor * @param {KeyboardEvent} event */ diff --git a/src/blocks/Title/TitleBlockEdit.jsx b/src/blocks/Title/TitleBlockEdit.jsx index fe9a2a9b..9ddf82e5 100644 --- a/src/blocks/Title/TitleBlockEdit.jsx +++ b/src/blocks/Title/TitleBlockEdit.jsx @@ -177,6 +177,8 @@ export const TitleBlockEdit = (props) => { [TitleOrDescription, className], // eslint-disable-line react-hooks/exhaustive-deps ); + editor.children = val; + if (typeof window.__SERVER__ !== 'undefined') { return
; } diff --git a/src/editor/SlateEditor.jsx b/src/editor/SlateEditor.jsx index f9b5eb0e..52e51b87 100644 --- a/src/editor/SlateEditor.jsx +++ b/src/editor/SlateEditor.jsx @@ -1,3 +1,4 @@ +import ReactDOM from 'react-dom'; import cx from 'classnames'; import { isEqual } from 'lodash'; import { Transforms, Editor } from 'slate'; // , Transforms @@ -11,7 +12,12 @@ import config from '@plone/volto/registry'; import { Element, Leaf } from './render'; import withTestingFeatures from './extensions/withTestingFeatures'; -import { makeEditor, toggleInlineFormat, toggleMark } from 'volto-slate/utils'; +import { + makeEditor, + toggleInlineFormat, + toggleMark, + parseDefaultSelection, +} from 'volto-slate/utils'; import { InlineToolbar } from './ui'; import EditorContext from './EditorContext'; @@ -54,11 +60,14 @@ class SlateEditor extends Component { this.savedSelection = null; - const uid = uuid(); + const uid = uuid(); // used to namespace the editor's plugins + + const { slate } = config.settings; this.state = { editor: this.createEditor(uid), showExpandedToolbar: config.settings.slate.showExpandedToolbar, + internalValue: this.props.value || slate.defaultValue(), uid, }; @@ -89,9 +98,12 @@ class SlateEditor extends Component { } handleChange(value) { - if (this.props.onChange && !isEqual(value, this.props.value)) { - this.props.onChange(value, this.editor); - } + ReactDOM.unstable_batchedUpdates(() => { + this.setState({ internalValue: value }); + if (this.props.onChange && !isEqual(value, this.props.value)) { + this.props.onChange(value, this.editor); + } + }); } multiDecorator([node, path]) { @@ -131,18 +143,37 @@ class SlateEditor extends Component { return; } + if ( + this.props.value && + !isEqual(this.props.value, this.state.internalValue) + ) { + const { editor } = this.state; + editor.children = this.props.value; + + if (this.props.defaultSelection) { + const selection = parseDefaultSelection( + editor, + this.props.defaultSelection, + ); + + ReactEditor.focus(editor); + Transforms.select(editor, selection); + } + + this.setState({ + // editor, + internalValue: this.props.value, + }); + return; + } + const { editor } = this.state; - // if the SlateEditor becomes selected from unselected if (!prevProps.selected && this.props.selected) { - // if the SlateEditor is not already selected - // if (!ReactEditor.isFocused(this.state.editor)) { - // || !editor.selection + // if the SlateEditor becomes selected from unselected - // TODO: why is this setTimeout wrapping the code in it? - // setTimeout(() => { - // TODO: why is this condition checked? if (window.getSelection().type === 'None') { + // TODO: why is this condition checked? Transforms.select( this.state.editor, Editor.range(this.state.editor, Editor.start(this.state.editor, [])), @@ -150,13 +181,8 @@ class SlateEditor extends Component { } ReactEditor.focus(this.state.editor); - // }, 100); // flush - // } } - // if (this.props.selected && this.editor && this.editor.selection) { - // this.editor.setSavedSelection(this.editor.selection); - if (this.props.selected && this.props.onUpdate) { this.props.onUpdate(editor); } @@ -175,7 +201,6 @@ class SlateEditor extends Component { render() { const { selected, - value, placeholder, onKeyDown, testingEditorRef, @@ -212,7 +237,7 @@ class SlateEditor extends Component { {selected ? ( diff --git a/src/editor/ui/InlineToolbar.jsx b/src/editor/ui/InlineToolbar.jsx index cef0d91d..c2e5012d 100644 --- a/src/editor/ui/InlineToolbar.jsx +++ b/src/editor/ui/InlineToolbar.jsx @@ -22,7 +22,12 @@ const InlineToolbar = (props) => { ); React.useEffect(() => { - const el = ReactEditor.toDOMNode(editor, editor); + let el; + try { + el = ReactEditor.toDOMNode(editor, editor); + } catch { + return; + } const toggleToolbar = () => { const selection = window.getSelection(); const { activeElement } = window.document; diff --git a/src/utils/volto-blocks.js b/src/utils/volto-blocks.js index 1a3f239d..1b7b9e6b 100644 --- a/src/utils/volto-blocks.js +++ b/src/utils/volto-blocks.js @@ -6,7 +6,7 @@ import { getBlocksFieldname, getBlocksLayoutFieldname, } from '@plone/volto/helpers'; -import { Transforms, Editor, Node, Text } from 'slate'; +import { Transforms, Editor, Node, Text, Path } from 'slate'; import { serializeNodesToText } from 'volto-slate/editor/render'; import { omit } from 'lodash'; import config from '@plone/volto/registry'; @@ -31,26 +31,37 @@ export function mergeSlateWithBlockBackward(editor, prevBlock, event) { // collapse the selection to its start point Transforms.collapse(editor, { edge: 'start' }); - Transforms.insertNodes(editor, prev, { - at: Editor.start(editor, []), - }); + let rangeRef; + let end; - const rangeRef = Editor.rangeRef(editor, { - anchor: Editor.start(editor, [1]), - focus: Editor.end(editor, [1]), - }); + Editor.withoutNormalizing(editor, () => { + // insert block #0 contents in block #1 contents, at the beginning + Transforms.insertNodes(editor, prev, { + at: Editor.start(editor, []), + }); - const source = rangeRef.current; + // the contents that should be moved into the `ul`, as the last `li` + rangeRef = Editor.rangeRef(editor, { + anchor: Editor.start(editor, [1]), + focus: Editor.end(editor, [1]), + }); - const end = Editor.end(editor, [0]); + const source = rangeRef.current; - let endPoint; + end = Editor.end(editor, [0]); + + let endPoint; + + Transforms.insertNodes(editor, { text: '' }, { at: end }); + + end = Editor.end(editor, [0]); - Editor.withoutNormalizing(editor, () => { Transforms.splitNodes(editor, { at: end, always: true, - match: (n) => Text.isText(n), + height: 1, + mode: 'highest', + match: (n) => n.type === 'li' || Text.isText(n), }); endPoint = Editor.end(editor, [0]); @@ -71,6 +82,10 @@ export function mergeSlateWithBlockBackward(editor, prevBlock, event) { rangeRef.unref(); + const [, lastPath] = Editor.last(editor, [0]); + + end = Editor.start(editor, Path.parent(lastPath)); + return end; }