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;
}