@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
725 lines (645 loc) • 19.5 kB
JavaScript
/**
* 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');
//# sourceMappingURL=actions.js.map