From 9adeaf7f6612afb25e318d2365db2357a326e3cc Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 28 Jul 2017 10:51:45 +0100 Subject: [PATCH 1/3] Framework: Extract the block editor from the editor chrome to make it reusable --- blocks/api/factory.js | 40 +- blocks/api/parser.js | 42 +- blocks/api/paste.js | 10 +- blocks/api/serializer.js | 20 +- blocks/editable/index.js | 4 +- blocks/library/heading/index.js | 7 +- blocks/library/text/index.js | 10 +- {editor => editor-chrome}/README.md | 0 editor-chrome/actions.js | 87 ++++ .../assets/stylesheets/_animations.scss | 0 .../assets/stylesheets/_mixins.scss | 0 .../assets/stylesheets/_variables.scss | 0 .../assets/stylesheets/_z-index.scss | 0 .../assets/stylesheets/main.scss | 0 .../document-title/index.js | 0 {editor => editor-chrome}/effects.js | 48 --- editor-chrome/header/index.js | 25 ++ .../header/mode-switcher/index.js | 0 .../header/mode-switcher/style.scss | 0 .../header/saved-state/index.js | 0 .../header/saved-state/style.scss | 0 .../header/saved-state/test/index.js | 0 {editor => editor-chrome}/header/style.scss | 15 - .../header/tools/index.js | 2 +- .../header/tools/preview-button.js | 0 .../header/tools/publish-button.js | 0 .../header/tools/style.scss | 0 editor-chrome/index.js | 100 +++++ {editor => editor-chrome}/layout/index.js | 33 +- {editor => editor-chrome}/layout/style.scss | 0 editor-chrome/modes/text-editor/index.js | 42 ++ .../modes/text-editor/style.scss | 0 editor-chrome/modes/visual-editor/index.js | 26 ++ editor-chrome/modes/visual-editor/style.scss | 20 + .../post-permalink/index.js | 0 .../post-permalink/style.scss | 0 {editor => editor-chrome}/post-title/index.js | 7 +- .../post-title/style.scss | 0 editor-chrome/selectors.js | 381 ++++++++++++++++++ .../sidebar/discussion-panel/index.js | 0 .../sidebar/featured-image/index.js | 0 .../sidebar/featured-image/style.scss | 0 {editor => editor-chrome}/sidebar/header.js | 2 +- {editor => editor-chrome}/sidebar/index.js | 2 +- .../sidebar/last-revision/index.js | 0 .../sidebar/last-revision/style.scss | 0 .../sidebar/page-attributes/index.js | 0 .../sidebar/page-attributes/test/index.js | 0 .../sidebar/post-excerpt/index.js | 0 .../sidebar/post-excerpt/style.scss | 0 .../sidebar/post-schedule/clock.js | 0 .../sidebar/post-schedule/index.js | 0 .../sidebar/post-schedule/style.scss | 0 .../sidebar/post-settings/index.js | 2 +- .../sidebar/post-settings/style.scss | 0 .../sidebar/post-status/index.js | 2 +- .../sidebar/post-sticky/index.js | 0 .../post-taxonomies/categories-selector.js | 0 .../sidebar/post-taxonomies/index.js | 0 .../sidebar/post-taxonomies/style.scss | 0 .../sidebar/post-taxonomies/tags-selector.js | 0 .../sidebar/post-trash/index.js | 0 .../sidebar/post-trash/style.scss | 0 .../sidebar/post-visibility/index.js | 0 .../sidebar/post-visibility/style.scss | 0 {editor => editor-chrome}/sidebar/style.scss | 0 editor-chrome/state.js | 217 ++++++++++ {editor => editor-chrome}/test/actions.js | 0 {editor => editor-chrome}/test/effects.js | 0 {editor => editor-chrome}/test/selectors.js | 0 {editor => editor-chrome}/test/state.js | 0 .../unsaved-changes-warning/index.js | 0 .../utils/test/undoable-reducer.js | 0 {editor => editor-chrome}/utils/test/url.js | 0 .../utils/undoable-reducer.js | 0 {editor => editor-chrome}/utils/url.js | 0 editor/actions.js | 139 +++---- .../block-inspector/class-name.js | 8 +- editor/{sidebar => }/block-inspector/index.js | 7 +- .../{sidebar => }/block-inspector/style.scss | 0 editor/header/index.js | 72 ---- editor/index.js | 101 +---- editor/inserter/full.js | 88 ++++ editor/inserter/index.js | 19 +- editor/inserter/menu.js | 30 +- editor/inserter/style.scss | 33 ++ editor/modes/text-editor/index.js | 117 ------ .../{visual-editor => visual}/block-list.js | 61 +-- .../{ => modes/visual}/block-mover/index.js | 17 +- .../visual}/block-mover/mover-label.js | 0 .../{ => modes/visual}/block-mover/style.scss | 0 .../visual}/block-mover/test/mover-label.js | 0 .../visual}/block-settings-menu/index.js | 21 +- .../visual}/block-settings-menu/style.scss | 0 .../visual}/block-switcher/index.js | 29 +- .../visual}/block-switcher/style.scss | 0 .../modes/{visual-editor => visual}/block.js | 42 +- .../modes/{visual-editor => visual}/index.js | 9 +- .../invalid-block-warning.js | 0 .../{visual-editor => visual}/style.scss | 47 --- editor/multi-selection-header/index.js | 67 +++ editor/multi-selection-header/style.scss | 14 + editor/provider/index.js | 66 +++ editor/selectors.js | 362 ++--------------- editor/state.js | 203 +--------- .../{sidebar => }/table-of-contents/index.js | 7 +- .../table-of-contents/style.scss | 0 lib/client-assets.php | 38 +- webpack.config.js | 3 +- 109 files changed, 1499 insertions(+), 1245 deletions(-) rename {editor => editor-chrome}/README.md (100%) create mode 100644 editor-chrome/actions.js rename {editor => editor-chrome}/assets/stylesheets/_animations.scss (100%) rename {editor => editor-chrome}/assets/stylesheets/_mixins.scss (100%) rename {editor => editor-chrome}/assets/stylesheets/_variables.scss (100%) rename {editor => editor-chrome}/assets/stylesheets/_z-index.scss (100%) rename {editor => editor-chrome}/assets/stylesheets/main.scss (100%) rename {editor => editor-chrome}/document-title/index.js (100%) rename {editor => editor-chrome}/effects.js (81%) create mode 100644 editor-chrome/header/index.js rename {editor => editor-chrome}/header/mode-switcher/index.js (100%) rename {editor => editor-chrome}/header/mode-switcher/style.scss (100%) rename {editor => editor-chrome}/header/saved-state/index.js (100%) rename {editor => editor-chrome}/header/saved-state/style.scss (100%) rename {editor => editor-chrome}/header/saved-state/test/index.js (100%) rename {editor => editor-chrome}/header/style.scss (83%) rename {editor => editor-chrome}/header/tools/index.js (97%) rename {editor => editor-chrome}/header/tools/preview-button.js (100%) rename {editor => editor-chrome}/header/tools/publish-button.js (100%) rename {editor => editor-chrome}/header/tools/style.scss (100%) create mode 100644 editor-chrome/index.js rename {editor => editor-chrome}/layout/index.js (51%) rename {editor => editor-chrome}/layout/style.scss (100%) create mode 100644 editor-chrome/modes/text-editor/index.js rename {editor => editor-chrome}/modes/text-editor/style.scss (100%) create mode 100644 editor-chrome/modes/visual-editor/index.js create mode 100644 editor-chrome/modes/visual-editor/style.scss rename {editor => editor-chrome}/post-permalink/index.js (100%) rename {editor => editor-chrome}/post-permalink/style.scss (100%) rename {editor => editor-chrome}/post-title/index.js (94%) rename {editor => editor-chrome}/post-title/style.scss (100%) create mode 100644 editor-chrome/selectors.js rename {editor => editor-chrome}/sidebar/discussion-panel/index.js (100%) rename {editor => editor-chrome}/sidebar/featured-image/index.js (100%) rename {editor => editor-chrome}/sidebar/featured-image/style.scss (100%) rename {editor => editor-chrome}/sidebar/header.js (95%) rename {editor => editor-chrome}/sidebar/index.js (93%) rename {editor => editor-chrome}/sidebar/last-revision/index.js (100%) rename {editor => editor-chrome}/sidebar/last-revision/style.scss (100%) rename {editor => editor-chrome}/sidebar/page-attributes/index.js (100%) rename {editor => editor-chrome}/sidebar/page-attributes/test/index.js (100%) rename {editor => editor-chrome}/sidebar/post-excerpt/index.js (100%) rename {editor => editor-chrome}/sidebar/post-excerpt/style.scss (100%) rename {editor => editor-chrome}/sidebar/post-schedule/clock.js (100%) rename {editor => editor-chrome}/sidebar/post-schedule/index.js (100%) rename {editor => editor-chrome}/sidebar/post-schedule/style.scss (100%) rename {editor => editor-chrome}/sidebar/post-settings/index.js (92%) rename {editor => editor-chrome}/sidebar/post-settings/style.scss (100%) rename {editor => editor-chrome}/sidebar/post-status/index.js (96%) rename {editor => editor-chrome}/sidebar/post-sticky/index.js (100%) rename {editor => editor-chrome}/sidebar/post-taxonomies/categories-selector.js (100%) rename {editor => editor-chrome}/sidebar/post-taxonomies/index.js (100%) rename {editor => editor-chrome}/sidebar/post-taxonomies/style.scss (100%) rename {editor => editor-chrome}/sidebar/post-taxonomies/tags-selector.js (100%) rename {editor => editor-chrome}/sidebar/post-trash/index.js (100%) rename {editor => editor-chrome}/sidebar/post-trash/style.scss (100%) rename {editor => editor-chrome}/sidebar/post-visibility/index.js (100%) rename {editor => editor-chrome}/sidebar/post-visibility/style.scss (100%) rename {editor => editor-chrome}/sidebar/style.scss (100%) create mode 100644 editor-chrome/state.js rename {editor => editor-chrome}/test/actions.js (100%) rename {editor => editor-chrome}/test/effects.js (100%) rename {editor => editor-chrome}/test/selectors.js (100%) rename {editor => editor-chrome}/test/state.js (100%) rename {editor => editor-chrome}/unsaved-changes-warning/index.js (100%) rename {editor => editor-chrome}/utils/test/undoable-reducer.js (100%) rename {editor => editor-chrome}/utils/test/url.js (100%) rename {editor => editor-chrome}/utils/undoable-reducer.js (100%) rename {editor => editor-chrome}/utils/url.js (100%) rename editor/{sidebar => }/block-inspector/class-name.js (88%) rename editor/{sidebar => }/block-inspector/index.js (88%) rename editor/{sidebar => }/block-inspector/style.scss (100%) delete mode 100644 editor/header/index.js create mode 100644 editor/inserter/full.js delete mode 100644 editor/modes/text-editor/index.js rename editor/modes/{visual-editor => visual}/block-list.js (82%) rename editor/{ => modes/visual}/block-mover/index.js (88%) rename editor/{ => modes/visual}/block-mover/mover-label.js (100%) rename editor/{ => modes/visual}/block-mover/style.scss (100%) rename editor/{ => modes/visual}/block-mover/test/mover-label.js (100%) rename editor/{ => modes/visual}/block-settings-menu/index.js (67%) rename editor/{ => modes/visual}/block-settings-menu/style.scss (100%) rename editor/{ => modes/visual}/block-switcher/index.js (78%) rename editor/{ => modes/visual}/block-switcher/style.scss (100%) rename editor/modes/{visual-editor => visual}/block.js (93%) rename editor/modes/{visual-editor => visual}/index.js (93%) rename editor/modes/{visual-editor => visual}/invalid-block-warning.js (100%) rename editor/modes/{visual-editor => visual}/style.scss (91%) create mode 100644 editor/multi-selection-header/index.js create mode 100644 editor/multi-selection-header/style.scss create mode 100644 editor/provider/index.js rename editor/{sidebar => }/table-of-contents/index.js (93%) rename editor/{sidebar => }/table-of-contents/style.scss (100%) diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 0244967f7bfe51..d6a3e5b0d7ba11 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -4,33 +4,22 @@ import uuid from 'uuid/v4'; import { get, castArray, findIndex, isObjectLike, find } from 'lodash'; -/** - * Internal dependencies - */ -import { getBlockType } from './registration'; - /** * Returns a block object given its type and attributes. * - * @param {String} name Block name + * @param {String} blockType Block type * @param {Object} attributes Block attributes * @return {Object} Block object */ -export function createBlock( name, attributes = {} ) { - // Get the type definition associated with a registered block. - const blockType = getBlockType( name ); - +export function createBlock( blockType, attributes = {} ) { // Do we need this? What purpose does it have? - let defaultAttributes; - if ( blockType ) { - defaultAttributes = blockType.defaultAttributes; - } + const defaultAttributes = blockType.defaultAttributes; // Blocks are stored with a unique ID, the assigned type name, // and the block attributes. return { uid: uuid(), - name, + name: blockType.name, isValid: true, attributes: { ...defaultAttributes, @@ -42,19 +31,18 @@ export function createBlock( name, attributes = {} ) { /** * Switch a block into one or more blocks of the new block type. * - * @param {Object} block Block object - * @param {string} name Block name - * @return {Array} Block object + * @param {Object} block Block object + * @param {string} sourceType Source Block type + * @param {string} destinationType Destination Block type + * @return {Array} Block object */ -export function switchToBlockType( block, name ) { +export function switchToBlockType( block, sourceType, destinationType ) { // Find the right transformation by giving priority to the "to" // transformation. - const destinationType = getBlockType( name ); - const sourceType = getBlockType( block.name ); const transformationsFrom = get( destinationType, 'transforms.from', [] ); const transformationsTo = get( sourceType, 'transforms.to', [] ); const transformation = - find( transformationsTo, t => t.blocks.indexOf( name ) !== -1 ) || + find( transformationsTo, t => t.blocks.indexOf( destinationType.name ) !== -1 ) || find( transformationsFrom, t => t.blocks.indexOf( block.name ) !== -1 ); // Stop if there is no valid transformation. (How did we get here?) @@ -74,13 +62,7 @@ export function switchToBlockType( block, name ) { // with an array instead. transformationResults = castArray( transformationResults ); - // Ensure that every block object returned by the transformation has a - // valid block type. - if ( transformationResults.some( ( result ) => ! getBlockType( result.name ) ) ) { - return null; - } - - const firstSwitchedBlock = findIndex( transformationResults, ( result ) => result.name === name ); + const firstSwitchedBlock = findIndex( transformationResults, ( result ) => result.name === destinationType.name ); // Ensure that at least one block object returned by the transformation has // the expected "destination" block type. diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 63af6229e39912..18deea99e32397 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -2,13 +2,12 @@ * External dependencies */ import { parse as hpqParse } from 'hpq'; -import { pickBy } from 'lodash'; +import { pickBy, find } from 'lodash'; /** * Internal dependencies */ import { parse as grammarParse } from './post.pegjs'; -import { getBlockType, getUnknownTypeHandler } from './registration'; import { createBlock } from './factory'; import { getBeautifulContent, getSaveContent } from './serializer'; @@ -63,31 +62,23 @@ export function getBlockAttributes( blockType, rawContent, attributes ) { /** * Creates a block with fallback to the unknown type handler. * - * @param {?String} name Block type name - * @param {String} rawContent Raw block content - * @param {?Object} attributes Attributes obtained from block delimiters - * @return {?Object} An initialized block object (if possible) + * @param {Object} blockType Block type name + * @param {Object} fallbackBlockType Fallback Block type name + * @param {String} rawContent Raw block content + * @param {?Object} attributes Attributes obtained from block delimiters + * @return {?Object} An initialized block object (if possible) */ -export function createBlockWithFallback( name, rawContent, attributes ) { - // Use type from block content, otherwise find unknown handler. - name = name || getUnknownTypeHandler(); - - // Try finding type for known block name, else fall back again. - let blockType = getBlockType( name ); - const fallbackBlock = getUnknownTypeHandler(); - if ( ! blockType ) { - name = fallbackBlock; - blockType = getBlockType( name ); - } +export function createBlockWithFallback( blockType, fallbackBlockType, rawContent, attributes ) { + const parsedBlockType = blockType || fallbackBlockType; // Include in set only if type were determined. // TODO do we ever expect there to not be an unknown type handler? - if ( blockType && ( rawContent || name !== fallbackBlock ) ) { + if ( parsedBlockType && ( rawContent || parsedBlockType !== fallbackBlockType ) ) { // TODO allow blocks to opt-in to receiving a tree instead of a string. // Gradually convert all blocks to this new format, then remove the // string serialization. const block = createBlock( - name, + blockType, getBlockAttributes( blockType, rawContent, attributes ) ); @@ -138,13 +129,18 @@ export function isValidBlock( rawContent, blockType, attributes ) { /** * Parses the post content with a PegJS grammar and returns a list of blocks. * - * @param {String} content The post content - * @return {Array} Block list + * @param {String} content The post content + * @param {Array} blockTypes Block Types + * @param {String} fallbackBlockName Fallback Block Name + * @return {Array} Block list */ -export function parseWithGrammar( content ) { +export function parseWithGrammar( content, blockTypes, fallbackBlockName ) { + const getBlockType = ( name ) => find( blockTypes, ( bt ) => bt.name === name ); + const fallbackBlockType = getBlockType( fallbackBlockName ); return grammarParse( content ).reduce( ( memo, blockNode ) => { const { blockName, rawContent, attrs } = blockNode; - const block = createBlockWithFallback( blockName, rawContent.trim(), attrs ); + const blockType = getBlockType( blockName ); + const block = createBlockWithFallback( blockType, fallbackBlockType, rawContent.trim(), attrs ); if ( block ) { memo.push( block ); } diff --git a/blocks/api/paste.js b/blocks/api/paste.js index 282729ac18b573..12770b8e7fd023 100644 --- a/blocks/api/paste.js +++ b/blocks/api/paste.js @@ -70,8 +70,10 @@ export function normaliseToBlockLevelNodes( nodes ) { } export default function( nodes ) { + const blockTypes = getBlockTypes(); + const unknownTypeBlockType = find( blockTypes, ( bt ) => bt.name === getUnknownTypeHandler() ); return normaliseToBlockLevelNodes( nodes ).map( ( node ) => { - const block = getBlockTypes().reduce( ( acc, blockType ) => { + const block = blockTypes.reduce( ( acc, blockType ) => { if ( acc ) { return acc; } @@ -83,17 +85,17 @@ export default function( nodes ) { return acc; } - const { name, defaultAttributes = [] } = blockType; + const { defaultAttributes = [] } = blockType; const attributes = parseBlockAttributes( node.outerHTML, transform.attributes ); - return createBlock( name, { ...defaultAttributes, ...attributes } ); + return createBlock( blockType, { ...defaultAttributes, ...attributes } ); }, null ); if ( block ) { return block; } - return createBlock( getUnknownTypeHandler(), { + return createBlock( unknownTypeBlockType, { content: node.outerHTML, } ); } ); diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index 9c56e2b703f2e1..0718f01be70de3 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { isEmpty, reduce, isObject } from 'lodash'; +import { isEmpty, reduce, isObject, find } from 'lodash'; import { html as beautifyHtml } from 'js-beautify'; import classnames from 'classnames'; @@ -13,7 +13,6 @@ import { Component, createElement, renderToString, cloneElement, Children } from /** * Internal dependencies */ -import { getBlockType } from './registration'; import { parseBlockAttributes } from './parser'; /** @@ -125,9 +124,8 @@ export function getBeautifulContent( content ) { } ); } -export function serializeBlock( block ) { +export function serializeBlock( block, blockType ) { const blockName = block.name; - const blockType = getBlockType( blockName ); let saveContent; if ( block.isValid ) { @@ -162,9 +160,15 @@ export function serializeBlock( block ) { /** * Takes a block list and returns the serialized post content. * - * @param {Array} blocks Block list - * @return {String} The post content + * @param {Array} blocks Block list + * @param {Array} blockTypes Block Types + * @return {String} The post content */ -export default function serialize( blocks ) { - return blocks.map( serializeBlock ).join( '\n\n' ); +export default function serialize( blocks, blockTypes ) { + const getBlockType = ( name ) => find( blockTypes, ( bt ) => bt.name === name ); + return blocks + .map( ( block ) => + serializeBlock( block, getBlockType( block.name ) ) + ) + .join( '\n\n' ); } diff --git a/blocks/editable/index.js b/blocks/editable/index.js index ab5230a1cd1d43..2e6c01c6aefc02 100644 --- a/blocks/editable/index.js +++ b/blocks/editable/index.js @@ -12,7 +12,7 @@ import 'element-closest'; * WordPress dependencies */ import { createElement, Component, renderToString } from 'element'; -import { parse, pasteHandler } from '../api'; +import { parse, pasteHandler, getBlockTypes, getUnknownTypeHandler } from '../api'; import { BACKSPACE, DELETE, ENTER } from 'utils/keycodes'; /** @@ -145,7 +145,7 @@ export default class Editable extends Component { // Internal paste, so parse. if ( childNodes.some( isBlockDelimiter ) ) { - blocks = parse( event.node.innerHTML.replace( /]+>/, '' ) ); + blocks = parse( getBlockTypes(), getUnknownTypeHandler(), event.node.innerHTML.replace( /]+>/, '' ) ); // External paste with block level content, so attempt to assign // blocks. } else if ( childNodes.some( isBlockPart ) ) { diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index b2bd04a4f5c690..5fe50c4f2995f6 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { isObject } from 'lodash'; +import { isObject, find } from 'lodash'; /** * WordPress dependencies @@ -113,8 +113,9 @@ registerBlockType( 'core/heading', { }; }, - edit( { attributes, setAttributes, focus, setFocus, mergeBlocks, insertBlocksAfter } ) { + edit( { attributes, setAttributes, focus, setFocus, mergeBlocks, insertBlocksAfter, editorConfig } ) { const { align, content, nodeName, placeholder } = attributes; + const getBlockType = ( name ) => find( editorConfig.blockTypes, ( bt ) => bt.name === name ); return [ focus && ( @@ -170,7 +171,7 @@ registerBlockType( 'core/heading', { setAttributes( { content: before } ); insertBlocksAfter( [ ...blocks, - createBlock( 'core/text', { content: after } ), + createBlock( getBlockType( 'core/text' ), { content: after } ), ] ); } } style={ { textAlign: align } } diff --git a/blocks/library/text/index.js b/blocks/library/text/index.js index 14b91331d218fd..0d3ce149beacff 100644 --- a/blocks/library/text/index.js +++ b/blocks/library/text/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { find } from 'lodash'; + /** * WordPress dependencies */ @@ -53,9 +58,10 @@ registerBlockType( 'core/text', { }; }, - edit( { attributes, setAttributes, insertBlocksAfter, focus, setFocus, mergeBlocks, onReplace } ) { + edit( { attributes, setAttributes, insertBlocksAfter, focus, setFocus, mergeBlocks, onReplace, editorConfig } ) { const { align, content, dropCap, placeholder } = attributes; const toggleDropCap = () => setAttributes( { dropCap: ! dropCap } ); + const getBlockType = ( name ) => find( editorConfig.blockTypes, ( bt ) => bt.name === name ); return [ focus && ( @@ -95,7 +101,7 @@ registerBlockType( 'core/text', { setAttributes( { content: before } ); insertBlocksAfter( [ ...blocks, - createBlock( 'core/text', { content: after } ), + createBlock( getBlockType( 'core/text' ), { content: after } ), ] ); } } onMerge={ mergeBlocks } diff --git a/editor/README.md b/editor-chrome/README.md similarity index 100% rename from editor/README.md rename to editor-chrome/README.md diff --git a/editor-chrome/actions.js b/editor-chrome/actions.js new file mode 100644 index 00000000000000..ef4d292e640ef7 --- /dev/null +++ b/editor-chrome/actions.js @@ -0,0 +1,87 @@ +/** + * External Dependencies + */ +import uuid from 'uuid/v4'; +import { partial } from 'lodash'; + +export function editPost( edits ) { + return { + type: 'EDIT_POST', + edits, + }; +} + +export function savePost() { + return { + type: 'REQUEST_POST_UPDATE', + }; +} + +export function trashPost( postId, postType ) { + return { + type: 'TRASH_POST', + postId, + postType, + }; +} + +/** + * Returns an action object used in signalling that the post should autosave. + * + * @return {Object} Action object + */ +export function autosave() { + return { + type: 'AUTOSAVE', + }; +} + +/** + * Returns an action object used in signalling that the post should be queued + * for autosave after a delay. + * + * @return {Object} Action object + */ +export function queueAutosave() { + return { + type: 'QUEUE_AUTOSAVE', + }; +} + +/** + * Returns an action object used to create a notice + * + * @param {String} status The notice status + * @param {WPElement} content The notice content + * @param {String} id The notice id + * + * @return {Object} Action object + */ +export function createNotice( status, content, id = uuid() ) { + return { + type: 'CREATE_NOTICE', + notice: { + id, + status, + content, + }, + }; +} + +/** + * Returns an action object used to remove a notice + * + * @param {String} id The notice id + * + * @return {Object} Action object + */ +export function removeNotice( id ) { + return { + type: 'REMOVE_NOTICE', + noticeId: id, + }; +} + +export const createSuccessNotice = partial( createNotice, 'success' ); +export const createErrorNotice = partial( createNotice, 'error' ); +export const createWarningNotice = partial( createNotice, 'warning' ); diff --git a/editor/assets/stylesheets/_animations.scss b/editor-chrome/assets/stylesheets/_animations.scss similarity index 100% rename from editor/assets/stylesheets/_animations.scss rename to editor-chrome/assets/stylesheets/_animations.scss diff --git a/editor/assets/stylesheets/_mixins.scss b/editor-chrome/assets/stylesheets/_mixins.scss similarity index 100% rename from editor/assets/stylesheets/_mixins.scss rename to editor-chrome/assets/stylesheets/_mixins.scss diff --git a/editor/assets/stylesheets/_variables.scss b/editor-chrome/assets/stylesheets/_variables.scss similarity index 100% rename from editor/assets/stylesheets/_variables.scss rename to editor-chrome/assets/stylesheets/_variables.scss diff --git a/editor/assets/stylesheets/_z-index.scss b/editor-chrome/assets/stylesheets/_z-index.scss similarity index 100% rename from editor/assets/stylesheets/_z-index.scss rename to editor-chrome/assets/stylesheets/_z-index.scss diff --git a/editor/assets/stylesheets/main.scss b/editor-chrome/assets/stylesheets/main.scss similarity index 100% rename from editor/assets/stylesheets/main.scss rename to editor-chrome/assets/stylesheets/main.scss diff --git a/editor/document-title/index.js b/editor-chrome/document-title/index.js similarity index 100% rename from editor/document-title/index.js rename to editor-chrome/document-title/index.js diff --git a/editor/effects.js b/editor-chrome/effects.js similarity index 81% rename from editor/effects.js rename to editor-chrome/effects.js index 5b665b13a19f5a..e915cabe559b63 100644 --- a/editor/effects.js +++ b/editor-chrome/effects.js @@ -7,7 +7,6 @@ import { get, uniqueId, debounce } from 'lodash'; /** * WordPress dependencies */ -import { serialize, getBlockType, switchToBlockType } from 'blocks'; import { __ } from 'i18n'; /** @@ -15,8 +14,6 @@ import { __ } from 'i18n'; */ import { getGutenbergURL, getWPAdminURL } from './utils/url'; import { - focusBlock, - replaceBlocks, createSuccessNotice, createErrorNotice, autosave, @@ -27,7 +24,6 @@ import { import { getCurrentPost, getCurrentPostType, - getBlocks, getPostEdits, isCurrentPostPublished, isEditedPostDirty, @@ -43,7 +39,6 @@ export default { const edits = getPostEdits( state ); const toSend = { ...edits, - content: serialize( getBlocks( state ) ), id: post.id, }; const transactionId = uniqueId(); @@ -177,49 +172,6 @@ export default { const message = action.error.message && action.error.code !== 'unknown_error' ? action.error.message : __( 'Trashing failed' ); store.dispatch( createErrorNotice( message ) ); }, - MERGE_BLOCKS( action, store ) { - const { dispatch } = store; - const [ blockA, blockB ] = action.blocks; - const blockType = getBlockType( blockA.name ); - - // Only focus the previous block if it's not mergeable - if ( ! blockType.merge ) { - dispatch( focusBlock( blockA.uid ) ); - return; - } - - // We can only merge blocks with similar types - // thus, we transform the block to merge first - const blocksWithTheSameType = blockA.name === blockB.name - ? [ blockB ] - : switchToBlockType( blockB, blockA.name ); - - // If the block types can not match, do nothing - if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) { - return; - } - - // Calling the merge to update the attributes and remove the block to be merged - const updatedAttributes = blockType.merge( - blockA.attributes, - blocksWithTheSameType[ 0 ].attributes - ); - - dispatch( focusBlock( blockA.uid, { offset: -1 } ) ); - dispatch( replaceBlocks( - [ blockA.uid, blockB.uid ], - [ - { - ...blockA, - attributes: { - ...blockA.attributes, - ...updatedAttributes, - }, - }, - ...blocksWithTheSameType.slice( 1 ), - ] - ) ); - }, AUTOSAVE( action, store ) { const { getState, dispatch } = store; const state = getState(); diff --git a/editor-chrome/header/index.js b/editor-chrome/header/index.js new file mode 100644 index 00000000000000..b6f9f23e285536 --- /dev/null +++ b/editor-chrome/header/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { MultiSelectionHeader } from 'editor'; + +/** + * Internal dependencies + */ +import './style.scss'; +import ModeSwitcher from './mode-switcher'; +import SavedState from './saved-state'; +import Tools from './tools'; + +function Header() { + return ( +
+ + + + +
+ ); +} + +export default Header; diff --git a/editor/header/mode-switcher/index.js b/editor-chrome/header/mode-switcher/index.js similarity index 100% rename from editor/header/mode-switcher/index.js rename to editor-chrome/header/mode-switcher/index.js diff --git a/editor/header/mode-switcher/style.scss b/editor-chrome/header/mode-switcher/style.scss similarity index 100% rename from editor/header/mode-switcher/style.scss rename to editor-chrome/header/mode-switcher/style.scss diff --git a/editor/header/saved-state/index.js b/editor-chrome/header/saved-state/index.js similarity index 100% rename from editor/header/saved-state/index.js rename to editor-chrome/header/saved-state/index.js diff --git a/editor/header/saved-state/style.scss b/editor-chrome/header/saved-state/style.scss similarity index 100% rename from editor/header/saved-state/style.scss rename to editor-chrome/header/saved-state/style.scss diff --git a/editor/header/saved-state/test/index.js b/editor-chrome/header/saved-state/test/index.js similarity index 100% rename from editor/header/saved-state/test/index.js rename to editor-chrome/header/saved-state/test/index.js diff --git a/editor/header/style.scss b/editor-chrome/header/style.scss similarity index 83% rename from editor/header/style.scss rename to editor-chrome/header/style.scss index 6251ba5dae434b..5726c439da202c 100644 --- a/editor/header/style.scss +++ b/editor-chrome/header/style.scss @@ -67,18 +67,3 @@ left: -$admin-sidebar-width-big; right: $admin-sidebar-width-big; } - -.editor-header-multi-select { - background: $blue-medium-100; - border-bottom: 1px solid $blue-medium-200; -} - -.editor-selected-count { - padding-right: $item-spacing; - color: $dark-gray-500; - border-right: 1px solid $light-gray-500; -} - -.editor-selected-clear { - margin: 0 0 0 auto; -} diff --git a/editor/header/tools/index.js b/editor-chrome/header/tools/index.js similarity index 97% rename from editor/header/tools/index.js rename to editor-chrome/header/tools/index.js index 2fc32fd7b17167..f4e00e1825209c 100644 --- a/editor/header/tools/index.js +++ b/editor-chrome/header/tools/index.js @@ -8,12 +8,12 @@ import { connect } from 'react-redux'; */ import { __ } from 'i18n'; import { IconButton } from 'components'; +import { Inserter } from 'editor'; /** * Internal dependencies */ import './style.scss'; -import Inserter from '../../inserter'; import PublishButton from './publish-button'; import PreviewButton from './preview-button'; import { isEditorSidebarOpened, hasEditorUndo, hasEditorRedo } from '../../selectors'; diff --git a/editor/header/tools/preview-button.js b/editor-chrome/header/tools/preview-button.js similarity index 100% rename from editor/header/tools/preview-button.js rename to editor-chrome/header/tools/preview-button.js diff --git a/editor/header/tools/publish-button.js b/editor-chrome/header/tools/publish-button.js similarity index 100% rename from editor/header/tools/publish-button.js rename to editor-chrome/header/tools/publish-button.js diff --git a/editor/header/tools/style.scss b/editor-chrome/header/tools/style.scss similarity index 100% rename from editor/header/tools/style.scss rename to editor-chrome/header/tools/style.scss diff --git a/editor-chrome/index.js b/editor-chrome/index.js new file mode 100644 index 00000000000000..5b92cf2ac2eadd --- /dev/null +++ b/editor-chrome/index.js @@ -0,0 +1,100 @@ +/** + * External dependencies + */ +import { Provider as ReduxProvider } from 'react-redux'; +import { Provider as SlotFillProvider } from 'react-slot-fill'; +import moment from 'moment-timezone'; +import 'moment-timezone/moment-timezone-utils'; + +/** + * WordPress dependencies + */ +import { parse, getBlockTypes, getCategories, getDefaultBlock, getUnknownTypeHandler } from 'blocks'; +import { render } from 'element'; +import { settings } from 'date'; + +/** + * Internal dependencies + */ +import './assets/stylesheets/main.scss'; +import Layout from './layout'; +import { createReduxStore } from './state'; + +// Configure moment globally +moment.locale( settings.l10n.locale ); +if ( settings.timezone.string ) { + moment.tz.setDefault( settings.timezone.string ); +} else { + const momentTimezone = { + name: 'WP', + abbrs: [ 'WP' ], + untils: [ null ], + offsets: [ -settings.timezone.offset * 60 ], + }; + const unpackedTimezone = moment.tz.pack( momentTimezone ); + moment.tz.add( unpackedTimezone ); + moment.tz.setDefault( 'WP' ); +} + +/** + * Initializes Redux state with bootstrapped post, if provided. + * + * @param {Redux.Store} store Redux store instance + * @param {Object} post Bootstrapped post object + * @param {Array} editorConfig Editor Config + */ +function preparePostState( store, post, editorConfig ) { + // Set current post into state + store.dispatch( { + type: 'RESET_POST', + post, + } ); + + // Parse content as blocks + if ( post.content.raw ) { + store.dispatch( { + type: 'RESET_BLOCKS', + blocks: parse( post.content.raw, editorConfig.blockTypes, editorConfig.fallbackBlockType ), + } ); + } + + // Include auto draft title in edits while not flagging post as dirty + if ( post.status === 'auto-draft' ) { + store.dispatch( { + type: 'SETUP_NEW_POST', + edits: { + title: post.title.raw, + }, + } ); + } +} + +/** + * Initializes and returns an instance of Editor. + * + * @param {String} id Unique identifier for editor instance + * @param {Object} post API entity for post to edit (type required) + */ +export function createEditorInstance( id, post ) { + const editorConfig = { + blockTypes: getBlockTypes(), + categories: getCategories(), + defaultBlockType: getDefaultBlock(), + fallbackBlockType: getUnknownTypeHandler(), + }; + + const store = createReduxStore(); + store.dispatch( { + type: 'LOAD_USER_DATA', + } ); + preparePostState( store, post, editorConfig ); + + render( + + + + + , + document.getElementById( id ) + ); +} diff --git a/editor/layout/index.js b/editor-chrome/layout/index.js similarity index 51% rename from editor/layout/index.js rename to editor-chrome/layout/index.js index 9055f185875d09..11fd6cbeeb7574 100644 --- a/editor/layout/index.js +++ b/editor-chrome/layout/index.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { NoticeList } from 'components'; +import { Provider as EditorProvider } from 'editor'; /** * Internal dependencies @@ -19,30 +20,35 @@ import TextEditor from '../modes/text-editor'; import VisualEditor from '../modes/visual-editor'; import UnsavedChangesWarning from '../unsaved-changes-warning'; import DocumentTitle from '../document-title'; -import { removeNotice } from '../actions'; +import { removeNotice, editPost } from '../actions'; import { getEditorMode, isEditorSidebarOpened, getNotices, + getEditedPostContent, } from '../selectors'; -function Layout( { mode, isSidebarOpened, notices, ...props } ) { +function Layout( { config, value, mode, isSidebarOpened, notices, ...props } ) { const className = classnames( 'editor-layout', { 'is-sidebar-opened': isSidebarOpened, } ); + const onChangeContent = ( content ) => props.editPost( { content } ); + return ( -
- - - -
-
- { mode === 'text' && } - { mode === 'visual' && } + +
+ + + +
+
+ { mode === 'text' && } + { mode === 'visual' && } +
+ { isSidebarOpened && }
- { isSidebarOpened && } -
+ ); } @@ -51,6 +57,7 @@ export default connect( mode: getEditorMode( state ), isSidebarOpened: isEditorSidebarOpened( state ), notices: getNotices( state ), + value: getEditedPostContent( state ), } ), - { removeNotice } + { removeNotice, editPost } )( Layout ); diff --git a/editor/layout/style.scss b/editor-chrome/layout/style.scss similarity index 100% rename from editor/layout/style.scss rename to editor-chrome/layout/style.scss diff --git a/editor-chrome/modes/text-editor/index.js b/editor-chrome/modes/text-editor/index.js new file mode 100644 index 00000000000000..fbc46e36a3ad81 --- /dev/null +++ b/editor-chrome/modes/text-editor/index.js @@ -0,0 +1,42 @@ +import Textarea from 'react-autosize-textarea'; + +/** + * Internal dependencies + */ +import './style.scss'; +import PostTitle from '../../post-title'; + +function TextEditor( { value, onChange } ) { + return ( +
+
+
+ + + + + + + + + + + + + +
+
+
+ +