diff --git a/tinymce-per-block/src/assets/stylesheets/main.scss b/tinymce-per-block/src/assets/stylesheets/main.scss
index 9bc8e97957e6d..d45f9dc9fab7a 100644
--- a/tinymce-per-block/src/assets/stylesheets/main.scss
+++ b/tinymce-per-block/src/assets/stylesheets/main.scss
@@ -11,6 +11,7 @@
@import '~renderers/block/block-list/style';
@import '~renderers/html/html-editor/style';
@import '~controls/editable-format-toolbar/style';
+@import '~controls/transform-block-toolbar/style';
@import '~inserter/style';
* {
diff --git a/tinymce-per-block/src/blocks/embed-block/form.js b/tinymce-per-block/src/blocks/embed-block/form.js
index 39ccf42e226de..dfd3b66ef55b4 100644
--- a/tinymce-per-block/src/blocks/embed-block/form.js
+++ b/tinymce-per-block/src/blocks/embed-block/form.js
@@ -24,7 +24,7 @@ export default class EmbedBlockForm extends Component {
render() {
const { block, isSelected, change, moveCursorUp, moveCursorDown,
- remove, focusConfig, focus, moveBlockUp, moveBlockDown, appendBlock } = this.props;
+ remove, focusConfig, focus, moveBlockUp, moveBlockDown, appendBlock, unselect } = this.props;
const removePrevious = () => {
if ( ! block.url ) {
@@ -81,7 +81,10 @@ export default class EmbedBlockForm extends Component {
moveCursorDown={ moveCursorDown }
splitValue={ splitValue }
value={ block.caption }
- onChange={ ( value ) => change( { caption: value } ) }
+ onChange={ ( value ) => {
+ change( { caption: value } );
+ unselect();
+ } }
placeholder="Write caption"
focusConfig={ focusConfig }
onFocusChange={ focus }
diff --git a/tinymce-per-block/src/blocks/heading-block/form.js b/tinymce-per-block/src/blocks/heading-block/form.js
index 7d59ef8dcb507..15757032b8956 100644
--- a/tinymce-per-block/src/blocks/heading-block/form.js
+++ b/tinymce-per-block/src/blocks/heading-block/form.js
@@ -12,6 +12,7 @@ import {
import InlineTextBlockForm from '../inline-text-block/form';
import EditableFormatToolbar from 'controls/editable-format-toolbar';
import BlockArrangement from 'controls/block-arrangement';
+import TransformBlockToolbar from 'controls/transform-block-toolbar';
export default class HeadingBlockForm extends Component {
bindForm = ( ref ) => {
@@ -32,7 +33,7 @@ export default class HeadingBlockForm extends Component {
};
render() {
- const { block, isSelected, moveBlockUp, moveBlockDown } = this.props;
+ const { block, isSelected, moveBlockUp, moveBlockDown, select, transform } = this.props;
const sizes = [
{ id: 'h1', icon: EditorHeading1Icon },
{ id: 'h2', icon: EditorHeading2Icon },
@@ -45,6 +46,9 @@ export default class HeadingBlockForm extends Component {
moveBlockUp={ moveBlockUp } moveBlockDown={ moveBlockDown } /> }
{ isSelected && (
+
+
+
{ sizes.map( ( { id, icon: Icon } ) =>
) }
-
+
{
+ return {
+ blockType: 'heading',
+ size: 'h2',
+ content
+ };
+};
+
registerBlock( 'heading', {
title: 'Heading',
form: form,
@@ -40,11 +48,11 @@ registerBlock( 'heading', {
rawContent
};
},
- create: () => {
- return {
- blockType: 'heading',
- content: '',
- size: 'h2'
- };
- }
+ create: createHeadingBlockWithContent,
+ transformations: [
+ {
+ blocks: [ 'text', 'quote' ],
+ transform: ( block ) => createHeadingBlockWithContent( block.content )
+ }
+ ]
} );
diff --git a/tinymce-per-block/src/blocks/html-block/form.js b/tinymce-per-block/src/blocks/html-block/form.js
index 9ca616f7a0282..0fcfc3b7ec80e 100644
--- a/tinymce-per-block/src/blocks/html-block/form.js
+++ b/tinymce-per-block/src/blocks/html-block/form.js
@@ -40,7 +40,7 @@ export default class HtmlBlockForm extends Component {
render() {
const { block, isSelected, change, moveCursorUp, moveCursorDown, appendBlock,
- mergeWithPrevious, remove, focusConfig, focus, moveBlockUp, moveBlockDown } = this.props;
+ mergeWithPrevious, remove, focusConfig, focus, moveBlockUp, moveBlockDown, select, unselect } = this.props;
const splitValue = ( left, right ) => {
change( { content: left } );
if ( right ) {
@@ -72,7 +72,7 @@ export default class HtmlBlockForm extends Component {
) }
-
+
change( { content: value } ) }
focusConfig={ focusConfig }
onFocusChange={ focus }
+ onType={ unselect }
/>
diff --git a/tinymce-per-block/src/blocks/image-block/form.js b/tinymce-per-block/src/blocks/image-block/form.js
index ae6d1389bac47..d756194df5a38 100644
--- a/tinymce-per-block/src/blocks/image-block/form.js
+++ b/tinymce-per-block/src/blocks/image-block/form.js
@@ -20,7 +20,7 @@ export default class ImageBlockForm extends Component {
render() {
const { block, change, moveCursorDown, moveCursorUp, remove, appendBlock,
- isSelected, focusConfig, focus, moveBlockUp, moveBlockDown } = this.props;
+ isSelected, focusConfig, focus, moveBlockUp, moveBlockDown, select, unselect } = this.props;
const removePrevious = () => {
if ( ! block.caption ) {
remove();
@@ -45,25 +45,30 @@ export default class ImageBlockForm extends Component {
}
- {
- ! focusConfig && focus();
- } }
- />
-
-
change( { caption: value } ) }
- placeholder="Write caption"
- focusConfig={ focusConfig }
- onFocusChange={ focus }
+
+
{
+ ! focusConfig && focus();
+ } }
/>
+
+ {
+ change( { caption: value } );
+ unselect();
+ } }
+ placeholder="Write caption"
+ focusConfig={ focusConfig }
+ onFocusChange={ focus }
+ />
+
);
diff --git a/tinymce-per-block/src/blocks/inline-text-block/form.js b/tinymce-per-block/src/blocks/inline-text-block/form.js
index cecb549288363..fe3bb430b4d44 100644
--- a/tinymce-per-block/src/blocks/inline-text-block/form.js
+++ b/tinymce-per-block/src/blocks/inline-text-block/form.js
@@ -37,7 +37,7 @@ export default class InlineTextBlockForm extends Component {
render() {
const { block, change, moveCursorUp, moveCursorDown, appendBlock,
- mergeWithPrevious, remove, setToolbarState, focus, focusConfig } = this.props;
+ mergeWithPrevious, remove, setToolbarState, focus, focusConfig, unselect } = this.props;
const splitValue = ( left, right ) => {
change( { content: left } );
@@ -65,6 +65,7 @@ export default class InlineTextBlockForm extends Component {
setToolbarState={ setToolbarState }
focusConfig={ focusConfig }
onFocusChange={ focus }
+ onType={ unselect }
inline
single
/>
diff --git a/tinymce-per-block/src/blocks/quote-block/form.js b/tinymce-per-block/src/blocks/quote-block/form.js
index bc53bfec94a0c..0f7ecfcf2b3cc 100644
--- a/tinymce-per-block/src/blocks/quote-block/form.js
+++ b/tinymce-per-block/src/blocks/quote-block/form.js
@@ -3,11 +3,13 @@
*/
import { createElement, Component } from 'wp-elements';
-import { EditableComponent, EnhancedInputComponent } from 'wp-blocks';
-import { serialize } from 'serializers/block';
-import { parse } from 'parsers/block';
+/**
+ * Internal dependencies
+ */
+import { EditableComponent } from 'wp-blocks';
import EditableFormatToolbar from 'controls/editable-format-toolbar';
import BlockArrangement from 'controls/block-arrangement';
+import TransformBlockToolbar from 'controls/transform-block-toolbar';
export default class QuoteBlockForm extends Component {
bindContent = ( ref ) => {
@@ -59,7 +61,7 @@ export default class QuoteBlockForm extends Component {
render() {
const { block, change, moveCursorUp, moveCursorDown, remove,
mergeWithPrevious, appendBlock, isSelected, focusConfig, focus,
- moveBlockUp, moveBlockDown } = this.props;
+ moveBlockUp, moveBlockDown, select, unselect, transform } = this.props;
const splitValue = ( left, right ) => {
change( { cite: left } );
appendBlock( {
@@ -78,13 +80,17 @@ export default class QuoteBlockForm extends Component {
moveBlockUp={ moveBlockUp } moveBlockDown={ moveBlockDown } /> }
{ isSelected &&
}
-
+
focus( Object.assign( { input: 'content' }, config ) ) }
+ onType={ unselect }
inline
/>
@@ -113,6 +120,7 @@ export default class QuoteBlockForm extends Component {
setToolbarState={ focusInput === 'cite' ? this.setToolbarState : undefined }
focusConfig={ focusInput === 'cite' ? focusConfig : null }
onFocusChange={ ( config ) => focus( Object.assign( { input: 'cite' }, config ) ) }
+ onType={ unselect }
inline
single
/>
diff --git a/tinymce-per-block/src/blocks/quote-block/index.js b/tinymce-per-block/src/blocks/quote-block/index.js
index 1935ed58da72a..ef2812c77c458 100644
--- a/tinymce-per-block/src/blocks/quote-block/index.js
+++ b/tinymce-per-block/src/blocks/quote-block/index.js
@@ -11,6 +11,14 @@ import {
*/
import form from './form';
+const createQuoteBlockWithContent = ( content = '' ) => {
+ return {
+ blockType: 'quote',
+ cite: '',
+ content
+ };
+};
+
registerBlock( 'quote', {
title: 'Quote',
form: form,
@@ -55,11 +63,11 @@ registerBlock( 'quote', {
rawContent
};
},
- create: () => {
- return {
- blockType: 'quote',
- cite: '',
- content: ''
- };
- }
+ create: createQuoteBlockWithContent,
+ transformations: [
+ {
+ blocks: [ 'text', 'heading' ],
+ transform: ( block ) => createQuoteBlockWithContent( block.content )
+ }
+ ]
} );
diff --git a/tinymce-per-block/src/blocks/text-block/form.js b/tinymce-per-block/src/blocks/text-block/form.js
index b5b3f288257c8..007c56d273ee3 100644
--- a/tinymce-per-block/src/blocks/text-block/form.js
+++ b/tinymce-per-block/src/blocks/text-block/form.js
@@ -6,6 +6,7 @@ import { createElement, Component } from 'wp-elements';
import EditableFormatToolbar from 'controls/editable-format-toolbar';
import AlignmentToolbar from 'controls/alignment-toolbar';
import BlockArrangement from 'controls/block-arrangement';
+import TransformBlockToolbar from 'controls/transform-block-toolbar';
import InlineTextBlockForm from 'blocks/inline-text-block/form';
import InserterButton from 'inserter/button';
@@ -28,7 +29,7 @@ export default class TextBlockForm extends Component {
};
render() {
- const { block, isSelected, focusConfig, moveBlockUp, moveBlockDown, replace } = this.props;
+ const { block, isSelected, focusConfig, moveBlockUp, moveBlockDown, replace, select, transform } = this.props;
const selectedTextAlign = block.align || 'left';
const style = {
textAlign: selectedTextAlign
@@ -40,6 +41,10 @@ export default class TextBlockForm extends Component {
moveBlockUp={ moveBlockUp } moveBlockDown={ moveBlockDown } /> }
{ isSelected &&
+
+
+
+
@@ -50,7 +55,7 @@ export default class TextBlockForm extends Component {
}
-
+
{ ! block.content.trim() && ! isSelected && focusConfig &&
replace( id ) } />
}
diff --git a/tinymce-per-block/src/blocks/text-block/index.js b/tinymce-per-block/src/blocks/text-block/index.js
index 147d437bfbd30..82a41f93c8c6b 100644
--- a/tinymce-per-block/src/blocks/text-block/index.js
+++ b/tinymce-per-block/src/blocks/text-block/index.js
@@ -9,6 +9,14 @@ import { EditorParagraphIcon } from 'dashicons';
*/
import form from './form';
+const createTextBlockWithContent = ( content = '' ) => {
+ return {
+ blockType: 'text',
+ align: 'no-align',
+ content
+ };
+};
+
registerBlock( 'text', {
title: 'Text',
form: form,
@@ -44,11 +52,11 @@ registerBlock( 'text', {
rawContent
};
},
- create: () => {
- return {
- blockType: 'text',
- content: '',
- align: 'no-align'
- };
- }
+ create: () => createTextBlockWithContent,
+ transformations: [
+ {
+ blocks: [ 'heading', 'quote' ],
+ transform: ( block ) => createTextBlockWithContent( block.content )
+ }
+ ]
} );
diff --git a/tinymce-per-block/src/controls/block-arrangement.js b/tinymce-per-block/src/controls/block-arrangement.js
index 84c8afe3f8903..d53067c205844 100644
--- a/tinymce-per-block/src/controls/block-arrangement.js
+++ b/tinymce-per-block/src/controls/block-arrangement.js
@@ -8,22 +8,14 @@ import { ArrowDownAlt2Icon, ArrowUpAlt2Icon } from 'dashicons';
export default function BlockArrangement( { block, moveBlockUp, moveBlockDown } ) {
const blockDefinition = getBlock( block.blockType );
const Icon = blockDefinition.icon;
- const onMoveUp = ( event ) => {
- event.stopPropagation();
- moveBlockUp();
- };
- const onMoveDown = ( event ) => {
- event.stopPropagation();
- moveBlockDown();
- };
return (
diff --git a/tinymce-per-block/src/controls/editable-format-toolbar/index.js b/tinymce-per-block/src/controls/editable-format-toolbar/index.js
index 4d6a56c2741d6..3e10f88a8148f 100644
--- a/tinymce-per-block/src/controls/editable-format-toolbar/index.js
+++ b/tinymce-per-block/src/controls/editable-format-toolbar/index.js
@@ -51,8 +51,7 @@ export default class EditableFormatToolbar extends Component {
} );
}
- toggleLinkModal = ( event ) => {
- event.stopPropagation();
+ toggleLinkModal = () => {
this.setState( {
linkModal: {
open: ! this.state.linkModal.open,
diff --git a/tinymce-per-block/src/controls/transform-block-toolbar/index.js b/tinymce-per-block/src/controls/transform-block-toolbar/index.js
new file mode 100644
index 0000000000000..873df1cb6b0dc
--- /dev/null
+++ b/tinymce-per-block/src/controls/transform-block-toolbar/index.js
@@ -0,0 +1,53 @@
+/**
+ * External dependencies
+ */
+import { createElement, Component } from 'wp-elements';
+import { getBlock, getBlocks } from 'wp-blocks';
+import { reduce } from 'lodash';
+
+export default class TransformBlockToolbar extends Component {
+ state = {
+ open: false
+ };
+
+ toggleMenu = () => {
+ this.setState( {
+ open: ! this.state.open
+ } );
+ };
+
+ render() {
+ const blockDefinition = getBlock( this.props.blockType );
+ const allowedBlocks = reduce( getBlocks(), ( memo, block ) => {
+ const transformation = block.transformations &&
+ block.transformations.find( t => t.blocks.indexOf( this.props.blockType ) !== -1 );
+ return transformation ? memo.concat( [ block ] ) : memo;
+ }, [] );
+ if ( ! allowedBlocks.length ) {
+ return null;
+ }
+ const BlockIcon = blockDefinition.icon;
+
+ return (
+
+
+ { this.state.open &&
+
+ { allowedBlocks.map( ( { id, title, icon: Icon } ) => (
+
this.props.onTransform( id ) }
+ className="transform-block-toolbar__menu-item"
+ >
+ { title }
+
+ ) ) }
+
+ }
+
+ );
+ }
+}
diff --git a/tinymce-per-block/src/controls/transform-block-toolbar/style.scss b/tinymce-per-block/src/controls/transform-block-toolbar/style.scss
new file mode 100644
index 0000000000000..0db6481f0d5c5
--- /dev/null
+++ b/tinymce-per-block/src/controls/transform-block-toolbar/style.scss
@@ -0,0 +1,71 @@
+.transform-block-toolbar .block-list__block-control {
+ width: auto;
+
+ .dashicon {
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .transform-block-toolbar__arrow {
+ display: inline-block;
+ vertical-align: middle;
+ border: 6px dashed $gray-dark-900;
+ height: 0;
+ line-height: 0;
+ width: 0;
+ z-index: 1;
+ border-top-style: solid;
+ border-bottom: none;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ }
+}
+
+.transform-block-toolbar__menu {
+ position: absolute;
+ top: 50px;
+ box-shadow: 0px 3px 20px rgba( 18, 24, 30, .1 ), 0px 1px 3px rgba( 18, 24, 30, .1 );
+ border: 1px solid #e0e5e9;
+ background: #fff;
+ z-index: 1;
+
+ input {
+ font-size: 13px;
+ }
+
+ &:before {
+ content: '';
+ border: 10px dashed #e0e5e9;
+ height: 0;
+ line-height: 0;
+ position: absolute;
+ width: 0;
+ top: -10px;
+ left: 10px;
+ border-bottom-style: solid;
+ border-top: none;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ }
+
+
+ .transform-block-toolbar__menu-item {
+ padding: 8px;
+ font-size: 12px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+
+ &.is-active,
+ &:hover {
+ background: #f0f2f4;
+ }
+
+ .dashicon {
+ margin-right: 8px;
+ fill: #191e23;
+ width: 24px;
+ height: 24px;
+ }
+ }
+}
diff --git a/tinymce-per-block/src/external/dashicons/icons/image-full-width.js b/tinymce-per-block/src/external/dashicons/icons/image-full-width.js
index 31b22d7843726..064652427a33f 100644
--- a/tinymce-per-block/src/external/dashicons/icons/image-full-width.js
+++ b/tinymce-per-block/src/external/dashicons/icons/image-full-width.js
@@ -5,7 +5,7 @@ import { createElement } from 'wp-elements';
// This is a gridicon
export default () => (
-