UNPKG

@wordpress/block-editor

Version:
1,605 lines (1,364 loc) 45.9 kB
/** * External dependencies */ import { flow, reduce, first, last, omit, without, mapValues, keys, isEqual, isEmpty, identity, difference, omitBy, pickBy } from 'lodash'; /** * WordPress dependencies */ import { combineReducers, select } from '@wordpress/data'; import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies */ import { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS } from './defaults'; import { insertAt, moveTo } from './array'; /** * 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; } /** * Given an array of blocks, returns an object where each key contains * the clientId of the block and the value is the parent of the block. * * @param {Array} blocks Blocks to map. * @param {?string} rootClientId Assumed root client ID. * * @return {Object} Block order map object. */ function mapBlockParents(blocks, rootClientId = '') { return blocks.reduce((result, block) => Object.assign(result, { [block.clientId]: rootClientId }, mapBlockParents(block.innerBlocks, block.clientId)), {}); } /** * 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 = identity) { 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. It also excludes * InnerBlocks controllers, like template parts. * * It is important to exclude the full inner block controller and not just the * inner blocks because in many cases, we need to persist the previous value of * an inner block controller. To do so, it must be excluded from the list of * client IDs which are considered to be part of the top-level entity. * * @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 ''. * @param {?Object} controlledInnerBlocks The InnerBlocks controller state. * * @return {Array} List of descendant client IDs. */ function getNestedBlockClientIds(blocksOrder, rootClientId = '', controlledInnerBlocks = {}) { return reduce(blocksOrder[rootClientId], (result, clientId) => { if (!!controlledInnerBlocks[clientId]) { return result; } return [...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. * * @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} lastAction Previously dispatched action. * * @return {boolean} Whether actions are updating the same block attribute. */ export function isUpdatingSameBlockAttribute(action, lastAction) { return action.type === 'UPDATE_BLOCK_ATTRIBUTES' && lastAction !== undefined && lastAction.type === 'UPDATE_BLOCK_ATTRIBUTES' && isEqual(action.clientIds, lastAction.clientIds) && hasSameKeys(action.attributes, lastAction.attributes); } /** * Utility returning an object with an empty object value for each key. * * @param {Array} objectKeys Keys to fill. * @return {Object} Object filled with empty object as values for each clientId. */ const fillKeysWithEmptyObject = objectKeys => { return objectKeys.reduce((result, key) => { result[key] = {}; return result; }, {}); }; /** * Higher-order reducer intended to compute a cache key for each block in the post. * A new instance of the cache key (empty object) is created each time the block object * needs to be refreshed (for any change in the block or its children). * * @param {Function} reducer Original reducer function. * * @return {Function} Enhanced reducer function. */ const withBlockCache = reducer => (state = {}, action) => { const newState = reducer(state, action); if (newState === state) { return state; } newState.cache = state.cache ? state.cache : {}; /** * For each clientId provided, traverses up parents, adding the provided clientIds * and each parent's clientId to the returned array. * * When calling this function consider that it uses the old state, so any state * modifications made by the `reducer` will not be present. * * @param {Array} clientIds an Array of block clientIds. * * @return {Array} The provided clientIds and all of their parent clientIds. */ const getBlocksWithParentsClientIds = clientIds => { return clientIds.reduce((result, clientId) => { let current = clientId; do { result.push(current); current = state.parents[current]; } while (current && !state.controlledInnerBlocks[current]); return result; }, []); }; switch (action.type) { case 'RESET_BLOCKS': newState.cache = mapValues(flattenBlocks(action.blocks), () => ({})); break; case 'RECEIVE_BLOCKS': case 'INSERT_BLOCKS': { const updatedBlockUids = keys(flattenBlocks(action.blocks)); if (action.rootClientId && !state.controlledInnerBlocks[action.rootClientId]) { updatedBlockUids.push(action.rootClientId); } newState.cache = { ...newState.cache, ...fillKeysWithEmptyObject(getBlocksWithParentsClientIds(updatedBlockUids)) }; break; } case 'UPDATE_BLOCK': newState.cache = { ...newState.cache, ...fillKeysWithEmptyObject(getBlocksWithParentsClientIds([action.clientId])) }; break; case 'UPDATE_BLOCK_ATTRIBUTES': newState.cache = { ...newState.cache, ...fillKeysWithEmptyObject(getBlocksWithParentsClientIds(action.clientIds)) }; break; case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': const parentClientIds = fillKeysWithEmptyObject(getBlocksWithParentsClientIds(action.replacedClientIds)); newState.cache = { ...omit(newState.cache, action.replacedClientIds), ...omit(parentClientIds, action.replacedClientIds), ...fillKeysWithEmptyObject(keys(flattenBlocks(action.blocks))) }; break; case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': newState.cache = { ...omit(newState.cache, action.removedClientIds), ...fillKeysWithEmptyObject(difference(getBlocksWithParentsClientIds(action.clientIds), action.clientIds)) }; break; case 'MOVE_BLOCKS_TO_POSITION': { const updatedBlockUids = [...action.clientIds]; if (action.fromRootClientId) { updatedBlockUids.push(action.fromRootClientId); } if (action.toRootClientId) { updatedBlockUids.push(action.toRootClientId); } newState.cache = { ...newState.cache, ...fillKeysWithEmptyObject(getBlocksWithParentsClientIds(updatedBlockUids)) }; break; } case 'MOVE_BLOCKS_UP': case 'MOVE_BLOCKS_DOWN': { const updatedBlockUids = []; if (action.rootClientId) { updatedBlockUids.push(action.rootClientId); } newState.cache = { ...newState.cache, ...fillKeysWithEmptyObject(getBlocksWithParentsClientIds(updatedBlockUids)) }; break; } case 'SAVE_REUSABLE_BLOCK_SUCCESS': { const updatedBlockUids = keys(omitBy(newState.attributes, (attributes, clientId) => { return newState.byClientId[clientId].name !== 'core/block' || attributes.ref !== action.updatedId; })); newState.cache = { ...newState.cache, ...fillKeysWithEmptyObject(getBlocksWithParentsClientIds(updatedBlockUids)) }; } } return newState; }; /** * Higher-order reducer intended to augment the blocks reducer, assigning an * `isPersistentChange` property value corresponding to whether a change in * state can be considered as persistent. All changes are considered persistent * except when updating the same block attribute as in the previous action. * * @param {Function} reducer Original reducer function. * * @return {Function} Enhanced reducer function. */ function withPersistentBlockChange(reducer) { let lastAction; let markNextChangeAsNotPersistent = false; return (state, action) => { let nextState = reducer(state, action); const isExplicitPersistentChange = action.type === 'MARK_LAST_CHANGE_AS_PERSISTENT' || markNextChangeAsNotPersistent; // Defer to previous state value (or default) unless changing or // explicitly marking as persistent. if (state === nextState && !isExplicitPersistentChange) { var _state$isPersistentCh; markNextChangeAsNotPersistent = action.type === 'MARK_NEXT_CHANGE_AS_NOT_PERSISTENT'; const nextIsPersistentChange = (_state$isPersistentCh = state === null || state === void 0 ? void 0 : state.isPersistentChange) !== null && _state$isPersistentCh !== void 0 ? _state$isPersistentCh : true; if (state.isPersistentChange === nextIsPersistentChange) { return state; } return { ...nextState, isPersistentChange: nextIsPersistentChange }; } nextState = { ...nextState, isPersistentChange: isExplicitPersistentChange ? !markNextChangeAsNotPersistent : !isUpdatingSameBlockAttribute(action, lastAction) }; // In comparing against the previous action, consider only those which // would have qualified as one which would have been ignored or not // have resulted in a changed state. lastAction = action; markNextChangeAsNotPersistent = action.type === 'MARK_NEXT_CHANGE_AS_NOT_PERSISTENT'; return nextState; }; } /** * Higher-order reducer intended to augment the blocks reducer, assigning an * `isIgnoredChange` property value corresponding to whether a change in state * can be considered as ignored. A change is considered ignored when the result * of an action not incurred by direct user interaction. * * @param {Function} reducer Original reducer function. * * @return {Function} Enhanced reducer function. */ function withIgnoredBlockChange(reducer) { /** * Set of action types for which a blocks state change should be ignored. * * @type {Set} */ const IGNORED_ACTION_TYPES = new Set(['RECEIVE_BLOCKS']); return (state, action) => { const nextState = reducer(state, action); if (nextState !== state) { nextState.isIgnoredChange = IGNORED_ACTION_TYPES.has(action.type); } return nextState; }; } /** * Higher-order reducer targeting the combined blocks 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) => { // Gets all children which need to be removed. const getAllChildren = clientIds => { let result = clientIds; for (let i = 0; i < result.length; i++) { if (!state.order[result[i]] || action.keepControlledInnerBlocks && action.keepControlledInnerBlocks[result[i]]) { continue; } if (result === clientIds) { result = [...result]; } result.push(...state.order[result[i]]); } return result; }; if (state) { switch (action.type) { case 'REMOVE_BLOCKS': action = { ...action, type: 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN', removedClientIds: getAllChildren(action.clientIds) }; break; case 'REPLACE_BLOCKS': action = { ...action, type: 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN', replacedClientIds: getAllChildren(action.clientIds) }; break; } } 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 and blocks controlled by inner blocks controllers) alone. * * @param {Function} reducer Original reducer function. * * @return {Function} Enhanced reducer function. */ const withBlockReset = reducer => (state, action) => { if (state && action.type === 'RESET_BLOCKS') { /** * A list of client IDs associated with the top level entity (like a * post or template). It excludes the client IDs of blocks associated * with other entities, like inner block controllers or reusable blocks. */ const visibleClientIds = getNestedBlockClientIds(state.order, '', state.controlledInnerBlocks); // pickBy returns only the truthy values from controlledInnerBlocks const controlledInnerBlocks = Object.keys(pickBy(state.controlledInnerBlocks)); /** * Each update operation consists of a few parts: * 1. First, the client IDs associated with the top level entity are * removed from the existing state key, leaving in place controlled * blocks (like reusable blocks and inner block controllers). * 2. Second, the blocks from the reset action are used to calculate the * individual state keys. This will re-populate the clientIDs which * were removed in step 1. * 3. In some cases, we remove the recalculated inner block controllers, * letting their old values persist. We need to do this because the * reset block action from a top-level entity is not aware of any * inner blocks inside InnerBlock controllers. So if the new values * were used, it would not take into account the existing InnerBlocks * which already exist in the state for inner block controllers. For * example, `attributes` uses the newly computed value for controllers * since attributes are stored in the top-level entity. But `order` * uses the previous value for the controllers since the new value * does not include the order of controlled inner blocks. So if the * new value was used, template parts would disappear from the editor * whenever you try to undo a change in the top level entity. */ return { ...state, byClientId: { ...omit(state.byClientId, visibleClientIds), ...getFlattenedBlocksWithoutAttributes(action.blocks) }, attributes: { ...omit(state.attributes, visibleClientIds), ...getFlattenedBlockAttributes(action.blocks) }, order: { ...omit(state.order, visibleClientIds), ...omit(mapBlockOrder(action.blocks), controlledInnerBlocks) }, parents: { ...omit(state.parents, visibleClientIds), ...mapBlockParents(action.blocks) }, cache: { ...omit(state.cache, visibleClientIds), ...omit(mapValues(flattenBlocks(action.blocks), () => ({})), controlledInnerBlocks) } }; } return reducer(state, action); }; /** * Higher-order reducer which targets the combined blocks reducer and handles * the `REPLACE_INNER_BLOCKS` action. When dispatched, this action the state * should become equivalent to the execution of a `REMOVE_BLOCKS` action * containing all the child's of the root block followed by the execution of * `INSERT_BLOCKS` with the new blocks. * * @param {Function} reducer Original reducer function. * * @return {Function} Enhanced reducer function. */ const withReplaceInnerBlocks = reducer => (state, action) => { if (action.type !== 'REPLACE_INNER_BLOCKS') { return reducer(state, action); } // Finds every nested inner block controller. We must check the action blocks // and not just the block parent state because some inner block controllers // should be deleted if specified, whereas others should not be deleted. If // a controlled should not be deleted, then we need to avoid deleting its // inner blocks from the block state because its inner blocks will not be // attached to the block in the action. const nestedControllers = {}; if (Object.keys(state.controlledInnerBlocks).length) { const stack = [...action.blocks]; while (stack.length) { const { innerBlocks, ...block } = stack.shift(); stack.push(...innerBlocks); if (!!state.controlledInnerBlocks[block.clientId]) { nestedControllers[block.clientId] = true; } } } // The `keepControlledInnerBlocks` prop will keep the inner blocks of the // marked block in the block state so that they can be reattached to the // marked block when we re-insert everything a few lines below. let stateAfterBlocksRemoval = state; if (state.order[action.rootClientId]) { stateAfterBlocksRemoval = reducer(stateAfterBlocksRemoval, { type: 'REMOVE_BLOCKS', keepControlledInnerBlocks: nestedControllers, clientIds: state.order[action.rootClientId] }); } let stateAfterInsert = stateAfterBlocksRemoval; if (action.blocks.length) { stateAfterInsert = reducer(stateAfterInsert, { ...action, type: 'INSERT_BLOCKS', index: 0 }); // We need to re-attach the block order of the controlled inner blocks. // Otherwise, an inner block controller's blocks will be deleted entirely // from its entity.. stateAfterInsert.order = { ...stateAfterInsert.order, ...reduce(nestedControllers, (result, value, key) => { if (state.order[key]) { result[key] = state.order[key]; } return result; }, {}) }; } return stateAfterInsert; }; /** * 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); }; /** * Reducer returning the blocks state. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ export const blocks = flow(combineReducers, withSaveReusableBlock, // needs to be before withBlockCache withBlockCache, // needs to be before withInnerBlocksRemoveCascade withInnerBlocksRemoveCascade, withReplaceInnerBlocks, // needs to be after withInnerBlocksRemoveCascade withBlockReset, withPersistentBlockChange, withIgnoredBlockChange)({ byClientId(state = {}, action) { switch (action.type) { case 'RESET_BLOCKS': return getFlattenedBlocksWithoutAttributes(action.blocks); case 'RECEIVE_BLOCKS': case 'INSERT_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 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': if (!action.blocks) { return state; } return { ...omit(state, action.replacedClientIds), ...getFlattenedBlocksWithoutAttributes(action.blocks) }; case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': return omit(state, action.removedClientIds); } return state; }, attributes(state = {}, action) { switch (action.type) { case 'RESET_BLOCKS': return getFlattenedBlockAttributes(action.blocks); case 'RECEIVE_BLOCKS': case 'INSERT_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': { // Avoid a state change if none of the block IDs are known. if (action.clientIds.every(id => !state[id])) { return state; } const next = action.clientIds.reduce((accumulator, id) => ({ ...accumulator, [id]: reduce(action.uniqueByBlock ? action.attributes[id] : action.attributes, (result, value, key) => { // Consider as updates only changed values. if (value !== result[key]) { result = getMutateSafeObject(state[id], result); result[key] = value; } return result; }, state[id]) }), {}); if (action.clientIds.every(id => next[id] === state[id])) { return state; } return { ...state, ...next }; } case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': if (!action.blocks) { return state; } return { ...omit(state, action.replacedClientIds), ...getFlattenedBlockAttributes(action.blocks) }; case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': return omit(state, action.removedClientIds); } return state; }, order(state = {}, action) { switch (action.type) { case 'RESET_BLOCKS': return mapBlockOrder(action.blocks); case 'RECEIVE_BLOCKS': return { ...state, ...omit(mapBlockOrder(action.blocks), '') }; case 'INSERT_BLOCKS': { const { rootClientId = '' } = action; const subState = state[rootClientId] || []; const mappedBlocks = mapBlockOrder(action.blocks, rootClientId); const { index = subState.length } = action; return { ...state, ...mappedBlocks, [rootClientId]: insertAt(subState, mappedBlocks[rootClientId], index) }; } case 'MOVE_BLOCKS_TO_POSITION': { const { fromRootClientId = '', toRootClientId = '', clientIds } = action; const { index = state[toRootClientId].length } = action; // Moving inside the same parent block if (fromRootClientId === toRootClientId) { const subState = state[toRootClientId]; const fromIndex = subState.indexOf(clientIds[0]); return { ...state, [toRootClientId]: moveTo(state[toRootClientId], fromIndex, index, clientIds.length) }; } // Moving from a parent block to another return { ...state, [fromRootClientId]: without(state[fromRootClientId], ...clientIds), [toRootClientId]: insertAt(state[toRootClientId], clientIds, 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_AUGMENTED_WITH_CHILDREN': { const { clientIds } = action; if (!action.blocks) { return state; } const mappedBlocks = mapBlockOrder(action.blocks); return flow([nextState => omit(nextState, action.replacedClientIds), 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_AUGMENTED_WITH_CHILDREN': return flow([// Remove inner block ordering for removed blocks nextState => omit(nextState, action.removedClientIds), // Remove deleted blocks from other blocks' orderings nextState => mapValues(nextState, subState => without(subState, ...action.removedClientIds))])(state); } return state; }, // While technically redundant data as the inverse of `order`, it serves as // an optimization for the selectors which derive the ancestry of a block. parents(state = {}, action) { switch (action.type) { case 'RESET_BLOCKS': return mapBlockParents(action.blocks); case 'RECEIVE_BLOCKS': return { ...state, ...mapBlockParents(action.blocks) }; case 'INSERT_BLOCKS': return { ...state, ...mapBlockParents(action.blocks, action.rootClientId || '') }; case 'MOVE_BLOCKS_TO_POSITION': { return { ...state, ...action.clientIds.reduce((accumulator, id) => { accumulator[id] = action.toRootClientId || ''; return accumulator; }, {}) }; } case 'REPLACE_BLOCKS_AUGMENTED_WITH_CHILDREN': return { ...omit(state, action.replacedClientIds), ...mapBlockParents(action.blocks, state[action.clientIds[0]]) }; case 'REMOVE_BLOCKS_AUGMENTED_WITH_CHILDREN': return omit(state, action.removedClientIds); } return state; }, controlledInnerBlocks(state = {}, { type, clientId, hasControlledInnerBlocks }) { if (type === 'SET_HAS_CONTROLLED_INNER_BLOCKS') { return { ...state, [clientId]: hasControlledInnerBlocks }; } 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 dragged block client id. * * @param {string[]} state Current state. * @param {Object} action Dispatched action. * * @return {string[]} Updated state. */ export function draggedBlocks(state = [], action) { switch (action.type) { case 'START_DRAGGING_BLOCKS': return action.clientIds; case 'STOP_DRAGGING_BLOCKS': return []; } 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; } /** * Internal helper reducer for selectionStart and selectionEnd. Can hold a block * selection, represented by an object with property clientId. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ function selectionHelper(state = {}, action) { switch (action.type) { case 'CLEAR_SELECTED_BLOCK': { if (state.clientId) { return {}; } return state; } case 'SELECT_BLOCK': if (action.clientId === state.clientId) { return state; } return { clientId: action.clientId }; case 'REPLACE_INNER_BLOCKS': case 'INSERT_BLOCKS': { if (!action.updateSelection || !action.blocks.length) { return state; } return { clientId: action.blocks[0].clientId }; } case 'REMOVE_BLOCKS': if (!action.clientIds || !action.clientIds.length || action.clientIds.indexOf(state.clientId) === -1) { return state; } return {}; case 'REPLACE_BLOCKS': { if (action.clientIds.indexOf(state.clientId) === -1) { return state; } const indexToSelect = action.indexToSelect || action.blocks.length - 1; const blockToSelect = action.blocks[indexToSelect]; if (!blockToSelect) { return {}; } if (blockToSelect.clientId === state.clientId) { return state; } return { clientId: blockToSelect.clientId }; } } return state; } /** * Reducer returning the selection state. * * @param {boolean} state Current state. * @param {Object} action Dispatched action. * * @return {boolean} Updated state. */ export function selection(state = {}, action) { var _state$selectionStart, _state$selectionEnd; switch (action.type) { case 'SELECTION_CHANGE': return { selectionStart: { clientId: action.clientId, attributeKey: action.attributeKey, offset: action.startOffset }, selectionEnd: { clientId: action.clientId, attributeKey: action.attributeKey, offset: action.endOffset } }; case 'RESET_SELECTION': const { selectionStart, selectionEnd } = action; return { selectionStart, selectionEnd }; case 'MULTI_SELECT': const { start, end } = action; return { selectionStart: { clientId: start }, selectionEnd: { clientId: end } }; case 'RESET_BLOCKS': const startClientId = state === null || state === void 0 ? void 0 : (_state$selectionStart = state.selectionStart) === null || _state$selectionStart === void 0 ? void 0 : _state$selectionStart.clientId; const endClientId = state === null || state === void 0 ? void 0 : (_state$selectionEnd = state.selectionEnd) === null || _state$selectionEnd === void 0 ? void 0 : _state$selectionEnd.clientId; // Do nothing if there's no selected block. if (!startClientId && !endClientId) { return state; } // If the start of the selection won't exist after reset, remove selection. if (!action.blocks.some(block => block.clientId === startClientId)) { return { selectionStart: {}, selectionEnd: {} }; } // If the end of the selection won't exist after reset, collapse selection. if (!action.blocks.some(block => block.clientId === endClientId)) { return { ...state, selectionEnd: state.selectionStart }; } } return { selectionStart: selectionHelper(state.selectionStart, action), selectionEnd: selectionHelper(state.selectionEnd, action) }; } /** * Reducer returning whether the user is multi-selecting. * * @param {boolean} state Current state. * @param {Object} action Dispatched action. * * @return {boolean} Updated state. */ export function isMultiSelecting(state = false, action) { switch (action.type) { case 'START_MULTI_SELECT': return true; case 'STOP_MULTI_SELECT': return false; } return state; } /** * Reducer returning whether selection is enabled. * * @param {boolean} state Current state. * @param {Object} action Dispatched action. * * @return {boolean} Updated state. */ export function isSelectionEnabled(state = true, action) { switch (action.type) { case 'TOGGLE_SELECTION': return action.isSelectionEnabled; } return state; } /** * Reducer returning the intial block selection. * * Currently this in only used to restore the selection after block deletion and * pasting new content.This reducer should eventually be removed in favour of setting * selection directly. * * @param {boolean} state Current state. * @param {Object} action Dispatched action. * * @return {number|null} Initial position: 0, -1 or null. */ export function initialPosition(state = null, action) { if (action.type === 'REPLACE_BLOCKS' && action.initialPosition !== undefined) { return action.initialPosition; } else if (['SELECT_BLOCK', 'RESET_SELECTION', 'INSERT_BLOCKS', 'REPLACE_INNER_BLOCKS'].includes(action.type)) { return action.initialPosition; } 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, __unstableWithInserter } = action; return { rootClientId, index, __unstableWithInserter }; 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 = SETTINGS_DEFAULTS, action) { switch (action.type) { case 'UPDATE_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) => { const { attributes, name: blockName } = block; const match = select(blocksStore).getActiveBlockVariation(blockName, attributes); // If a block variation match is found change the name to be the same with the // one that is used for block variations in the Inserter (`getItemFromVariation`). let id = match !== null && match !== void 0 && match.name ? `${blockName}/${match.name}` : blockName; const insert = { name: id }; if (blockName === 'core/block') { insert.ref = attributes.ref; id += '/' + 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; }; /** * Reducer returning whether the navigation mode is enabled or not. * * @param {string} state Current state. * @param {Object} action Dispatched action. * * @return {string} Updated state. */ export function isNavigationMode(state = false, action) { // Let inserting block always trigger Edit mode. if (action.type === 'INSERT_BLOCKS') { return false; } if (action.type === 'SET_NAVIGATION_MODE') { return action.isNavigationMode; } return state; } /** * Reducer returning whether the block moving mode is enabled or not. * * @param {string|null} state Current state. * @param {Object} action Dispatched action. * * @return {string|null} Updated state. */ export function hasBlockMovingClientId(state = null, action) { // Let inserting block always trigger Edit mode. if (action.type === 'SET_BLOCK_MOVING_MODE') { return action.hasBlockMovingClientId; } if (action.type === 'SET_NAVIGATION_MODE') { return null; } return state; } /** * Reducer return an updated state representing the most recent block attribute * update. The state is structured as an object where the keys represent the * client IDs of blocks, the values a subset of attributes from the most recent * block update. The state is always reset to null if the last action is * anything other than an attributes update. * * @param {Object<string,Object>} state Current state. * @param {Object} action Action object. * * @return {[string,Object]} Updated state. */ export function lastBlockAttributesChange(state, action) { switch (action.type) { case 'UPDATE_BLOCK': if (!action.updates.attributes) { break; } return { [action.clientId]: action.updates.attributes }; case 'UPDATE_BLOCK_ATTRIBUTES': return action.clientIds.reduce((accumulator, id) => ({ ...accumulator, [id]: action.uniqueByBlock ? action.attributes[id] : action.attributes }), {}); } return null; } /** * Reducer returning automatic change state. * * @param {boolean} state Current state. * @param {Object} action Dispatched action. * * @return {string} Updated state. */ export function automaticChangeStatus(state, action) { switch (action.type) { case 'MARK_AUTOMATIC_CHANGE': return 'pending'; case 'MARK_AUTOMATIC_CHANGE_FINAL': if (state === 'pending') { return 'final'; } return; case 'SELECTION_CHANGE': // As long as the state is not final, ignore any selection changes. if (state !== 'final') { return state; } return; // Undoing an automatic change should still be possible after mouse // move. case 'START_TYPING': case 'STOP_TYPING': return state; } // Reset the state by default (for any action not handled). } /** * Reducer returning current highlighted block. * * @param {boolean} state Current highlighted block. * @param {Object} action Dispatched action. * * @return {string} Updated state. */ export function highlightedBlock(state, action) { switch (action.type) { case 'TOGGLE_BLOCK_HIGHLIGHT': const { clientId, isHighlighted } = action; if (isHighlighted) { return clientId; } else if (state === clientId) { return null; } return state; case 'SELECT_BLOCK': if (action.clientId !== state) { return null; } } return state; } /** * Reducer returning the block insertion event list state. * * @param {Object} state Current state. * @param {Object} action Dispatched action. * * @return {Object} Updated state. */ export function lastBlockInserted(state = {}, action) { var _action$meta; switch (action.type) { case 'INSERT_BLOCKS': if (!action.blocks.length) { return state; } const clientId = action.blocks[0].clientId; const source = (_action$meta = action.meta) === null || _action$meta === void 0 ? void 0 : _action$meta.source; return { clientId, source }; case 'RESET_BLOCKS': return {}; } return state; } export default combineReducers({ blocks, isTyping, draggedBlocks, isCaretWithinFormattedText, selection, isMultiSelecting, isSelectionEnabled, initialPosition, blocksMode, blockListSettings, insertionPoint, template, settings, preferences, lastBlockAttributesChange, isNavigationMode, hasBlockMovingClientId, automaticChangeStatus, highlightedBlock, lastBlockInserted }); //# sourceMappingURL=reducer.js.map