UNPKG

@wordpress/editor

Version:
824 lines (753 loc) 19.6 kB
/** * External dependencies */ import { has } from 'lodash'; /** * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; import { controls } from '@wordpress/data'; import { apiFetch } from '@wordpress/data-controls'; import { parse, synchronizeBlocksWithTemplate, __unstableSerializeAndClean, } from '@wordpress/blocks'; import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ import { STORE_NAME, TRASH_POST_NOTICE_ID } from './constants'; import { getNotificationArgumentsForSaveSuccess, getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; /** * Returns an action generator 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 {Array?} template Block Template. */ export function* setupEditor( post, edits, template ) { // In order to ensure maximum of a single parse during setup, edits are // included as part of editor setup action. Assume edited content as // canonical if provided, falling back to post. let content; if ( has( edits, [ 'content' ] ) ) { content = edits.content; } else { content = post.content.raw; } let blocks = parse( content ); // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; if ( isNewPost && template ) { blocks = synchronizeBlocksWithTemplate( blocks, template ); } yield resetPost( post ); yield { type: 'SETUP_EDITOR', post, edits, template, }; yield resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false, } ); yield setupEditorState( post ); if ( edits && Object.keys( edits ).some( ( key ) => edits[ key ] !== ( has( post, [ key, 'raw' ] ) ? post[ key ].raw : post[ key ] ) ) ) { yield editPost( edits ); } } /** * Returns an action object signalling that the editor is being destroyed and * that any necessary state or side-effect cleanup should occur. * * @return {Object} Action object. */ export function __experimentalTearDownEditor() { return { type: 'TEAR_DOWN_EDITOR' }; } /** * Returns an action object used in signalling that the latest version of the * post has been received, either by initialization or save. * * @param {Object} post Post object. * * @return {Object} Action object. */ export function resetPost( post ) { return { type: 'RESET_POST', post, }; } /** * Returns an action object used in signalling that the latest autosave of the * post has been received, by initialization or autosave. * * @deprecated since 5.6. Callers should use the `receiveAutosaves( postId, autosave )` * selector from the '@wordpress/core-data' package. * * @param {Object} newAutosave Autosave post object. * * @return {Object} Action object. */ export function* resetAutosave( newAutosave ) { deprecated( 'resetAutosave action (`core/editor` store)', { since: '5.3', alternative: 'receiveAutosaves action (`core` store)', } ); const postId = yield controls.select( STORE_NAME, 'getCurrentPostId' ); yield controls.dispatch( 'core', 'receiveAutosaves', postId, newAutosave ); return { type: '__INERT__' }; } /** * Action for dispatching that a post update request has started. * * @param {Object} options * * @return {Object} An action object */ export function __experimentalRequestPostUpdateStart( options = {} ) { return { type: 'REQUEST_POST_UPDATE_START', options, }; } /** * Action for dispatching that a post update request has finished. * * @param {Object} options * * @return {Object} An action object */ export function __experimentalRequestPostUpdateFinish( options = {} ) { return { type: 'REQUEST_POST_UPDATE_FINISH', options, }; } /** * Returns an action object used in signalling that a patch of updates for the * latest version of the post have been received. * * @return {Object} Action object. * @deprecated since Gutenberg 9.7.0. */ export function updatePost() { deprecated( "wp.data.dispatch( 'core/editor' ).updatePost", { since: '5.7', alternative: 'User the core entitires store instead', } ); return { type: 'DO_NOTHING', }; } /** * Returns an action object used to setup the editor state when first opening * an editor. * * @param {Object} post Post object. * * @return {Object} Action object. */ export function setupEditorState( post ) { return { type: 'SETUP_EDITOR_STATE', post, }; } /** * Returns an action object used in signalling that attributes of the post have * been edited. * * @param {Object} edits Post attributes to edit. * @param {Object} options Options for the edit. * * @yield {Object} Action object or control. */ export function* editPost( edits, options ) { const { id, type } = yield controls.select( STORE_NAME, 'getCurrentPost' ); yield controls.dispatch( 'core', 'editEntityRecord', 'postType', type, id, edits, options ); } /** * Action generator for saving the current post in the editor. * * @param {Object} options */ export function* savePost( options = {} ) { if ( ! ( yield controls.select( STORE_NAME, 'isEditedPostSaveable' ) ) ) { return; } let edits = { content: yield controls.select( STORE_NAME, 'getEditedPostContent' ), }; if ( ! options.isAutosave ) { yield controls.dispatch( STORE_NAME, 'editPost', edits, { undoIgnore: true, } ); } yield __experimentalRequestPostUpdateStart( options ); const previousRecord = yield controls.select( STORE_NAME, 'getCurrentPost' ); edits = { id: previousRecord.id, ...( yield controls.select( 'core', 'getEntityRecordNonTransientEdits', 'postType', previousRecord.type, previousRecord.id ) ), ...edits, }; yield controls.dispatch( 'core', 'saveEntityRecord', 'postType', previousRecord.type, edits, options ); yield __experimentalRequestPostUpdateFinish( options ); const error = yield controls.select( 'core', 'getLastEntitySaveError', 'postType', previousRecord.type, previousRecord.id ); if ( error ) { const args = getNotificationArgumentsForSaveFail( { post: previousRecord, edits, error, } ); if ( args.length ) { yield controls.dispatch( noticesStore, 'createErrorNotice', ...args ); } } else { const updatedRecord = yield controls.select( STORE_NAME, 'getCurrentPost' ); const args = getNotificationArgumentsForSaveSuccess( { previousPost: previousRecord, post: updatedRecord, postType: yield controls.resolveSelect( 'core', 'getPostType', updatedRecord.type ), options, } ); if ( args.length ) { yield controls.dispatch( noticesStore, 'createSuccessNotice', ...args ); } // Make sure that any edits after saving create an undo level and are // considered for change detection. if ( ! options.isAutosave ) { yield controls.dispatch( 'core/block-editor', '__unstableMarkLastChangeAsPersistent' ); } } } /** * Action generator for handling refreshing the current post. */ export function* refreshPost() { const post = yield controls.select( STORE_NAME, 'getCurrentPost' ); const postTypeSlug = yield controls.select( STORE_NAME, 'getCurrentPostType' ); const postType = yield controls.resolveSelect( 'core', 'getPostType', postTypeSlug ); const newPost = yield apiFetch( { // Timestamp arg allows caller to bypass browser caching, which is // expected for this specific function. path: `/wp/v2/${ postType.rest_base }/${ post.id }` + `?context=edit&_timestamp=${ Date.now() }`, } ); yield controls.dispatch( STORE_NAME, 'resetPost', newPost ); } /** * Action generator for trashing the current post in the editor. */ export function* trashPost() { const postTypeSlug = yield controls.select( STORE_NAME, 'getCurrentPostType' ); const postType = yield controls.resolveSelect( 'core', 'getPostType', postTypeSlug ); yield controls.dispatch( noticesStore, 'removeNotice', TRASH_POST_NOTICE_ID ); try { const post = yield controls.select( STORE_NAME, 'getCurrentPost' ); yield apiFetch( { path: `/wp/v2/${ postType.rest_base }/${ post.id }`, method: 'DELETE', } ); yield controls.dispatch( STORE_NAME, 'savePost' ); } catch ( error ) { yield controls.dispatch( noticesStore, 'createErrorNotice', ...getNotificationArgumentsForTrashFail( { error } ) ); } } /** * Action generator used in signalling that the post should autosave. This * includes server-side autosaving (default) and client-side (a.k.a. local) * autosaving (e.g. on the Web, the post might be committed to Session * Storage). * * @param {Object?} options Extra flags to identify the autosave. */ export function* autosave( { local = false, ...options } = {} ) { if ( local ) { const post = yield controls.select( STORE_NAME, 'getCurrentPost' ); const isPostNew = yield controls.select( STORE_NAME, 'isEditedPostNew' ); const title = yield controls.select( STORE_NAME, 'getEditedPostAttribute', 'title' ); const content = yield controls.select( STORE_NAME, 'getEditedPostAttribute', 'content' ); const excerpt = yield controls.select( STORE_NAME, 'getEditedPostAttribute', 'excerpt' ); yield { type: 'LOCAL_AUTOSAVE_SET', postId: post.id, isPostNew, title, content, excerpt, }; } else { yield controls.dispatch( STORE_NAME, 'savePost', { isAutosave: true, ...options, } ); } } /** * Returns an action object used in signalling that undo history should * restore last popped state. * * @yield {Object} Action object. */ export function* redo() { yield controls.dispatch( 'core', 'redo' ); } /** * Returns an action object used in signalling that undo history should pop. * * @yield {Object} Action object. */ export function* undo() { yield controls.dispatch( 'core', '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 to lock the editor. * * @param {Object} lock Details about the post lock status, user, and nonce. * * @return {Object} Action object. */ export function updatePostLock( lock ) { return { type: 'UPDATE_POST_LOCK', lock, }; } /** * Returns an action object used in signalling that the user has enabled the * publish sidebar. * * @return {Object} Action object */ export function enablePublishSidebar() { return { type: 'ENABLE_PUBLISH_SIDEBAR', }; } /** * Returns an action object used in signalling that the user has disabled the * publish sidebar. * * @return {Object} Action object */ export function disablePublishSidebar() { return { type: 'DISABLE_PUBLISH_SIDEBAR', }; } /** * Returns an action object used to signal that post saving is locked. * * @param {string} lockName The lock name. * * @example * ``` * const { subscribe } = wp.data; * * const initialPostStatus = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'status' ); * * // Only allow publishing posts that are set to a future date. * if ( 'publish' !== initialPostStatus ) { * * // Track locking. * let locked = false; * * // Watch for the publish event. * let unssubscribe = subscribe( () => { * const currentPostStatus = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'status' ); * if ( 'publish' !== currentPostStatus ) { * * // Compare the post date to the current date, lock the post if the date isn't in the future. * const postDate = new Date( wp.data.select( 'core/editor' ).getEditedPostAttribute( 'date' ) ); * const currentDate = new Date(); * if ( postDate.getTime() <= currentDate.getTime() ) { * if ( ! locked ) { * locked = true; * wp.data.dispatch( 'core/editor' ).lockPostSaving( 'futurelock' ); * } * } else { * if ( locked ) { * locked = false; * wp.data.dispatch( 'core/editor' ).unlockPostSaving( 'futurelock' ); * } * } * } * } ); * } * ``` * * @return {Object} Action object */ export function lockPostSaving( lockName ) { return { type: 'LOCK_POST_SAVING', lockName, }; } /** * Returns an action object used to signal that post saving is unlocked. * * @param {string} lockName The lock name. * * @example * ``` * // Unlock post saving with the lock key `mylock`: * wp.data.dispatch( 'core/editor' ).unlockPostSaving( 'mylock' ); * ``` * * @return {Object} Action object */ export function unlockPostSaving( lockName ) { return { type: 'UNLOCK_POST_SAVING', lockName, }; } /** * Returns an action object used to signal that post autosaving is locked. * * @param {string} lockName The lock name. * * @example * ``` * // Lock post autosaving with the lock key `mylock`: * wp.data.dispatch( 'core/editor' ).lockPostAutosaving( 'mylock' ); * ``` * * @return {Object} Action object */ export function lockPostAutosaving( lockName ) { return { type: 'LOCK_POST_AUTOSAVING', lockName, }; } /** * Returns an action object used to signal that post autosaving is unlocked. * * @param {string} lockName The lock name. * * @example * ``` * // Unlock post saving with the lock key `mylock`: * wp.data.dispatch( 'core/editor' ).unlockPostAutosaving( 'mylock' ); * ``` * * @return {Object} Action object */ export function unlockPostAutosaving( lockName ) { return { type: 'UNLOCK_POST_AUTOSAVING', lockName, }; } /** * Returns an action object used to signal that the blocks have been updated. * * @param {Array} blocks Block Array. * @param {?Object} options Optional options. * * @yield {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { const { __unstableShouldCreateUndoLevel, selection } = options; const edits = { blocks, selection }; if ( __unstableShouldCreateUndoLevel !== false ) { const { id, type } = yield controls.select( STORE_NAME, 'getCurrentPost' ); const noChange = ( yield controls.select( 'core', 'getEditedEntityRecord', 'postType', type, id ) ).blocks === edits.blocks; if ( noChange ) { return yield controls.dispatch( 'core', '__unstableCreateUndoLevel', 'postType', type, id ); } // We create a new function here on every persistent edit // to make sure the edit makes the post dirty and creates // a new undo level. edits.content = ( { blocks: blocksForSerialization = [] } ) => __unstableSerializeAndClean( blocksForSerialization ); } yield* editPost( edits ); } /* * Returns an action object used in signalling that the post editor settings have been updated. * * @param {Object} settings Updated settings * * @return {Object} Action object */ export function updateEditorSettings( settings ) { return { type: 'UPDATE_EDITOR_SETTINGS', settings, }; } /** * Backward compatibility */ const getBlockEditorAction = ( name ) => function* ( ...args ) { deprecated( "`wp.data.dispatch( 'core/editor' )." + name + '`', { since: '5.3', alternative: "`wp.data.dispatch( 'core/block-editor' )." + name + '`', } ); yield controls.dispatch( 'core/block-editor', name, ...args ); }; /** * @see resetBlocks in core/block-editor store. */ export const resetBlocks = getBlockEditorAction( 'resetBlocks' ); /** * @see receiveBlocks in core/block-editor store. */ export const receiveBlocks = getBlockEditorAction( 'receiveBlocks' ); /** * @see updateBlock in core/block-editor store. */ export const updateBlock = getBlockEditorAction( 'updateBlock' ); /** * @see updateBlockAttributes in core/block-editor store. */ export const updateBlockAttributes = getBlockEditorAction( 'updateBlockAttributes' ); /** * @see selectBlock in core/block-editor store. */ export const selectBlock = getBlockEditorAction( 'selectBlock' ); /** * @see startMultiSelect in core/block-editor store. */ export const startMultiSelect = getBlockEditorAction( 'startMultiSelect' ); /** * @see stopMultiSelect in core/block-editor store. */ export const stopMultiSelect = getBlockEditorAction( 'stopMultiSelect' ); /** * @see multiSelect in core/block-editor store. */ export const multiSelect = getBlockEditorAction( 'multiSelect' ); /** * @see clearSelectedBlock in core/block-editor store. */ export const clearSelectedBlock = getBlockEditorAction( 'clearSelectedBlock' ); /** * @see toggleSelection in core/block-editor store. */ export const toggleSelection = getBlockEditorAction( 'toggleSelection' ); /** * @see replaceBlocks in core/block-editor store. */ export const replaceBlocks = getBlockEditorAction( 'replaceBlocks' ); /** * @see replaceBlock in core/block-editor store. */ export const replaceBlock = getBlockEditorAction( 'replaceBlock' ); /** * @see moveBlocksDown in core/block-editor store. */ export const moveBlocksDown = getBlockEditorAction( 'moveBlocksDown' ); /** * @see moveBlocksUp in core/block-editor store. */ export const moveBlocksUp = getBlockEditorAction( 'moveBlocksUp' ); /** * @see moveBlockToPosition in core/block-editor store. */ export const moveBlockToPosition = getBlockEditorAction( 'moveBlockToPosition' ); /** * @see insertBlock in core/block-editor store. */ export const insertBlock = getBlockEditorAction( 'insertBlock' ); /** * @see insertBlocks in core/block-editor store. */ export const insertBlocks = getBlockEditorAction( 'insertBlocks' ); /** * @see showInsertionPoint in core/block-editor store. */ export const showInsertionPoint = getBlockEditorAction( 'showInsertionPoint' ); /** * @see hideInsertionPoint in core/block-editor store. */ export const hideInsertionPoint = getBlockEditorAction( 'hideInsertionPoint' ); /** * @see setTemplateValidity in core/block-editor store. */ export const setTemplateValidity = getBlockEditorAction( 'setTemplateValidity' ); /** * @see synchronizeTemplate in core/block-editor store. */ export const synchronizeTemplate = getBlockEditorAction( 'synchronizeTemplate' ); /** * @see mergeBlocks in core/block-editor store. */ export const mergeBlocks = getBlockEditorAction( 'mergeBlocks' ); /** * @see removeBlocks in core/block-editor store. */ export const removeBlocks = getBlockEditorAction( 'removeBlocks' ); /** * @see removeBlock in core/block-editor store. */ export const removeBlock = getBlockEditorAction( 'removeBlock' ); /** * @see toggleBlockMode in core/block-editor store. */ export const toggleBlockMode = getBlockEditorAction( 'toggleBlockMode' ); /** * @see startTyping in core/block-editor store. */ export const startTyping = getBlockEditorAction( 'startTyping' ); /** * @see stopTyping in core/block-editor store. */ export const stopTyping = getBlockEditorAction( 'stopTyping' ); /** * @see enterFormattedText in core/block-editor store. */ export const enterFormattedText = getBlockEditorAction( 'enterFormattedText' ); /** * @see exitFormattedText in core/block-editor store. */ export const exitFormattedText = getBlockEditorAction( 'exitFormattedText' ); /** * @see insertDefaultBlock in core/block-editor store. */ export const insertDefaultBlock = getBlockEditorAction( 'insertDefaultBlock' ); /** * @see updateBlockListSettings in core/block-editor store. */ export const updateBlockListSettings = getBlockEditorAction( 'updateBlockListSettings' );