diff --git a/.eslintrc.js b/.eslintrc.js index af4bda32427a9..803296cfb8aaa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -87,6 +87,10 @@ module.exports = { selector: 'ImportDeclaration[source.value=/^edit-post(\\u002F|$)/]', message: 'Use @wordpress/edit-post as import path instead.', }, + { + selector: 'ImportDeclaration[source.value=/^block-editor(\\u002F|$)/]', + message: 'Use @wordpress/block-editor as import path instead.', + }, { selector: 'ImportDeclaration[source.value=/^viewport(\\u002F|$)/]', message: 'Use @wordpress/viewport as import path instead.', diff --git a/docs/designers-developers/developers/data/README.md b/docs/designers-developers/developers/data/README.md index 5233753c1445e..b6809ab75075b 100644 --- a/docs/designers-developers/developers/data/README.md +++ b/docs/designers-developers/developers/data/README.md @@ -3,7 +3,8 @@ - [**core**: WordPress Core Data](../../docs/designers-developers/developers/data/data-core.md) - [**core/annotations**: Annotations](../../docs/designers-developers/developers/data/data-core-annotations.md) - [**core/blocks**: Block Types Data](../../docs/designers-developers/developers/data/data-core-blocks.md) - - [**core/editor**: The Editor’s Data](../../docs/designers-developers/developers/data/data-core-editor.md) + - [**core/block-editor**: The Block Editor’s Data](../../docs/designers-developers/developers/data/data-core-block-editor.md) + - [**core/editor**: The Post Editor’s Data](../../docs/designers-developers/developers/data/data-core-editor.md) - [**core/edit-post**: The Editor’s UI Data](../../docs/designers-developers/developers/data/data-core-edit-post.md) - [**core/notices**: Notices Data](../../docs/designers-developers/developers/data/data-core-notices.md) - [**core/nux**: The NUX (New User Experience) Data](../../docs/designers-developers/developers/data/data-core-nux.md) diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md new file mode 100644 index 0000000000000..bcacdd68de76e --- /dev/null +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -0,0 +1,1022 @@ +# **core/block-editor**: The Block Editor’s Data + +## Selectors + +### getBlockDependantsCacheBust + +Returns a new reference when the inner blocks of a given block client ID +change. This is used exclusively as a memoized selector dependant, relying +on this selector's shared return value and recursively those of its inner +blocks defined as dependencies. This abuses mechanics of the selector +memoization to return from the original selector function only when +dependants change. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +### getBlockName + +Returns a block's name given its client ID, or null if no block exists with +the client ID. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Block name. + +### isBlockValid + +Returns whether a block is valid or not. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Is Valid. + +### getBlockAttributes + +Returns a block's attributes given its client ID, or null if no block exists with +the client ID. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Block attributes. + +### getBlock + +Returns a block given its client ID. This is a parsed copy of the block, +containing its `blockName`, `clientId`, and current `attributes` state. This +is not the block's registration settings, which must be retrieved from the +blocks module registration store. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Parsed block object. + +### getBlocks + +Returns all block objects for the current post being edited as an array in +the order they appear in the post. + +Note: It's important to memoize this selector to avoid return a new instance +on each call + +*Parameters* + + * state: Editor state. + * rootClientId: Optional root client ID of block list. + +*Returns* + +Post blocks. + +### getClientIdsOfDescendants + +Returns an array containing the clientIds of all descendants +of the blocks given. + +*Parameters* + + * state: Global application state. + * clientIds: Array of blocks to inspect. + +*Returns* + +ids of descendants. + +### getClientIdsWithDescendants + +Returns an array containing the clientIds of the top-level blocks +and their descendants of any depth (for nested blocks). + +*Parameters* + + * state: Global application state. + +*Returns* + +ids of top-level and descendant blocks. + +### getGlobalBlockCount + +Returns the total number of blocks, or the total number of blocks with a specific name in a post. +The number returned includes nested blocks. + +*Parameters* + + * state: Global application state. + * blockName: Optional block name, if specified only blocks of that type will be counted. + +*Returns* + +Number of blocks in the post, or number of blocks with name equal to blockName. + +### getBlocksByClientId + +Given an array of block client IDs, returns the corresponding array of block +objects. + +*Parameters* + + * state: Editor state. + * clientIds: Client IDs for which blocks are to be returned. + +*Returns* + +Block objects. + +### getBlockCount + +Returns the number of blocks currently present in the post. + +*Parameters* + + * state: Editor state. + * rootClientId: Optional root client ID of block list. + +*Returns* + +Number of blocks in the post. + +### getBlockSelectionStart + +Returns the current block selection start. This value may be null, and it +may represent either a singular block selection or multi-selection start. +A selection is singular if its start and end match. + +*Parameters* + + * state: Global application state. + +*Returns* + +Client ID of block selection start. + +### getBlockSelectionEnd + +Returns the current block selection end. This value may be null, and it +may represent either a singular block selection or multi-selection end. +A selection is singular if its start and end match. + +*Parameters* + + * state: Global application state. + +*Returns* + +Client ID of block selection end. + +### getSelectedBlockCount + +Returns the number of blocks currently selected in the post. + +*Parameters* + + * state: Global application state. + +*Returns* + +Number of blocks selected in the post. + +### hasSelectedBlock + +Returns true if there is a single selected block, or false otherwise. + +*Parameters* + + * state: Editor state. + +*Returns* + +Whether a single block is selected. + +### getSelectedBlockClientId + +Returns the currently selected block client ID, or null if there is no +selected block. + +*Parameters* + + * state: Editor state. + +*Returns* + +Selected block client ID. + +### getSelectedBlock + +Returns the currently selected block, or null if there is no selected block. + +*Parameters* + + * state: Global application state. + +*Returns* + +Selected block. + +### getBlockRootClientId + +Given a block client ID, returns the root block from which the block is +nested, an empty string for top-level blocks, or null if the block does not +exist. + +*Parameters* + + * state: Editor state. + * clientId: Block from which to find root client ID. + +*Returns* + +Root client ID, if exists + +### getBlockHierarchyRootClientId + +Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. + +*Parameters* + + * state: Editor state. + * clientId: Block from which to find root client ID. + +*Returns* + +Root client ID + +### getAdjacentBlockClientId + +Returns the client ID of the block adjacent one at the given reference +startClientId and modifier directionality. Defaults start startClientId to +the selected block, and direction as next block. Returns null if there is no +adjacent block. + +*Parameters* + + * state: Editor state. + * startClientId: Optional client ID of block from which to + search. + * modifier: Directionality multiplier (1 next, -1 + previous). + +*Returns* + +Return the client ID of the block, or null if none exists. + +### getPreviousBlockClientId + +Returns the previous block's client ID from the given reference start ID. +Defaults start to the selected block. Returns null if there is no previous +block. + +*Parameters* + + * state: Editor state. + * startClientId: Optional client ID of block from which to + search. + +*Returns* + +Adjacent block's client ID, or null if none exists. + +### getNextBlockClientId + +Returns the next block's client ID from the given reference start ID. +Defaults start to the selected block. Returns null if there is no next +block. + +*Parameters* + + * state: Editor state. + * startClientId: Optional client ID of block from which to + search. + +*Returns* + +Adjacent block's client ID, or null if none exists. + +### getSelectedBlocksInitialCaretPosition + +Returns the initial caret position for the selected block. +This position is to used to position the caret properly when the selected block changes. + +*Parameters* + + * state: Global application state. + +*Returns* + +Selected block. + +### getMultiSelectedBlockClientIds + +Returns the current multi-selection set of block client IDs, or an empty +array if there is no multi-selection. + +*Parameters* + + * state: Editor state. + +*Returns* + +Multi-selected block client IDs. + +### getMultiSelectedBlocks + +Returns the current multi-selection set of blocks, or an empty array if +there is no multi-selection. + +*Parameters* + + * state: Editor state. + +*Returns* + +Multi-selected block objects. + +### getFirstMultiSelectedBlockClientId + +Returns the client ID of the first block in the multi-selection set, or null +if there is no multi-selection. + +*Parameters* + + * state: Editor state. + +*Returns* + +First block client ID in the multi-selection set. + +### getLastMultiSelectedBlockClientId + +Returns the client ID of the last block in the multi-selection set, or null +if there is no multi-selection. + +*Parameters* + + * state: Editor state. + +*Returns* + +Last block client ID in the multi-selection set. + +### isFirstMultiSelectedBlock + +Returns true if a multi-selection exists, and the block corresponding to the +specified client ID is the first block of the multi-selection set, or false +otherwise. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Whether block is first in multi-selection. + +### isBlockMultiSelected + +Returns true if the client ID occurs within the block multi-selection, or +false otherwise. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Whether block is in multi-selection set. + +### isAncestorMultiSelected + +Returns true if an ancestor of the block is multi-selected, or false +otherwise. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Whether an ancestor of the block is in multi-selection + set. + +### getMultiSelectedBlocksStartClientId + +Returns the client ID of the block which begins the multi-selection set, or +null if there is no multi-selection. + +This is not necessarily the first client ID in the selection. + +*Parameters* + + * state: Editor state. + +*Returns* + +Client ID of block beginning multi-selection. + +### getMultiSelectedBlocksEndClientId + +Returns the client ID of the block which ends the multi-selection set, or +null if there is no multi-selection. + +This is not necessarily the last client ID in the selection. + +*Parameters* + + * state: Editor state. + +*Returns* + +Client ID of block ending multi-selection. + +### getBlockOrder + +Returns an array containing all block client IDs in the editor in the order +they appear. Optionally accepts a root client ID of the block list for which +the order should be returned, defaulting to the top-level block order. + +*Parameters* + + * state: Editor state. + * rootClientId: Optional root client ID of block list. + +*Returns* + +Ordered client IDs of editor blocks. + +### getBlockIndex + +Returns the index at which the block corresponding to the specified client +ID occurs within the block order, or `-1` if the block does not exist. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + * rootClientId: Optional root client ID of block list. + +*Returns* + +Index at which block exists in order. + +### isBlockSelected + +Returns true if the block corresponding to the specified client ID is +currently selected and no multi-selection exists, or false otherwise. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Whether block is selected and multi-selection exists. + +### hasSelectedInnerBlock + +Returns true if one of the block's inner blocks is selected. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + * deep: Perform a deep check. + +*Returns* + +Whether the block as an inner block selected + +### isBlockWithinSelection + +Returns true if the block corresponding to the specified client ID is +currently selected but isn't the last of the selected blocks. Here "last" +refers to the block sequence in the document, _not_ the sequence of +multi-selection, which is why `state.blockSelection.end` isn't used. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Whether block is selected and not the last in the + selection. + +### hasMultiSelection + +Returns true if a multi-selection has been made, or false otherwise. + +*Parameters* + + * state: Editor state. + +*Returns* + +Whether multi-selection has been made. + +### isMultiSelecting + +Whether in the process of multi-selecting or not. This flag is only true +while the multi-selection is being selected (by mouse move), and is false +once the multi-selection has been settled. + +*Parameters* + + * state: Global application state. + +*Returns* + +True if multi-selecting, false if not. + +### isSelectionEnabled + +Selector that returns if multi-selection is enabled or not. + +*Parameters* + + * state: Global application state. + +*Returns* + +True if it should be possible to multi-select blocks, false if multi-selection is disabled. + +### getBlockMode + +Returns the block's editing mode, defaulting to "visual" if not explicitly +assigned. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Block editing mode. + +### isTyping + +Returns true if the user is typing, or false otherwise. + +*Parameters* + + * state: Global application state. + +*Returns* + +Whether user is typing. + +### isCaretWithinFormattedText + +Returns true if the caret is within formatted text, or false otherwise. + +*Parameters* + + * state: Global application state. + +*Returns* + +Whether the caret is within formatted text. + +### getBlockInsertionPoint + +Returns the insertion point, the index at which the new inserted block would +be placed. Defaults to the last index. + +*Parameters* + + * state: Editor state. + +*Returns* + +Insertion point object with `rootClientId`, `index`. + +### isBlockInsertionPointVisible + +Returns true if we should show the block insertion point. + +*Parameters* + + * state: Global application state. + +*Returns* + +Whether the insertion point is visible or not. + +### isValidTemplate + +Returns whether the blocks matches the template or not. + +*Parameters* + + * state: null + +*Returns* + +Whether the template is valid or not. + +### getTemplate + +Returns the defined block template + +*Parameters* + + * state: null + +*Returns* + +Block Template + +### getTemplateLock + +Returns the defined block template lock. Optionally accepts a root block +client ID as context, otherwise defaulting to the global context. + +*Parameters* + + * state: Editor state. + * rootClientId: Optional block root client ID. + +*Returns* + +Block Template Lock + +### canInsertBlockType + +Determines if the given block type is allowed to be inserted into the block list. + +*Parameters* + + * state: Editor state. + * blockName: The name of the block type, e.g.' core/paragraph'. + * rootClientId: Optional root client ID of block list. + +*Returns* + +Whether the given block type is allowed to be inserted. + +### getInserterItems + +Determines the items that appear in the inserter. Includes both static +items (e.g. a regular block type) and dynamic items (e.g. a reusable block). + +Each item object contains what's necessary to display a button in the +inserter and handle its selection. + +The 'utility' property indicates how useful we think an item will be to the +user. There are 4 levels of utility: + +1. Blocks that are contextually useful (utility = 3) +2. Blocks that have been previously inserted (utility = 2) +3. Blocks that are in the common category (utility = 1) +4. All other blocks (utility = 0) + +The 'frecency' property is a heuristic (https://en.wikipedia.org/wiki/Frecency) +that combines block usage frequenty and recency. + +Items are returned ordered descendingly by their 'utility' and 'frecency'. + +*Parameters* + + * state: Editor state. + * rootClientId: Optional root client ID of block list. + +*Returns* + +Items that appear in inserter. + +### hasInserterItems + +Determines whether there are items to show in the inserter. + +*Parameters* + + * state: Editor state. + * rootClientId: Optional root client ID of block list. + +*Returns* + +Items that appear in inserter. + +### getBlockListSettings + +Returns the Block List settings of a block, if any exist. + +*Parameters* + + * state: Editor state. + * clientId: Block client ID. + +*Returns* + +Block settings of the block if set. + +### getEditorSettings + +Returns the editor settings. + +*Parameters* + + * state: Editor state. + +*Returns* + +The editor settings object. + +### hasEditorUndo + +Returns true if any past editor history snapshots exist, or false otherwise. + +*Parameters* + + * state: Global application state. + +*Returns* + +Whether undo history exists. + +### hasEditorRedo + +Returns true if any future editor history snapshots exist, or false +otherwise. + +*Parameters* + + * state: Global application state. + +*Returns* + +Whether redo history exists. + +## Actions + +### resetBlocks + +Returns an action object used in signalling that blocks state should be +reset to the specified array of blocks, taking precedence over any other +content reflected as an edit in state. + +*Parameters* + + * blocks: Array of blocks. + +### receiveBlocks + +Returns an action object used in signalling that blocks have been received. +Unlike resetBlocks, these should be appended to the existing known set, not +replacing. + +*Parameters* + + * blocks: Array of block objects. + +### updateBlockAttributes + +Returns an action object used in signalling that the block attributes with +the specified client ID has been updated. + +*Parameters* + + * clientId: Block client ID. + * attributes: Block attributes to be merged. + +### updateBlock + +Returns an action object used in signalling that the block with the +specified client ID has been updated. + +*Parameters* + + * clientId: Block client ID. + * updates: Block attributes to be merged. + +### selectBlock + +Returns an action object used in signalling that the block with the +specified client ID has been selected, optionally accepting a position +value reflecting its selection directionality. An initialPosition of -1 +reflects a reverse selection. + +*Parameters* + + * clientId: Block client ID. + * initialPosition: Optional initial position. Pass as -1 to + reflect reverse selection. + +### toggleSelection + +Returns an action object that enables or disables block selection. + +*Parameters* + + * boolean: [isSelectionEnabled=true] Whether block selection should + be enabled. + +### replaceBlocks + +Returns an action object signalling that a blocks should be replaced with +one or more replacement blocks. + +*Parameters* + + * clientIds: Block client ID(s) to replace. + * blocks: Replacement block(s). + +### replaceBlock + +Returns an action object signalling that a single block should be replaced +with one or more replacement blocks. + +*Parameters* + + * clientId: Block client ID to replace. + * block: Replacement block(s). + +### moveBlockToPosition + +Returns an action object signalling that an indexed block should be moved +to a new index. + +*Parameters* + + * clientId: The client ID of the block. + * fromRootClientId: Root client ID source. + * toRootClientId: Root client ID destination. + * index: The index to move the block into. + +### insertBlock + +Returns an action object used in signalling that a single block should be +inserted, optionally at a specific index respective a root block list. + +*Parameters* + + * block: Block object to insert. + * index: Index at which block should be inserted. + * rootClientId: Optional root client ID of block list on which to insert. + * updateSelection: If true block selection will be updated. If false, block selection will not change. Defaults to true. + +### insertBlocks + +Returns an action object used in signalling that an array of blocks should +be inserted, optionally at a specific index respective a root block list. + +*Parameters* + + * blocks: Block objects to insert. + * index: Index at which block should be inserted. + * rootClientId: Optional root cliente ID of block list on which to insert. + * updateSelection: If true block selection will be updated. If false, block selection will not change. Defaults to true. + +### showInsertionPoint + +Returns an action object used in signalling that the insertion point should +be shown. + +*Parameters* + + * rootClientId: Optional root client ID of block list on + which to insert. + * index: Index at which block should be inserted. + +### hideInsertionPoint + +Returns an action object hiding the insertion point. + +### setTemplateValidity + +Returns an action object resetting the template validity. + +*Parameters* + + * isValid: template validity flag. + +### synchronizeTemplate + +Returns an action object synchronize the template with the list of blocks + +### mergeBlocks + +Returns an action object used in signalling that two blocks should be merged + +*Parameters* + + * firstBlockClientId: Client ID of the first block to merge. + * secondBlockClientId: Client ID of the second block to merge. + +### removeBlocks + +Returns an action object used in signalling that the blocks corresponding to +the set of specified client IDs are to be removed. + +*Parameters* + + * clientIds: Client IDs of blocks to remove. + * selectPrevious: True if the previous block should be + selected when a block is removed. + +### removeBlock + +Returns an action object used in signalling that the block with the +specified client ID is to be removed. + +*Parameters* + + * clientId: Client ID of block to remove. + * selectPrevious: True if the previous block should be + selected when a block is removed. + +### toggleBlockMode + +Returns an action object used to toggle the block editing mode between +visual and HTML modes. + +*Parameters* + + * clientId: Block client ID. + +### startTyping + +Returns an action object used in signalling that the user has begun to type. + +### stopTyping + +Returns an action object used in signalling that the user has stopped typing. + +### enterFormattedText + +Returns an action object used in signalling that the caret has entered formatted text. + +### exitFormattedText + +Returns an action object used in signalling that the user caret has exited formatted text. + +### insertDefaultBlock + +Returns an action object used in signalling that a new block of the default +type should be added to the block list. + +*Parameters* + + * attributes: Optional attributes of the block to assign. + * rootClientId: Optional root client ID of block list on which + to append. + * index: Optional index where to insert the default block + +### updateBlockListSettings + +Returns an action object that changes the nested settings of a given block. + +*Parameters* + + * clientId: Client ID of the block whose nested setting are + being received. + * settings: Object with the new settings for the nested block. + +### updateEditorSettings + +Returns an action object used in signalling that the editor settings have been updated. + +*Parameters* + + * settings: Updated settings + +### redo + +Returns an action object used in signalling that undo history should +restore last popped state. + +### undo + +Returns an action object used in signalling that undo history should pop. + +### createUndoLevel + +Returns an action object used in signalling that undo history record should +be created. \ No newline at end of file diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 800537ba65d22..b35f1fec8fc68 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -1,28 +1,7 @@ -# **core/editor**: The Editor’s Data +# **core/editor**: The Post Editor’s Data ## Selectors -### hasEditorUndo - -Returns true if any past editor history snapshots exist, or false otherwise. - -*Parameters* - - * state: Global application state. - -### hasEditorRedo - -Returns true if any future editor history snapshots exist, or false -otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether redo history exists. - ### isEditedPostNew Returns true if the currently edited post is yet to be saved, or false if @@ -32,10 +11,6 @@ the post has been saved. * state: Global application state. -*Returns* - -Whether the post is new. - ### hasChangedContent Returns true if content includes unsaved changes, or false otherwise. @@ -365,117 +340,71 @@ and modified date are the same. Whether the edited post has a floating date value. -### getBlockDependantsCacheBust - -Returns a new reference when the inner blocks of a given block client ID -change. This is used exclusively as a memoized selector dependant, relying -on this selector's shared return value and recursively those of its inner -blocks defined as dependencies. This abuses mechanics of the selector -memoization to return from the original selector function only when -dependants change. - -*Parameters* - - * state: Editor state. - * clientId: Block client ID. - -*Returns* - -A value whose reference will change only when inner blocks of - the given block client ID change. - -### getBlockName - -Returns a block's name given its client ID, or null if no block exists with -the client ID. - -*Parameters* - - * state: Editor state. - * clientId: Block client ID. - -*Returns* - -Block name. - -### isBlockValid +### isSavingPost -Returns whether a block is valid or not. +Returns true if the post is currently being saved, or false otherwise. *Parameters* - * state: Editor state. - * clientId: Block client ID. + * state: Global application state. *Returns* -Is Valid. +Whether post is being saved. -### getBlockAttributes +### didPostSaveRequestSucceed -Returns a block's attributes given its client ID, or null if no block exists with -the client ID. +Returns true if a previous post save was attempted successfully, or false +otherwise. *Parameters* - * state: Editor state. - * clientId: Block client ID. + * state: Global application state. *Returns* -Block attributes. +Whether the post was saved successfully. -### getBlock +### didPostSaveRequestFail -Returns a block given its client ID. This is a parsed copy of the block, -containing its `blockName`, `clientId`, and current `attributes` state. This -is not the block's registration settings, which must be retrieved from the -blocks module registration store. +Returns true if a previous post save was attempted but failed, or false +otherwise. *Parameters* - * state: Editor state. - * clientId: Block client ID. + * state: Global application state. *Returns* -Parsed block object. - -### getBlocks +Whether the post save failed. -Returns all block objects for the current post being edited as an array in -the order they appear in the post. +### isAutosavingPost -Note: It's important to memoize this selector to avoid return a new instance -on each call +Returns true if the post is autosaving, or false otherwise. *Parameters* - * state: Editor state. - * rootClientId: Optional root client ID of block list. + * state: Global application state. *Returns* -Post blocks. +Whether the post is autosaving. -### getClientIdsOfDescendants +### isPreviewingPost -Returns an array containing the clientIds of all descendants -of the blocks given. +Returns true if the post is being previewed, or false otherwise. *Parameters* * state: Global application state. - * clientIds: Array of blocks to inspect. *Returns* -ids of descendants. +Whether the post is being previewed. -### getClientIdsWithDescendants +### getEditedPostPreviewLink -Returns an array containing the clientIds of the top-level blocks -and their descendants of any depth (for nested blocks). +Returns the post preview link *Parameters* @@ -483,117 +412,117 @@ and their descendants of any depth (for nested blocks). *Returns* -ids of top-level and descendant blocks. +Preview Link. -### getGlobalBlockCount +### getSuggestedPostFormat -Returns the total number of blocks, or the total number of blocks with a specific name in a post. -The number returned includes nested blocks. +Returns a suggested post format for the current post, inferred only if there +is a single block within the post and it is of a type known to match a +default post format. Returns null if the format cannot be determined. *Parameters* * state: Global application state. - * blockName: Optional block name, if specified only blocks of that type will be counted. *Returns* -Number of blocks in the post, or number of blocks with name equal to blockName. +Suggested post format. -### getBlocksByClientId +### getBlocksForSerialization -Given an array of block client IDs, returns the corresponding array of block -objects. +Returns a set of blocks which are to be used in consideration of the post's +generated save content. *Parameters* * state: Editor state. - * clientIds: Client IDs for which blocks are to be returned. *Returns* -Block objects. +Filtered set of blocks for save. -### getBlockCount +### getEditedPostContent -Returns the number of blocks currently present in the post. +Returns the content of the post being edited, preferring raw string edit +before falling back to serialization of block state. *Parameters* - * state: Editor state. - * rootClientId: Optional root client ID of block list. + * state: Global application state. *Returns* -Number of blocks in the post. +Post content. -### getBlockSelectionStart +### __experimentalGetReusableBlock -Returns the current block selection start. This value may be null, and it -may represent either a singular block selection or multi-selection start. -A selection is singular if its start and end match. +Returns the reusable block with the given ID. *Parameters* * state: Global application state. + * ref: The reusable block's ID. *Returns* -Client ID of block selection start. +The reusable block, or null if none exists. -### getBlockSelectionEnd +### __experimentalIsSavingReusableBlock -Returns the current block selection end. This value may be null, and it -may represent either a singular block selection or multi-selection end. -A selection is singular if its start and end match. +Returns whether or not the reusable block with the given ID is being saved. *Parameters* * state: Global application state. + * ref: The reusable block's ID. *Returns* -Client ID of block selection end. +Whether or not the reusable block is being saved. -### getSelectedBlockCount +### __experimentalIsFetchingReusableBlock -Returns the number of blocks currently selected in the post. +Returns true if the reusable block with the given ID is being fetched, or +false otherwise. *Parameters* * state: Global application state. + * ref: The reusable block's ID. *Returns* -Number of blocks selected in the post. +Whether the reusable block is being fetched. -### hasSelectedBlock +### __experimentalGetReusableBlocks -Returns true if there is a single selected block, or false otherwise. +Returns an array of all reusable blocks. *Parameters* - * state: Editor state. + * state: Global application state. *Returns* -Whether a single block is selected. +An array of all reusable blocks. -### getSelectedBlockClientId +### getStateBeforeOptimisticTransaction -Returns the currently selected block client ID, or null if there is no -selected block. +Returns state object prior to a specified optimist transaction ID, or `null` +if the transaction corresponding to the given ID cannot be found. *Parameters* - * state: Editor state. + * state: Current global application state. + * transactionId: Optimist transaction ID. *Returns* -Selected block client ID. +Global application state prior to transaction. -### getSelectedBlock +### isPublishingPost -Returns the currently selected block, or null if there is no selected block. +Returns true if the post is being published, or false otherwise. *Parameters* @@ -601,91 +530,76 @@ Returns the currently selected block, or null if there is no selected block. *Returns* -Selected block. +Whether post is being published. -### getBlockRootClientId +### isPermalinkEditable -Given a block client ID, returns the root block from which the block is -nested, an empty string for top-level blocks, or null if the block does not -exist. +Returns whether the permalink is editable or not. *Parameters* * state: Editor state. - * clientId: Block from which to find root client ID. *Returns* -Root client ID, if exists +Whether or not the permalink is editable. -### getBlockHierarchyRootClientId +### getPermalink -Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. +Returns the permalink for the post. *Parameters* * state: Editor state. - * clientId: Block from which to find root client ID. *Returns* -Root client ID +The permalink, or null if the post is not viewable. -### getAdjacentBlockClientId +### getPermalinkParts -Returns the client ID of the block adjacent one at the given reference -startClientId and modifier directionality. Defaults start startClientId to -the selected block, and direction as next block. Returns null if there is no -adjacent block. +Returns the permalink for a post, split into it's three parts: the prefix, +the postName, and the suffix. *Parameters* * state: Editor state. - * startClientId: Optional client ID of block from which to - search. - * modifier: Directionality multiplier (1 next, -1 - previous). *Returns* -Return the client ID of the block, or null if none exists. +An object containing the prefix, postName, and suffix for + the permalink, or null if the post is not viewable. -### getPreviousBlockClientId +### inSomeHistory -Returns the previous block's client ID from the given reference start ID. -Defaults start to the selected block. Returns null if there is no previous -block. +Returns true if an optimistic transaction is pending commit, for which the +before state satisfies the given predicate function. *Parameters* * state: Editor state. - * startClientId: Optional client ID of block from which to - search. + * predicate: Function given state, returning true if match. *Returns* -Adjacent block's client ID, or null if none exists. +Whether predicate matches for some history. -### getNextBlockClientId +### getBlockListSettings -Returns the next block's client ID from the given reference start ID. -Defaults start to the selected block. Returns null if there is no next -block. +Returns the Block List settings of a block, if any exist. *Parameters* * state: Editor state. - * startClientId: Optional client ID of block from which to - search. + * clientId: Block client ID. *Returns* -Adjacent block's client ID, or null if none exists. +Block settings of the block if set. -### getSelectedBlocksInitialCaretPosition +### isPostLocked -Returns the initial caret position for the selected block. -This position is to used to position the caret properly when the selected block changes. +Returns whether the post is locked. *Parameters* @@ -693,1098 +607,205 @@ This position is to used to position the caret properly when the selected block *Returns* -Selected block. +Is locked. -### getMultiSelectedBlockClientIds +### isPostSavingLocked -Returns the current multi-selection set of block client IDs, or an empty -array if there is no multi-selection. +Returns whether post saving is locked. *Parameters* - * state: Editor state. + * state: Global application state. *Returns* -Multi-selected block client IDs. +Is locked. -### getMultiSelectedBlocks +### isPostLockTakeover -Returns the current multi-selection set of blocks, or an empty array if -there is no multi-selection. +Returns whether the edition of the post has been taken over. *Parameters* - * state: Editor state. + * state: Global application state. *Returns* -Multi-selected block objects. +Is post lock takeover. -### getFirstMultiSelectedBlockClientId +### getPostLockUser -Returns the client ID of the first block in the multi-selection set, or null -if there is no multi-selection. +Returns details about the post lock user. *Parameters* - * state: Editor state. + * state: Global application state. *Returns* -First block client ID in the multi-selection set. +A user object. -### getLastMultiSelectedBlockClientId +### getActivePostLock -Returns the client ID of the last block in the multi-selection set, or null -if there is no multi-selection. +Returns the active post lock. *Parameters* - * state: Editor state. + * state: Global application state. *Returns* -Last block client ID in the multi-selection set. +The lock object. -### isFirstMultiSelectedBlock +### canUserUseUnfilteredHTML -Returns true if a multi-selection exists, and the block corresponding to the -specified client ID is the first block of the multi-selection set, or false -otherwise. +Returns whether or not the user has the unfiltered_html capability. *Parameters* * state: Editor state. - * clientId: Block client ID. *Returns* -Whether block is first in multi-selection. +Whether the user can or can't post unfiltered HTML. -### isBlockMultiSelected +### isPublishSidebarEnabled -Returns true if the client ID occurs within the block multi-selection, or -false otherwise. +Returns whether the pre-publish panel should be shown +or skipped when the user clicks the "publish" button. *Parameters* - * state: Editor state. - * clientId: Block client ID. + * state: Global application state. *Returns* -Whether block is in multi-selection set. +Whether the pre-publish panel should be shown or not. -### isAncestorMultiSelected +### getEditorBlocks -Returns true if an ancestor of the block is multi-selected, or false -otherwise. +Return the current block list. *Parameters* - * state: Editor state. - * clientId: Block client ID. + * state: null *Returns* -Whether an ancestor of the block is in multi-selection - set. +Block list. -### getMultiSelectedBlocksStartClientId +## Actions -Returns the client ID of the block which begins the multi-selection set, or -null if there is no multi-selection. +### setupEditor -This is not necessarily the first client ID in the selection. +Returns an action object used in signalling that editor has initialized with +the specified post object and editor settings. *Parameters* - * state: Editor state. + * post: Post object. + * edits: Initial edited attributes object. + * template: Block Template. -*Returns* +### resetPost + +Returns an action object used in signalling that the latest version of the +post has been received, either by initialization or save. -Client ID of block beginning multi-selection. +*Parameters* -### getMultiSelectedBlocksEndClientId + * post: Post object. -Returns the client ID of the block which ends the multi-selection set, or -null if there is no multi-selection. +### resetAutosave -This is not necessarily the last client ID in the selection. +Returns an action object used in signalling that the latest autosave of the +post has been received, by initialization or autosave. *Parameters* - * state: Editor state. + * post: Autosave post object. -*Returns* +### updatePost -Client ID of block ending multi-selection. +Returns an action object used in signalling that a patch of updates for the +latest version of the post have been received. -### getBlockOrder +*Parameters* -Returns an array containing all block client IDs in the editor in the order -they appear. Optionally accepts a root client ID of the block list for which -the order should be returned, defaulting to the top-level block order. + * edits: Updated post fields. -*Parameters* +### setupEditorState - * state: Editor state. - * rootClientId: Optional root client ID of block list. +Returns an action object used to setup the editor state when first opening an editor. -*Returns* +*Parameters* -Ordered client IDs of editor blocks. + * post: Post object. + * blocks: Array of blocks. -### getBlockIndex +### editPost -Returns the index at which the block corresponding to the specified client -ID occurs within the block order, or `-1` if the block does not exist. +Returns an action object used in signalling that attributes of the post have +been edited. *Parameters* - * state: Editor state. - * clientId: Block client ID. - * rootClientId: Optional root client ID of block list. + * edits: Post attributes to edit. -*Returns* +### savePost -Index at which block exists in order. +Returns an action object to save the post. -### isBlockSelected +*Parameters* -Returns true if the block corresponding to the specified client ID is -currently selected and no multi-selection exists, or false otherwise. + * options: Options for the save. + * options.isAutosave: Perform an autosave if true. -*Parameters* +### autosave - * state: Editor state. - * clientId: Block client ID. +Returns an action object used in signalling that the post should autosave. -*Returns* +*Parameters* -Whether block is selected and multi-selection exists. + * options: Extra flags to identify the autosave. -### hasSelectedInnerBlock +### updatePostLock -Returns true if one of the block's inner blocks is selected. +Returns an action object used to lock the editor. *Parameters* - * state: Editor state. - * clientId: Block client ID. - * deep: Perform a deep check. + * lock: Details about the post lock status, user, and nonce. -*Returns* +### __experimentalFetchReusableBlocks -Whether the block as an inner block selected +Returns an action object used to fetch a single reusable block or all +reusable blocks from the REST API into the store. -### isBlockWithinSelection +*Parameters* -Returns true if the block corresponding to the specified client ID is -currently selected but isn't the last of the selected blocks. Here "last" -refers to the block sequence in the document, _not_ the sequence of -multi-selection, which is why `state.blockSelection.end` isn't used. + * id: If given, only a single reusable block with this ID will + be fetched. -*Parameters* +### __experimentalReceiveReusableBlocks - * state: Editor state. - * clientId: Block client ID. +Returns an action object used in signalling that reusable blocks have been +received. `results` is an array of objects containing: + - `reusableBlock` - Details about how the reusable block is persisted. + - `parsedBlock` - The original block. -*Returns* +*Parameters* -Whether block is selected and not the last in the - selection. + * results: Reusable blocks received. -### hasMultiSelection +### __experimentalSaveReusableBlock -Returns true if a multi-selection has been made, or false otherwise. +Returns an action object used to save a reusable block that's in the store to +the REST API. *Parameters* - * state: Editor state. - -*Returns* - -Whether multi-selection has been made. - -### isMultiSelecting - -Whether in the process of multi-selecting or not. This flag is only true -while the multi-selection is being selected (by mouse move), and is false -once the multi-selection has been settled. - -*Parameters* - - * state: Global application state. - -*Returns* - -True if multi-selecting, false if not. - -### isSelectionEnabled - -Selector that returns if multi-selection is enabled or not. - -*Parameters* - - * state: Global application state. - -*Returns* - -True if it should be possible to multi-select blocks, false if multi-selection is disabled. - -### getBlockMode - -Returns the block's editing mode, defaulting to "visual" if not explicitly -assigned. - -*Parameters* - - * state: Editor state. - * clientId: Block client ID. - -*Returns* - -Block editing mode. - -### isTyping - -Returns true if the user is typing, or false otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether user is typing. - -### isCaretWithinFormattedText - -Returns true if the caret is within formatted text, or false otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether the caret is within formatted text. - -### getBlockInsertionPoint - -Returns the insertion point, the index at which the new inserted block would -be placed. Defaults to the last index. - -*Parameters* - - * state: Editor state. - -*Returns* - -Insertion point object with `rootClientId`, `index`. - -### isBlockInsertionPointVisible - -Returns true if we should show the block insertion point. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether the insertion point is visible or not. - -### isValidTemplate - -Returns whether the blocks matches the template or not. - -*Parameters* - - * state: null - -*Returns* - -Whether the template is valid or not. - -### getTemplate - -Returns the defined block template - -*Parameters* - - * state: null - -*Returns* - -Block Template - -### getTemplateLock - -Returns the defined block template lock. Optionally accepts a root block -client ID as context, otherwise defaulting to the global context. - -*Parameters* - - * state: Editor state. - * rootClientId: Optional block root client ID. - -*Returns* - -Block Template Lock - -### isSavingPost - -Returns true if the post is currently being saved, or false otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether post is being saved. - -### didPostSaveRequestSucceed - -Returns true if a previous post save was attempted successfully, or false -otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether the post was saved successfully. - -### didPostSaveRequestFail - -Returns true if a previous post save was attempted but failed, or false -otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether the post save failed. - -### isAutosavingPost - -Returns true if the post is autosaving, or false otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether the post is autosaving. - -### isPreviewingPost - -Returns true if the post is being previewed, or false otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether the post is being previewed. - -### getEditedPostPreviewLink - -Returns the post preview link - -*Parameters* - - * state: Global application state. - -*Returns* - -Preview Link. - -### getSuggestedPostFormat - -Returns a suggested post format for the current post, inferred only if there -is a single block within the post and it is of a type known to match a -default post format. Returns null if the format cannot be determined. - -*Parameters* - - * state: Global application state. - -*Returns* - -Suggested post format. - -### getBlocksForSerialization - -Returns a set of blocks which are to be used in consideration of the post's -generated save content. - -*Parameters* - - * state: Editor state. - -*Returns* - -Filtered set of blocks for save. - -### getEditedPostContent - -Returns the content of the post being edited, preferring raw string edit -before falling back to serialization of block state. - -*Parameters* - - * state: Global application state. - -*Returns* - -Post content. - -### canInsertBlockType - -Determines if the given block type is allowed to be inserted into the block list. - -*Parameters* - - * state: Editor state. - * blockName: The name of the block type, e.g.' core/paragraph'. - * rootClientId: Optional root client ID of block list. - -*Returns* - -Whether the given block type is allowed to be inserted. - -### getInserterItems - -Determines the items that appear in the inserter. Includes both static -items (e.g. a regular block type) and dynamic items (e.g. a reusable block). - -Each item object contains what's necessary to display a button in the -inserter and handle its selection. - -The 'utility' property indicates how useful we think an item will be to the -user. There are 4 levels of utility: - -1. Blocks that are contextually useful (utility = 3) -2. Blocks that have been previously inserted (utility = 2) -3. Blocks that are in the common category (utility = 1) -4. All other blocks (utility = 0) - -The 'frecency' property is a heuristic (https://en.wikipedia.org/wiki/Frecency) -that combines block usage frequenty and recency. - -Items are returned ordered descendingly by their 'utility' and 'frecency'. - -*Parameters* - - * state: Editor state. - * rootClientId: Optional root client ID of block list. - -*Returns* - -Items that appear in inserter. - -### hasInserterItems - -Determines whether there are items to show in the inserter. - -*Parameters* - - * state: Editor state. - * rootClientId: Optional root client ID of block list. - -*Returns* - -Items that appear in inserter. - -### __experimentalGetReusableBlock - -Returns the reusable block with the given ID. - -*Parameters* - - * state: Global application state. - * ref: The reusable block's ID. - -*Returns* - -The reusable block, or null if none exists. - -### __experimentalIsSavingReusableBlock - -Returns whether or not the reusable block with the given ID is being saved. - -*Parameters* - - * state: Global application state. - * ref: The reusable block's ID. - -*Returns* - -Whether or not the reusable block is being saved. - -### __experimentalIsFetchingReusableBlock - -Returns true if the reusable block with the given ID is being fetched, or -false otherwise. - -*Parameters* - - * state: Global application state. - * ref: The reusable block's ID. - -*Returns* - -Whether the reusable block is being fetched. - -### __experimentalGetReusableBlocks - -Returns an array of all reusable blocks. - -*Parameters* - - * state: Global application state. - -*Returns* - -An array of all reusable blocks. - -### getStateBeforeOptimisticTransaction - -Returns state object prior to a specified optimist transaction ID, or `null` -if the transaction corresponding to the given ID cannot be found. - -*Parameters* - - * state: Current global application state. - * transactionId: Optimist transaction ID. - -*Returns* - -Global application state prior to transaction. - -### isPublishingPost - -Returns true if the post is being published, or false otherwise. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether post is being published. - -### isPermalinkEditable - -Returns whether the permalink is editable or not. - -*Parameters* - - * state: Editor state. - -*Returns* - -Whether or not the permalink is editable. - -### getPermalink - -Returns the permalink for the post. - -*Parameters* - - * state: Editor state. - -*Returns* - -The permalink, or null if the post is not viewable. - -### getPermalinkParts - -Returns the permalink for a post, split into it's three parts: the prefix, -the postName, and the suffix. - -*Parameters* - - * state: Editor state. - -*Returns* - -An object containing the prefix, postName, and suffix for - the permalink, or null if the post is not viewable. - -### inSomeHistory - -Returns true if an optimistic transaction is pending commit, for which the -before state satisfies the given predicate function. - -*Parameters* - - * state: Editor state. - * predicate: Function given state, returning true if match. - -*Returns* - -Whether predicate matches for some history. - -### getBlockListSettings - -Returns the Block List settings of a block, if any exist. - -*Parameters* - - * state: Editor state. - * clientId: Block client ID. - -*Returns* - -Block settings of the block if set. - -### getEditorSettings - -Returns the editor settings. - -*Parameters* - - * state: Editor state. - -*Returns* - -The editor settings object. - -### getTokenSettings - -Returns the token settings. - -*Parameters* - - * state: Editor state. - * name: Token name. - -*Returns* - -Token settings object, or the named token settings object if set. - -### isPostLocked - -Returns whether the post is locked. - -*Parameters* - - * state: Global application state. - -*Returns* - -Is locked. - -### isPostSavingLocked - -Returns whether post saving is locked. - -*Parameters* - - * state: Global application state. - -*Returns* - -Is locked. - -### isPostLockTakeover - -Returns whether the edition of the post has been taken over. - -*Parameters* - - * state: Global application state. - -*Returns* - -Is post lock takeover. - -### getPostLockUser - -Returns details about the post lock user. - -*Parameters* - - * state: Global application state. - -*Returns* - -A user object. - -### getActivePostLock - -Returns the active post lock. - -*Parameters* - - * state: Global application state. - -*Returns* - -The lock object. - -### canUserUseUnfilteredHTML - -Returns whether or not the user has the unfiltered_html capability. - -*Parameters* - - * state: Editor state. - -*Returns* - -Whether the user can or can't post unfiltered HTML. - -### isPublishSidebarEnabled - -Returns whether the pre-publish panel should be shown -or skipped when the user clicks the "publish" button. - -*Parameters* - - * state: Global application state. - -*Returns* - -Whether the pre-publish panel should be shown or not. - -## Actions - -### setupEditor - -Returns an action object used in signalling that editor has initialized with -the specified post object and editor settings. - -*Parameters* - - * post: Post object. - * edits: Initial edited attributes object. - -### resetPost - -Returns an action object used in signalling that the latest version of the -post has been received, either by initialization or save. - -*Parameters* - - * post: Post object. - -### resetAutosave - -Returns an action object used in signalling that the latest autosave of the -post has been received, by initialization or autosave. - -*Parameters* - - * post: Autosave post object. - -### updatePost - -Returns an action object used in signalling that a patch of updates for the -latest version of the post have been received. - -*Parameters* - - * edits: Updated post fields. - -### setupEditorState - -Returns an action object used to setup the editor state when first opening an editor. - -*Parameters* - - * post: Post object. - * blocks: Array of blocks. - -### resetBlocks - -Returns an action object used in signalling that blocks state should be -reset to the specified array of blocks, taking precedence over any other -content reflected as an edit in state. - -*Parameters* - - * blocks: Array of blocks. - -### receiveBlocks - -Returns an action object used in signalling that blocks have been received. -Unlike resetBlocks, these should be appended to the existing known set, not -replacing. - -*Parameters* - - * blocks: Array of block objects. - -### updateBlockAttributes - -Returns an action object used in signalling that the block attributes with -the specified client ID has been updated. - -*Parameters* - - * clientId: Block client ID. - * attributes: Block attributes to be merged. - -### updateBlock - -Returns an action object used in signalling that the block with the -specified client ID has been updated. - -*Parameters* - - * clientId: Block client ID. - * updates: Block attributes to be merged. - -### selectBlock - -Returns an action object used in signalling that the block with the -specified client ID has been selected, optionally accepting a position -value reflecting its selection directionality. An initialPosition of -1 -reflects a reverse selection. - -*Parameters* - - * clientId: Block client ID. - * initialPosition: Optional initial position. Pass as -1 to - reflect reverse selection. - -### toggleSelection - -Returns an action object that enables or disables block selection. - -*Parameters* - - * boolean: [isSelectionEnabled=true] Whether block selection should - be enabled. - -### replaceBlocks - -Returns an action object signalling that a blocks should be replaced with -one or more replacement blocks. - -*Parameters* - - * clientIds: Block client ID(s) to replace. - * blocks: Replacement block(s). - -### replaceBlock - -Returns an action object signalling that a single block should be replaced -with one or more replacement blocks. - -*Parameters* - - * clientId: Block client ID to replace. - * block: Replacement block(s). - -### moveBlockToPosition - -Returns an action object signalling that an indexed block should be moved -to a new index. - -*Parameters* - - * clientId: The client ID of the block. - * fromRootClientId: Root client ID source. - * toRootClientId: Root client ID destination. - * index: The index to move the block into. - -### insertBlock - -Returns an action object used in signalling that a single block should be -inserted, optionally at a specific index respective a root block list. - -*Parameters* - - * block: Block object to insert. - * index: Index at which block should be inserted. - * rootClientId: Optional root client ID of block list on which to insert. - * updateSelection: If true block selection will be updated. If false, block selection will not change. Defaults to true. - -### insertBlocks - -Returns an action object used in signalling that an array of blocks should -be inserted, optionally at a specific index respective a root block list. - -*Parameters* - - * blocks: Block objects to insert. - * index: Index at which block should be inserted. - * rootClientId: Optional root cliente ID of block list on which to insert. - * updateSelection: If true block selection will be updated. If false, block selection will not change. Defaults to true. - -### showInsertionPoint - -Returns an action object used in signalling that the insertion point should -be shown. - -*Parameters* - - * rootClientId: Optional root client ID of block list on - which to insert. - * index: Index at which block should be inserted. - -### hideInsertionPoint - -Returns an action object hiding the insertion point. - -### setTemplateValidity - -Returns an action object resetting the template validity. - -*Parameters* - - * isValid: template validity flag. - -### synchronizeTemplate - -Returns an action object synchronize the template with the list of blocks - -### editPost - -Returns an action object used in signalling that attributes of the post have -been edited. - -*Parameters* - - * edits: Post attributes to edit. - -### savePost - -Returns an action object to save the post. - -*Parameters* - - * options: Options for the save. - * options.isAutosave: Perform an autosave if true. - -### mergeBlocks - -Returns an action object used in signalling that two blocks should be merged - -*Parameters* - - * firstBlockClientId: Client ID of the first block to merge. - * secondBlockClientId: Client ID of the second block to merge. - -### autosave - -Returns an action object used in signalling that the post should autosave. - -*Parameters* - - * options: Extra flags to identify the autosave. - -### redo - -Returns an action object used in signalling that undo history should -restore last popped state. - -### undo - -Returns an action object used in signalling that undo history should pop. - -### createUndoLevel - -Returns an action object used in signalling that undo history record should -be created. - -### removeBlocks - -Returns an action object used in signalling that the blocks corresponding to -the set of specified client IDs are to be removed. - -*Parameters* - - * clientIds: Client IDs of blocks to remove. - * selectPrevious: True if the previous block should be - selected when a block is removed. - -### removeBlock - -Returns an action object used in signalling that the block with the -specified client ID is to be removed. - -*Parameters* - - * clientId: Client ID of block to remove. - * selectPrevious: True if the previous block should be - selected when a block is removed. - -### toggleBlockMode - -Returns an action object used to toggle the block editing mode between -visual and HTML modes. - -*Parameters* - - * clientId: Block client ID. - -### startTyping - -Returns an action object used in signalling that the user has begun to type. - -### stopTyping - -Returns an action object used in signalling that the user has stopped typing. - -### enterFormattedText - -Returns an action object used in signalling that the caret has entered formatted text. - -### exitFormattedText - -Returns an action object used in signalling that the user caret has exited formatted text. - -### updatePostLock - -Returns an action object used to lock the editor. - -*Parameters* - - * lock: Details about the post lock status, user, and nonce. - -### __experimentalFetchReusableBlocks - -Returns an action object used to fetch a single reusable block or all -reusable blocks from the REST API into the store. - -*Parameters* - - * id: If given, only a single reusable block with this ID will - be fetched. - -### __experimentalReceiveReusableBlocks - -Returns an action object used in signalling that reusable blocks have been -received. `results` is an array of objects containing: - - `reusableBlock` - Details about how the reusable block is persisted. - - `parsedBlock` - The original block. - -*Parameters* - - * results: Reusable blocks received. - -### __experimentalSaveReusableBlock - -Returns an action object used to save a reusable block that's in the store to -the REST API. - -*Parameters* - - * id: The ID of the reusable block to save. + * id: The ID of the reusable block to save. ### __experimentalDeleteReusableBlock @@ -1820,36 +841,6 @@ Returns an action object used to convert a static block into a reusable block. * clientIds: The client IDs of the block to detach. -### insertDefaultBlock - -Returns an action object used in signalling that a new block of the default -type should be added to the block list. - -*Parameters* - - * attributes: Optional attributes of the block to assign. - * rootClientId: Optional root client ID of block list on which - to append. - * index: Optional index where to insert the default block - -### updateBlockListSettings - -Returns an action object that changes the nested settings of a given block. - -*Parameters* - - * clientId: Client ID of the block whose nested setting are - being received. - * settings: Object with the new settings for the nested block. - -### updateEditorSettings - -Returns an action object used in signalling that the editor settings have been updated. - -*Parameters* - - * settings: Updated settings - ### enablePublishSidebar Returns an action object used in signalling that the user has enabled the publish sidebar. @@ -1872,4 +863,12 @@ Returns an action object used to signal that post saving is unlocked. *Parameters* - * lockName: The lock name. \ No newline at end of file + * lockName: The lock name. + +### updateEditorBlocks + +Returns an action object used to signal that the blocks have been updated. + +*Parameters* + + * blocks: Block Array. \ No newline at end of file diff --git a/docs/manifest.json b/docs/manifest.json index 4dc762893f571..34b83408e86cd 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -425,6 +425,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/blob/README.md", "parent": "packages" }, + { + "title": "@wordpress/block-editor", + "slug": "packages-block-editor", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/block-editor/README.md", + "parent": "packages" + }, { "title": "@wordpress/block-library", "slug": "packages-block-library", @@ -1074,7 +1080,13 @@ "parent": "data" }, { - "title": "The Editor’s Data", + "title": "The Block Editor’s Data", + "slug": "data-core-block-editor", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-block-editor.md", + "parent": "data" + }, + { + "title": "The Post Editor’s Data", "slug": "data-core-editor", "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/data/data-core-editor.md", "parent": "data" diff --git a/docs/tool/config.js b/docs/tool/config.js index 87900c4c624fa..6dd2341e3d3c8 100644 --- a/docs/tool/config.js +++ b/docs/tool/config.js @@ -25,8 +25,13 @@ module.exports = { selectors: [ path.resolve( root, 'packages/blocks/src/store/selectors.js' ) ], actions: [ path.resolve( root, 'packages/blocks/src/store/actions.js' ) ], }, + 'core/block-editor': { + title: 'The Block Editor’s Data', + selectors: [ path.resolve( root, 'packages/block-editor/src/store/selectors.js' ) ], + actions: [ path.resolve( root, 'packages/block-editor/src/store/actions.js' ) ], + }, 'core/editor': { - title: 'The Editor’s Data', + title: 'The Post Editor’s Data', selectors: [ path.resolve( root, 'packages/editor/src/store/selectors.js' ) ], actions: [ path.resolve( root, 'packages/editor/src/store/actions.js' ) ], }, diff --git a/lib/packages-dependencies.php b/lib/packages-dependencies.php index 34c70851ff727..e8f6ae5463dfc 100644 --- a/lib/packages-dependencies.php +++ b/lib/packages-dependencies.php @@ -45,6 +45,15 @@ ), 'wp-block-serialization-default-parser' => array(), 'wp-block-serialization-spec-parser' => array(), + 'wp-block-editor' => array( + 'lodash', + 'wp-blocks', + 'wp-compose', + 'wp-components', + 'wp-data', + 'wp-element', + 'wp-i18n', + ), 'wp-blocks' => array( 'lodash', 'wp-autop', @@ -137,6 +146,7 @@ 'wp-a11y', 'wp-api-fetch', 'wp-blob', + 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', diff --git a/package-lock.json b/package-lock.json index 52818081f01b4..a5f33d2c6c056 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2340,6 +2340,21 @@ "@babel/runtime": "^7.0.0" } }, + "@wordpress/block-editor": { + "version": "file:packages/block-editor", + "requires": { + "@babel/runtime": "^7.0.0", + "@wordpress/blocks": "file:packages/blocks", + "@wordpress/components": "file:packages/components", + "@wordpress/compose": "file:packages/compose", + "@wordpress/data": "file:packages/data", + "@wordpress/element": "file:packages/element", + "@wordpress/i18n": "file:packages/i18n", + "lodash": "^4.17.10", + "redux-optimist": "^1.0.0", + "rememo": "^3.0.0" + } + }, "@wordpress/block-library": { "version": "file:packages/block-library", "requires": { diff --git a/package.json b/package.json index 95ce65ad2682f..363bd0474812e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/autop": "file:packages/autop", "@wordpress/blob": "file:packages/blob", + "@wordpress/block-editor": "file:packages/block-editor", "@wordpress/block-library": "file:packages/block-library", "@wordpress/block-serialization-default-parser": "file:packages/block-serialization-default-parser", "@wordpress/block-serialization-spec-parser": "file:packages/block-serialization-spec-parser", diff --git a/packages/block-editor/.npmrc b/packages/block-editor/.npmrc new file mode 100644 index 0000000000000..43c97e719a5a8 --- /dev/null +++ b/packages/block-editor/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md new file mode 100644 index 0000000000000..b5527e39e4e7c --- /dev/null +++ b/packages/block-editor/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0 (Unreleased) + +### New Features + +- Initial version. diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md new file mode 100644 index 0000000000000..9e07cf10eb2ce --- /dev/null +++ b/packages/block-editor/README.md @@ -0,0 +1,13 @@ +# Block Editor + +Generic Block Editor Module. + +## Installation + +Install the module + +```bash +npm install @wordpress/block-editor --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json new file mode 100644 index 0000000000000..d4f3cf7f757c7 --- /dev/null +++ b/packages/block-editor/package.json @@ -0,0 +1,38 @@ +{ + "name": "@wordpress/block-editor", + "version": "1.0.0-alpha.0", + "description": "Generic Block Editor.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "editor", + "blocks" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/block-editor/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "dependencies": { + "@babel/runtime": "^7.0.0", + "@wordpress/blocks": "file:../blocks", + "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", + "@wordpress/data": "file:../data", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "lodash": "^4.17.10", + "refx": "^3.0.0", + "rememo": "^3.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js new file mode 100644 index 0000000000000..cb2cca8f110ba --- /dev/null +++ b/packages/block-editor/src/components/index.js @@ -0,0 +1 @@ +export { default as BlockEditorProvider } from './provider'; diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js new file mode 100644 index 0000000000000..8e547fa95423a --- /dev/null +++ b/packages/block-editor/src/components/provider/index.js @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import { flow } from 'lodash'; + +/** + * WordPress Dependencies + */ +import { createElement, Component } from '@wordpress/element'; +import { DropZoneProvider, SlotFillProvider } from '@wordpress/components'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; + +class BlockEditorProvider extends Component { + constructor( props ) { + super( ...arguments ); + props.updateEditorSettings( props.settings ); + props.initBlocks( props.value ); + this.persistedValue = props.select( 'core/block-editor' ).getBlocks(); + } + + componentDidUpdate( prevProps ) { + if ( this.props.settings !== prevProps.settings ) { + this.props.updateEditorSettings( this.props.settings ); + } + + if ( + this.props.blocks !== prevProps.blocks && + this.props.blocks !== this.persistedValue + ) { + this.persistedValue = this.props.blocks; + this.props.onChange( this.props.blocks ); + } + } + + render() { + const { + children, + } = this.props; + + const providers = [ + // Slot / Fill provider: + // + // - context.getSlot + // - context.registerSlot + // - context.unregisterSlot + [ + SlotFillProvider, + ], + + // DropZone provider: + [ + DropZoneProvider, + ], + ]; + + const createEditorElement = flow( + providers.map( ( [ Provider, props ] ) => ( + ( arg ) => createElement( Provider, props, arg ) + ) ) + ); + + return createEditorElement( children ); + } +} + +export default compose( [ + withSelect( ( select ) => { + return { + blocks: select( 'core/block-editor' ).getBlocks(), + select, + }; + } ), + withDispatch( ( dispatch ) => { + const { + updateEditorSettings, + initBlocks, + } = dispatch( 'core/block-editor' ); + + return { + updateEditorSettings, + initBlocks, + }; + } ), +] )( BlockEditorProvider ); diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js new file mode 100644 index 0000000000000..024b0225b3c3e --- /dev/null +++ b/packages/block-editor/src/index.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ + +import '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './store'; + +export * from './components'; diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js new file mode 100644 index 0000000000000..3bf63d4a3dece --- /dev/null +++ b/packages/block-editor/src/store/actions.js @@ -0,0 +1,500 @@ +/** + * External Dependencies + */ +import { castArray } from 'lodash'; + +/** + * WordPress dependencies + */ +import { getDefaultBlockName, createBlock } from '@wordpress/blocks'; + +/** + * Returns an action object used in signalling that blocks state should be + * intialized using a specified array of blocks, + * + * This action reset the undo/redo history + * + * @param {Array} blocks Array of blocks. + * + * @return {Object} Action object. + */ +export function initBlocks( blocks ) { + return { + type: 'INIT_BLOCKS', + blocks, + }; +} + +/** + * Returns an action object used in signalling that blocks state should be + * reset to the specified array of blocks, taking precedence over any other + * content reflected as an edit in state. + * + * @param {Array} blocks Array of blocks. + * + * @return {Object} Action object. + */ +export function resetBlocks( blocks ) { + return { + type: 'RESET_BLOCKS', + blocks, + }; +} + +/** + * Returns an action object used in signalling that blocks have been received. + * Unlike resetBlocks, these should be appended to the existing known set, not + * replacing. + * + * @param {Object[]} blocks Array of block objects. + * + * @return {Object} Action object. + */ +export function receiveBlocks( blocks ) { + return { + type: 'RECEIVE_BLOCKS', + blocks, + }; +} + +/** + * Returns an action object used in signalling that the block attributes with + * the specified client ID has been updated. + * + * @param {string} clientId Block client ID. + * @param {Object} attributes Block attributes to be merged. + * + * @return {Object} Action object. + */ +export function updateBlockAttributes( clientId, attributes ) { + return { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId, + attributes, + }; +} + +/** + * Returns an action object used in signalling that the block with the + * specified client ID has been updated. + * + * @param {string} clientId Block client ID. + * @param {Object} updates Block attributes to be merged. + * + * @return {Object} Action object. + */ +export function updateBlock( clientId, updates ) { + return { + type: 'UPDATE_BLOCK', + clientId, + updates, + }; +} + +/** + * Returns an action object used in signalling that the block with the + * specified client ID has been selected, optionally accepting a position + * value reflecting its selection directionality. An initialPosition of -1 + * reflects a reverse selection. + * + * @param {string} clientId Block client ID. + * @param {?number} initialPosition Optional initial position. Pass as -1 to + * reflect reverse selection. + * + * @return {Object} Action object. + */ +export function selectBlock( clientId, initialPosition = null ) { + return { + type: 'SELECT_BLOCK', + initialPosition, + clientId, + }; +} + +export function startMultiSelect() { + return { + type: 'START_MULTI_SELECT', + }; +} + +export function stopMultiSelect() { + return { + type: 'STOP_MULTI_SELECT', + }; +} + +export function multiSelect( start, end ) { + return { + type: 'MULTI_SELECT', + start, + end, + }; +} + +export function clearSelectedBlock() { + return { + type: 'CLEAR_SELECTED_BLOCK', + }; +} + +/** + * Returns an action object that enables or disables block selection. + * + * @param {boolean} [isSelectionEnabled=true] Whether block selection should + * be enabled. + + * @return {Object} Action object. + */ +export function toggleSelection( isSelectionEnabled = true ) { + return { + type: 'TOGGLE_SELECTION', + isSelectionEnabled, + }; +} + +/** + * Returns an action object signalling that a blocks should be replaced with + * one or more replacement blocks. + * + * @param {(string|string[])} clientIds Block client ID(s) to replace. + * @param {(Object|Object[])} blocks Replacement block(s). + * + * @return {Object} Action object. + */ +export function replaceBlocks( clientIds, blocks ) { + return { + type: 'REPLACE_BLOCKS', + clientIds: castArray( clientIds ), + blocks: castArray( blocks ), + time: Date.now(), + }; +} + +/** + * Returns an action object signalling that a single block should be replaced + * with one or more replacement blocks. + * + * @param {(string|string[])} clientId Block client ID to replace. + * @param {(Object|Object[])} block Replacement block(s). + * + * @return {Object} Action object. + */ +export function replaceBlock( clientId, block ) { + return replaceBlocks( clientId, block ); +} + +/** + * Higher-order action creator which, given the action type to dispatch creates + * an action creator for managing block movement. + * + * @param {string} type Action type to dispatch. + * + * @return {Function} Action creator. + */ +function createOnMove( type ) { + return ( clientIds, rootClientId ) => { + return { + clientIds: castArray( clientIds ), + type, + rootClientId, + }; + }; +} + +export const moveBlocksDown = createOnMove( 'MOVE_BLOCKS_DOWN' ); +export const moveBlocksUp = createOnMove( 'MOVE_BLOCKS_UP' ); + +/** + * Returns an action object signalling that an indexed block should be moved + * to a new index. + * + * @param {?string} clientId The client ID of the block. + * @param {?string} fromRootClientId Root client ID source. + * @param {?string} toRootClientId Root client ID destination. + * @param {number} index The index to move the block into. + * + * @return {Object} Action object. + */ +export function moveBlockToPosition( clientId, fromRootClientId, toRootClientId, index ) { + return { + type: 'MOVE_BLOCK_TO_POSITION', + fromRootClientId, + toRootClientId, + clientId, + index, + }; +} + +/** + * Returns an action object used in signalling that a single block should be + * inserted, optionally at a specific index respective a root block list. + * + * @param {Object} block Block object to insert. + * @param {?number} index Index at which block should be inserted. + * @param {?string} rootClientId Optional root client ID of block list on which to insert. + * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. + * + * @return {Object} Action object. + */ +export function insertBlock( block, index, rootClientId, updateSelection = true ) { + return insertBlocks( [ block ], index, rootClientId, updateSelection ); +} + +/** + * Returns an action object used in signalling that an array of blocks should + * be inserted, optionally at a specific index respective a root block list. + * + * @param {Object[]} blocks Block objects to insert. + * @param {?number} index Index at which block should be inserted. + * @param {?string} rootClientId Optional root cliente ID of block list on which to insert. + * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. + * + * @return {Object} Action object. + */ +export function insertBlocks( blocks, index, rootClientId, updateSelection = true ) { + return { + type: 'INSERT_BLOCKS', + blocks: castArray( blocks ), + index, + rootClientId, + time: Date.now(), + updateSelection, + }; +} + +/** + * Returns an action object used in signalling that the insertion point should + * be shown. + * + * @param {?string} rootClientId Optional root client ID of block list on + * which to insert. + * @param {?number} index Index at which block should be inserted. + * + * @return {Object} Action object. + */ +export function showInsertionPoint( rootClientId, index ) { + return { + type: 'SHOW_INSERTION_POINT', + rootClientId, + index, + }; +} + +/** + * Returns an action object hiding the insertion point. + * + * @return {Object} Action object. + */ +export function hideInsertionPoint() { + return { + type: 'HIDE_INSERTION_POINT', + }; +} + +/** + * Returns an action object resetting the template validity. + * + * @param {boolean} isValid template validity flag. + * + * @return {Object} Action object. + */ +export function setTemplateValidity( isValid ) { + return { + type: 'SET_TEMPLATE_VALIDITY', + isValid, + }; +} + +/** + * Returns an action object synchronize the template with the list of blocks + * + * @return {Object} Action object. + */ +export function synchronizeTemplate() { + return { + type: 'SYNCHRONIZE_TEMPLATE', + }; +} + +/** + * Returns an action object used in signalling that two blocks should be merged + * + * @param {string} firstBlockClientId Client ID of the first block to merge. + * @param {string} secondBlockClientId Client ID of the second block to merge. + * + * @return {Object} Action object. + */ +export function mergeBlocks( firstBlockClientId, secondBlockClientId ) { + return { + type: 'MERGE_BLOCKS', + blocks: [ firstBlockClientId, secondBlockClientId ], + }; +} + +/** + * Returns an action object used in signalling that the blocks corresponding to + * the set of specified client IDs are to be removed. + * + * @param {string|string[]} clientIds Client IDs of blocks to remove. + * @param {boolean} selectPrevious True if the previous block should be + * selected when a block is removed. + * + * @return {Object} Action object. + */ +export function removeBlocks( clientIds, selectPrevious = true ) { + return { + type: 'REMOVE_BLOCKS', + clientIds: castArray( clientIds ), + selectPrevious, + }; +} + +/** + * Returns an action object used in signalling that the block with the + * specified client ID is to be removed. + * + * @param {string} clientId Client ID of block to remove. + * @param {boolean} selectPrevious True if the previous block should be + * selected when a block is removed. + * + * @return {Object} Action object. + */ +export function removeBlock( clientId, selectPrevious ) { + return removeBlocks( [ clientId ], selectPrevious ); +} + +/** + * Returns an action object used to toggle the block editing mode between + * visual and HTML modes. + * + * @param {string} clientId Block client ID. + * + * @return {Object} Action object. + */ +export function toggleBlockMode( clientId ) { + return { + type: 'TOGGLE_BLOCK_MODE', + clientId, + }; +} + +/** + * Returns an action object used in signalling that the user has begun to type. + * + * @return {Object} Action object. + */ +export function startTyping() { + return { + type: 'START_TYPING', + }; +} + +/** + * Returns an action object used in signalling that the user has stopped typing. + * + * @return {Object} Action object. + */ +export function stopTyping() { + return { + type: 'STOP_TYPING', + }; +} + +/** + * Returns an action object used in signalling that the caret has entered formatted text. + * + * @return {Object} Action object. + */ +export function enterFormattedText() { + return { + type: 'ENTER_FORMATTED_TEXT', + }; +} + +/** + * Returns an action object used in signalling that the user caret has exited formatted text. + * + * @return {Object} Action object. + */ +export function exitFormattedText() { + return { + type: 'EXIT_FORMATTED_TEXT', + }; +} + +/** + * Returns an action object used in signalling that a new block of the default + * type should be added to the block list. + * + * @param {?Object} attributes Optional attributes of the block to assign. + * @param {?string} rootClientId Optional root client ID of block list on which + * to append. + * @param {?number} index Optional index where to insert the default block + * + * @return {Object} Action object + */ +export function insertDefaultBlock( attributes, rootClientId, index ) { + const block = createBlock( getDefaultBlockName(), attributes ); + + return insertBlock( block, index, rootClientId ); +} + +/** + * Returns an action object that changes the nested settings of a given block. + * + * @param {string} clientId Client ID of the block whose nested setting are + * being received. + * @param {Object} settings Object with the new settings for the nested block. + * + * @return {Object} Action object + */ +export function updateBlockListSettings( clientId, settings ) { + return { + type: 'UPDATE_BLOCK_LIST_SETTINGS', + clientId, + settings, + }; +} + +/* + * Returns an action object used in signalling that the editor settings have been updated. + * + * @param {Object} settings Updated settings + * + * @return {Object} Action object + */ +export function updateEditorSettings( settings ) { + return { + type: 'UPDATE_EDITOR_SETTINGS', + settings, + }; +} + +/** + * Returns an action object used in signalling that undo history should + * restore last popped state. + * + * @return {Object} Action object. + */ +export function redo() { + return { type: 'REDO' }; +} + +/** + * Returns an action object used in signalling that undo history should pop. + * + * @return {Object} Action object. + */ +export function undo() { + return { type: 'UNDO' }; +} + +/** + * Returns an action object used in signalling that undo history record should + * be created. + * + * @return {Object} Action object. + */ +export function createUndoLevel() { + return { type: 'CREATE_UNDO_LEVEL' }; +} diff --git a/packages/block-editor/src/store/array.js b/packages/block-editor/src/store/array.js new file mode 100644 index 0000000000000..176d8936450af --- /dev/null +++ b/packages/block-editor/src/store/array.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { castArray } from 'lodash'; + +/** + * Insert one or multiple elements into a given position of an array. + * + * @param {Array} array Source array. + * @param {*} elements Elements to insert. + * @param {number} index Insert Position. + * + * @return {Array} Result. + */ +export function insertAt( array, elements, index ) { + return [ + ...array.slice( 0, index ), + ...castArray( elements ), + ...array.slice( index ), + ]; +} + +/** + * Moves an element in an array. + * + * @param {Array} array Source array. + * @param {number} from Source index. + * @param {number} to Destination index. + * @param {number} count Number of elements to move. + * + * @return {Array} Result. + */ +export function moveTo( array, from, to, count = 1 ) { + const withoutMovedElements = [ ...array ]; + withoutMovedElements.splice( from, count ); + return insertAt( + withoutMovedElements, + array.slice( from, from + count ), + to, + ); +} diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js new file mode 100644 index 0000000000000..31d1574d6283a --- /dev/null +++ b/packages/block-editor/src/store/defaults.js @@ -0,0 +1,133 @@ +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; + +export const PREFERENCES_DEFAULTS = { + insertUsage: {}, +}; + +/** + * The default editor settings + * + * alignWide boolean Enable/Disable Wide/Full Alignments + * colors Array Palette colors + * fontSizes Array Available font sizes + * imageSizes Array Available image sizes + * maxWidth number Max width to constraint resizing + * blockTypes boolean|Array Allowed block types + * hasFixedToolbar boolean Whether or not the editor toolbar is fixed + * focusMode boolean Whether the focus mode is enabled or not + * richEditingEnabled boolean Whether rich editing is enabled or not + */ +export const EDITOR_SETTINGS_DEFAULTS = { + alignWide: false, + colors: [ + { + name: __( 'Pale pink' ), + slug: 'pale-pink', + color: '#f78da7', + }, + { name: __( 'Vivid red' ), + slug: 'vivid-red', + color: '#cf2e2e', + }, + { + name: __( 'Luminous vivid orange' ), + slug: 'luminous-vivid-orange', + color: '#ff6900', + }, + { + name: __( 'Luminous vivid amber' ), + slug: 'luminous-vivid-amber', + color: '#fcb900', + }, + { + name: __( 'Light green cyan' ), + slug: 'light-green-cyan', + color: '#7bdcb5', + }, + { + name: __( 'Vivid green cyan' ), + slug: 'vivid-green-cyan', + color: '#00d084', + }, + { + name: __( 'Pale cyan blue' ), + slug: 'pale-cyan-blue', + color: '#8ed1fc', + }, + { + name: __( 'Vivid cyan blue' ), + slug: 'vivid-cyan-blue', + color: '#0693e3', + }, + { + name: __( 'Very light gray' ), + slug: 'very-light-gray', + color: '#eeeeee', + }, + { + name: __( 'Cyan bluish gray' ), + slug: 'cyan-bluish-gray', + color: '#abb8c3', + }, + { + name: __( 'Very dark gray' ), + slug: 'very-dark-gray', + color: '#313131', + }, + ], + + fontSizes: [ + { + name: _x( 'Small', 'font size name' ), + size: 13, + slug: 'small', + }, + { + name: _x( 'Normal', 'font size name' ), + size: 16, + slug: 'normal', + }, + { + name: _x( 'Medium', 'font size name' ), + size: 20, + slug: 'medium', + }, + { + name: _x( 'Large', 'font size name' ), + size: 36, + slug: 'large', + }, + { + name: _x( 'Huge', 'font size name' ), + size: 48, + slug: 'huge', + }, + ], + + imageSizes: [ + { slug: 'thumbnail', label: __( 'Thumbnail' ) }, + { slug: 'medium', label: __( 'Medium' ) }, + { slug: 'large', label: __( 'Large' ) }, + { slug: 'full', label: __( 'Full Size' ) }, + ], + + // This is current max width of the block inner area + // It's used to constraint image resizing and this value could be overridden later by themes + maxWidth: 580, + + // Allowed block types for the editor, defaulting to true (all supported). + allowedBlockTypes: true, + + // Maximum upload size in bytes allowed for the site. + maxUploadFileSize: 0, + + // List of allowed mime types and file extensions. + allowedMimeTypes: null, + + // Whether richs editing is enabled or not. + richEditingEnabled: true, +}; + diff --git a/packages/block-editor/src/store/effects.js b/packages/block-editor/src/store/effects.js new file mode 100644 index 0000000000000..a902460644b69 --- /dev/null +++ b/packages/block-editor/src/store/effects.js @@ -0,0 +1,196 @@ +/** + * External dependencies + */ +import { last } from 'lodash'; + +/** + * WordPress dependencies + */ +import { speak } from '@wordpress/a11y'; +import { + getBlockType, + doBlocksMatchTemplate, + switchToBlockType, + synchronizeBlocksWithTemplate, +} from '@wordpress/blocks'; +import { _n, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { + replaceBlocks, + selectBlock, + setTemplateValidity, + insertDefaultBlock, + resetBlocks, +} from './actions'; +import { + getBlock, + getBlocks, + getSelectedBlockCount, + getBlockCount, + getBlockRootClientId, + getPreviousBlockClientId, + getSelectedBlockClientId, + getTemplateLock, + getTemplate, + isValidTemplate, +} from './selectors'; + +/** + * Block validity is a function of blocks state (at the point of a + * reset) and the template setting. As a compromise to its placement + * across distinct parts of state, it is implemented here as a side- + * effect of the block reset action. + * + * @param {Object} action RESET_BLOCKS action. + * @param {Object} store Store instance. + * + * @return {?Object} New validity set action if validity has changed. + */ +export function validateBlocksToTemplate( action, store ) { + const state = store.getState(); + const template = getTemplate( state ); + const templateLock = getTemplateLock( state ); + + // Unlocked templates are considered always valid because they act + // as default values only. + const isBlocksValidToTemplate = ( + ! template || + templateLock !== 'all' || + doBlocksMatchTemplate( action.blocks, template ) + ); + + // Update if validity has changed. + if ( isBlocksValidToTemplate !== isValidTemplate( state ) ) { + return setTemplateValidity( isBlocksValidToTemplate ); + } +} + +/** + * Effect handler which will return a block select action to select the block + * occurring before the selected block in the previous state, unless it is the + * same block or the action includes a falsey `selectPrevious` option flag. + * + * @param {Object} action Action which had initiated the effect handler. + * @param {Object} store Store instance. + * + * @return {?Object} Block select action to select previous, if applicable. + */ +export function selectPreviousBlock( action, store ) { + // if the action says previous block should not be selected don't do anything. + if ( ! action.selectPrevious ) { + return; + } + + const firstRemovedBlockClientId = action.clientIds[ 0 ]; + const state = store.getState(); + const selectedBlockClientId = getSelectedBlockClientId( state ); + + // recreate the state before the block was removed. + const previousState = { ...state, editor: { present: last( state.editor.past ) } }; + + // rootClientId of the removed block. + const rootClientId = getBlockRootClientId( previousState, firstRemovedBlockClientId ); + + // Client ID of the block that was before the removed block or the + // rootClientId if the removed block was first amongst its siblings. + const blockClientIdToSelect = getPreviousBlockClientId( previousState, firstRemovedBlockClientId ) || rootClientId; + + // Dispatch select block action if the currently selected block + // is not already the block we want to be selected. + if ( blockClientIdToSelect !== selectedBlockClientId ) { + return selectBlock( blockClientIdToSelect, -1 ); + } +} + +/** + * Effect handler which will return a default block insertion action if there + * are no other blocks at the root of the editor. This is expected to be used + * in actions which may result in no blocks remaining in the editor (removal, + * replacement, etc). + * + * @param {Object} action Action which had initiated the effect handler. + * @param {Object} store Store instance. + * + * @return {?Object} Default block insert action, if no other blocks exist. + */ +export function ensureDefaultBlock( action, store ) { + if ( ! getBlockCount( store.getState() ) ) { + return insertDefaultBlock(); + } +} + +export default { + MERGE_BLOCKS( action, store ) { + const { dispatch } = store; + const state = store.getState(); + const [ firstBlockClientId, secondBlockClientId ] = action.blocks; + const blockA = getBlock( state, firstBlockClientId ); + const blockType = getBlockType( blockA.name ); + + // Only focus the previous block if it's not mergeable + if ( ! blockType.merge ) { + dispatch( selectBlock( blockA.clientId ) ); + return; + } + + // We can only merge blocks with similar types + // thus, we transform the block to merge first + const blockB = getBlock( state, secondBlockClientId ); + 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( selectBlock( blockA.clientId, -1 ) ); + dispatch( replaceBlocks( + [ blockA.clientId, blockB.clientId ], + [ + { + ...blockA, + attributes: { + ...blockA.attributes, + ...updatedAttributes, + }, + }, + ...blocksWithTheSameType.slice( 1 ), + ] + ) ); + }, + RESET_BLOCKS: [ + validateBlocksToTemplate, + ], + REMOVE_BLOCKS: [ + selectPreviousBlock, + ensureDefaultBlock, + ], + REPLACE_BLOCKS: [ + ensureDefaultBlock, + ], + MULTI_SELECT: ( action, { getState } ) => { + const blockCount = getSelectedBlockCount( getState() ); + + /* translators: %s: number of selected blocks */ + speak( sprintf( _n( '%s block selected.', '%s blocks selected.', blockCount ), blockCount ), 'assertive' ); + }, + SYNCHRONIZE_TEMPLATE( action, { getState } ) { + const state = getState(); + const blocks = getBlocks( state ); + const template = getTemplate( state ); + const updatedBlockList = synchronizeBlocksWithTemplate( blocks, template ); + + return resetBlocks( updatedBlockList ); + }, +}; diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js new file mode 100644 index 0000000000000..7d270f08cfcee --- /dev/null +++ b/packages/block-editor/src/store/index.js @@ -0,0 +1,26 @@ +/** + * WordPress Dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import applyMiddlewares from './middlewares'; +import * as selectors from './selectors'; +import * as actions from './actions'; + +/** + * Module Constants + */ +const MODULE_KEY = 'core/block-editor'; + +const store = registerStore( MODULE_KEY, { + reducer, + selectors, + actions, +} ); +applyMiddlewares( store ); + +export default store; diff --git a/packages/block-editor/src/store/middlewares.js b/packages/block-editor/src/store/middlewares.js new file mode 100644 index 0000000000000..6381132bb81e0 --- /dev/null +++ b/packages/block-editor/src/store/middlewares.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import refx from 'refx'; +import multi from 'redux-multi'; +import { flowRight } from 'lodash'; + +/** + * Internal dependencies + */ +import effects from './effects'; + +/** + * Applies the custom middlewares used specifically in the editor module. + * + * @param {Object} store Store Object. + * + * @return {Object} Update Store Object. + */ +function applyMiddlewares( store ) { + const middlewares = [ + refx( effects ), + multi, + ]; + + let enhancedDispatch = () => { + throw new Error( + 'Dispatching while constructing your middleware is not allowed. ' + + 'Other middleware would not be applied to this dispatch.' + ); + }; + let chain = []; + + const middlewareAPI = { + getState: store.getState, + dispatch: ( ...args ) => enhancedDispatch( ...args ), + }; + chain = middlewares.map( ( middleware ) => middleware( middlewareAPI ) ); + enhancedDispatch = flowRight( ...chain )( store.dispatch ); + + store.dispatch = enhancedDispatch; + return store; +} + +export default applyMiddlewares; diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js new file mode 100644 index 0000000000000..aa2cc74b5867b --- /dev/null +++ b/packages/block-editor/src/store/reducer.js @@ -0,0 +1,844 @@ +/** + * External dependencies + */ +import { + flow, + reduce, + first, + last, + omit, + without, + mapValues, + keys, + isEqual, + isEmpty, + overSome, + get, +} from 'lodash'; + +/** + * WordPress dependencies + */ +import { combineReducers } from '@wordpress/data'; +import { isReusableBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import withHistory from '../utils/with-history'; +import { + PREFERENCES_DEFAULTS, + EDITOR_SETTINGS_DEFAULTS, +} from './defaults'; +import { insertAt, moveTo } from './array'; + +/** + * Returns a post attribute value, flattening nested rendered content using its + * raw value in place of its original object form. + * + * @param {*} value Original value. + * + * @return {*} Raw value. + */ +export function getPostRawValue( value ) { + if ( value && 'object' === typeof value && 'raw' in value ) { + return value.raw; + } + + return value; +} + +/** + * Given an array of blocks, returns an object where each key is a nesting + * context, the value of which is an array of block client IDs existing within + * that nesting context. + * + * @param {Array} blocks Blocks to map. + * @param {?string} rootClientId Assumed root client ID. + * + * @return {Object} Block order map object. + */ +function mapBlockOrder( blocks, rootClientId = '' ) { + const result = { [ rootClientId ]: [] }; + + blocks.forEach( ( block ) => { + const { clientId, innerBlocks } = block; + + result[ rootClientId ].push( clientId ); + + Object.assign( result, mapBlockOrder( innerBlocks, clientId ) ); + } ); + + return result; +} + +/** + * Helper method to iterate through all blocks, recursing into inner blocks, + * applying a transformation function to each one. + * Returns a flattened object with the transformed blocks. + * + * @param {Array} blocks Blocks to flatten. + * @param {Function} transform Transforming function to be applied to each block. + * + * @return {Object} Flattened object. + */ +function flattenBlocks( blocks, transform ) { + const result = {}; + + const stack = [ ...blocks ]; + while ( stack.length ) { + const { innerBlocks, ...block } = stack.shift(); + stack.push( ...innerBlocks ); + result[ block.clientId ] = transform( block ); + } + + return result; +} + +/** + * Given an array of blocks, returns an object containing all blocks, without + * attributes, recursing into inner blocks. Keys correspond to the block client + * ID, the value of which is the attributes object. + * + * @param {Array} blocks Blocks to flatten. + * + * @return {Object} Flattened block attributes object. + */ +function getFlattenedBlocksWithoutAttributes( blocks ) { + return flattenBlocks( blocks, ( block ) => omit( block, 'attributes' ) ); +} + +/** + * Given an array of blocks, returns an object containing all block attributes, + * recursing into inner blocks. Keys correspond to the block client ID, the + * value of which is the attributes object. + * + * @param {Array} blocks Blocks to flatten. + * + * @return {Object} Flattened block attributes object. + */ +function getFlattenedBlockAttributes( blocks ) { + return flattenBlocks( blocks, ( block ) => block.attributes ); +} + +/** + * Returns an object against which it is safe to perform mutating operations, + * given the original object and its current working copy. + * + * @param {Object} original Original object. + * @param {Object} working Working object. + * + * @return {Object} Mutation-safe object. + */ +function getMutateSafeObject( original, working ) { + if ( original === working ) { + return { ...original }; + } + + return working; +} + +/** + * Returns true if the two object arguments have the same keys, or false + * otherwise. + * + * @param {Object} a First object. + * @param {Object} b Second object. + * + * @return {boolean} Whether the two objects have the same keys. + */ +export function hasSameKeys( a, b ) { + return isEqual( keys( a ), keys( b ) ); +} + +/** + * Returns true if, given the currently dispatching action and the previously + * dispatched action, the two actions are updating the same block attribute, or + * false otherwise. + * + * @param {Object} action Currently dispatching action. + * @param {Object} previousAction Previously dispatched action. + * + * @return {boolean} Whether actions are updating the same block attribute. + */ +export function isUpdatingSameBlockAttribute( action, previousAction ) { + return ( + action.type === 'UPDATE_BLOCK_ATTRIBUTES' && + action.clientId === previousAction.clientId && + hasSameKeys( action.attributes, previousAction.attributes ) + ); +} + +/** + * Returns true if, given the currently dispatching action and the previously + * dispatched action, the two actions are editing the same post property, or + * false otherwise. + * + * @param {Object} action Currently dispatching action. + * @param {Object} previousAction Previously dispatched action. + * + * @return {boolean} Whether actions are updating the same post property. + */ +export function isUpdatingSamePostProperty( action, previousAction ) { + return ( + action.type === 'EDIT_POST' && + hasSameKeys( action.edits, previousAction.edits ) + ); +} + +/** + * Returns true if, given the currently dispatching action and the previously + * dispatched action, the two actions are modifying the same property such that + * undo history should be batched. + * + * @param {Object} action Currently dispatching action. + * @param {Object} previousAction Previously dispatched action. + * + * @return {boolean} Whether to overwrite present state. + */ +export function shouldOverwriteState( action, previousAction ) { + if ( ! previousAction || action.type !== previousAction.type ) { + return false; + } + + return overSome( [ + isUpdatingSameBlockAttribute, + isUpdatingSamePostProperty, + ] )( action, previousAction ); +} + +/** + * Higher-order reducer targeting the combined editor reducer, augmenting + * block client IDs in remove action to include cascade of inner blocks. + * + * @param {Function} reducer Original reducer function. + * + * @return {Function} Enhanced reducer function. + */ +const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { + if ( state && action.type === 'REMOVE_BLOCKS' ) { + const clientIds = [ ...action.clientIds ]; + + // For each removed client ID, include its inner blocks to remove, + // recursing into those so long as inner blocks exist. + for ( let i = 0; i < clientIds.length; i++ ) { + clientIds.push( ...state.blocks.order[ clientIds[ i ] ] ); + } + + action = { ...action, clientIds }; + } + + return reducer( state, action ); +}; + +/** + * Undoable reducer returning the editor post state, including blocks parsed + * from current HTML markup. + * + * Handles the following state keys: + * - edits: an object describing changes to be made to the current post, in + * the format accepted by the WP REST API + * - blocks: post content blocks + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @returns {Object} Updated state. + */ +export const editor = flow( [ + combineReducers, + + withInnerBlocksRemoveCascade, + + // Track undo history, starting at editor initialization. + withHistory( { + resetTypes: [ 'INIT_BLOCKS' ], + ignoreTypes: [ 'RECEIVE_BLOCKS' ], + shouldOverwriteState, + } ), +] )( { + blocks: combineReducers( { + byClientId( state = {}, action ) { + switch ( action.type ) { + case 'INIT_BLOCKS': + case 'RESET_BLOCKS': + return getFlattenedBlocksWithoutAttributes( action.blocks ); + case 'RECEIVE_BLOCKS': + return { + ...state, + ...getFlattenedBlocksWithoutAttributes( action.blocks ), + }; + + case 'UPDATE_BLOCK': + // Ignore updates if block isn't known + if ( ! state[ action.clientId ] ) { + return state; + } + + // Do nothing if only attributes change. + const changes = omit( action.updates, 'attributes' ); + if ( isEmpty( changes ) ) { + return state; + } + + return { + ...state, + [ action.clientId ]: { + ...state[ action.clientId ], + ...changes, + }, + }; + + case 'INSERT_BLOCKS': + return { + ...state, + ...getFlattenedBlocksWithoutAttributes( action.blocks ), + }; + + case 'REPLACE_BLOCKS': + if ( ! action.blocks ) { + return state; + } + + return { + ...omit( state, action.clientIds ), + ...getFlattenedBlocksWithoutAttributes( action.blocks ), + }; + + case 'REMOVE_BLOCKS': + return omit( state, action.clientIds ); + } + + return state; + }, + + attributes( state = {}, action ) { + switch ( action.type ) { + case 'INIT_BLOCKS': + case 'RESET_BLOCKS': + return getFlattenedBlockAttributes( action.blocks ); + + case 'RECEIVE_BLOCKS': + return { + ...state, + ...getFlattenedBlockAttributes( action.blocks ), + }; + + case 'UPDATE_BLOCK': + // Ignore updates if block isn't known or there are no attribute changes. + if ( ! state[ action.clientId ] || ! action.updates.attributes ) { + return state; + } + + return { + ...state, + [ action.clientId ]: { + ...state[ action.clientId ], + ...action.updates.attributes, + }, + }; + + case 'UPDATE_BLOCK_ATTRIBUTES': + // Ignore updates if block isn't known + if ( ! state[ action.clientId ] ) { + return state; + } + + // Consider as updates only changed values + const nextAttributes = reduce( action.attributes, ( result, value, key ) => { + if ( value !== result[ key ] ) { + result = getMutateSafeObject( state[ action.clientId ], result ); + result[ key ] = value; + } + + return result; + }, state[ action.clientId ] ); + + // Skip update if nothing has been changed. The reference will + // match the original block if `reduce` had no changed values. + if ( nextAttributes === state[ action.clientId ] ) { + return state; + } + + // Otherwise replace attributes in state + return { + ...state, + [ action.clientId ]: nextAttributes, + }; + + case 'INSERT_BLOCKS': + return { + ...state, + ...getFlattenedBlockAttributes( action.blocks ), + }; + + case 'REPLACE_BLOCKS': + if ( ! action.blocks ) { + return state; + } + + return { + ...omit( state, action.clientIds ), + ...getFlattenedBlockAttributes( action.blocks ), + }; + + case 'REMOVE_BLOCKS': + return omit( state, action.clientIds ); + } + + return state; + }, + + order( state = {}, action ) { + switch ( action.type ) { + case 'INIT_BLOCKS': + case 'RESET_BLOCKS': + return mapBlockOrder( action.blocks ); + + case 'RECEIVE_BLOCKS': + return { + ...state, + ...omit( mapBlockOrder( action.blocks ), '' ), + }; + + case 'INSERT_BLOCKS': { + const { rootClientId = '', blocks } = action; + const subState = state[ rootClientId ] || []; + const mappedBlocks = mapBlockOrder( blocks, rootClientId ); + const { index = subState.length } = action; + + return { + ...state, + ...mappedBlocks, + [ rootClientId ]: insertAt( subState, mappedBlocks[ rootClientId ], index ), + }; + } + + case 'MOVE_BLOCK_TO_POSITION': { + const { fromRootClientId = '', toRootClientId = '', clientId } = action; + const { index = state[ toRootClientId ].length } = action; + + // Moving inside the same parent block + if ( fromRootClientId === toRootClientId ) { + const subState = state[ toRootClientId ]; + const fromIndex = subState.indexOf( clientId ); + return { + ...state, + [ toRootClientId ]: moveTo( state[ toRootClientId ], fromIndex, index ), + }; + } + + // Moving from a parent block to another + return { + ...state, + [ fromRootClientId ]: without( state[ fromRootClientId ], clientId ), + [ toRootClientId ]: insertAt( state[ toRootClientId ], clientId, index ), + }; + } + + case 'MOVE_BLOCKS_UP': { + const { clientIds, rootClientId = '' } = action; + const firstClientId = first( clientIds ); + const subState = state[ rootClientId ]; + + if ( ! subState.length || firstClientId === first( subState ) ) { + return state; + } + + const firstIndex = subState.indexOf( firstClientId ); + + return { + ...state, + [ rootClientId ]: moveTo( subState, firstIndex, firstIndex - 1, clientIds.length ), + }; + } + + case 'MOVE_BLOCKS_DOWN': { + const { clientIds, rootClientId = '' } = action; + const firstClientId = first( clientIds ); + const lastClientId = last( clientIds ); + const subState = state[ rootClientId ]; + + if ( ! subState.length || lastClientId === last( subState ) ) { + return state; + } + + const firstIndex = subState.indexOf( firstClientId ); + + return { + ...state, + [ rootClientId ]: moveTo( subState, firstIndex, firstIndex + 1, clientIds.length ), + }; + } + + case 'REPLACE_BLOCKS': { + const { blocks, clientIds } = action; + if ( ! blocks ) { + return state; + } + + const mappedBlocks = mapBlockOrder( blocks ); + + return flow( [ + ( nextState ) => omit( nextState, clientIds ), + ( nextState ) => ( { + ...nextState, + ...omit( mappedBlocks, '' ), + } ), + ( nextState ) => mapValues( nextState, ( subState ) => ( + reduce( subState, ( result, clientId ) => { + if ( clientId === clientIds[ 0 ] ) { + return [ + ...result, + ...mappedBlocks[ '' ], + ]; + } + + if ( clientIds.indexOf( clientId ) === -1 ) { + result.push( clientId ); + } + + return result; + }, [] ) + ) ), + ] )( state ); + } + + case 'REMOVE_BLOCKS': + return flow( [ + // Remove inner block ordering for removed blocks + ( nextState ) => omit( nextState, action.clientIds ), + + // Remove deleted blocks from other blocks' orderings + ( nextState ) => mapValues( nextState, ( subState ) => ( + without( subState, ...action.clientIds ) + ) ), + ] )( state ); + } + + return state; + }, + } ), +} ); + +/** + * Reducer returning typing state. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function isTyping( state = false, action ) { + switch ( action.type ) { + case 'START_TYPING': + return true; + + case 'STOP_TYPING': + return false; + } + + return state; +} + +/** + * Reducer returning whether the caret is within formatted text. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function isCaretWithinFormattedText( state = false, action ) { + switch ( action.type ) { + case 'ENTER_FORMATTED_TEXT': + return true; + + case 'EXIT_FORMATTED_TEXT': + return false; + } + + return state; +} + +/** + * Reducer returning the block selection's state. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function blockSelection( state = { + start: null, + end: null, + isMultiSelecting: false, + isEnabled: true, + initialPosition: null, +}, action ) { + switch ( action.type ) { + case 'CLEAR_SELECTED_BLOCK': + if ( state.start === null && state.end === null && ! state.isMultiSelecting ) { + return state; + } + + return { + ...state, + start: null, + end: null, + isMultiSelecting: false, + initialPosition: null, + }; + case 'START_MULTI_SELECT': + if ( state.isMultiSelecting ) { + return state; + } + + return { + ...state, + isMultiSelecting: true, + initialPosition: null, + }; + case 'STOP_MULTI_SELECT': + if ( ! state.isMultiSelecting ) { + return state; + } + + return { + ...state, + isMultiSelecting: false, + initialPosition: null, + }; + case 'MULTI_SELECT': + return { + ...state, + start: action.start, + end: action.end, + initialPosition: null, + }; + case 'SELECT_BLOCK': + if ( action.clientId === state.start && action.clientId === state.end ) { + return state; + } + return { + ...state, + start: action.clientId, + end: action.clientId, + initialPosition: action.initialPosition, + }; + case 'INSERT_BLOCKS': { + if ( action.updateSelection ) { + return { + ...state, + start: action.blocks[ 0 ].clientId, + end: action.blocks[ 0 ].clientId, + initialPosition: null, + isMultiSelecting: false, + }; + } + return state; + } + case 'REMOVE_BLOCKS': + if ( ! action.clientIds || ! action.clientIds.length || action.clientIds.indexOf( state.start ) === -1 ) { + return state; + } + return { + ...state, + start: null, + end: null, + initialPosition: null, + isMultiSelecting: false, + }; + case 'REPLACE_BLOCKS': + if ( action.clientIds.indexOf( state.start ) === -1 ) { + return state; + } + + // If there is replacement block(s), assign first's client ID as + // the next selected block. If empty replacement, reset to null. + const nextSelectedBlockClientId = get( action.blocks, [ 0, 'clientId' ], null ); + if ( nextSelectedBlockClientId === state.start && nextSelectedBlockClientId === state.end ) { + return state; + } + + return { + ...state, + start: nextSelectedBlockClientId, + end: nextSelectedBlockClientId, + initialPosition: null, + isMultiSelecting: false, + }; + case 'TOGGLE_SELECTION': + return { + ...state, + isEnabled: action.isSelectionEnabled, + }; + } + + return state; +} + +export function blocksMode( state = {}, action ) { + if ( action.type === 'TOGGLE_BLOCK_MODE' ) { + const { clientId } = action; + return { + ...state, + [ clientId ]: state[ clientId ] && state[ clientId ] === 'html' ? 'visual' : 'html', + }; + } + + return state; +} + +/** + * Reducer returning the block insertion point visibility, either null if there + * is not an explicit insertion point assigned, or an object of its `index` and + * `rootClientId`. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function insertionPoint( state = null, action ) { + switch ( action.type ) { + case 'SHOW_INSERTION_POINT': + const { rootClientId, index } = action; + return { rootClientId, index }; + + case 'HIDE_INSERTION_POINT': + return null; + } + + return state; +} + +/** + * Reducer returning whether the post blocks match the defined template or not. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function template( state = { isValid: true }, action ) { + switch ( action.type ) { + case 'SET_TEMPLATE_VALIDITY': + return { + ...state, + isValid: action.isValid, + }; + } + + return state; +} + +/** + * Reducer returning the editor setting. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function settings( state = EDITOR_SETTINGS_DEFAULTS, action ) { + switch ( action.type ) { + case 'UPDATE_EDITOR_SETTINGS': + return { + ...state, + ...action.settings, + }; + } + + return state; +} + +/** + * Reducer returning the user preferences. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {string} Updated state. + */ +export function preferences( state = PREFERENCES_DEFAULTS, action ) { + switch ( action.type ) { + case 'INSERT_BLOCKS': + case 'REPLACE_BLOCKS': + return action.blocks.reduce( ( prevState, block ) => { + let id = block.name; + const insert = { name: block.name }; + if ( isReusableBlock( block ) ) { + insert.ref = block.attributes.ref; + id += '/' + block.attributes.ref; + } + + return { + ...prevState, + insertUsage: { + ...prevState.insertUsage, + [ id ]: { + time: action.time, + count: prevState.insertUsage[ id ] ? prevState.insertUsage[ id ].count + 1 : 1, + insert, + }, + }, + }; + }, state ); + } + + return state; +} + +/** + * Reducer returning an object where each key is a block client ID, its value + * representing the settings for its nested blocks. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export const blockListSettings = ( state = {}, action ) => { + switch ( action.type ) { + // Even if the replaced blocks have the same client ID, our logic + // should correct the state. + case 'REPLACE_BLOCKS' : + case 'REMOVE_BLOCKS': { + return omit( state, action.clientIds ); + } + case 'UPDATE_BLOCK_LIST_SETTINGS': { + const { clientId } = action; + if ( ! action.settings ) { + if ( state.hasOwnProperty( clientId ) ) { + return omit( state, clientId ); + } + + return state; + } + + if ( isEqual( state[ clientId ], action.settings ) ) { + return state; + } + + return { + ...state, + [ clientId ]: action.settings, + }; + } + } + return state; +}; + +export default combineReducers( { + editor, + isTyping, + isCaretWithinFormattedText, + blockSelection, + blocksMode, + blockListSettings, + insertionPoint, + template, + settings, + preferences, +} ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js new file mode 100644 index 0000000000000..2a9eb8ce6d121 --- /dev/null +++ b/packages/block-editor/src/store/selectors.js @@ -0,0 +1,1371 @@ +/** + * External dependencies + */ +import { + castArray, + flatMap, + first, + get, + includes, + isArray, + isBoolean, + last, + map, + orderBy, + reduce, + some, +} from 'lodash'; +import createSelector from 'rememo'; + +/** + * WordPress dependencies + */ +import { + getBlockType, + getBlockTypes, + hasBlockSupport, + hasChildBlocksWithInserterSupport, +} from '@wordpress/blocks'; + +/*** + * Module constants + */ +export const POST_UPDATE_TRANSACTION_ID = 'post-update'; +export const INSERTER_UTILITY_HIGH = 3; +export const INSERTER_UTILITY_MEDIUM = 2; +export const INSERTER_UTILITY_LOW = 1; +export const INSERTER_UTILITY_NONE = 0; +const MILLISECONDS_PER_HOUR = 3600 * 1000; +const MILLISECONDS_PER_DAY = 24 * 3600 * 1000; +const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; + +/** + * Shared reference to an empty array for cases where it is important to avoid + * returning a new array reference on every invocation, as in a connected or + * other pure component which performs `shouldComponentUpdate` check on props. + * This should be used as a last resort, since the normalized data should be + * maintained by the reducer result in state. + * + * @type {Array} + */ +const EMPTY_ARRAY = []; + +/** + * Returns a new reference when the inner blocks of a given block client ID + * change. This is used exclusively as a memoized selector dependant, relying + * on this selector's shared return value and recursively those of its inner + * blocks defined as dependencies. This abuses mechanics of the selector + * memoization to return from the original selector function only when + * dependants change. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {*} A value whose reference will change only when inner blocks of + * the given block client ID change. + */ +export const getBlockDependantsCacheBust = createSelector( + () => [], + ( state, clientId ) => map( + getBlockOrder( state, clientId ), + ( innerBlockClientId ) => getBlock( state, innerBlockClientId ), + ), +); + +/** + * Returns a block's name given its client ID, or null if no block exists with + * the client ID. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {string} Block name. + */ +export function getBlockName( state, clientId ) { + const block = state.editor.present.blocks.byClientId[ clientId ]; + return block ? block.name : null; +} + +/** + * Returns whether a block is valid or not. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {boolean} Is Valid. + */ +export function isBlockValid( state, clientId ) { + const block = state.editor.present.blocks.byClientId[ clientId ]; + return !! block && block.isValid; +} + +/** + * Returns a block's attributes given its client ID, or null if no block exists with + * the client ID. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {Object?} Block attributes. + */ +export const getBlockAttributes = createSelector( + ( state, clientId ) => { + const block = state.editor.present.blocks.byClientId[ clientId ]; + if ( ! block ) { + return null; + } + + let attributes = state.editor.present.blocks.attributes[ clientId ]; + + // Inject custom source attribute values. + // + // TODO: Create generic external sourcing pattern, not explicitly + // targeting meta attributes. + const type = getBlockType( block.name ); + if ( type ) { + attributes = reduce( type.attributes, ( result, value, key ) => { + if ( value.source === 'meta' ) { + if ( result === attributes ) { + result = { ...result }; + } + + result[ key ] = getPostMeta( state, value.meta ); + } + + return result; + }, attributes ); + } + + return attributes; + }, + ( state, clientId ) => [ + state.editor.present.blocks.byClientId[ clientId ], + state.editor.present.blocks.attributes[ clientId ], + get( state, [ 'editor', 'settings', 'meta' ], EMPTY_ARRAY ), + ] +); + +/** + * Returns a block given its client ID. This is a parsed copy of the block, + * containing its `blockName`, `clientId`, and current `attributes` state. This + * is not the block's registration settings, which must be retrieved from the + * blocks module registration store. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {Object} Parsed block object. + */ +export const getBlock = createSelector( + ( state, clientId ) => { + const block = state.editor.present.blocks.byClientId[ clientId ]; + if ( ! block ) { + return null; + } + + return { + ...block, + attributes: getBlockAttributes( state, clientId ), + innerBlocks: getBlocks( state, clientId ), + }; + }, + ( state, clientId ) => [ + ...getBlockAttributes.getDependants( state, clientId ), + getBlockDependantsCacheBust( state, clientId ), + ] +); + +export const __unstableGetBlockWithoutInnerBlocks = createSelector( + ( state, clientId ) => { + const block = state.editor.present.blocks.byClientId[ clientId ]; + if ( ! block ) { + return null; + } + + return { + ...block, + attributes: getBlockAttributes( state, clientId ), + }; + }, + ( state, clientId ) => [ + state.editor.present.blocks.byClientId[ clientId ], + ...getBlockAttributes.getDependants( state, clientId ), + ] +); + +/** + * Returns all block objects for the current post being edited as an array in + * the order they appear in the post. + * + * Note: It's important to memoize this selector to avoid return a new instance + * on each call + * + * @param {Object} state Editor state. + * @param {?String} rootClientId Optional root client ID of block list. + * + * @return {Object[]} Post blocks. + */ +export const getBlocks = createSelector( + ( state, rootClientId ) => { + return map( + getBlockOrder( state, rootClientId ), + ( clientId ) => getBlock( state, clientId ) + ); + }, + ( state ) => [ state.editor.present.blocks ] +); + +/** + * Returns an array containing the clientIds of all descendants + * of the blocks given. + * + * @param {Object} state Global application state. + * @param {Array} clientIds Array of blocks to inspect. + * + * @return {Array} ids of descendants. + */ +export const getClientIdsOfDescendants = ( state, clientIds ) => flatMap( clientIds, ( clientId ) => { + const descendants = getBlockOrder( state, clientId ); + return [ ...descendants, ...getClientIdsOfDescendants( state, descendants ) ]; +} ); + +/** + * Returns an array containing the clientIds of the top-level blocks + * and their descendants of any depth (for nested blocks). + * + * @param {Object} state Global application state. + * + * @return {Array} ids of top-level and descendant blocks. + */ +export const getClientIdsWithDescendants = createSelector( + ( state ) => { + const topLevelIds = getBlockOrder( state ); + return [ ...topLevelIds, ...getClientIdsOfDescendants( state, topLevelIds ) ]; + }, + ( state ) => [ + state.editor.present.blocks.order, + ] +); + +/** + * Returns the total number of blocks, or the total number of blocks with a specific name in a post. + * The number returned includes nested blocks. + * + * @param {Object} state Global application state. + * @param {?String} blockName Optional block name, if specified only blocks of that type will be counted. + * + * @return {number} Number of blocks in the post, or number of blocks with name equal to blockName. + */ +export const getGlobalBlockCount = createSelector( + ( state, blockName ) => { + const clientIds = getClientIdsWithDescendants( state ); + if ( ! blockName ) { + return clientIds.length; + } + return reduce( clientIds, ( count, clientId ) => { + const block = state.editor.present.blocks.byClientId[ clientId ]; + return block.name === blockName ? count + 1 : count; + }, 0 ); + }, + ( state ) => [ + state.editor.present.blocks.order, + state.editor.present.blocks.byClientId, + ] +); + +/** + * Given an array of block client IDs, returns the corresponding array of block + * objects. + * + * @param {Object} state Editor state. + * @param {string[]} clientIds Client IDs for which blocks are to be returned. + * + * @return {WPBlock[]} Block objects. + */ +export const getBlocksByClientId = createSelector( + ( state, clientIds ) => map( + castArray( clientIds ), + ( clientId ) => getBlock( state, clientId ) + ), + ( state ) => [ + get( state, [ 'editor', 'settings', 'meta' ], EMPTY_ARRAY ), + state.editor.present.blocks, + ] +); + +/** + * Returns the number of blocks currently present in the post. + * + * @param {Object} state Editor state. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {number} Number of blocks in the post. + */ +export function getBlockCount( state, rootClientId ) { + return getBlockOrder( state, rootClientId ).length; +} + +/** + * Returns the current block selection start. This value may be null, and it + * may represent either a singular block selection or multi-selection start. + * A selection is singular if its start and end match. + * + * @param {Object} state Global application state. + * + * @return {?string} Client ID of block selection start. + */ +export function getBlockSelectionStart( state ) { + return state.blockSelection.start; +} + +/** + * Returns the current block selection end. This value may be null, and it + * may represent either a singular block selection or multi-selection end. + * A selection is singular if its start and end match. + * + * @param {Object} state Global application state. + * + * @return {?string} Client ID of block selection end. + */ +export function getBlockSelectionEnd( state ) { + return state.blockSelection.end; +} + +/** + * Returns the number of blocks currently selected in the post. + * + * @param {Object} state Global application state. + * + * @return {number} Number of blocks selected in the post. + */ +export function getSelectedBlockCount( state ) { + const multiSelectedBlockCount = getMultiSelectedBlockClientIds( state ).length; + + if ( multiSelectedBlockCount ) { + return multiSelectedBlockCount; + } + + return state.blockSelection.start ? 1 : 0; +} + +/** + * Returns true if there is a single selected block, or false otherwise. + * + * @param {Object} state Editor state. + * + * @return {boolean} Whether a single block is selected. + */ +export function hasSelectedBlock( state ) { + const { start, end } = state.blockSelection; + return !! start && start === end; +} + +/** + * Returns the currently selected block client ID, or null if there is no + * selected block. + * + * @param {Object} state Editor state. + * + * @return {?string} Selected block client ID. + */ +export function getSelectedBlockClientId( state ) { + const { start, end } = state.blockSelection; + // We need to check the block exists because the current state.blockSelection reducer + // doesn't take into account the UNDO / REDO actions to update selection. + // To be removed when that's fixed. + return start && start === end && !! state.editor.present.blocks.byClientId[ start ] ? start : null; +} + +/** + * Returns the currently selected block, or null if there is no selected block. + * + * @param {Object} state Global application state. + * + * @return {?Object} Selected block. + */ +export function getSelectedBlock( state ) { + const clientId = getSelectedBlockClientId( state ); + return clientId ? getBlock( state, clientId ) : null; +} + +/** + * Given a block client ID, returns the root block from which the block is + * nested, an empty string for top-level blocks, or null if the block does not + * exist. + * + * @param {Object} state Editor state. + * @param {string} clientId Block from which to find root client ID. + * + * @return {?string} Root client ID, if exists + */ +export const getBlockRootClientId = createSelector( + ( state, clientId ) => { + const { order } = state.editor.present.blocks; + + for ( const rootClientId in order ) { + if ( includes( order[ rootClientId ], clientId ) ) { + return rootClientId; + } + } + + return null; + }, + ( state ) => [ + state.editor.present.blocks.order, + ] +); + +/** + * Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. + * + * @param {Object} state Editor state. + * @param {string} clientId Block from which to find root client ID. + * + * @return {string} Root client ID + */ +export const getBlockHierarchyRootClientId = createSelector( + ( state, clientId ) => { + let rootClientId = clientId; + let current = clientId; + while ( rootClientId ) { + current = rootClientId; + rootClientId = getBlockRootClientId( state, current ); + } + + return current; + }, + ( state ) => [ + state.editor.present.blocks.order, + ] +); + +/** + * Returns the client ID of the block adjacent one at the given reference + * startClientId and modifier directionality. Defaults start startClientId to + * the selected block, and direction as next block. Returns null if there is no + * adjacent block. + * + * @param {Object} state Editor state. + * @param {?string} startClientId Optional client ID of block from which to + * search. + * @param {?number} modifier Directionality multiplier (1 next, -1 + * previous). + * + * @return {?string} Return the client ID of the block, or null if none exists. + */ +export function getAdjacentBlockClientId( state, startClientId, modifier = 1 ) { + // Default to selected block. + if ( startClientId === undefined ) { + startClientId = getSelectedBlockClientId( state ); + } + + // Try multi-selection starting at extent based on modifier. + if ( startClientId === undefined ) { + if ( modifier < 0 ) { + startClientId = getFirstMultiSelectedBlockClientId( state ); + } else { + startClientId = getLastMultiSelectedBlockClientId( state ); + } + } + + // Validate working start client ID. + if ( ! startClientId ) { + return null; + } + + // Retrieve start block root client ID, being careful to allow the falsey + // empty string top-level root by explicitly testing against null. + const rootClientId = getBlockRootClientId( state, startClientId ); + if ( rootClientId === null ) { + return null; + } + + const { order } = state.editor.present.blocks; + const orderSet = order[ rootClientId ]; + const index = orderSet.indexOf( startClientId ); + const nextIndex = ( index + ( 1 * modifier ) ); + + // Block was first in set and we're attempting to get previous. + if ( nextIndex < 0 ) { + return null; + } + + // Block was last in set and we're attempting to get next. + if ( nextIndex === orderSet.length ) { + return null; + } + + // Assume incremented index is within the set. + return orderSet[ nextIndex ]; +} + +/** + * Returns the previous block's client ID from the given reference start ID. + * Defaults start to the selected block. Returns null if there is no previous + * block. + * + * @param {Object} state Editor state. + * @param {?string} startClientId Optional client ID of block from which to + * search. + * + * @return {?string} Adjacent block's client ID, or null if none exists. + */ +export function getPreviousBlockClientId( state, startClientId ) { + return getAdjacentBlockClientId( state, startClientId, -1 ); +} + +/** + * Returns the next block's client ID from the given reference start ID. + * Defaults start to the selected block. Returns null if there is no next + * block. + * + * @param {Object} state Editor state. + * @param {?string} startClientId Optional client ID of block from which to + * search. + * + * @return {?string} Adjacent block's client ID, or null if none exists. + */ +export function getNextBlockClientId( state, startClientId ) { + return getAdjacentBlockClientId( state, startClientId, 1 ); +} + +/** + * Returns the initial caret position for the selected block. + * This position is to used to position the caret properly when the selected block changes. + * + * @param {Object} state Global application state. + * + * @return {?Object} Selected block. + */ +export function getSelectedBlocksInitialCaretPosition( state ) { + const { start, end } = state.blockSelection; + if ( start !== end || ! start ) { + return null; + } + + return state.blockSelection.initialPosition; +} + +/** + * Returns the current multi-selection set of block client IDs, or an empty + * array if there is no multi-selection. + * + * @param {Object} state Editor state. + * + * @return {Array} Multi-selected block client IDs. + */ +export const getMultiSelectedBlockClientIds = createSelector( + ( state ) => { + const { start, end } = state.blockSelection; + if ( start === end ) { + return []; + } + + // Retrieve root client ID to aid in retrieving relevant nested block + // order, being careful to allow the falsey empty string top-level root + // by explicitly testing against null. + const rootClientId = getBlockRootClientId( state, start ); + if ( rootClientId === null ) { + return []; + } + + const blockOrder = getBlockOrder( state, rootClientId ); + const startIndex = blockOrder.indexOf( start ); + const endIndex = blockOrder.indexOf( end ); + + if ( startIndex > endIndex ) { + return blockOrder.slice( endIndex, startIndex + 1 ); + } + + return blockOrder.slice( startIndex, endIndex + 1 ); + }, + ( state ) => [ + state.editor.present.blocks.order, + state.blockSelection.start, + state.blockSelection.end, + ], +); + +/** + * Returns the current multi-selection set of blocks, or an empty array if + * there is no multi-selection. + * + * @param {Object} state Editor state. + * + * @return {Array} Multi-selected block objects. + */ +export const getMultiSelectedBlocks = createSelector( + ( state ) => { + const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds( state ); + if ( ! multiSelectedBlockClientIds.length ) { + return EMPTY_ARRAY; + } + + return multiSelectedBlockClientIds.map( ( clientId ) => getBlock( state, clientId ) ); + }, + ( state ) => [ + ...getMultiSelectedBlockClientIds.getDependants( state ), + state.editor.present.blocks, + get( state, [ 'editor', 'settings', 'meta' ], EMPTY_ARRAY ), + ] +); + +/** + * Returns the client ID of the first block in the multi-selection set, or null + * if there is no multi-selection. + * + * @param {Object} state Editor state. + * + * @return {?string} First block client ID in the multi-selection set. + */ +export function getFirstMultiSelectedBlockClientId( state ) { + return first( getMultiSelectedBlockClientIds( state ) ) || null; +} + +/** + * Returns the client ID of the last block in the multi-selection set, or null + * if there is no multi-selection. + * + * @param {Object} state Editor state. + * + * @return {?string} Last block client ID in the multi-selection set. + */ +export function getLastMultiSelectedBlockClientId( state ) { + return last( getMultiSelectedBlockClientIds( state ) ) || null; +} + +/** + * Checks if possibleAncestorId is an ancestor of possibleDescendentId. + * + * @param {Object} state Editor state. + * @param {string} possibleAncestorId Possible ancestor client ID. + * @param {string} possibleDescendentId Possible descent client ID. + * + * @return {boolean} True if possibleAncestorId is an ancestor + * of possibleDescendentId, and false otherwise. + */ +const isAncestorOf = createSelector( + ( state, possibleAncestorId, possibleDescendentId ) => { + let idToCheck = possibleDescendentId; + while ( possibleAncestorId !== idToCheck && idToCheck ) { + idToCheck = getBlockRootClientId( state, idToCheck ); + } + return possibleAncestorId === idToCheck; + }, + ( state ) => [ + state.editor.present.blocks.order, + ], +); + +/** + * Returns true if a multi-selection exists, and the block corresponding to the + * specified client ID is the first block of the multi-selection set, or false + * otherwise. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {boolean} Whether block is first in multi-selection. + */ +export function isFirstMultiSelectedBlock( state, clientId ) { + return getFirstMultiSelectedBlockClientId( state ) === clientId; +} + +/** + * Returns true if the client ID occurs within the block multi-selection, or + * false otherwise. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {boolean} Whether block is in multi-selection set. + */ +export function isBlockMultiSelected( state, clientId ) { + return getMultiSelectedBlockClientIds( state ).indexOf( clientId ) !== -1; +} + +/** + * Returns true if an ancestor of the block is multi-selected, or false + * otherwise. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {boolean} Whether an ancestor of the block is in multi-selection + * set. + */ +export const isAncestorMultiSelected = createSelector( + ( state, clientId ) => { + let ancestorClientId = clientId; + let isMultiSelected = false; + while ( ancestorClientId && ! isMultiSelected ) { + ancestorClientId = getBlockRootClientId( state, ancestorClientId ); + isMultiSelected = isBlockMultiSelected( state, ancestorClientId ); + } + return isMultiSelected; + }, + ( state ) => [ + state.editor.present.blocks.order, + state.blockSelection.start, + state.blockSelection.end, + ], +); +/** + * Returns the client ID of the block which begins the multi-selection set, or + * null if there is no multi-selection. + * + * This is not necessarily the first client ID in the selection. + * + * @see getFirstMultiSelectedBlockClientId + * + * @param {Object} state Editor state. + * + * @return {?string} Client ID of block beginning multi-selection. + */ +export function getMultiSelectedBlocksStartClientId( state ) { + const { start, end } = state.blockSelection; + if ( start === end ) { + return null; + } + return start || null; +} + +/** + * Returns the client ID of the block which ends the multi-selection set, or + * null if there is no multi-selection. + * + * This is not necessarily the last client ID in the selection. + * + * @see getLastMultiSelectedBlockClientId + * + * @param {Object} state Editor state. + * + * @return {?string} Client ID of block ending multi-selection. + */ +export function getMultiSelectedBlocksEndClientId( state ) { + const { start, end } = state.blockSelection; + if ( start === end ) { + return null; + } + return end || null; +} + +/** + * Returns an array containing all block client IDs in the editor in the order + * they appear. Optionally accepts a root client ID of the block list for which + * the order should be returned, defaulting to the top-level block order. + * + * @param {Object} state Editor state. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {Array} Ordered client IDs of editor blocks. + */ +export function getBlockOrder( state, rootClientId ) { + return state.editor.present.blocks.order[ rootClientId || '' ] || EMPTY_ARRAY; +} + +/** + * Returns the index at which the block corresponding to the specified client + * ID occurs within the block order, or `-1` if the block does not exist. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {number} Index at which block exists in order. + */ +export function getBlockIndex( state, clientId, rootClientId ) { + return getBlockOrder( state, rootClientId ).indexOf( clientId ); +} + +/** + * Returns true if the block corresponding to the specified client ID is + * currently selected and no multi-selection exists, or false otherwise. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {boolean} Whether block is selected and multi-selection exists. + */ +export function isBlockSelected( state, clientId ) { + const { start, end } = state.blockSelection; + + if ( start !== end ) { + return false; + } + + return start === clientId; +} + +/** + * Returns true if one of the block's inner blocks is selected. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * @param {boolean} deep Perform a deep check. + * + * @return {boolean} Whether the block as an inner block selected + */ +export function hasSelectedInnerBlock( state, clientId, deep = false ) { + return some( + getBlockOrder( state, clientId ), + ( innerClientId ) => ( + isBlockSelected( state, innerClientId ) || + isBlockMultiSelected( state, innerClientId ) || + ( deep && hasSelectedInnerBlock( state, innerClientId, deep ) ) + ) + ); +} + +/** + * Returns true if the block corresponding to the specified client ID is + * currently selected but isn't the last of the selected blocks. Here "last" + * refers to the block sequence in the document, _not_ the sequence of + * multi-selection, which is why `state.blockSelection.end` isn't used. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {boolean} Whether block is selected and not the last in the + * selection. + */ +export function isBlockWithinSelection( state, clientId ) { + if ( ! clientId ) { + return false; + } + + const clientIds = getMultiSelectedBlockClientIds( state ); + const index = clientIds.indexOf( clientId ); + return index > -1 && index < clientIds.length - 1; +} + +/** + * Returns true if a multi-selection has been made, or false otherwise. + * + * @param {Object} state Editor state. + * + * @return {boolean} Whether multi-selection has been made. + */ +export function hasMultiSelection( state ) { + const { start, end } = state.blockSelection; + return start !== end; +} + +/** + * Whether in the process of multi-selecting or not. This flag is only true + * while the multi-selection is being selected (by mouse move), and is false + * once the multi-selection has been settled. + * + * @see hasMultiSelection + * + * @param {Object} state Global application state. + * + * @return {boolean} True if multi-selecting, false if not. + */ +export function isMultiSelecting( state ) { + return state.blockSelection.isMultiSelecting; +} + +/** + * Selector that returns if multi-selection is enabled or not. + * + * @param {Object} state Global application state. + * + * @return {boolean} True if it should be possible to multi-select blocks, false if multi-selection is disabled. + */ +export function isSelectionEnabled( state ) { + return state.blockSelection.isEnabled; +} + +/** + * Returns the block's editing mode, defaulting to "visual" if not explicitly + * assigned. + * + * @param {Object} state Editor state. + * @param {string} clientId Block client ID. + * + * @return {Object} Block editing mode. + */ +export function getBlockMode( state, clientId ) { + return state.blocksMode[ clientId ] || 'visual'; +} + +/** + * Returns true if the user is typing, or false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether user is typing. + */ +export function isTyping( state ) { + return state.isTyping; +} + +/** + * Returns true if the caret is within formatted text, or false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether the caret is within formatted text. + */ +export function isCaretWithinFormattedText( state ) { + return state.isCaretWithinFormattedText; +} + +/** + * Returns the insertion point, the index at which the new inserted block would + * be placed. Defaults to the last index. + * + * @param {Object} state Editor state. + * + * @return {Object} Insertion point object with `rootClientId`, `index`. + */ +export function getBlockInsertionPoint( state ) { + let rootClientId, index; + + const { insertionPoint, blockSelection } = state; + if ( insertionPoint !== null ) { + return insertionPoint; + } + + const { end } = blockSelection; + if ( end ) { + rootClientId = getBlockRootClientId( state, end ) || undefined; + index = getBlockIndex( state, end, rootClientId ) + 1; + } else { + index = getBlockOrder( state ).length; + } + + return { rootClientId, index }; +} + +/** + * Returns true if we should show the block insertion point. + * + * @param {Object} state Global application state. + * + * @return {?boolean} Whether the insertion point is visible or not. + */ +export function isBlockInsertionPointVisible( state ) { + return state.insertionPoint !== null; +} + +/** + * Returns whether the blocks matches the template or not. + * + * @param {boolean} state + * @return {?boolean} Whether the template is valid or not. + */ +export function isValidTemplate( state ) { + return state.template.isValid; +} + +/** + * Returns the defined block template + * + * @param {boolean} state + * @return {?Array} Block Template + */ +export function getTemplate( state ) { + return state.settings.template; +} + +/** + * Returns the defined block template lock. Optionally accepts a root block + * client ID as context, otherwise defaulting to the global context. + * + * @param {Object} state Editor state. + * @param {?string} rootClientId Optional block root client ID. + * + * @return {?string} Block Template Lock + */ +export function getTemplateLock( state, rootClientId ) { + if ( ! rootClientId ) { + return state.settings.templateLock; + } + + const blockListSettings = getBlockListSettings( state, rootClientId ); + if ( ! blockListSettings ) { + return null; + } + + return blockListSettings.templateLock; +} + +/** + * Determines if the given block type is allowed to be inserted into the block list. + * This function is not exported and not memoized because using a memoized selector + * inside another memoized selector is just a waste of time. + * + * @param {Object} state Editor state. + * @param {string} blockName The name of the block type, e.g.' core/paragraph'. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {boolean} Whether the given block type is allowed to be inserted. + */ +const canInsertBlockTypeUnmemoized = ( state, blockName, rootClientId = null ) => { + const checkAllowList = ( list, item, defaultResult = null ) => { + if ( isBoolean( list ) ) { + return list; + } + if ( isArray( list ) ) { + return includes( list, item ); + } + return defaultResult; + }; + + const blockType = getBlockType( blockName ); + if ( ! blockType ) { + return false; + } + + const { allowedBlockTypes } = getEditorSettings( state ); + + const isBlockAllowedInEditor = checkAllowList( allowedBlockTypes, blockName, true ); + if ( ! isBlockAllowedInEditor ) { + return false; + } + + const isLocked = !! getTemplateLock( state, rootClientId ); + if ( isLocked ) { + return false; + } + + const parentBlockListSettings = getBlockListSettings( state, rootClientId ); + const parentAllowedBlocks = get( parentBlockListSettings, [ 'allowedBlocks' ] ); + const hasParentAllowedBlock = checkAllowList( parentAllowedBlocks, blockName ); + + const blockAllowedParentBlocks = blockType.parent; + const parentName = getBlockName( state, rootClientId ); + const hasBlockAllowedParent = checkAllowList( blockAllowedParentBlocks, parentName ); + + if ( hasParentAllowedBlock !== null && hasBlockAllowedParent !== null ) { + return hasParentAllowedBlock || hasBlockAllowedParent; + } else if ( hasParentAllowedBlock !== null ) { + return hasParentAllowedBlock; + } else if ( hasBlockAllowedParent !== null ) { + return hasBlockAllowedParent; + } + + return true; +}; + +/** + * Determines if the given block type is allowed to be inserted into the block list. + * + * @param {Object} state Editor state. + * @param {string} blockName The name of the block type, e.g.' core/paragraph'. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {boolean} Whether the given block type is allowed to be inserted. + */ +export const canInsertBlockType = createSelector( + canInsertBlockTypeUnmemoized, + ( state, blockName, rootClientId ) => [ + state.blockListSettings[ rootClientId ], + state.editor.present.blocks.byClientId[ rootClientId ], + state.settings.allowedBlockTypes, + state.settings.templateLock, + ], +); + +/** + * Returns information about how recently and frequently a block has been inserted. + * + * @param {Object} state Global application state. + * @param {string} id A string which identifies the insert, e.g. 'core/block/12' + * + * @return {?{ time: number, count: number }} An object containing `time` which is when the last + * insert occured as a UNIX epoch, and `count` which is + * the number of inserts that have occurred. + */ +function getInsertUsage( state, id ) { + return state.preferences.insertUsage[ id ] || null; +} + +/** + * Returns whether we can show a block type in the inserter + * + * @param {Object} state Global State + * @param {Object} blockType BlockType + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {boolean} Whether the given block type is allowed to be shown in the inserter. + */ +const canIncludeBlockTypeInInserter = ( state, blockType, rootClientId ) => { + if ( ! hasBlockSupport( blockType, 'inserter', true ) ) { + return false; + } + + return canInsertBlockTypeUnmemoized( state, blockType.name, rootClientId ); +}; + +/** + * Returns whether we can show a reusable block in the inserter + * + * @param {Object} state Global State + * @param {Object} reusableBlock Reusable block object + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {boolean} Whether the given block type is allowed to be shown in the inserter. + */ +const canIncludeReusableBlockInInserter = ( state, reusableBlock, rootClientId ) => { + if ( ! canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) ) { + return false; + } + + const referencedBlockName = getBlockName( state, reusableBlock.clientId ); + if ( ! referencedBlockName ) { + return false; + } + + const referencedBlockType = getBlockType( referencedBlockName ); + if ( ! referencedBlockType ) { + return false; + } + + if ( ! canInsertBlockTypeUnmemoized( state, referencedBlockName, rootClientId ) ) { + return false; + } + + if ( isAncestorOf( state, reusableBlock.clientId, rootClientId ) ) { + return false; + } + + return true; +}; + +/** + * Determines the items that appear in the inserter. Includes both static + * items (e.g. a regular block type) and dynamic items (e.g. a reusable block). + * + * Each item object contains what's necessary to display a button in the + * inserter and handle its selection. + * + * The 'utility' property indicates how useful we think an item will be to the + * user. There are 4 levels of utility: + * + * 1. Blocks that are contextually useful (utility = 3) + * 2. Blocks that have been previously inserted (utility = 2) + * 3. Blocks that are in the common category (utility = 1) + * 4. All other blocks (utility = 0) + * + * The 'frecency' property is a heuristic (https://en.wikipedia.org/wiki/Frecency) + * that combines block usage frequenty and recency. + * + * Items are returned ordered descendingly by their 'utility' and 'frecency'. + * + * @param {Object} state Editor state. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {Editor.InserterItem[]} Items that appear in inserter. + * + * @typedef {Object} Editor.InserterItem + * @property {string} id Unique identifier for the item. + * @property {string} name The type of block to create. + * @property {Object} initialAttributes Attributes to pass to the newly created block. + * @property {string} title Title of the item, as it appears in the inserter. + * @property {string} icon Dashicon for the item, as it appears in the inserter. + * @property {string} category Block category that the item is associated with. + * @property {string[]} keywords Keywords that can be searched to find this item. + * @property {boolean} isDisabled Whether or not the user should be prevented from inserting + * this item. + * @property {number} utility How useful we think this item is, between 0 and 3. + * @property {number} frecency Hueristic that combines frequency and recency. + */ +export const getInserterItems = createSelector( + ( state, rootClientId = null ) => { + const calculateUtility = ( category, count, isContextual ) => { + if ( isContextual ) { + return INSERTER_UTILITY_HIGH; + } else if ( count > 0 ) { + return INSERTER_UTILITY_MEDIUM; + } else if ( category === 'common' ) { + return INSERTER_UTILITY_LOW; + } + return INSERTER_UTILITY_NONE; + }; + + const calculateFrecency = ( time, count ) => { + if ( ! time ) { + return count; + } + + // The selector is cached, which means Date.now() is the last time that the + // relevant state changed. This suits our needs. + const duration = Date.now() - time; + + switch ( true ) { + case duration < MILLISECONDS_PER_HOUR: + return count * 4; + case duration < MILLISECONDS_PER_DAY: + return count * 2; + case duration < MILLISECONDS_PER_WEEK: + return count / 2; + default: + return count / 4; + } + }; + + const buildBlockTypeInserterItem = ( blockType ) => { + const id = blockType.name; + + let isDisabled = false; + if ( ! hasBlockSupport( blockType.name, 'multiple', true ) ) { + isDisabled = some( getBlocksByClientId( state, getClientIdsWithDescendants( state ) ), { name: blockType.name } ); + } + + const isContextual = isArray( blockType.parent ); + const { time, count = 0 } = getInsertUsage( state, id ) || {}; + + return { + id, + name: blockType.name, + initialAttributes: {}, + title: blockType.title, + icon: blockType.icon, + category: blockType.category, + keywords: blockType.keywords, + isDisabled, + utility: calculateUtility( blockType.category, count, isContextual ), + frecency: calculateFrecency( time, count ), + hasChildBlocksWithInserterSupport: hasChildBlocksWithInserterSupport( blockType.name ), + }; + }; + + const buildReusableBlockInserterItem = ( reusableBlock ) => { + const id = `core/block/${ reusableBlock.id }`; + + const referencedBlockName = getBlockName( state, reusableBlock.clientId ); + const referencedBlockType = getBlockType( referencedBlockName ); + + const { time, count = 0 } = getInsertUsage( state, id ) || {}; + const utility = calculateUtility( 'reusable', count, false ); + const frecency = calculateFrecency( time, count ); + + return { + id, + name: 'core/block', + initialAttributes: { ref: reusableBlock.id }, + title: reusableBlock.title, + icon: referencedBlockType.icon, + category: 'reusable', + keywords: [], + isDisabled: false, + utility, + frecency, + }; + }; + + const blockTypeInserterItems = getBlockTypes() + .filter( ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) ) + .map( buildBlockTypeInserterItem ); + + const reusableBlockInserterItems = getReusableBlocks( state ) + .filter( ( block ) => canIncludeReusableBlockInInserter( state, block, rootClientId ) ) + .map( buildReusableBlockInserterItem ); + + return orderBy( + [ ...blockTypeInserterItems, ...reusableBlockInserterItems ], + [ 'utility', 'frecency' ], + [ 'desc', 'desc' ] + ); + }, + ( state, rootClientId ) => [ + state.blockListSettings[ rootClientId ], + state.editor.present.blocks.byClientId, + state.editor.present.blocks.order, + state.preferences.insertUsage, + state.settings.allowedBlockTypes, + state.settings.templateLock, + getReusableBlocks( state ), + getBlockTypes(), + ], +); + +/** + * Determines whether there are items to show in the inserter. + * @param {Object} state Editor state. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {boolean} Items that appear in inserter. + */ +export const hasInserterItems = createSelector( + ( state, rootClientId = null ) => { + const hasBlockType = some( + getBlockTypes(), + ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) + ); + if ( hasBlockType ) { + return true; + } + const hasReusableBlock = some( + getReusableBlocks( state ), + ( block ) => canIncludeReusableBlockInInserter( state, block, rootClientId ) + ); + + return hasReusableBlock; + }, + ( state, rootClientId ) => [ + state.blockListSettings[ rootClientId ], + state.editor.present.blocks.byClientId, + state.settings.allowedBlockTypes, + state.settings.templateLock, + getReusableBlocks( state ), + getBlockTypes(), + ], +); + +/** + * Returns the Block List settings of a block, if any exist. + * + * @param {Object} state Editor state. + * @param {?string} clientId Block client ID. + * + * @return {?Object} Block settings of the block if set. + */ +export function getBlockListSettings( state, clientId ) { + return state.blockListSettings[ clientId ]; +} + +/** + * Returns the editor settings. + * + * @param {Object} state Editor state. + * + * @return {Object} The editor settings object. + */ +export function getEditorSettings( state ) { + return state.settings; +} + +/** + * Returns true if any past editor history snapshots exist, or false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether undo history exists. + */ +export function hasEditorUndo( state ) { + return state.editor.past.length > 0; +} + +/** + * Returns true if any future editor history snapshots exist, or false + * otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether redo history exists. + */ +export function hasEditorRedo( state ) { + return state.editor.future.length > 0; +} + +function getPostMeta( state, key ) { + return get( state, [ 'editor', 'settings', 'meta', key ] ); +} + +function getReusableBlocks( state ) { + return get( state, [ 'editor', 'settings', 'reusableBlocks' ], EMPTY_ARRAY ); +} diff --git a/packages/editor/src/utils/with-history/README.md b/packages/block-editor/src/utils/with-history/README.md similarity index 100% rename from packages/editor/src/utils/with-history/README.md rename to packages/block-editor/src/utils/with-history/README.md diff --git a/packages/editor/src/utils/with-history/index.js b/packages/block-editor/src/utils/with-history/index.js similarity index 100% rename from packages/editor/src/utils/with-history/index.js rename to packages/block-editor/src/utils/with-history/index.js diff --git a/packages/editor/src/utils/with-history/test/index.js b/packages/block-editor/src/utils/with-history/test/index.js similarity index 100% rename from packages/editor/src/utils/with-history/test/index.js rename to packages/block-editor/src/utils/with-history/test/index.js diff --git a/packages/editor/src/components/post-text-editor/index.js b/packages/editor/src/components/post-text-editor/index.js index c08e59f35a400..38fa6dcdf72d0 100644 --- a/packages/editor/src/components/post-text-editor/index.js +++ b/packages/editor/src/components/post-text-editor/index.js @@ -94,13 +94,15 @@ export default compose( [ }; } ), withDispatch( ( dispatch ) => { - const { editPost, resetBlocks } = dispatch( 'core/editor' ); + const { editPost, updateEditorBlocks, resetBlocks } = dispatch( 'core/editor' ); return { onChange( content ) { editPost( { content } ); }, onPersist( content ) { - resetBlocks( parse( content ) ); + const blocks = parse( content ); + resetBlocks( blocks ); + updateEditorBlocks( blocks ); }, }; } ), diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index ebe4edf43e653..1be52b4d68850 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -1,16 +1,16 @@ /** * External dependencies */ -import { flow, map } from 'lodash'; +import { map } from 'lodash'; /** * WordPress Dependencies */ import { compose } from '@wordpress/compose'; -import { createElement, Component } from '@wordpress/element'; -import { DropZoneProvider, SlotFillProvider } from '@wordpress/components'; -import { withDispatch } from '@wordpress/data'; +import { Component } from '@wordpress/element'; +import { withDispatch, withSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +import { BlockEditorProvider } from '@wordpress/block-editor'; /** * Internal dependencies @@ -26,9 +26,8 @@ class EditorProvider extends Component { return; } - props.updateEditorSettings( props.settings ); props.updatePostLock( props.settings.postLock ); - props.setupEditor( props.post, props.initialEdits ); + props.setupEditor( props.post, props.initialEdits, props.settings.template ); if ( props.settings.autosave ) { props.createWarningNotice( @@ -67,55 +66,45 @@ class EditorProvider extends Component { } ); } - componentDidUpdate( prevProps ) { - if ( this.props.settings !== prevProps.settings ) { - this.props.updateEditorSettings( this.props.settings ); - } - } - render() { - const { - children, - } = this.props; - - const providers = [ - // Slot / Fill provider: - // - // - context.getSlot - // - context.registerSlot - // - context.unregisterSlot - [ - SlotFillProvider, - ], + const { children, settings, blocks, updateEditorBlocks, isReady } = this.props; - // DropZone provider: - [ - DropZoneProvider, - ], - ]; + if ( ! isReady ) { + return null; + } - const createEditorElement = flow( - providers.map( ( [ Provider, props ] ) => ( - ( arg ) => createElement( Provider, props, arg ) - ) ) + return ( + + { children } + ); - - return createEditorElement( children ); } } -export default withDispatch( ( dispatch ) => { - const { - setupEditor, - updateEditorSettings, - updatePostLock, - } = dispatch( 'core/editor' ); - const { createWarningNotice } = dispatch( 'core/notices' ); - - return { - setupEditor, - updateEditorSettings, - updatePostLock, - createWarningNotice, - }; -} )( EditorProvider ); +export default compose( [ + withSelect( ( select ) => { + return { + isReady: select( 'core/editor' ).isEditorReady(), + blocks: select( 'core/editor' ).getEditorBlocks(), + }; + } ), + withDispatch( ( dispatch ) => { + const { + setupEditor, + updatePostLock, + updateEditorBlocks, + } = dispatch( 'core/editor' ); + const { createWarningNotice } = dispatch( 'core/notices' ); + + return { + setupEditor, + updatePostLock, + createWarningNotice, + updateEditorBlocks, + }; + } ), +] )( EditorProvider ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 6302ce8894756..b4dacc0f41d5d 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -3,25 +3,22 @@ */ import { castArray } from 'lodash'; -/** - * WordPress dependencies - */ -import { getDefaultBlockName, createBlock } from '@wordpress/blocks'; - /** * Returns an action object used in signalling that editor has initialized with * the specified post object and editor settings. * - * @param {Object} post Post object. - * @param {Object} edits Initial edited attributes object. + * @param {Object} post Post object. + * @param {Object} edits Initial edited attributes object. + * @param {Array?} template Block Template. * * @return {Object} Action object. */ -export function setupEditor( post, edits ) { +export function setupEditor( post, edits, template ) { return { type: 'SETUP_EDITOR', post, edits, + template, }; } @@ -74,306 +71,13 @@ export function updatePost( edits ) { * Returns an action object used to setup the editor state when first opening an editor. * * @param {Object} post Post object. - * @param {Array} blocks Array of blocks. * * @return {Object} Action object. */ -export function setupEditorState( post, blocks ) { +export function setupEditorState( post ) { return { type: 'SETUP_EDITOR_STATE', post, - blocks, - }; -} - -/** - * Returns an action object used in signalling that blocks state should be - * reset to the specified array of blocks, taking precedence over any other - * content reflected as an edit in state. - * - * @param {Array} blocks Array of blocks. - * - * @return {Object} Action object. - */ -export function resetBlocks( blocks ) { - return { - type: 'RESET_BLOCKS', - blocks, - }; -} - -/** - * Returns an action object used in signalling that blocks have been received. - * Unlike resetBlocks, these should be appended to the existing known set, not - * replacing. - * - * @param {Object[]} blocks Array of block objects. - * - * @return {Object} Action object. - */ -export function receiveBlocks( blocks ) { - return { - type: 'RECEIVE_BLOCKS', - blocks, - }; -} - -/** - * Returns an action object used in signalling that the block attributes with - * the specified client ID has been updated. - * - * @param {string} clientId Block client ID. - * @param {Object} attributes Block attributes to be merged. - * - * @return {Object} Action object. - */ -export function updateBlockAttributes( clientId, attributes ) { - return { - type: 'UPDATE_BLOCK_ATTRIBUTES', - clientId, - attributes, - }; -} - -/** - * Returns an action object used in signalling that the block with the - * specified client ID has been updated. - * - * @param {string} clientId Block client ID. - * @param {Object} updates Block attributes to be merged. - * - * @return {Object} Action object. - */ -export function updateBlock( clientId, updates ) { - return { - type: 'UPDATE_BLOCK', - clientId, - updates, - }; -} - -/** - * Returns an action object used in signalling that the block with the - * specified client ID has been selected, optionally accepting a position - * value reflecting its selection directionality. An initialPosition of -1 - * reflects a reverse selection. - * - * @param {string} clientId Block client ID. - * @param {?number} initialPosition Optional initial position. Pass as -1 to - * reflect reverse selection. - * - * @return {Object} Action object. - */ -export function selectBlock( clientId, initialPosition = null ) { - return { - type: 'SELECT_BLOCK', - initialPosition, - clientId, - }; -} - -export function startMultiSelect() { - return { - type: 'START_MULTI_SELECT', - }; -} - -export function stopMultiSelect() { - return { - type: 'STOP_MULTI_SELECT', - }; -} - -export function multiSelect( start, end ) { - return { - type: 'MULTI_SELECT', - start, - end, - }; -} - -export function clearSelectedBlock() { - return { - type: 'CLEAR_SELECTED_BLOCK', - }; -} - -/** - * Returns an action object that enables or disables block selection. - * - * @param {boolean} [isSelectionEnabled=true] Whether block selection should - * be enabled. - - * @return {Object} Action object. - */ -export function toggleSelection( isSelectionEnabled = true ) { - return { - type: 'TOGGLE_SELECTION', - isSelectionEnabled, - }; -} - -/** - * Returns an action object signalling that a blocks should be replaced with - * one or more replacement blocks. - * - * @param {(string|string[])} clientIds Block client ID(s) to replace. - * @param {(Object|Object[])} blocks Replacement block(s). - * - * @return {Object} Action object. - */ -export function replaceBlocks( clientIds, blocks ) { - return { - type: 'REPLACE_BLOCKS', - clientIds: castArray( clientIds ), - blocks: castArray( blocks ), - time: Date.now(), - }; -} - -/** - * Returns an action object signalling that a single block should be replaced - * with one or more replacement blocks. - * - * @param {(string|string[])} clientId Block client ID to replace. - * @param {(Object|Object[])} block Replacement block(s). - * - * @return {Object} Action object. - */ -export function replaceBlock( clientId, block ) { - return replaceBlocks( clientId, block ); -} - -/** - * Higher-order action creator which, given the action type to dispatch creates - * an action creator for managing block movement. - * - * @param {string} type Action type to dispatch. - * - * @return {Function} Action creator. - */ -function createOnMove( type ) { - return ( clientIds, rootClientId ) => { - return { - clientIds: castArray( clientIds ), - type, - rootClientId, - }; - }; -} - -export const moveBlocksDown = createOnMove( 'MOVE_BLOCKS_DOWN' ); -export const moveBlocksUp = createOnMove( 'MOVE_BLOCKS_UP' ); - -/** - * Returns an action object signalling that an indexed block should be moved - * to a new index. - * - * @param {?string} clientId The client ID of the block. - * @param {?string} fromRootClientId Root client ID source. - * @param {?string} toRootClientId Root client ID destination. - * @param {number} index The index to move the block into. - * - * @return {Object} Action object. - */ -export function moveBlockToPosition( clientId, fromRootClientId, toRootClientId, index ) { - return { - type: 'MOVE_BLOCK_TO_POSITION', - fromRootClientId, - toRootClientId, - clientId, - index, - }; -} - -/** - * Returns an action object used in signalling that a single block should be - * inserted, optionally at a specific index respective a root block list. - * - * @param {Object} block Block object to insert. - * @param {?number} index Index at which block should be inserted. - * @param {?string} rootClientId Optional root client ID of block list on which to insert. - * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. - * - * @return {Object} Action object. - */ -export function insertBlock( block, index, rootClientId, updateSelection = true ) { - return insertBlocks( [ block ], index, rootClientId, updateSelection ); -} - -/** - * Returns an action object used in signalling that an array of blocks should - * be inserted, optionally at a specific index respective a root block list. - * - * @param {Object[]} blocks Block objects to insert. - * @param {?number} index Index at which block should be inserted. - * @param {?string} rootClientId Optional root cliente ID of block list on which to insert. - * @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true. - * - * @return {Object} Action object. - */ -export function insertBlocks( blocks, index, rootClientId, updateSelection = true ) { - return { - type: 'INSERT_BLOCKS', - blocks: castArray( blocks ), - index, - rootClientId, - time: Date.now(), - updateSelection, - }; -} - -/** - * Returns an action object used in signalling that the insertion point should - * be shown. - * - * @param {?string} rootClientId Optional root client ID of block list on - * which to insert. - * @param {?number} index Index at which block should be inserted. - * - * @return {Object} Action object. - */ -export function showInsertionPoint( rootClientId, index ) { - return { - type: 'SHOW_INSERTION_POINT', - rootClientId, - index, - }; -} - -/** - * Returns an action object hiding the insertion point. - * - * @return {Object} Action object. - */ -export function hideInsertionPoint() { - return { - type: 'HIDE_INSERTION_POINT', - }; -} - -/** - * Returns an action object resetting the template validity. - * - * @param {boolean} isValid template validity flag. - * - * @return {Object} Action object. - */ -export function setTemplateValidity( isValid ) { - return { - type: 'SET_TEMPLATE_VALIDITY', - isValid, - }; -} - -/** - * Returns an action object synchronize the template with the list of blocks - * - * @return {Object} Action object. - */ -export function synchronizeTemplate() { - return { - type: 'SYNCHRONIZE_TEMPLATE', }; } @@ -421,21 +125,6 @@ export function trashPost( postId, postType ) { }; } -/** - * Returns an action object used in signalling that two blocks should be merged - * - * @param {string} firstBlockClientId Client ID of the first block to merge. - * @param {string} secondBlockClientId Client ID of the second block to merge. - * - * @return {Object} Action object. - */ -export function mergeBlocks( firstBlockClientId, secondBlockClientId ) { - return { - type: 'MERGE_BLOCKS', - blocks: [ firstBlockClientId, secondBlockClientId ], - }; -} - /** * Returns an action object used in signalling that the post should autosave. * @@ -447,126 +136,6 @@ export function autosave( options ) { return savePost( { isAutosave: true, ...options } ); } -/** - * Returns an action object used in signalling that undo history should - * restore last popped state. - * - * @return {Object} Action object. - */ -export function redo() { - return { type: 'REDO' }; -} - -/** - * Returns an action object used in signalling that undo history should pop. - * - * @return {Object} Action object. - */ -export function undo() { - return { type: 'UNDO' }; -} - -/** - * Returns an action object used in signalling that undo history record should - * be created. - * - * @return {Object} Action object. - */ -export function createUndoLevel() { - return { type: 'CREATE_UNDO_LEVEL' }; -} - -/** - * Returns an action object used in signalling that the blocks corresponding to - * the set of specified client IDs are to be removed. - * - * @param {string|string[]} clientIds Client IDs of blocks to remove. - * @param {boolean} selectPrevious True if the previous block should be - * selected when a block is removed. - * - * @return {Object} Action object. - */ -export function removeBlocks( clientIds, selectPrevious = true ) { - return { - type: 'REMOVE_BLOCKS', - clientIds: castArray( clientIds ), - selectPrevious, - }; -} - -/** - * Returns an action object used in signalling that the block with the - * specified client ID is to be removed. - * - * @param {string} clientId Client ID of block to remove. - * @param {boolean} selectPrevious True if the previous block should be - * selected when a block is removed. - * - * @return {Object} Action object. - */ -export function removeBlock( clientId, selectPrevious ) { - return removeBlocks( [ clientId ], selectPrevious ); -} - -/** - * Returns an action object used to toggle the block editing mode between - * visual and HTML modes. - * - * @param {string} clientId Block client ID. - * - * @return {Object} Action object. - */ -export function toggleBlockMode( clientId ) { - return { - type: 'TOGGLE_BLOCK_MODE', - clientId, - }; -} - -/** - * Returns an action object used in signalling that the user has begun to type. - * - * @return {Object} Action object. - */ -export function startTyping() { - return { - type: 'START_TYPING', - }; -} - -/** - * Returns an action object used in signalling that the user has stopped typing. - * - * @return {Object} Action object. - */ -export function stopTyping() { - return { - type: 'STOP_TYPING', - }; -} - -/** - * Returns an action object used in signalling that the caret has entered formatted text. - * - * @return {Object} Action object. - */ -export function enterFormattedText() { - return { - type: 'ENTER_FORMATTED_TEXT', - }; -} - -/** - * Returns an action object used in signalling that the user caret has exited formatted text. - * - * @return {Object} Action object. - */ -export function exitFormattedText() { - return { - type: 'EXIT_FORMATTED_TEXT', - }; -} - /** * Returns an action object used to lock the editor. * @@ -687,53 +256,6 @@ export function __experimentalConvertBlockToReusable( clientIds ) { clientIds: castArray( clientIds ), }; } -/** - * Returns an action object used in signalling that a new block of the default - * type should be added to the block list. - * - * @param {?Object} attributes Optional attributes of the block to assign. - * @param {?string} rootClientId Optional root client ID of block list on which - * to append. - * @param {?number} index Optional index where to insert the default block - * - * @return {Object} Action object - */ -export function insertDefaultBlock( attributes, rootClientId, index ) { - const block = createBlock( getDefaultBlockName(), attributes ); - - return insertBlock( block, index, rootClientId ); -} - -/** - * Returns an action object that changes the nested settings of a given block. - * - * @param {string} clientId Client ID of the block whose nested setting are - * being received. - * @param {Object} settings Object with the new settings for the nested block. - * - * @return {Object} Action object - */ -export function updateBlockListSettings( clientId, settings ) { - return { - type: 'UPDATE_BLOCK_LIST_SETTINGS', - clientId, - settings, - }; -} - -/* - * Returns an action object used in signalling that the editor settings have been updated. - * - * @param {Object} settings Updated settings - * - * @return {Object} Action object - */ -export function updateEditorSettings( settings ) { - return { - type: 'UPDATE_EDITOR_SETTINGS', - settings, - }; -} /** * Returns an action object used in signalling that the user has enabled the publish sidebar. @@ -784,3 +306,61 @@ export function unlockPostSaving( lockName ) { lockName, }; } + +/** + * Returns an action object used to signal that the blocks have been updated. + * + * @param {Array} blocks Block Array. + * + * @return {Object} Action object + */ +export function updateEditorBlocks( blocks ) { + return { + type: 'UPDATE_BLOCKS', + blocks, + }; +} + +/** + * Backward compatibility + */ + +const getBlockEditorAction = ( name ) => ( ...args ) => { + window.wp.data.dispatch( 'core/block-editor' )[ name ]( ...args ); + return { type: 'DO_NOTHING' }; +}; + +export const resetBlocks = getBlockEditorAction( 'resetBlocks' ); +export const receiveBlocks = getBlockEditorAction( 'receiveBlocks' ); +export const updateBlock = getBlockEditorAction( 'updateBlock' ); +export const updateBlockAttributes = getBlockEditorAction( 'updateBlockAttributes' ); +export const selectBlock = getBlockEditorAction( 'selectBlock' ); +export const startMultiSelect = getBlockEditorAction( 'startMultiSelect' ); +export const stopMultiSelect = getBlockEditorAction( 'stopMultiSelect' ); +export const multiSelect = getBlockEditorAction( 'multiSelect' ); +export const clearSelectedBlock = getBlockEditorAction( 'clearSelectedBlock' ); +export const toggleSelection = getBlockEditorAction( 'toggleSelection' ); +export const replaceBlocks = getBlockEditorAction( 'replaceBlocks' ); +export const moveBlocksDown = getBlockEditorAction( 'moveBlocksDown' ); +export const moveBlocksUp = getBlockEditorAction( 'moveBlocksUp' ); +export const moveBlockToPosition = getBlockEditorAction( 'moveBlockToPosition' ); +export const insertBlock = getBlockEditorAction( 'insertBlock' ); +export const insertBlocks = getBlockEditorAction( 'insertBlocks' ); +export const showInsertionPoint = getBlockEditorAction( 'showInsertionPoint' ); +export const hideInsertionPoint = getBlockEditorAction( 'hideInsertionPoint' ); +export const setTemplateValidity = getBlockEditorAction( 'setTemplateValidity' ); +export const synchronizeTemplate = getBlockEditorAction( 'synchronizeTemplate' ); +export const mergeBlocks = getBlockEditorAction( 'mergeBlocks' ); +export const removeBlocks = getBlockEditorAction( 'removeBlocks' ); +export const removeBlock = getBlockEditorAction( 'removeBlock' ); +export const toggleBlockMode = getBlockEditorAction( 'toggleBlockMode' ); +export const startTyping = getBlockEditorAction( 'startTyping' ); +export const stopTyping = getBlockEditorAction( 'stopTyping' ); +export const enterFormattedText = getBlockEditorAction( 'enterFormattedText' ); +export const exitFormattedText = getBlockEditorAction( 'exitFormattedText' ); +export const insertDefaultBlock = getBlockEditorAction( 'insertDefaultBlock' ); +export const updateBlockListSettings = getBlockEditorAction( 'updateBlockListSettings' ); +export const updateEditorSettings = getBlockEditorAction( 'updateEditorSettings' ); +export const undo = getBlockEditorAction( 'undo' ); +export const redo = getBlockEditorAction( 'redo' ); +export const createUndoLevel = getBlockEditorAction( 'createUndoLevel' ); diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index dc4111ef641d4..32fd438302c69 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -1,137 +1,7 @@ -/** - * WordPress dependencies - */ -import { __, _x } from '@wordpress/i18n'; - export const PREFERENCES_DEFAULTS = { - insertUsage: {}, isPublishSidebarEnabled: true, }; -/** - * The default editor settings - * - * alignWide boolean Enable/Disable Wide/Full Alignments - * colors Array Palette colors - * fontSizes Array Available font sizes - * imageSizes Array Available image sizes - * maxWidth number Max width to constraint resizing - * blockTypes boolean|Array Allowed block types - * hasFixedToolbar boolean Whether or not the editor toolbar is fixed - * focusMode boolean Whether the focus mode is enabled or not - * richEditingEnabled boolean Whether rich editing is enabled or not - */ -export const EDITOR_SETTINGS_DEFAULTS = { - alignWide: false, - colors: [ - { - name: __( 'Pale pink' ), - slug: 'pale-pink', - color: '#f78da7', - }, - { name: __( 'Vivid red' ), - slug: 'vivid-red', - color: '#cf2e2e', - }, - { - name: __( 'Luminous vivid orange' ), - slug: 'luminous-vivid-orange', - color: '#ff6900', - }, - { - name: __( 'Luminous vivid amber' ), - slug: 'luminous-vivid-amber', - color: '#fcb900', - }, - { - name: __( 'Light green cyan' ), - slug: 'light-green-cyan', - color: '#7bdcb5', - }, - { - name: __( 'Vivid green cyan' ), - slug: 'vivid-green-cyan', - color: '#00d084', - }, - { - name: __( 'Pale cyan blue' ), - slug: 'pale-cyan-blue', - color: '#8ed1fc', - }, - { - name: __( 'Vivid cyan blue' ), - slug: 'vivid-cyan-blue', - color: '#0693e3', - }, - { - name: __( 'Very light gray' ), - slug: 'very-light-gray', - color: '#eeeeee', - }, - { - name: __( 'Cyan bluish gray' ), - slug: 'cyan-bluish-gray', - color: '#abb8c3', - }, - { - name: __( 'Very dark gray' ), - slug: 'very-dark-gray', - color: '#313131', - }, - ], - - fontSizes: [ - { - name: _x( 'Small', 'font size name' ), - size: 13, - slug: 'small', - }, - { - name: _x( 'Normal', 'font size name' ), - size: 16, - slug: 'normal', - }, - { - name: _x( 'Medium', 'font size name' ), - size: 20, - slug: 'medium', - }, - { - name: _x( 'Large', 'font size name' ), - size: 36, - slug: 'large', - }, - { - name: _x( 'Huge', 'font size name' ), - size: 48, - slug: 'huge', - }, - ], - - imageSizes: [ - { slug: 'thumbnail', label: __( 'Thumbnail' ) }, - { slug: 'medium', label: __( 'Medium' ) }, - { slug: 'large', label: __( 'Large' ) }, - { slug: 'full', label: __( 'Full Size' ) }, - ], - - // This is current max width of the block inner area - // It's used to constraint image resizing and this value could be overridden later by themes - maxWidth: 580, - - // Allowed block types for the editor, defaulting to true (all supported). - allowedBlockTypes: true, - - // Maximum upload size in bytes allowed for the site. - maxUploadFileSize: 0, - - // List of allowed mime types and file extensions. - allowedMimeTypes: null, - - // Whether richs editing is enabled or not. - richEditingEnabled: true, -}; - /** * Default initial edits state. * diff --git a/packages/editor/src/store/effects.js b/packages/editor/src/store/effects.js index 18e753d3f70bf..724d7728a79c8 100644 --- a/packages/editor/src/store/effects.js +++ b/packages/editor/src/store/effects.js @@ -1,44 +1,23 @@ /** * External dependencies */ -import { compact, last, has } from 'lodash'; +import { has } from 'lodash'; /** * WordPress dependencies */ -import { speak } from '@wordpress/a11y'; import { parse, - getBlockType, - switchToBlockType, - doBlocksMatchTemplate, synchronizeBlocksWithTemplate, } from '@wordpress/blocks'; -import { _n, sprintf } from '@wordpress/i18n'; /** * Internal dependencies */ import { setupEditorState, - replaceBlocks, - selectBlock, - resetBlocks, - setTemplateValidity, - insertDefaultBlock, + updateEditorBlocks, } from './actions'; -import { - getBlock, - getBlockRootClientId, - getBlocks, - getBlockCount, - getPreviousBlockClientId, - getSelectedBlockClientId, - getSelectedBlockCount, - getTemplate, - getTemplateLock, - isValidTemplate, -} from './selectors'; import { fetchReusableBlocks, saveReusableBlocks, @@ -56,90 +35,6 @@ import { refreshPost, } from './effects/posts'; -/** - * Block validity is a function of blocks state (at the point of a - * reset) and the template setting. As a compromise to its placement - * across distinct parts of state, it is implemented here as a side- - * effect of the block reset action. - * - * @param {Object} action RESET_BLOCKS action. - * @param {Object} store Store instance. - * - * @return {?Object} New validity set action if validity has changed. - */ -export function validateBlocksToTemplate( action, store ) { - const state = store.getState(); - const template = getTemplate( state ); - const templateLock = getTemplateLock( state ); - - // Unlocked templates are considered always valid because they act - // as default values only. - const isBlocksValidToTemplate = ( - ! template || - templateLock !== 'all' || - doBlocksMatchTemplate( action.blocks, template ) - ); - - // Update if validity has changed. - if ( isBlocksValidToTemplate !== isValidTemplate( state ) ) { - return setTemplateValidity( isBlocksValidToTemplate ); - } -} - -/** - * Effect handler which will return a block select action to select the block - * occurring before the selected block in the previous state, unless it is the - * same block or the action includes a falsey `selectPrevious` option flag. - * - * @param {Object} action Action which had initiated the effect handler. - * @param {Object} store Store instance. - * - * @return {?Object} Block select action to select previous, if applicable. - */ -export function selectPreviousBlock( action, store ) { - // if the action says previous block should not be selected don't do anything. - if ( ! action.selectPrevious ) { - return; - } - - const firstRemovedBlockClientId = action.clientIds[ 0 ]; - const state = store.getState(); - const selectedBlockClientId = getSelectedBlockClientId( state ); - - // recreate the state before the block was removed. - const previousState = { ...state, editor: { present: last( state.editor.past ) } }; - - // rootClientId of the removed block. - const rootClientId = getBlockRootClientId( previousState, firstRemovedBlockClientId ); - - // Client ID of the block that was before the removed block or the - // rootClientId if the removed block was first amongst its siblings. - const blockClientIdToSelect = getPreviousBlockClientId( previousState, firstRemovedBlockClientId ) || rootClientId; - - // Dispatch select block action if the currently selected block - // is not already the block we want to be selected. - if ( blockClientIdToSelect !== selectedBlockClientId ) { - return selectBlock( blockClientIdToSelect, -1 ); - } -} - -/** - * Effect handler which will return a default block insertion action if there - * are no other blocks at the root of the editor. This is expected to be used - * in actions which may result in no blocks remaining in the editor (removal, - * replacement, etc). - * - * @param {Object} action Action which had initiated the effect handler. - * @param {Object} store Store instance. - * - * @return {?Object} Default block insert action, if no other blocks exist. - */ -export function ensureDefaultBlock( action, store ) { - if ( ! getBlockCount( store.getState() ) ) { - return insertDefaultBlock(); - } -} - export default { REQUEST_POST_UPDATE: ( action, store ) => { requestPostUpdate( action, store ); @@ -153,55 +48,8 @@ export default { REFRESH_POST: ( action, store ) => { refreshPost( action, store ); }, - MERGE_BLOCKS( action, store ) { - const { dispatch } = store; - const state = store.getState(); - const [ firstBlockClientId, secondBlockClientId ] = action.blocks; - const blockA = getBlock( state, firstBlockClientId ); - const blockType = getBlockType( blockA.name ); - - // Only focus the previous block if it's not mergeable - if ( ! blockType.merge ) { - dispatch( selectBlock( blockA.clientId ) ); - return; - } - - // We can only merge blocks with similar types - // thus, we transform the block to merge first - const blockB = getBlock( state, secondBlockClientId ); - 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( selectBlock( blockA.clientId, -1 ) ); - dispatch( replaceBlocks( - [ blockA.clientId, blockB.clientId ], - [ - { - ...blockA, - attributes: { - ...blockA.attributes, - ...updatedAttributes, - }, - }, - ...blocksWithTheSameType.slice( 1 ), - ] - ) ); - }, - SETUP_EDITOR( action, store ) { - const { post, edits } = action; - const state = store.getState(); + SETUP_EDITOR( action ) { + const { post, edits, template } = action; // In order to ensure maximum of a single parse during setup, edits are // included as part of editor setup action. Assume edited content as @@ -217,33 +65,14 @@ export default { // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; - const template = getTemplate( state ); if ( isNewPost && template ) { blocks = synchronizeBlocksWithTemplate( blocks, template ); } - const setupAction = setupEditorState( post, blocks ); - - return compact( [ - setupAction, - - // TODO: This is temporary, necessary only so long as editor setup - // is a separate action from block resetting. - // - // See: https://github.com/WordPress/gutenberg/pull/9403 - validateBlocksToTemplate( setupAction, store ), - ] ); - }, - RESET_BLOCKS: [ - validateBlocksToTemplate, - ], - SYNCHRONIZE_TEMPLATE( action, { getState } ) { - const state = getState(); - const blocks = getBlocks( state ); - const template = getTemplate( state ); - const updatedBlockList = synchronizeBlocksWithTemplate( blocks, template ); - - return resetBlocks( updatedBlockList ); + return [ + updateEditorBlocks( blocks ), + setupEditorState( post ), + ]; }, FETCH_REUSABLE_BLOCKS: ( action, store ) => { fetchReusableBlocks( action, store ); @@ -257,17 +86,4 @@ export default { RECEIVE_REUSABLE_BLOCKS: receiveReusableBlocks, CONVERT_BLOCK_TO_STATIC: convertBlockToStatic, CONVERT_BLOCK_TO_REUSABLE: convertBlockToReusable, - REMOVE_BLOCKS: [ - selectPreviousBlock, - ensureDefaultBlock, - ], - REPLACE_BLOCKS: [ - ensureDefaultBlock, - ], - MULTI_SELECT: ( action, { getState } ) => { - const blockCount = getSelectedBlockCount( getState() ); - - /* translators: %s: number of selected blocks */ - speak( sprintf( _n( '%s block selected.', '%s blocks selected.', blockCount ), blockCount ), 'assertive' ); - }, }; diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 06061290396de..fe5d65f8c98dc 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -5,15 +5,10 @@ import optimist from 'redux-optimist'; import { flow, reduce, - first, - last, omit, - without, mapValues, - omitBy, keys, isEqual, - isEmpty, overSome, get, } from 'lodash'; @@ -21,22 +16,18 @@ import { /** * WordPress dependencies */ -import { isReusableBlock } from '@wordpress/blocks'; import { combineReducers } from '@wordpress/data'; import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import withHistory from '../utils/with-history'; -import withChangeDetection from '../utils/with-change-detection'; import { PREFERENCES_DEFAULTS, - EDITOR_SETTINGS_DEFAULTS, INITIAL_EDITS_DEFAULTS, } from './defaults'; -import { insertAt, moveTo } from './array'; import { EDIT_MERGE_PROPERTIES } from './constants'; +import withChangeDetection from '../utils/with-change-detection'; /** * Returns a post attribute value, flattening nested rendered content using its @@ -54,101 +45,6 @@ export function getPostRawValue( value ) { return value; } -/** - * Given an array of blocks, returns an object where each key is a nesting - * context, the value of which is an array of block client IDs existing within - * that nesting context. - * - * @param {Array} blocks Blocks to map. - * @param {?string} rootClientId Assumed root client ID. - * - * @return {Object} Block order map object. - */ -function mapBlockOrder( blocks, rootClientId = '' ) { - const result = { [ rootClientId ]: [] }; - - blocks.forEach( ( block ) => { - const { clientId, innerBlocks } = block; - - result[ rootClientId ].push( clientId ); - - Object.assign( result, mapBlockOrder( innerBlocks, clientId ) ); - } ); - - return result; -} - -/** - * Helper method to iterate through all blocks, recursing into inner blocks, - * applying a transformation function to each one. - * Returns a flattened object with the transformed blocks. - * - * @param {Array} blocks Blocks to flatten. - * @param {Function} transform Transforming function to be applied to each block. - * - * @return {Object} Flattened object. - */ -function flattenBlocks( blocks, transform ) { - const result = {}; - - const stack = [ ...blocks ]; - while ( stack.length ) { - const { innerBlocks, ...block } = stack.shift(); - stack.push( ...innerBlocks ); - result[ block.clientId ] = transform( block ); - } - - return result; -} - -/** - * Given an array of blocks, returns an object containing all blocks, without - * attributes, recursing into inner blocks. Keys correspond to the block client - * ID, the value of which is the attributes object. - * - * @param {Array} blocks Blocks to flatten. - * - * @return {Object} Flattened block attributes object. - */ -function getFlattenedBlocksWithoutAttributes( blocks ) { - return flattenBlocks( blocks, ( block ) => omit( block, 'attributes' ) ); -} - -/** - * Given an array of blocks, returns an object containing all block attributes, - * recursing into inner blocks. Keys correspond to the block client ID, the - * value of which is the attributes object. - * - * @param {Array} blocks Blocks to flatten. - * - * @return {Object} Flattened block attributes object. - */ -function getFlattenedBlockAttributes( blocks ) { - return flattenBlocks( blocks, ( block ) => block.attributes ); -} - -/** - * Given a block order map object, returns *all* of the block client IDs that are - * a descendant of the given root client ID. - * - * Calling this with `rootClientId` set to `''` results in a list of client IDs - * that are in the post. That is, it excludes blocks like fetched reusable - * blocks which are stored into state but not visible. - * - * @param {Object} blocksOrder Object that maps block client IDs to a list of - * nested block client IDs. - * @param {?string} rootClientId The root client ID to search. Defaults to ''. - * - * @return {Array} List of descendant client IDs. - */ -function getNestedBlockClientIds( blocksOrder, rootClientId = '' ) { - return reduce( blocksOrder[ rootClientId ], ( result, clientId ) => [ - ...result, - clientId, - ...getNestedBlockClientIds( blocksOrder, clientId ), - ], [] ); -} - /** * Returns an object against which it is safe to perform mutating operations, * given the original object and its current working copy. @@ -235,100 +131,6 @@ export function shouldOverwriteState( action, previousAction ) { ] )( action, previousAction ); } -/** - * Higher-order reducer targeting the combined editor reducer, augmenting - * block client IDs in remove action to include cascade of inner blocks. - * - * @param {Function} reducer Original reducer function. - * - * @return {Function} Enhanced reducer function. - */ -const withInnerBlocksRemoveCascade = ( reducer ) => ( state, action ) => { - if ( state && action.type === 'REMOVE_BLOCKS' ) { - const clientIds = [ ...action.clientIds ]; - - // For each removed client ID, include its inner blocks to remove, - // recursing into those so long as inner blocks exist. - for ( let i = 0; i < clientIds.length; i++ ) { - clientIds.push( ...state.blocks.order[ clientIds[ i ] ] ); - } - - action = { ...action, clientIds }; - } - - return reducer( state, action ); -}; - -/** - * Higher-order reducer which targets the combined blocks reducer and handles - * the `RESET_BLOCKS` action. When dispatched, this action will replace all - * blocks that exist in the post, leaving blocks that exist only in state (e.g. - * reusable blocks) alone. - * - * @param {Function} reducer Original reducer function. - * - * @return {Function} Enhanced reducer function. - */ -const withBlockReset = ( reducer ) => ( state, action ) => { - if ( state && action.type === 'RESET_BLOCKS' ) { - const visibleClientIds = getNestedBlockClientIds( state.order ); - return { - ...state, - byClientId: { - ...omit( state.byClientId, visibleClientIds ), - ...getFlattenedBlocksWithoutAttributes( action.blocks ), - }, - attributes: { - ...omit( state.attributes, visibleClientIds ), - ...getFlattenedBlockAttributes( action.blocks ), - }, - order: { - ...omit( state.order, visibleClientIds ), - ...mapBlockOrder( action.blocks ), - }, - }; - } - - return reducer( state, action ); -}; - -/** - * Higher-order reducer which targets the combined blocks reducer and handles - * the `SAVE_REUSABLE_BLOCK_SUCCESS` action. This action can't be handled by - * regular reducers and needs a higher-order reducer since it needs access to - * both `byClientId` and `attributes` simultaneously. - * - * @param {Function} reducer Original reducer function. - * - * @return {Function} Enhanced reducer function. - */ -const withSaveReusableBlock = ( reducer ) => ( state, action ) => { - if ( state && action.type === 'SAVE_REUSABLE_BLOCK_SUCCESS' ) { - const { id, updatedId } = action; - - // If a temporary reusable block is saved, we swap the temporary id with the final one - if ( id === updatedId ) { - return state; - } - - state = { ...state }; - - state.attributes = mapValues( state.attributes, ( attributes, clientId ) => { - const { name } = state.byClientId[ clientId ]; - if ( name === 'core/block' && attributes.ref === id ) { - return { - ...attributes, - ref: updatedId, - }; - } - - return attributes; - } ); - } - - return reducer( state, action ); -}; - /** * Undoable reducer returning the editor post state, including blocks parsed * from current HTML markup. @@ -345,16 +147,19 @@ const withSaveReusableBlock = ( reducer ) => ( state, action ) => { */ export const editor = flow( [ combineReducers, +] )( { + // Track whether changes exist, resetting at each post save. Relies on + // editor initialization firing post reset as an effect. + blocks: withChangeDetection( { + resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ], + } )( ( state = { value: [] }, action ) => { + switch ( action.type ) { + case 'UPDATE_BLOCKS': + return { value: action.blocks }; + } - withInnerBlocksRemoveCascade, - - // Track undo history, starting at editor initialization. - withHistory( { - resetTypes: [ 'SETUP_EDITOR_STATE' ], - ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], - shouldOverwriteState, + return state; } ), -] )( { edits( state = {}, action ) { switch ( action.type ) { case 'EDIT_POST': @@ -374,14 +179,6 @@ export const editor = flow( [ return result; }, state ); - - case 'RESET_BLOCKS': - if ( 'content' in state ) { - return omit( state, 'content' ); - } - - return state; - case 'UPDATE_POST': case 'RESET_POST': const getCanonicalValue = action.type === 'UPDATE_POST' ? @@ -397,284 +194,16 @@ export const editor = flow( [ delete result[ key ]; return result; }, state ); + case 'UPDATE_BLOCKS': + if ( 'content' in state ) { + return omit( state, 'content' ); + } + + return state; } return state; }, - - blocks: flow( [ - combineReducers, - - withBlockReset, - - withSaveReusableBlock, - - // Track whether changes exist, resetting at each post save. Relies on - // editor initialization firing post reset as an effect. - withChangeDetection( { - resetTypes: [ 'SETUP_EDITOR_STATE', 'REQUEST_POST_UPDATE_START' ], - ignoreTypes: [ 'RECEIVE_BLOCKS', 'RESET_POST', 'UPDATE_POST' ], - } ), - ] )( { - byClientId( state = {}, action ) { - switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return getFlattenedBlocksWithoutAttributes( action.blocks ); - - case 'RECEIVE_BLOCKS': - return { - ...state, - ...getFlattenedBlocksWithoutAttributes( action.blocks ), - }; - - case 'UPDATE_BLOCK': - // Ignore updates if block isn't known - if ( ! state[ action.clientId ] ) { - return state; - } - - // Do nothing if only attributes change. - const changes = omit( action.updates, 'attributes' ); - if ( isEmpty( changes ) ) { - return state; - } - - return { - ...state, - [ action.clientId ]: { - ...state[ action.clientId ], - ...changes, - }, - }; - - case 'INSERT_BLOCKS': - return { - ...state, - ...getFlattenedBlocksWithoutAttributes( action.blocks ), - }; - - case 'REPLACE_BLOCKS': - if ( ! action.blocks ) { - return state; - } - - return { - ...omit( state, action.clientIds ), - ...getFlattenedBlocksWithoutAttributes( action.blocks ), - }; - - case 'REMOVE_BLOCKS': - return omit( state, action.clientIds ); - } - - return state; - }, - - attributes( state = {}, action ) { - switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return getFlattenedBlockAttributes( action.blocks ); - - case 'RECEIVE_BLOCKS': - return { - ...state, - ...getFlattenedBlockAttributes( action.blocks ), - }; - - case 'UPDATE_BLOCK': - // Ignore updates if block isn't known or there are no attribute changes. - if ( ! state[ action.clientId ] || ! action.updates.attributes ) { - return state; - } - - return { - ...state, - [ action.clientId ]: { - ...state[ action.clientId ], - ...action.updates.attributes, - }, - }; - - case 'UPDATE_BLOCK_ATTRIBUTES': - // Ignore updates if block isn't known - if ( ! state[ action.clientId ] ) { - return state; - } - - // Consider as updates only changed values - const nextAttributes = reduce( action.attributes, ( result, value, key ) => { - if ( value !== result[ key ] ) { - result = getMutateSafeObject( state[ action.clientId ], result ); - result[ key ] = value; - } - - return result; - }, state[ action.clientId ] ); - - // Skip update if nothing has been changed. The reference will - // match the original block if `reduce` had no changed values. - if ( nextAttributes === state[ action.clientId ] ) { - return state; - } - - // Otherwise replace attributes in state - return { - ...state, - [ action.clientId ]: nextAttributes, - }; - - case 'INSERT_BLOCKS': - return { - ...state, - ...getFlattenedBlockAttributes( action.blocks ), - }; - - case 'REPLACE_BLOCKS': - if ( ! action.blocks ) { - return state; - } - - return { - ...omit( state, action.clientIds ), - ...getFlattenedBlockAttributes( action.blocks ), - }; - - case 'REMOVE_BLOCKS': - return omit( state, action.clientIds ); - } - - return state; - }, - - order( state = {}, action ) { - switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return mapBlockOrder( action.blocks ); - - case 'RECEIVE_BLOCKS': - return { - ...state, - ...omit( mapBlockOrder( action.blocks ), '' ), - }; - - case 'INSERT_BLOCKS': { - const { rootClientId = '', blocks } = action; - const subState = state[ rootClientId ] || []; - const mappedBlocks = mapBlockOrder( blocks, rootClientId ); - const { index = subState.length } = action; - - return { - ...state, - ...mappedBlocks, - [ rootClientId ]: insertAt( subState, mappedBlocks[ rootClientId ], index ), - }; - } - - case 'MOVE_BLOCK_TO_POSITION': { - const { fromRootClientId = '', toRootClientId = '', clientId } = action; - const { index = state[ toRootClientId ].length } = action; - - // Moving inside the same parent block - if ( fromRootClientId === toRootClientId ) { - const subState = state[ toRootClientId ]; - const fromIndex = subState.indexOf( clientId ); - return { - ...state, - [ toRootClientId ]: moveTo( state[ toRootClientId ], fromIndex, index ), - }; - } - - // Moving from a parent block to another - return { - ...state, - [ fromRootClientId ]: without( state[ fromRootClientId ], clientId ), - [ toRootClientId ]: insertAt( state[ toRootClientId ], clientId, index ), - }; - } - - case 'MOVE_BLOCKS_UP': { - const { clientIds, rootClientId = '' } = action; - const firstClientId = first( clientIds ); - const subState = state[ rootClientId ]; - - if ( ! subState.length || firstClientId === first( subState ) ) { - return state; - } - - const firstIndex = subState.indexOf( firstClientId ); - - return { - ...state, - [ rootClientId ]: moveTo( subState, firstIndex, firstIndex - 1, clientIds.length ), - }; - } - - case 'MOVE_BLOCKS_DOWN': { - const { clientIds, rootClientId = '' } = action; - const firstClientId = first( clientIds ); - const lastClientId = last( clientIds ); - const subState = state[ rootClientId ]; - - if ( ! subState.length || lastClientId === last( subState ) ) { - return state; - } - - const firstIndex = subState.indexOf( firstClientId ); - - return { - ...state, - [ rootClientId ]: moveTo( subState, firstIndex, firstIndex + 1, clientIds.length ), - }; - } - - case 'REPLACE_BLOCKS': { - const { blocks, clientIds } = action; - if ( ! blocks ) { - return state; - } - - const mappedBlocks = mapBlockOrder( blocks ); - - return flow( [ - ( nextState ) => omit( nextState, clientIds ), - ( nextState ) => ( { - ...nextState, - ...omit( mappedBlocks, '' ), - } ), - ( nextState ) => mapValues( nextState, ( subState ) => ( - reduce( subState, ( result, clientId ) => { - if ( clientId === clientIds[ 0 ] ) { - return [ - ...result, - ...mappedBlocks[ '' ], - ]; - } - - if ( clientIds.indexOf( clientId ) === -1 ) { - result.push( clientId ); - } - - return result; - }, [] ) - ) ), - ] )( state ); - } - - case 'REMOVE_BLOCKS': - return flow( [ - // Remove inner block ordering for removed blocks - ( nextState ) => omit( nextState, action.clientIds ), - - // Remove deleted blocks from other blocks' orderings - ( nextState ) => mapValues( nextState, ( subState ) => ( - without( subState, ...action.clientIds ) - ) ), - ] )( state ); - } - - return state; - }, - } ), } ); /** @@ -966,26 +495,6 @@ export function template( state = { isValid: true }, action ) { return state; } -/** - * Reducer returning the editor setting. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function settings( state = EDITOR_SETTINGS_DEFAULTS, action ) { - switch ( action.type ) { - case 'UPDATE_EDITOR_SETTINGS': - return { - ...state, - ...action.settings, - }; - } - - return state; -} - /** * Reducer returning the user preferences. * @@ -996,35 +505,6 @@ export function settings( state = EDITOR_SETTINGS_DEFAULTS, action ) { */ export function preferences( state = PREFERENCES_DEFAULTS, action ) { switch ( action.type ) { - case 'INSERT_BLOCKS': - case 'REPLACE_BLOCKS': - return action.blocks.reduce( ( prevState, block ) => { - let id = block.name; - const insert = { name: block.name }; - if ( isReusableBlock( block ) ) { - insert.ref = block.attributes.ref; - id += '/' + block.attributes.ref; - } - - return { - ...prevState, - insertUsage: { - ...prevState.insertUsage, - [ id ]: { - time: action.time, - count: prevState.insertUsage[ id ] ? prevState.insertUsage[ id ].count + 1 : 1, - insert, - }, - }, - }; - }, state ); - - case 'REMOVE_REUSABLE_BLOCK': - return { - ...state, - insertUsage: omitBy( state.insertUsage, ( { insert } ) => insert.ref === action.id ), - }; - case 'ENABLE_PUBLISH_SIDEBAR': return { ...state, @@ -1329,16 +809,19 @@ export function previewLink( state = null, action ) { return state; } +export function isReady( state = false, action ) { + switch ( action.type ) { + case 'SETUP_EDITOR_STATE': + return true; + } + + return state; +} + export default optimist( combineReducers( { editor, initialEdits, currentPost, - isTyping, - isCaretWithinFormattedText, - blockSelection, - blocksMode, - blockListSettings, - insertionPoint, preferences, saving, postLock, @@ -1346,6 +829,6 @@ export default optimist( combineReducers( { template, autosave, previewLink, - settings, postSavingLock, + isReady, } ) ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 06ee816ce1cc0..cd2564f39ac76 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -2,20 +2,10 @@ * External dependencies */ import { - castArray, - flatMap, find, - first, get, has, - includes, - isArray, - isBoolean, - last, map, - orderBy, - reduce, - some, } from 'lodash'; import createSelector from 'rememo'; @@ -24,10 +14,6 @@ import createSelector from 'rememo'; */ import { serialize, - getBlockType, - getBlockTypes, - hasBlockSupport, - hasChildBlocksWithInserterSupport, getFreeformContentHandlerName, isUnmodifiedDefaultBlock, } from '@wordpress/blocks'; @@ -50,45 +36,8 @@ export const INSERTER_UTILITY_HIGH = 3; export const INSERTER_UTILITY_MEDIUM = 2; export const INSERTER_UTILITY_LOW = 1; export const INSERTER_UTILITY_NONE = 0; -const MILLISECONDS_PER_HOUR = 3600 * 1000; -const MILLISECONDS_PER_DAY = 24 * 3600 * 1000; -const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; const ONE_MINUTE_IN_MS = 60 * 1000; -/** - * Shared reference to an empty array for cases where it is important to avoid - * returning a new array reference on every invocation, as in a connected or - * other pure component which performs `shouldComponentUpdate` check on props. - * This should be used as a last resort, since the normalized data should be - * maintained by the reducer result in state. - * - * @type {Array} - */ -const EMPTY_ARRAY = []; - -/** - * Returns true if any past editor history snapshots exist, or false otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether undo history exists. - */ -export function hasEditorUndo( state ) { - return state.editor.past.length > 0; -} - -/** - * Returns true if any future editor history snapshots exist, or false - * otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether redo history exists. - */ -export function hasEditorRedo( state ) { - return state.editor.future.length > 0; -} - /** * Returns true if the currently edited post is yet to be saved, or false if * the post has been saved. @@ -110,14 +59,14 @@ export function isEditedPostNew( state ) { */ export function hasChangedContent( state ) { return ( - state.editor.present.blocks.isDirty || + state.editor.blocks.isDirty || // `edits` is intended to contain only values which are different from // the saved post, so the mere presence of a property is an indicator // that the value is different than what is known to be saved. While // content in Visual mode is represented by the blocks state, in Text // mode it is tracked by `edits.content`. - 'content' in state.editor.present.edits + 'content' in state.editor.edits ); } @@ -137,7 +86,7 @@ export function isEditedPostDirty( state ) { // Edits should contain only fields which differ from the saved post (reset // at initial load and save complete). Thus, a non-empty edits state can be // inferred to contain unsaved values. - if ( Object.keys( state.editor.present.edits ).length > 0 ) { + if ( Object.keys( state.editor.edits ).length > 0 ) { return true; } @@ -232,11 +181,11 @@ export const getPostEdits = createSelector( ( state ) => { return { ...state.initialEdits, - ...state.editor.present.edits, + ...state.editor.edits, }; }, ( state ) => [ - state.editor.present.edits, + state.editor.edits, state.initialEdits, ] ); @@ -579,1571 +528,293 @@ export function isEditedPostDateFloating( state ) { } /** - * Returns a new reference when the inner blocks of a given block client ID - * change. This is used exclusively as a memoized selector dependant, relying - * on this selector's shared return value and recursively those of its inner - * blocks defined as dependencies. This abuses mechanics of the selector - * memoization to return from the original selector function only when - * dependants change. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {*} A value whose reference will change only when inner blocks of - * the given block client ID change. + * Returns true if the post is currently being saved, or false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether post is being saved. */ -export const getBlockDependantsCacheBust = createSelector( - () => [], - ( state, clientId ) => map( - getBlockOrder( state, clientId ), - ( innerBlockClientId ) => getBlock( state, innerBlockClientId ), - ), -); +export function isSavingPost( state ) { + return state.saving.requesting; +} /** - * Returns a block's name given its client ID, or null if no block exists with - * the client ID. + * Returns true if a previous post save was attempted successfully, or false + * otherwise. * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. + * @param {Object} state Global application state. * - * @return {string} Block name. + * @return {boolean} Whether the post was saved successfully. */ -export function getBlockName( state, clientId ) { - const block = state.editor.present.blocks.byClientId[ clientId ]; - return block ? block.name : null; +export function didPostSaveRequestSucceed( state ) { + return state.saving.successful; } /** - * Returns whether a block is valid or not. + * Returns true if a previous post save was attempted but failed, or false + * otherwise. * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. + * @param {Object} state Global application state. * - * @return {boolean} Is Valid. + * @return {boolean} Whether the post save failed. */ -export function isBlockValid( state, clientId ) { - const block = state.editor.present.blocks.byClientId[ clientId ]; - return !! block && block.isValid; +export function didPostSaveRequestFail( state ) { + return !! state.saving.error; } /** - * Returns a block's attributes given its client ID, or null if no block exists with - * the client ID. + * Returns true if the post is autosaving, or false otherwise. * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. + * @param {Object} state Global application state. * - * @return {Object?} Block attributes. + * @return {boolean} Whether the post is autosaving. */ -export const getBlockAttributes = createSelector( - ( state, clientId ) => { - const block = state.editor.present.blocks.byClientId[ clientId ]; - if ( ! block ) { - return null; - } +export function isAutosavingPost( state ) { + return isSavingPost( state ) && !! state.saving.options.isAutosave; +} - let attributes = state.editor.present.blocks.attributes[ clientId ]; - - // Inject custom source attribute values. - // - // TODO: Create generic external sourcing pattern, not explicitly - // targeting meta attributes. - const type = getBlockType( block.name ); - if ( type ) { - attributes = reduce( type.attributes, ( result, value, key ) => { - if ( value.source === 'meta' ) { - if ( result === attributes ) { - result = { ...result }; - } - - result[ key ] = getPostMeta( state, value.meta ); - } - - return result; - }, attributes ); - } +/** + * Returns true if the post is being previewed, or false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether the post is being previewed. + */ +export function isPreviewingPost( state ) { + return isSavingPost( state ) && !! state.saving.options.isPreview; +} - return attributes; - }, - ( state, clientId ) => [ - state.editor.present.blocks.byClientId[ clientId ], - state.editor.present.blocks.attributes[ clientId ], - state.editor.present.edits.meta, - state.initialEdits.meta, - state.currentPost.meta, - ] -); +/** + * Returns the post preview link + * + * @param {Object} state Global application state. + * + * @return {string?} Preview Link. + */ +export function getEditedPostPreviewLink( state ) { + const featuredImageId = getEditedPostAttribute( state, 'featured_media' ); + const previewLink = state.previewLink; + if ( previewLink && featuredImageId ) { + return addQueryArgs( previewLink, { _thumbnail_id: featuredImageId } ); + } + + return previewLink; +} /** - * Returns a block given its client ID. This is a parsed copy of the block, - * containing its `blockName`, `clientId`, and current `attributes` state. This - * is not the block's registration settings, which must be retrieved from the - * blocks module registration store. + * Returns a suggested post format for the current post, inferred only if there + * is a single block within the post and it is of a type known to match a + * default post format. Returns null if the format cannot be determined. * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. + * @param {Object} state Global application state. * - * @return {Object} Parsed block object. + * @return {?string} Suggested post format. */ -export const getBlock = createSelector( - ( state, clientId ) => { - const block = state.editor.present.blocks.byClientId[ clientId ]; - if ( ! block ) { - return null; - } +export function getSuggestedPostFormat( state ) { + const blocks = state.editor.blocks.value; - return { - ...block, - attributes: getBlockAttributes( state, clientId ), - innerBlocks: getBlocks( state, clientId ), - }; - }, - ( state, clientId ) => [ - ...getBlockAttributes.getDependants( state, clientId ), - getBlockDependantsCacheBust( state, clientId ), - ] -); + let name; + // If there is only one block in the content of the post grab its name + // so we can derive a suitable post format from it. + if ( blocks.length === 1 ) { + name = blocks[ 0 ].name; + } -export const __unstableGetBlockWithoutInnerBlocks = createSelector( - ( state, clientId ) => { - const block = state.editor.present.blocks.byClientId[ clientId ]; - if ( ! block ) { - return null; + // If there are two blocks in the content and the last one is a text blocks + // grab the name of the first one to also suggest a post format from it. + if ( blocks.length === 2 ) { + if ( blocks[ 1 ].name === 'core/paragraph' ) { + name = blocks[ 0 ].name; } + } - return { - ...block, - attributes: getBlockAttributes( state, clientId ), - }; - }, - ( state, clientId ) => [ - state.editor.present.blocks.byClientId[ clientId ], - ...getBlockAttributes.getDependants( state, clientId ), - ] -); + // We only convert to default post formats in core. + switch ( name ) { + case 'core/image': + return 'image'; + case 'core/quote': + case 'core/pullquote': + return 'quote'; + case 'core/gallery': + return 'gallery'; + case 'core/video': + case 'core-embed/youtube': + case 'core-embed/vimeo': + return 'video'; + case 'core/audio': + case 'core-embed/spotify': + case 'core-embed/soundcloud': + return 'audio'; + } -function getPostMeta( state, key ) { - return has( state, [ 'editor', 'present', 'edits', 'meta', key ] ) ? - get( state, [ 'editor', 'present', 'edits', 'meta', key ] ) : - get( state, [ 'currentPost', 'meta', key ] ); + return null; } /** - * Returns all block objects for the current post being edited as an array in - * the order they appear in the post. - * - * Note: It's important to memoize this selector to avoid return a new instance - * on each call + * Returns a set of blocks which are to be used in consideration of the post's + * generated save content. * - * @param {Object} state Editor state. - * @param {?String} rootClientId Optional root client ID of block list. + * @param {Object} state Editor state. * - * @return {Object[]} Post blocks. + * @return {WPBlock[]} Filtered set of blocks for save. */ -export const getBlocks = createSelector( - ( state, rootClientId ) => { - return map( - getBlockOrder( state, rootClientId ), - ( clientId ) => getBlock( state, clientId ) - ); - }, - ( state ) => [ state.editor.present.blocks ] -); +export function getBlocksForSerialization( state ) { + const blocks = state.editor.blocks.value; -/** - * Returns an array containing the clientIds of all descendants - * of the blocks given. - * - * @param {Object} state Global application state. - * @param {Array} clientIds Array of blocks to inspect. - * - * @return {Array} ids of descendants. - */ -export const getClientIdsOfDescendants = ( state, clientIds ) => flatMap( clientIds, ( clientId ) => { - const descendants = getBlockOrder( state, clientId ); - return [ ...descendants, ...getClientIdsOfDescendants( state, descendants ) ]; -} ); + // A single unmodified default block is assumed to be equivalent to an + // empty post. + const isSingleUnmodifiedDefaultBlock = ( + blocks.length === 1 && + isUnmodifiedDefaultBlock( blocks[ 0 ] ) + ); + + if ( isSingleUnmodifiedDefaultBlock ) { + return []; + } + + return blocks; +} /** - * Returns an array containing the clientIds of the top-level blocks - * and their descendants of any depth (for nested blocks). + * Returns the content of the post being edited, preferring raw string edit + * before falling back to serialization of block state. * * @param {Object} state Global application state. * - * @return {Array} ids of top-level and descendant blocks. + * @return {string} Post content. */ -export const getClientIdsWithDescendants = createSelector( +export const getEditedPostContent = createSelector( ( state ) => { - const topLevelIds = getBlockOrder( state ); - return [ ...topLevelIds, ...getClientIdsOfDescendants( state, topLevelIds ) ]; + const edits = getPostEdits( state ); + if ( 'content' in edits ) { + return edits.content; + } + + const blocks = getBlocksForSerialization( state ); + const content = serialize( blocks ); + + // For compatibility purposes, treat a post consisting of a single + // freeform block as legacy content and downgrade to a pre-block-editor + // removep'd content format. + const isSingleFreeformBlock = ( + blocks.length === 1 && + blocks[ 0 ].name === getFreeformContentHandlerName() + ); + + if ( isSingleFreeformBlock ) { + return removep( content ); + } + + return content; }, ( state ) => [ - state.editor.present.blocks.order, - ] + state.editor.blocks.value, + state.editor.edits.content, + state.initialEdits.content, + ], ); /** - * Returns the total number of blocks, or the total number of blocks with a specific name in a post. - * The number returned includes nested blocks. + * Returns the reusable block with the given ID. * - * @param {Object} state Global application state. - * @param {?String} blockName Optional block name, if specified only blocks of that type will be counted. + * @param {Object} state Global application state. + * @param {number|string} ref The reusable block's ID. * - * @return {number} Number of blocks in the post, or number of blocks with name equal to blockName. + * @return {Object} The reusable block, or null if none exists. */ -export const getGlobalBlockCount = createSelector( - ( state, blockName ) => { - const clientIds = getClientIdsWithDescendants( state ); - if ( ! blockName ) { - return clientIds.length; +export const __experimentalGetReusableBlock = createSelector( + ( state, ref ) => { + const block = state.reusableBlocks.data[ ref ]; + if ( ! block ) { + return null; } - return reduce( clientIds, ( count, clientId ) => { - const block = state.editor.present.blocks.byClientId[ clientId ]; - return block.name === blockName ? count + 1 : count; - }, 0 ); + + const isTemporary = isNaN( parseInt( ref ) ); + + return { + ...block, + id: isTemporary ? ref : +ref, + isTemporary, + }; }, - ( state ) => [ - state.editor.present.blocks.order, - state.editor.present.blocks.byClientId, - ] + ( state, ref ) => [ + state.reusableBlocks.data[ ref ], + ], ); /** - * Given an array of block client IDs, returns the corresponding array of block - * objects. + * Returns whether or not the reusable block with the given ID is being saved. * - * @param {Object} state Editor state. - * @param {string[]} clientIds Client IDs for which blocks are to be returned. + * @param {Object} state Global application state. + * @param {string} ref The reusable block's ID. * - * @return {WPBlock[]} Block objects. + * @return {boolean} Whether or not the reusable block is being saved. */ -export const getBlocksByClientId = createSelector( - ( state, clientIds ) => map( - castArray( clientIds ), - ( clientId ) => getBlock( state, clientId ) - ), - ( state ) => [ - state.editor.present.edits.meta, - state.initialEdits.meta, - state.currentPost.meta, - state.editor.present.blocks, - ] -); +export function __experimentalIsSavingReusableBlock( state, ref ) { + return state.reusableBlocks.isSaving[ ref ] || false; +} /** - * Returns the number of blocks currently present in the post. + * Returns true if the reusable block with the given ID is being fetched, or + * false otherwise. * - * @param {Object} state Editor state. - * @param {?string} rootClientId Optional root client ID of block list. + * @param {Object} state Global application state. + * @param {string} ref The reusable block's ID. * - * @return {number} Number of blocks in the post. + * @return {boolean} Whether the reusable block is being fetched. */ -export function getBlockCount( state, rootClientId ) { - return getBlockOrder( state, rootClientId ).length; +export function __experimentalIsFetchingReusableBlock( state, ref ) { + return !! state.reusableBlocks.isFetching[ ref ]; } /** - * Returns the current block selection start. This value may be null, and it - * may represent either a singular block selection or multi-selection start. - * A selection is singular if its start and end match. + * Returns an array of all reusable blocks. * * @param {Object} state Global application state. * - * @return {?string} Client ID of block selection start. + * @return {Array} An array of all reusable blocks. */ -export function getBlockSelectionStart( state ) { - return state.blockSelection.start; +export function __experimentalGetReusableBlocks( state ) { + return map( + state.reusableBlocks.data, + ( value, ref ) => __experimentalGetReusableBlock( state, ref ) + ); } /** - * Returns the current block selection end. This value may be null, and it - * may represent either a singular block selection or multi-selection end. - * A selection is singular if its start and end match. + * Returns state object prior to a specified optimist transaction ID, or `null` + * if the transaction corresponding to the given ID cannot be found. * - * @param {Object} state Global application state. + * @param {Object} state Current global application state. + * @param {Object} transactionId Optimist transaction ID. * - * @return {?string} Client ID of block selection end. + * @return {Object} Global application state prior to transaction. */ -export function getBlockSelectionEnd( state ) { - return state.blockSelection.end; +export function getStateBeforeOptimisticTransaction( state, transactionId ) { + const transaction = find( state.optimist, ( entry ) => ( + entry.beforeState && + get( entry.action, [ 'optimist', 'id' ] ) === transactionId + ) ); + + return transaction ? transaction.beforeState : null; } /** - * Returns the number of blocks currently selected in the post. + * Returns true if the post is being published, or false otherwise. * * @param {Object} state Global application state. * - * @return {number} Number of blocks selected in the post. + * @return {boolean} Whether post is being published. */ -export function getSelectedBlockCount( state ) { - const multiSelectedBlockCount = getMultiSelectedBlockClientIds( state ).length; +export function isPublishingPost( state ) { + if ( ! isSavingPost( state ) ) { + return false; + } - if ( multiSelectedBlockCount ) { - return multiSelectedBlockCount; - } - - return state.blockSelection.start ? 1 : 0; -} - -/** - * Returns true if there is a single selected block, or false otherwise. - * - * @param {Object} state Editor state. - * - * @return {boolean} Whether a single block is selected. - */ -export function hasSelectedBlock( state ) { - const { start, end } = state.blockSelection; - return !! start && start === end; -} - -/** - * Returns the currently selected block client ID, or null if there is no - * selected block. - * - * @param {Object} state Editor state. - * - * @return {?string} Selected block client ID. - */ -export function getSelectedBlockClientId( state ) { - const { start, end } = state.blockSelection; - // We need to check the block exists because the current state.blockSelection reducer - // doesn't take into account the UNDO / REDO actions to update selection. - // To be removed when that's fixed. - return start && start === end && !! state.editor.present.blocks.byClientId[ start ] ? start : null; -} - -/** - * Returns the currently selected block, or null if there is no selected block. - * - * @param {Object} state Global application state. - * - * @return {?Object} Selected block. - */ -export function getSelectedBlock( state ) { - const clientId = getSelectedBlockClientId( state ); - return clientId ? getBlock( state, clientId ) : null; -} - -/** - * Given a block client ID, returns the root block from which the block is - * nested, an empty string for top-level blocks, or null if the block does not - * exist. - * - * @param {Object} state Editor state. - * @param {string} clientId Block from which to find root client ID. - * - * @return {?string} Root client ID, if exists - */ -export const getBlockRootClientId = createSelector( - ( state, clientId ) => { - const { order } = state.editor.present.blocks; - - for ( const rootClientId in order ) { - if ( includes( order[ rootClientId ], clientId ) ) { - return rootClientId; - } - } - - return null; - }, - ( state ) => [ - state.editor.present.blocks.order, - ] -); - -/** - * Given a block client ID, returns the root of the hierarchy from which the block is nested, return the block itself for root level blocks. - * - * @param {Object} state Editor state. - * @param {string} clientId Block from which to find root client ID. - * - * @return {string} Root client ID - */ -export const getBlockHierarchyRootClientId = createSelector( - ( state, clientId ) => { - let rootClientId = clientId; - let current = clientId; - while ( rootClientId ) { - current = rootClientId; - rootClientId = getBlockRootClientId( state, current ); - } - - return current; - }, - ( state ) => [ - state.editor.present.blocks.order, - ] -); - -/** - * Returns the client ID of the block adjacent one at the given reference - * startClientId and modifier directionality. Defaults start startClientId to - * the selected block, and direction as next block. Returns null if there is no - * adjacent block. - * - * @param {Object} state Editor state. - * @param {?string} startClientId Optional client ID of block from which to - * search. - * @param {?number} modifier Directionality multiplier (1 next, -1 - * previous). - * - * @return {?string} Return the client ID of the block, or null if none exists. - */ -export function getAdjacentBlockClientId( state, startClientId, modifier = 1 ) { - // Default to selected block. - if ( startClientId === undefined ) { - startClientId = getSelectedBlockClientId( state ); - } - - // Try multi-selection starting at extent based on modifier. - if ( startClientId === undefined ) { - if ( modifier < 0 ) { - startClientId = getFirstMultiSelectedBlockClientId( state ); - } else { - startClientId = getLastMultiSelectedBlockClientId( state ); - } - } - - // Validate working start client ID. - if ( ! startClientId ) { - return null; - } - - // Retrieve start block root client ID, being careful to allow the falsey - // empty string top-level root by explicitly testing against null. - const rootClientId = getBlockRootClientId( state, startClientId ); - if ( rootClientId === null ) { - return null; - } - - const { order } = state.editor.present.blocks; - const orderSet = order[ rootClientId ]; - const index = orderSet.indexOf( startClientId ); - const nextIndex = ( index + ( 1 * modifier ) ); - - // Block was first in set and we're attempting to get previous. - if ( nextIndex < 0 ) { - return null; - } - - // Block was last in set and we're attempting to get next. - if ( nextIndex === orderSet.length ) { - return null; - } - - // Assume incremented index is within the set. - return orderSet[ nextIndex ]; -} - -/** - * Returns the previous block's client ID from the given reference start ID. - * Defaults start to the selected block. Returns null if there is no previous - * block. - * - * @param {Object} state Editor state. - * @param {?string} startClientId Optional client ID of block from which to - * search. - * - * @return {?string} Adjacent block's client ID, or null if none exists. - */ -export function getPreviousBlockClientId( state, startClientId ) { - return getAdjacentBlockClientId( state, startClientId, -1 ); -} - -/** - * Returns the next block's client ID from the given reference start ID. - * Defaults start to the selected block. Returns null if there is no next - * block. - * - * @param {Object} state Editor state. - * @param {?string} startClientId Optional client ID of block from which to - * search. - * - * @return {?string} Adjacent block's client ID, or null if none exists. - */ -export function getNextBlockClientId( state, startClientId ) { - return getAdjacentBlockClientId( state, startClientId, 1 ); -} - -/** - * Returns the initial caret position for the selected block. - * This position is to used to position the caret properly when the selected block changes. - * - * @param {Object} state Global application state. - * - * @return {?Object} Selected block. - */ -export function getSelectedBlocksInitialCaretPosition( state ) { - const { start, end } = state.blockSelection; - if ( start !== end || ! start ) { - return null; - } - - return state.blockSelection.initialPosition; -} - -/** - * Returns the current multi-selection set of block client IDs, or an empty - * array if there is no multi-selection. - * - * @param {Object} state Editor state. - * - * @return {Array} Multi-selected block client IDs. - */ -export const getMultiSelectedBlockClientIds = createSelector( - ( state ) => { - const { start, end } = state.blockSelection; - if ( start === end ) { - return []; - } - - // Retrieve root client ID to aid in retrieving relevant nested block - // order, being careful to allow the falsey empty string top-level root - // by explicitly testing against null. - const rootClientId = getBlockRootClientId( state, start ); - if ( rootClientId === null ) { - return []; - } - - const blockOrder = getBlockOrder( state, rootClientId ); - const startIndex = blockOrder.indexOf( start ); - const endIndex = blockOrder.indexOf( end ); - - if ( startIndex > endIndex ) { - return blockOrder.slice( endIndex, startIndex + 1 ); - } - - return blockOrder.slice( startIndex, endIndex + 1 ); - }, - ( state ) => [ - state.editor.present.blocks.order, - state.blockSelection.start, - state.blockSelection.end, - ], -); - -/** - * Returns the current multi-selection set of blocks, or an empty array if - * there is no multi-selection. - * - * @param {Object} state Editor state. - * - * @return {Array} Multi-selected block objects. - */ -export const getMultiSelectedBlocks = createSelector( - ( state ) => { - const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds( state ); - if ( ! multiSelectedBlockClientIds.length ) { - return EMPTY_ARRAY; - } - - return multiSelectedBlockClientIds.map( ( clientId ) => getBlock( state, clientId ) ); - }, - ( state ) => [ - ...getMultiSelectedBlockClientIds.getDependants( state ), - state.editor.present.blocks, - state.editor.present.edits.meta, - state.initialEdits.meta, - state.currentPost.meta, - ] -); - -/** - * Returns the client ID of the first block in the multi-selection set, or null - * if there is no multi-selection. - * - * @param {Object} state Editor state. - * - * @return {?string} First block client ID in the multi-selection set. - */ -export function getFirstMultiSelectedBlockClientId( state ) { - return first( getMultiSelectedBlockClientIds( state ) ) || null; -} - -/** - * Returns the client ID of the last block in the multi-selection set, or null - * if there is no multi-selection. - * - * @param {Object} state Editor state. - * - * @return {?string} Last block client ID in the multi-selection set. - */ -export function getLastMultiSelectedBlockClientId( state ) { - return last( getMultiSelectedBlockClientIds( state ) ) || null; -} - -/** - * Checks if possibleAncestorId is an ancestor of possibleDescendentId. - * - * @param {Object} state Editor state. - * @param {string} possibleAncestorId Possible ancestor client ID. - * @param {string} possibleDescendentId Possible descent client ID. - * - * @return {boolean} True if possibleAncestorId is an ancestor - * of possibleDescendentId, and false otherwise. - */ -const isAncestorOf = createSelector( - ( state, possibleAncestorId, possibleDescendentId ) => { - let idToCheck = possibleDescendentId; - while ( possibleAncestorId !== idToCheck && idToCheck ) { - idToCheck = getBlockRootClientId( state, idToCheck ); - } - return possibleAncestorId === idToCheck; - }, - ( state ) => [ - state.editor.present.blocks.order, - ], -); - -/** - * Returns true if a multi-selection exists, and the block corresponding to the - * specified client ID is the first block of the multi-selection set, or false - * otherwise. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {boolean} Whether block is first in multi-selection. - */ -export function isFirstMultiSelectedBlock( state, clientId ) { - return getFirstMultiSelectedBlockClientId( state ) === clientId; -} - -/** - * Returns true if the client ID occurs within the block multi-selection, or - * false otherwise. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {boolean} Whether block is in multi-selection set. - */ -export function isBlockMultiSelected( state, clientId ) { - return getMultiSelectedBlockClientIds( state ).indexOf( clientId ) !== -1; -} - -/** - * Returns true if an ancestor of the block is multi-selected, or false - * otherwise. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {boolean} Whether an ancestor of the block is in multi-selection - * set. - */ -export const isAncestorMultiSelected = createSelector( - ( state, clientId ) => { - let ancestorClientId = clientId; - let isMultiSelected = false; - while ( ancestorClientId && ! isMultiSelected ) { - ancestorClientId = getBlockRootClientId( state, ancestorClientId ); - isMultiSelected = isBlockMultiSelected( state, ancestorClientId ); - } - return isMultiSelected; - }, - ( state ) => [ - state.editor.present.blocks.order, - state.blockSelection.start, - state.blockSelection.end, - ], -); -/** - * Returns the client ID of the block which begins the multi-selection set, or - * null if there is no multi-selection. - * - * This is not necessarily the first client ID in the selection. - * - * @see getFirstMultiSelectedBlockClientId - * - * @param {Object} state Editor state. - * - * @return {?string} Client ID of block beginning multi-selection. - */ -export function getMultiSelectedBlocksStartClientId( state ) { - const { start, end } = state.blockSelection; - if ( start === end ) { - return null; - } - return start || null; -} - -/** - * Returns the client ID of the block which ends the multi-selection set, or - * null if there is no multi-selection. - * - * This is not necessarily the last client ID in the selection. - * - * @see getLastMultiSelectedBlockClientId - * - * @param {Object} state Editor state. - * - * @return {?string} Client ID of block ending multi-selection. - */ -export function getMultiSelectedBlocksEndClientId( state ) { - const { start, end } = state.blockSelection; - if ( start === end ) { - return null; - } - return end || null; -} - -/** - * Returns an array containing all block client IDs in the editor in the order - * they appear. Optionally accepts a root client ID of the block list for which - * the order should be returned, defaulting to the top-level block order. - * - * @param {Object} state Editor state. - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {Array} Ordered client IDs of editor blocks. - */ -export function getBlockOrder( state, rootClientId ) { - return state.editor.present.blocks.order[ rootClientId || '' ] || EMPTY_ARRAY; -} - -/** - * Returns the index at which the block corresponding to the specified client - * ID occurs within the block order, or `-1` if the block does not exist. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {number} Index at which block exists in order. - */ -export function getBlockIndex( state, clientId, rootClientId ) { - return getBlockOrder( state, rootClientId ).indexOf( clientId ); -} - -/** - * Returns true if the block corresponding to the specified client ID is - * currently selected and no multi-selection exists, or false otherwise. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {boolean} Whether block is selected and multi-selection exists. - */ -export function isBlockSelected( state, clientId ) { - const { start, end } = state.blockSelection; - - if ( start !== end ) { - return false; - } - - return start === clientId; -} - -/** - * Returns true if one of the block's inner blocks is selected. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * @param {boolean} deep Perform a deep check. - * - * @return {boolean} Whether the block as an inner block selected - */ -export function hasSelectedInnerBlock( state, clientId, deep = false ) { - return some( - getBlockOrder( state, clientId ), - ( innerClientId ) => ( - isBlockSelected( state, innerClientId ) || - isBlockMultiSelected( state, innerClientId ) || - ( deep && hasSelectedInnerBlock( state, innerClientId, deep ) ) - ) - ); -} - -/** - * Returns true if the block corresponding to the specified client ID is - * currently selected but isn't the last of the selected blocks. Here "last" - * refers to the block sequence in the document, _not_ the sequence of - * multi-selection, which is why `state.blockSelection.end` isn't used. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {boolean} Whether block is selected and not the last in the - * selection. - */ -export function isBlockWithinSelection( state, clientId ) { - if ( ! clientId ) { - return false; - } - - const clientIds = getMultiSelectedBlockClientIds( state ); - const index = clientIds.indexOf( clientId ); - return index > -1 && index < clientIds.length - 1; -} - -/** - * Returns true if a multi-selection has been made, or false otherwise. - * - * @param {Object} state Editor state. - * - * @return {boolean} Whether multi-selection has been made. - */ -export function hasMultiSelection( state ) { - const { start, end } = state.blockSelection; - return start !== end; -} - -/** - * Whether in the process of multi-selecting or not. This flag is only true - * while the multi-selection is being selected (by mouse move), and is false - * once the multi-selection has been settled. - * - * @see hasMultiSelection - * - * @param {Object} state Global application state. - * - * @return {boolean} True if multi-selecting, false if not. - */ -export function isMultiSelecting( state ) { - return state.blockSelection.isMultiSelecting; -} - -/** - * Selector that returns if multi-selection is enabled or not. - * - * @param {Object} state Global application state. - * - * @return {boolean} True if it should be possible to multi-select blocks, false if multi-selection is disabled. - */ -export function isSelectionEnabled( state ) { - return state.blockSelection.isEnabled; -} - -/** - * Returns the block's editing mode, defaulting to "visual" if not explicitly - * assigned. - * - * @param {Object} state Editor state. - * @param {string} clientId Block client ID. - * - * @return {Object} Block editing mode. - */ -export function getBlockMode( state, clientId ) { - return state.blocksMode[ clientId ] || 'visual'; -} - -/** - * Returns true if the user is typing, or false otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether user is typing. - */ -export function isTyping( state ) { - return state.isTyping; -} - -/** - * Returns true if the caret is within formatted text, or false otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether the caret is within formatted text. - */ -export function isCaretWithinFormattedText( state ) { - return state.isCaretWithinFormattedText; -} - -/** - * Returns the insertion point, the index at which the new inserted block would - * be placed. Defaults to the last index. - * - * @param {Object} state Editor state. - * - * @return {Object} Insertion point object with `rootClientId`, `index`. - */ -export function getBlockInsertionPoint( state ) { - let rootClientId, index; - - const { insertionPoint, blockSelection } = state; - if ( insertionPoint !== null ) { - return insertionPoint; - } - - const { end } = blockSelection; - if ( end ) { - rootClientId = getBlockRootClientId( state, end ) || undefined; - index = getBlockIndex( state, end, rootClientId ) + 1; - } else { - index = getBlockOrder( state ).length; - } - - return { rootClientId, index }; -} - -/** - * Returns true if we should show the block insertion point. - * - * @param {Object} state Global application state. - * - * @return {?boolean} Whether the insertion point is visible or not. - */ -export function isBlockInsertionPointVisible( state ) { - return state.insertionPoint !== null; -} - -/** - * Returns whether the blocks matches the template or not. - * - * @param {boolean} state - * @return {?boolean} Whether the template is valid or not. - */ -export function isValidTemplate( state ) { - return state.template.isValid; -} - -/** - * Returns the defined block template - * - * @param {boolean} state - * @return {?Array} Block Template - */ -export function getTemplate( state ) { - return state.settings.template; -} - -/** - * Returns the defined block template lock. Optionally accepts a root block - * client ID as context, otherwise defaulting to the global context. - * - * @param {Object} state Editor state. - * @param {?string} rootClientId Optional block root client ID. - * - * @return {?string} Block Template Lock - */ -export function getTemplateLock( state, rootClientId ) { - if ( ! rootClientId ) { - return state.settings.templateLock; - } - - const blockListSettings = getBlockListSettings( state, rootClientId ); - if ( ! blockListSettings ) { - return null; - } - - return blockListSettings.templateLock; -} - -/** - * Returns true if the post is currently being saved, or false otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether post is being saved. - */ -export function isSavingPost( state ) { - return state.saving.requesting; -} - -/** - * Returns true if a previous post save was attempted successfully, or false - * otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether the post was saved successfully. - */ -export function didPostSaveRequestSucceed( state ) { - return state.saving.successful; -} - -/** - * Returns true if a previous post save was attempted but failed, or false - * otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether the post save failed. - */ -export function didPostSaveRequestFail( state ) { - return !! state.saving.error; -} - -/** - * Returns true if the post is autosaving, or false otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether the post is autosaving. - */ -export function isAutosavingPost( state ) { - return isSavingPost( state ) && !! state.saving.options.isAutosave; -} - -/** - * Returns true if the post is being previewed, or false otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether the post is being previewed. - */ -export function isPreviewingPost( state ) { - return isSavingPost( state ) && !! state.saving.options.isPreview; -} - -/** - * Returns the post preview link - * - * @param {Object} state Global application state. - * - * @return {string?} Preview Link. - */ -export function getEditedPostPreviewLink( state ) { - const featuredImageId = getEditedPostAttribute( state, 'featured_media' ); - const previewLink = state.previewLink; - if ( previewLink && featuredImageId ) { - return addQueryArgs( previewLink, { _thumbnail_id: featuredImageId } ); - } - - return previewLink; -} - -/** - * Returns a suggested post format for the current post, inferred only if there - * is a single block within the post and it is of a type known to match a - * default post format. Returns null if the format cannot be determined. - * - * @param {Object} state Global application state. - * - * @return {?string} Suggested post format. - */ -export function getSuggestedPostFormat( state ) { - const blocks = getBlockOrder( state ); - - let name; - // If there is only one block in the content of the post grab its name - // so we can derive a suitable post format from it. - if ( blocks.length === 1 ) { - name = getBlockName( state, blocks[ 0 ] ); - } - - // If there are two blocks in the content and the last one is a text blocks - // grab the name of the first one to also suggest a post format from it. - if ( blocks.length === 2 ) { - if ( getBlockName( state, blocks[ 1 ] ) === 'core/paragraph' ) { - name = getBlockName( state, blocks[ 0 ] ); - } - } - - // We only convert to default post formats in core. - switch ( name ) { - case 'core/image': - return 'image'; - case 'core/quote': - case 'core/pullquote': - return 'quote'; - case 'core/gallery': - return 'gallery'; - case 'core/video': - case 'core-embed/youtube': - case 'core-embed/vimeo': - return 'video'; - case 'core/audio': - case 'core-embed/spotify': - case 'core-embed/soundcloud': - return 'audio'; - } - - return null; -} - -/** - * Returns a set of blocks which are to be used in consideration of the post's - * generated save content. - * - * @param {Object} state Editor state. - * - * @return {WPBlock[]} Filtered set of blocks for save. - */ -export function getBlocksForSerialization( state ) { - const blocks = getBlocks( state ); - - // A single unmodified default block is assumed to be equivalent to an - // empty post. - const isSingleUnmodifiedDefaultBlock = ( - blocks.length === 1 && - isUnmodifiedDefaultBlock( blocks[ 0 ] ) - ); - - if ( isSingleUnmodifiedDefaultBlock ) { - return []; - } - - return blocks; -} - -/** - * Returns the content of the post being edited, preferring raw string edit - * before falling back to serialization of block state. - * - * @param {Object} state Global application state. - * - * @return {string} Post content. - */ -export const getEditedPostContent = createSelector( - ( state ) => { - const edits = getPostEdits( state ); - if ( 'content' in edits ) { - return edits.content; - } - - const blocks = getBlocksForSerialization( state ); - const content = serialize( blocks ); - - // For compatibility purposes, treat a post consisting of a single - // freeform block as legacy content and downgrade to a pre-block-editor - // removep'd content format. - const isSingleFreeformBlock = ( - blocks.length === 1 && - blocks[ 0 ].name === getFreeformContentHandlerName() - ); - - if ( isSingleFreeformBlock ) { - return removep( content ); - } - - return content; - }, - ( state ) => [ - state.editor.present.blocks, - state.editor.present.edits.content, - state.initialEdits.content, - ], -); - -/** - * Determines if the given block type is allowed to be inserted into the block list. - * This function is not exported and not memoized because using a memoized selector - * inside another memoized selector is just a waste of time. - * - * @param {Object} state Editor state. - * @param {string} blockName The name of the block type, e.g.' core/paragraph'. - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {boolean} Whether the given block type is allowed to be inserted. - */ -const canInsertBlockTypeUnmemoized = ( state, blockName, rootClientId = null ) => { - const checkAllowList = ( list, item, defaultResult = null ) => { - if ( isBoolean( list ) ) { - return list; - } - if ( isArray( list ) ) { - return includes( list, item ); - } - return defaultResult; - }; - - const blockType = getBlockType( blockName ); - if ( ! blockType ) { - return false; - } - - const { allowedBlockTypes } = getEditorSettings( state ); - - const isBlockAllowedInEditor = checkAllowList( allowedBlockTypes, blockName, true ); - if ( ! isBlockAllowedInEditor ) { - return false; - } - - const isLocked = !! getTemplateLock( state, rootClientId ); - if ( isLocked ) { - return false; - } - - const parentBlockListSettings = getBlockListSettings( state, rootClientId ); - const parentAllowedBlocks = get( parentBlockListSettings, [ 'allowedBlocks' ] ); - const hasParentAllowedBlock = checkAllowList( parentAllowedBlocks, blockName ); - - const blockAllowedParentBlocks = blockType.parent; - const parentName = getBlockName( state, rootClientId ); - const hasBlockAllowedParent = checkAllowList( blockAllowedParentBlocks, parentName ); - - if ( hasParentAllowedBlock !== null && hasBlockAllowedParent !== null ) { - return hasParentAllowedBlock || hasBlockAllowedParent; - } else if ( hasParentAllowedBlock !== null ) { - return hasParentAllowedBlock; - } else if ( hasBlockAllowedParent !== null ) { - return hasBlockAllowedParent; - } - - return true; -}; - -/** - * Determines if the given block type is allowed to be inserted into the block list. - * - * @param {Object} state Editor state. - * @param {string} blockName The name of the block type, e.g.' core/paragraph'. - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {boolean} Whether the given block type is allowed to be inserted. - */ -export const canInsertBlockType = createSelector( - canInsertBlockTypeUnmemoized, - ( state, blockName, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.editor.present.blocks.byClientId[ rootClientId ], - state.settings.allowedBlockTypes, - state.settings.templateLock, - ], -); - -/** - * Returns information about how recently and frequently a block has been inserted. - * - * @param {Object} state Global application state. - * @param {string} id A string which identifies the insert, e.g. 'core/block/12' - * - * @return {?{ time: number, count: number }} An object containing `time` which is when the last - * insert occured as a UNIX epoch, and `count` which is - * the number of inserts that have occurred. - */ -function getInsertUsage( state, id ) { - return state.preferences.insertUsage[ id ] || null; -} - -/** - * Returns whether we can show a block type in the inserter - * - * @param {Object} state Global State - * @param {Object} blockType BlockType - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {boolean} Whether the given block type is allowed to be shown in the inserter. - */ -const canIncludeBlockTypeInInserter = ( state, blockType, rootClientId ) => { - if ( ! hasBlockSupport( blockType, 'inserter', true ) ) { - return false; - } - - return canInsertBlockTypeUnmemoized( state, blockType.name, rootClientId ); -}; - -/** - * Returns whether we can show a reusable block in the inserter - * - * @param {Object} state Global State - * @param {Object} reusableBlock Reusable block object - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {boolean} Whether the given block type is allowed to be shown in the inserter. - */ -const canIncludeReusableBlockInInserter = ( state, reusableBlock, rootClientId ) => { - if ( ! canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) ) { - return false; - } - - const referencedBlockName = getBlockName( state, reusableBlock.clientId ); - if ( ! referencedBlockName ) { - return false; - } - - const referencedBlockType = getBlockType( referencedBlockName ); - if ( ! referencedBlockType ) { - return false; - } - - if ( ! canInsertBlockTypeUnmemoized( state, referencedBlockName, rootClientId ) ) { - return false; - } - - if ( isAncestorOf( state, reusableBlock.clientId, rootClientId ) ) { - return false; - } - - return true; -}; - -/** - * Determines the items that appear in the inserter. Includes both static - * items (e.g. a regular block type) and dynamic items (e.g. a reusable block). - * - * Each item object contains what's necessary to display a button in the - * inserter and handle its selection. - * - * The 'utility' property indicates how useful we think an item will be to the - * user. There are 4 levels of utility: - * - * 1. Blocks that are contextually useful (utility = 3) - * 2. Blocks that have been previously inserted (utility = 2) - * 3. Blocks that are in the common category (utility = 1) - * 4. All other blocks (utility = 0) - * - * The 'frecency' property is a heuristic (https://en.wikipedia.org/wiki/Frecency) - * that combines block usage frequenty and recency. - * - * Items are returned ordered descendingly by their 'utility' and 'frecency'. - * - * @param {Object} state Editor state. - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {Editor.InserterItem[]} Items that appear in inserter. - * - * @typedef {Object} Editor.InserterItem - * @property {string} id Unique identifier for the item. - * @property {string} name The type of block to create. - * @property {Object} initialAttributes Attributes to pass to the newly created block. - * @property {string} title Title of the item, as it appears in the inserter. - * @property {string} icon Dashicon for the item, as it appears in the inserter. - * @property {string} category Block category that the item is associated with. - * @property {string[]} keywords Keywords that can be searched to find this item. - * @property {boolean} isDisabled Whether or not the user should be prevented from inserting - * this item. - * @property {number} utility How useful we think this item is, between 0 and 3. - * @property {number} frecency Hueristic that combines frequency and recency. - */ -export const getInserterItems = createSelector( - ( state, rootClientId = null ) => { - const calculateUtility = ( category, count, isContextual ) => { - if ( isContextual ) { - return INSERTER_UTILITY_HIGH; - } else if ( count > 0 ) { - return INSERTER_UTILITY_MEDIUM; - } else if ( category === 'common' ) { - return INSERTER_UTILITY_LOW; - } - return INSERTER_UTILITY_NONE; - }; - - const calculateFrecency = ( time, count ) => { - if ( ! time ) { - return count; - } - - // The selector is cached, which means Date.now() is the last time that the - // relevant state changed. This suits our needs. - const duration = Date.now() - time; - - switch ( true ) { - case duration < MILLISECONDS_PER_HOUR: - return count * 4; - case duration < MILLISECONDS_PER_DAY: - return count * 2; - case duration < MILLISECONDS_PER_WEEK: - return count / 2; - default: - return count / 4; - } - }; - - const buildBlockTypeInserterItem = ( blockType ) => { - const id = blockType.name; - - let isDisabled = false; - if ( ! hasBlockSupport( blockType.name, 'multiple', true ) ) { - isDisabled = some( getBlocksByClientId( state, getClientIdsWithDescendants( state ) ), { name: blockType.name } ); - } - - const isContextual = isArray( blockType.parent ); - const { time, count = 0 } = getInsertUsage( state, id ) || {}; - - return { - id, - name: blockType.name, - initialAttributes: {}, - title: blockType.title, - icon: blockType.icon, - category: blockType.category, - keywords: blockType.keywords, - isDisabled, - utility: calculateUtility( blockType.category, count, isContextual ), - frecency: calculateFrecency( time, count ), - hasChildBlocksWithInserterSupport: hasChildBlocksWithInserterSupport( blockType.name ), - }; - }; - - const buildReusableBlockInserterItem = ( reusableBlock ) => { - const id = `core/block/${ reusableBlock.id }`; - - const referencedBlockName = getBlockName( state, reusableBlock.clientId ); - const referencedBlockType = getBlockType( referencedBlockName ); - - const { time, count = 0 } = getInsertUsage( state, id ) || {}; - const utility = calculateUtility( 'reusable', count, false ); - const frecency = calculateFrecency( time, count ); - - return { - id, - name: 'core/block', - initialAttributes: { ref: reusableBlock.id }, - title: reusableBlock.title, - icon: referencedBlockType.icon, - category: 'reusable', - keywords: [], - isDisabled: false, - utility, - frecency, - }; - }; - - const blockTypeInserterItems = getBlockTypes() - .filter( ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) ) - .map( buildBlockTypeInserterItem ); - - const reusableBlockInserterItems = __experimentalGetReusableBlocks( state ) - .filter( ( block ) => canIncludeReusableBlockInInserter( state, block, rootClientId ) ) - .map( buildReusableBlockInserterItem ); - - return orderBy( - [ ...blockTypeInserterItems, ...reusableBlockInserterItems ], - [ 'utility', 'frecency' ], - [ 'desc', 'desc' ] - ); - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.editor.present.blocks.byClientId, - state.editor.present.blocks.order, - state.preferences.insertUsage, - state.settings.allowedBlockTypes, - state.settings.templateLock, - state.reusableBlocks.data, - getBlockTypes(), - ], -); - -/** - * Determines whether there are items to show in the inserter. - * @param {Object} state Editor state. - * @param {?string} rootClientId Optional root client ID of block list. - * - * @return {boolean} Items that appear in inserter. - */ -export const hasInserterItems = createSelector( - ( state, rootClientId = null ) => { - const hasBlockType = some( - getBlockTypes(), - ( blockType ) => canIncludeBlockTypeInInserter( state, blockType, rootClientId ) - ); - if ( hasBlockType ) { - return true; - } - const hasReusableBlock = some( - __experimentalGetReusableBlocks( state ), - ( block ) => canIncludeReusableBlockInInserter( state, block, rootClientId ) - ); - - return hasReusableBlock; - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.editor.present.blocks.byClientId, - state.settings.allowedBlockTypes, - state.settings.templateLock, - state.reusableBlocks.data, - getBlockTypes(), - ], -); - -/** - * Returns the reusable block with the given ID. - * - * @param {Object} state Global application state. - * @param {number|string} ref The reusable block's ID. - * - * @return {Object} The reusable block, or null if none exists. - */ -export const __experimentalGetReusableBlock = createSelector( - ( state, ref ) => { - const block = state.reusableBlocks.data[ ref ]; - if ( ! block ) { - return null; - } - - const isTemporary = isNaN( parseInt( ref ) ); - - return { - ...block, - id: isTemporary ? ref : +ref, - isTemporary, - }; - }, - ( state, ref ) => [ - state.reusableBlocks.data[ ref ], - ], -); - -/** - * Returns whether or not the reusable block with the given ID is being saved. - * - * @param {Object} state Global application state. - * @param {string} ref The reusable block's ID. - * - * @return {boolean} Whether or not the reusable block is being saved. - */ -export function __experimentalIsSavingReusableBlock( state, ref ) { - return state.reusableBlocks.isSaving[ ref ] || false; -} - -/** - * Returns true if the reusable block with the given ID is being fetched, or - * false otherwise. - * - * @param {Object} state Global application state. - * @param {string} ref The reusable block's ID. - * - * @return {boolean} Whether the reusable block is being fetched. - */ -export function __experimentalIsFetchingReusableBlock( state, ref ) { - return !! state.reusableBlocks.isFetching[ ref ]; -} - -/** - * Returns an array of all reusable blocks. - * - * @param {Object} state Global application state. - * - * @return {Array} An array of all reusable blocks. - */ -export function __experimentalGetReusableBlocks( state ) { - return map( - state.reusableBlocks.data, - ( value, ref ) => __experimentalGetReusableBlock( state, ref ) - ); -} - -/** - * Returns state object prior to a specified optimist transaction ID, or `null` - * if the transaction corresponding to the given ID cannot be found. - * - * @param {Object} state Current global application state. - * @param {Object} transactionId Optimist transaction ID. - * - * @return {Object} Global application state prior to transaction. - */ -export function getStateBeforeOptimisticTransaction( state, transactionId ) { - const transaction = find( state.optimist, ( entry ) => ( - entry.beforeState && - get( entry.action, [ 'optimist', 'id' ] ) === transactionId - ) ); - - return transaction ? transaction.beforeState : null; -} - -/** - * Returns true if the post is being published, or false otherwise. - * - * @param {Object} state Global application state. - * - * @return {boolean} Whether post is being published. - */ -export function isPublishingPost( state ) { - if ( ! isSavingPost( state ) ) { - return false; - } - - // Saving is optimistic, so assume that current post would be marked as - // published if publishing - if ( ! isCurrentPostPublished( state ) ) { - return false; + // Saving is optimistic, so assume that current post would be marked as + // published if publishing + if ( ! isCurrentPostPublished( state ) ) { + return false; } // Use post update transaction ID to retrieve the state prior to the @@ -2241,45 +912,6 @@ export function inSomeHistory( state, predicate ) { ) ); } -/** - * Returns the Block List settings of a block, if any exist. - * - * @param {Object} state Editor state. - * @param {?string} clientId Block client ID. - * - * @return {?Object} Block settings of the block if set. - */ -export function getBlockListSettings( state, clientId ) { - return state.blockListSettings[ clientId ]; -} - -/** - * Returns the editor settings. - * - * @param {Object} state Editor state. - * - * @return {Object} The editor settings object. - */ -export function getEditorSettings( state ) { - return state.settings; -} - -/** - * Returns the token settings. - * - * @param {Object} state Editor state. - * @param {?string} name Token name. - * - * @return {Object} Token settings object, or the named token settings object if set. - */ -export function getTokenSettings( state, name ) { - if ( ! name ) { - return state.tokens; - } - - return state.tokens[ name ]; -} - /** * Returns whether the post is locked. * @@ -2360,3 +992,88 @@ export function isPublishSidebarEnabled( state ) { } return PREFERENCES_DEFAULTS.isPublishSidebarEnabled; } + +/** + * Return the current block list. + * + * @param {Object} state + * @return {Array} Block list. + */ +export function getEditorBlocks( state ) { + return state.editor.blocks.value; +} + +/** + * Is the editor ready + * + * @param {Object} state + * @return {boolean} is Ready. + */ +export function isEditorReady( state ) { + return state.isReady; +} + +/** + * Backward compatibility + */ + +const getBlockEditorSelector = ( name ) => ( state, ...args ) => { + return window.wp.data.select( 'core/block-editor' )[ name ]( ...args ); +}; + +export const getBlockDependantsCacheBust = getBlockEditorSelector( 'getBlockDependantsCacheBust' ); +export const getBlockName = getBlockEditorSelector( 'getBlockName' ); +export const isBlockValid = getBlockEditorSelector( 'isBlockValid' ); +export const getBlockAttributes = getBlockEditorSelector( 'getBlockAttributes' ); +export const getBlock = getBlockEditorSelector( 'getBlock' ); +export const getBlocks = getBlockEditorSelector( 'getBlocks' ); +export const __unstableGetBlockWithoutInnerBlocks = getBlockEditorSelector( '__unstableGetBlockWithoutInnerBlocks' ); +export const getClientIdsOfDescendants = getBlockEditorSelector( 'getClientIdsOfDescendants' ); +export const getClientIdsWithDescendants = getBlockEditorSelector( 'getClientIdsWithDescendants' ); +export const getGlobalBlockCount = getBlockEditorSelector( 'getGlobalBlockCount' ); +export const getBlocksByClientId = getBlockEditorSelector( 'getBlocksByClientId' ); +export const getBlockCount = getBlockEditorSelector( 'getBlockCount' ); +export const getBlockSelectionStart = getBlockEditorSelector( 'getBlockSelectionStart' ); +export const getBlockSelectionEnd = getBlockEditorSelector( 'getBlockSelectionEnd' ); +export const getSelectedBlockCount = getBlockEditorSelector( 'getSelectedBlockCount' ); +export const hasSelectedBlock = getBlockEditorSelector( 'hasSelectedBlock' ); +export const getSelectedBlockClientId = getBlockEditorSelector( 'getSelectedBlockClientId' ); +export const getSelectedBlock = getBlockEditorSelector( 'getSelectedBlock' ); +export const getBlockRootClientId = getBlockEditorSelector( 'getBlockRootClientId' ); +export const getBlockHierarchyRootClientId = getBlockEditorSelector( 'getBlockHierarchyRootClientId' ); +export const getAdjacentBlockClientId = getBlockEditorSelector( 'getAdjacentBlockClientId' ); +export const getPreviousBlockClientId = getBlockEditorSelector( 'getPreviousBlockClientId' ); +export const getNextBlockClientId = getBlockEditorSelector( 'getNextBlockClientId' ); +export const getSelectedBlocksInitialCaretPosition = getBlockEditorSelector( 'getSelectedBlocksInitialCaretPosition' ); +export const getMultiSelectedBlockClientIds = getBlockEditorSelector( 'getMultiSelectedBlockClientIds' ); +export const getMultiSelectedBlocks = getBlockEditorSelector( 'getMultiSelectedBlocks' ); +export const getFirstMultiSelectedBlockClientId = getBlockEditorSelector( 'getFirstMultiSelectedBlockClientId' ); +export const getLastMultiSelectedBlockClientId = getBlockEditorSelector( 'getLastMultiSelectedBlockClientId' ); +export const isFirstMultiSelectedBlock = getBlockEditorSelector( 'isFirstMultiSelectedBlock' ); +export const isBlockMultiSelected = getBlockEditorSelector( 'isBlockMultiSelected' ); +export const isAncestorMultiSelected = getBlockEditorSelector( 'isAncestorMultiSelected' ); +export const getMultiSelectedBlocksStartClientId = getBlockEditorSelector( 'getMultiSelectedBlocksStartClientId' ); +export const getMultiSelectedBlocksEndClientId = getBlockEditorSelector( 'getMultiSelectedBlocksEndClientId' ); +export const getBlockOrder = getBlockEditorSelector( 'getBlockOrder' ); +export const getBlockIndex = getBlockEditorSelector( 'getBlockIndex' ); +export const isBlockSelected = getBlockEditorSelector( 'isBlockSelected' ); +export const hasSelectedInnerBlock = getBlockEditorSelector( 'hasSelectedInnerBlock' ); +export const isBlockWithinSelection = getBlockEditorSelector( 'isBlockWithinSelection' ); +export const hasMultiSelection = getBlockEditorSelector( 'hasMultiSelection' ); +export const isMultiSelecting = getBlockEditorSelector( 'isMultiSelecting' ); +export const isSelectionEnabled = getBlockEditorSelector( 'isSelectionEnabled' ); +export const getBlockMode = getBlockEditorSelector( 'getBlockMode' ); +export const isTyping = getBlockEditorSelector( 'isTyping' ); +export const isCaretWithinFormattedText = getBlockEditorSelector( 'isCaretWithinFormattedText' ); +export const getBlockInsertionPoint = getBlockEditorSelector( 'getBlockInsertionPoint' ); +export const isBlockInsertionPointVisible = getBlockEditorSelector( 'isBlockInsertionPointVisible' ); +export const isValidTemplate = getBlockEditorSelector( 'isValidTemplate' ); +export const getTemplate = getBlockEditorSelector( 'getTemplate' ); +export const getTemplateLock = getBlockEditorSelector( 'getTemplateLock' ); +export const canInsertBlockType = getBlockEditorSelector( 'canInsertBlockType' ); +export const getInserterItems = getBlockEditorSelector( 'getInserterItems' ); +export const hasInserterItems = getBlockEditorSelector( 'hasInserterItems' ); +export const hasEditorUndo = getBlockEditorSelector( 'hasEditorUndo' ); +export const hasEditorRedo = getBlockEditorSelector( 'hasEditorRedo' ); +export const getEditorSettings = getBlockEditorSelector( 'getEditorSettings' ); +export const getBlockListSettings = getBlockEditorSelector( 'getBlockListSettings' ); diff --git a/test/e2e/test-plugins/align-hook.php b/test/e2e/test-plugins/align-hook.php index 8ecc005651e11..bf32356a535f0 100644 --- a/test/e2e/test-plugins/align-hook.php +++ b/test/e2e/test-plugins/align-hook.php @@ -7,15 +7,19 @@ * @package gutenberg-test-align-hook */ -wp_enqueue_script( - 'gutenberg-test-align-hook', - plugins_url( 'align-hook/index.js', __FILE__ ), - array( - 'wp-blocks', - 'wp-element', - 'wp-editor', - 'wp-i18n', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'align-hook/index.js' ), - true -); +function enqueue_align_plugin_script() { + wp_enqueue_script( + 'gutenberg-test-align-hook', + plugins_url( 'align-hook/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-element', + 'wp-editor', + 'wp-i18n', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'align-hook/index.js' ), + true + ); +} + +add_action( 'init', 'enqueue_align_plugin_script' ); diff --git a/test/e2e/test-plugins/block-icons.php b/test/e2e/test-plugins/block-icons.php index eb97b52cf9eb0..39c067cb2a72a 100644 --- a/test/e2e/test-plugins/block-icons.php +++ b/test/e2e/test-plugins/block-icons.php @@ -7,17 +7,21 @@ * @package gutenberg-test-block-icons */ -wp_enqueue_script( - 'gutenberg-test-block-icons', - plugins_url( 'block-icons/index.js', __FILE__ ), - array( - 'wp-blocks', - 'wp-components', - 'wp-element', - 'wp-editor', - 'wp-hooks', - 'wp-i18n', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'block-icons/index.js' ), - true -); +function enqueue_block_icons_plugin_script() { + wp_enqueue_script( + 'gutenberg-test-block-icons', + plugins_url( 'block-icons/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-components', + 'wp-element', + 'wp-editor', + 'wp-hooks', + 'wp-i18n', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'block-icons/index.js' ), + true + ); +} + +add_action( 'init', 'enqueue_block_icons_plugin_script' ); diff --git a/test/e2e/test-plugins/container-without-paragraph.php b/test/e2e/test-plugins/container-without-paragraph.php index e9876f4c95b88..10a426e0c828b 100644 --- a/test/e2e/test-plugins/container-without-paragraph.php +++ b/test/e2e/test-plugins/container-without-paragraph.php @@ -7,14 +7,18 @@ * @package gutenberg-test-container-without-paragraph */ -wp_enqueue_script( - 'gutenberg-test-container-without-paragraph', - plugins_url( 'container-without-paragraph/index.js', __FILE__ ), - array( - 'wp-blocks', - 'wp-element', - 'wp-editor', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'container-without-paragraph/index.js' ), - true -); +function enqueue_container_without_paragraph_plugin_script() { + wp_enqueue_script( + 'gutenberg-test-container-without-paragraph', + plugins_url( 'container-without-paragraph/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-element', + 'wp-editor', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'container-without-paragraph/index.js' ), + true + ); +} + +add_action( 'init', 'enqueue_container_without_paragraph_plugin_script' ); diff --git a/test/e2e/test-plugins/deprecated-node-matcher.php b/test/e2e/test-plugins/deprecated-node-matcher.php index bd72e4aa15157..6701c28bd7252 100644 --- a/test/e2e/test-plugins/deprecated-node-matcher.php +++ b/test/e2e/test-plugins/deprecated-node-matcher.php @@ -7,15 +7,19 @@ * @package gutenberg-test-deprecated-node-matcher */ -wp_enqueue_script( - 'gutenberg-test-deprecated-node-matcher', - plugins_url( 'deprecated-node-matcher/index.js', __FILE__ ), - array( - 'lodash', - 'wp-blocks', - 'wp-element', - 'wp-editor', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'deprecated-node-matcher/index.js' ), - true -); +function enqueue_deprecated_node_matcher_plugin_script() { + wp_enqueue_script( + 'gutenberg-test-deprecated-node-matcher', + plugins_url( 'deprecated-node-matcher/index.js', __FILE__ ), + array( + 'lodash', + 'wp-blocks', + 'wp-element', + 'wp-editor', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'deprecated-node-matcher/index.js' ), + true + ); +} + +add_action( 'init', 'enqueue_deprecated_node_matcher_plugin_script' ); diff --git a/test/e2e/test-plugins/hooks-api.php b/test/e2e/test-plugins/hooks-api.php index 38f44860716dd..3549615ae40c7 100644 --- a/test/e2e/test-plugins/hooks-api.php +++ b/test/e2e/test-plugins/hooks-api.php @@ -7,17 +7,21 @@ * @package gutenberg-test-hooks-api */ -wp_enqueue_script( - 'gutenberg-test-hooks-api', - plugins_url( 'hooks-api/index.js', __FILE__ ), - array( - 'wp-blocks', - 'wp-components', - 'wp-element', - 'wp-editor', - 'wp-hooks', - 'wp-i18n', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'hooks-api/index.js' ), - true -); +function enqueue_hooks_plugin_script() { + wp_enqueue_script( + 'gutenberg-test-hooks-api', + plugins_url( 'hooks-api/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-components', + 'wp-element', + 'wp-editor', + 'wp-hooks', + 'wp-i18n', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'hooks-api/index.js' ), + true + ); +} + +add_action( 'init', 'enqueue_hooks_plugin_script' ); diff --git a/test/e2e/test-plugins/inner-blocks-templates.php b/test/e2e/test-plugins/inner-blocks-templates.php index 9454cfc7cb40a..db70a05daef3e 100644 --- a/test/e2e/test-plugins/inner-blocks-templates.php +++ b/test/e2e/test-plugins/inner-blocks-templates.php @@ -7,17 +7,21 @@ * @package gutenberg-test-inner-blocks-templates */ -wp_enqueue_script( - 'gutenberg-test-inner-blocks-templates', - plugins_url( 'inner-blocks-templates/index.js', __FILE__ ), - array( - 'wp-blocks', - 'wp-components', - 'wp-element', - 'wp-editor', - 'wp-hooks', - 'wp-i18n', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'inner-blocks-templates/index.js' ), - true -); +function enqueue_container_without_paragraph_plugin_script() { + wp_enqueue_script( + 'gutenberg-test-inner-blocks-templates', + plugins_url( 'inner-blocks-templates/index.js', __FILE__ ), + array( + 'wp-blocks', + 'wp-components', + 'wp-element', + 'wp-editor', + 'wp-hooks', + 'wp-i18n', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'inner-blocks-templates/index.js' ), + true + ); +} + +add_action( 'init', 'enqueue_container_without_paragraph_plugin_script' ); diff --git a/test/e2e/test-plugins/plugins-api.php b/test/e2e/test-plugins/plugins-api.php index 64a8cb2364448..589b21f421687 100644 --- a/test/e2e/test-plugins/plugins-api.php +++ b/test/e2e/test-plugins/plugins-api.php @@ -7,64 +7,68 @@ * @package gutenberg-test-plugin-plugins-api */ -wp_enqueue_script( - 'gutenberg-test-plugins-api-post-status-info', - plugins_url( 'plugins-api/post-status-info.js', __FILE__ ), - array( - 'wp-edit-post', - 'wp-element', - 'wp-i18n', - 'wp-plugins', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/post-status-info.js' ), - true -); +function enqueue_plugins_api_plugin_scripts() { + wp_enqueue_script( + 'gutenberg-test-plugins-api-post-status-info', + plugins_url( 'plugins-api/post-status-info.js', __FILE__ ), + array( + 'wp-edit-post', + 'wp-element', + 'wp-i18n', + 'wp-plugins', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/post-status-info.js' ), + true + ); -wp_enqueue_script( - 'gutenberg-test-plugins-api-publish-pane;', - plugins_url( 'plugins-api/publish-panel.js', __FILE__ ), - array( - 'wp-edit-post', - 'wp-element', - 'wp-i18n', - 'wp-plugins', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/publish-panel.js' ), - true -); + wp_enqueue_script( + 'gutenberg-test-plugins-api-publish-pane;', + plugins_url( 'plugins-api/publish-panel.js', __FILE__ ), + array( + 'wp-edit-post', + 'wp-element', + 'wp-i18n', + 'wp-plugins', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/publish-panel.js' ), + true + ); -wp_enqueue_script( - 'gutenberg-test-plugins-api-sidebar', - plugins_url( 'plugins-api/sidebar.js', __FILE__ ), - array( - 'wp-components', - 'wp-compose', - 'wp-data', - 'wp-edit-post', - 'wp-editor', - 'wp-element', - 'wp-i18n', - 'wp-plugins', - 'wp-annotations', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/sidebar.js' ), - true -); + wp_enqueue_script( + 'gutenberg-test-plugins-api-sidebar', + plugins_url( 'plugins-api/sidebar.js', __FILE__ ), + array( + 'wp-components', + 'wp-compose', + 'wp-data', + 'wp-edit-post', + 'wp-editor', + 'wp-element', + 'wp-i18n', + 'wp-plugins', + 'wp-annotations', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/sidebar.js' ), + true + ); -wp_enqueue_script( - 'gutenberg-test-annotations-sidebar', - plugins_url( 'plugins-api/annotations-sidebar.js', __FILE__ ), - array( - 'wp-components', - 'wp-compose', - 'wp-data', - 'wp-edit-post', - 'wp-editor', - 'wp-element', - 'wp-i18n', - 'wp-plugins', - 'wp-annotations', - ), - filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/annotations-sidebar.js' ), - true -); + wp_enqueue_script( + 'gutenberg-test-annotations-sidebar', + plugins_url( 'plugins-api/annotations-sidebar.js', __FILE__ ), + array( + 'wp-components', + 'wp-compose', + 'wp-data', + 'wp-edit-post', + 'wp-editor', + 'wp-element', + 'wp-i18n', + 'wp-plugins', + 'wp-annotations', + ), + filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/annotations-sidebar.js' ), + true + ); +} + +add_action( 'init', 'enqueue_plugins_api_plugin_scripts' );