@quillforms/block-editor
Version:
458 lines (418 loc) • 12 kB
text/typescript
/* eslint-disable no-nested-ternary */
/**
* External dependencies
*/
import { cloneDeep, size, identity, forEach } from 'lodash';
import type { Reducer } from 'redux';
/**
* Internal Dependencies
*/
import {
SET_BLOCK_ATTRIBUTES,
INSERT_BLOCK,
REORDER_BLOCKS,
DELETE_BLOCK,
SET_CURRENT_BLOCK,
SET_CURRENT_CHILD_BLOCK,
SETUP_STORE,
SET_BLOCKS,
} from './constants';
import type { BlockEditorActionTypes, BlockEditorPureState } from './types';
import type { FormBlocks, FormBlock } from '@quillforms/types';
import { sanitizeBlockAttributes } from '@quillforms/blocks';
const generateBlockId = (): string => {
return Math.random().toString(36).substr(2, 9);
};
/**
* Sort blocks. We should have welcome screens at first then others then thankyou screens.
*
* @param {Object} blocks The blocks array.
*
* @return { Object } The sorted blocks
*/
function sortBlocks(blocks: FormBlocks): FormBlocks {
const priority = ['WELCOME_SCREENS', 'OTHERS', 'THANKYOU_SCREENS'];
blocks.sort((a, b) => {
const getCategory = (block) => {
switch (block.name) {
case 'welcome-screen':
return 'WELCOME_SCREENS';
case 'thankyou-screen':
return 'THANKYOU_SCREENS';
default:
return 'OTHERS';
}
};
const ap = priority.indexOf(getCategory(a));
const bp = priority.indexOf(getCategory(b));
return ap - bp;
});
return blocks;
}
// Initial State
const initialState: BlockEditorPureState = {
currentBlockId: undefined,
currentChildBlockId: undefined,
blocks: [],
};
// /**
// * Utility returning an object with an empty object value for each key.
// *
// * @param {string[]} objectKeys Keys to fill.
// * @return {Object} Object filled with empty object as values for each clientId.
// */
// const fillKeysWithEmptyObject = (
// objectKeys: string[]
// ): Record< string, {} > => {
// return objectKeys.reduce( ( result, key ) => {
// result[ key ] = {};
// 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 {FormBlocks} blocks Blocks to flatten.
*
* @param transform
* @return {Array} Flattened object.
*/
export function flattenBlocks(
blocks: FormBlocks,
transform = identity
): FormBlocks {
const result = [];
const stack = [...blocks];
while (stack.length) {
// @ts-expect-error
const { innerBlocks, ...block } = stack.shift();
if (innerBlocks) {
forEach(innerBlocks, ($block, index) => {
innerBlocks[index] = {
...$block,
parentId: block.id,
};
});
stack.push(...innerBlocks);
}
result[block.id] = transform(block);
}
return result;
}
/**
* Reducer returning the form object.
*
* @param { BlockEditorPureState} state Current state.
* @param {BlockEditorActionTypes} action Dispatched action.
*
* @return {BlockEditorPureState} Updated state.
*/
const BlockEditorReducer: Reducer<
BlockEditorPureState,
BlockEditorActionTypes
> = (state = initialState, action): BlockEditorPureState => {
switch (action.type) {
// SET UP STORE
case SETUP_STORE: {
const { initialPayload } = action;
if (initialPayload.length > 0 && !state.currentBlockId) {
return {
blocks: initialPayload,
currentBlockId: initialPayload[0].id,
currentChildBlockId: undefined,
};
}
return {
...state,
blocks: initialPayload,
};
}
case SET_BLOCKS: {
const { blocks } = action;
return {
...state,
blocks,
};
}
case 'REPLACE_BLOCK_NAME': {
const { blockId, name, parentId } = action;
const blocks = [...state.blocks];
let blockIndex;
let parentIndex;
if (typeof parentId !== 'undefined') {
parentIndex = blocks.findIndex(
(item) => item.id === parentId
);
if (parentIndex !== -1 && size(blocks[parentIndex]?.innerBlocks) > 0) {
blockIndex = blocks[parentIndex]?.innerBlocks?.findIndex(
(item) => item.id === blockId
);
}
else {
return state;
}
}
else {
blockIndex = blocks.findIndex(
(item) => item.id === blockId
);
}
// const blockIndex = blocks.findIndex((block) => block.id === blockId);
if (blockIndex === -1 || (parentId && (parentIndex === -1 || !blocks[parentIndex]?.innerBlocks))) {
return state;
}
if (parentId && parentIndex > -1) {
blocks[parentIndex].innerBlocks[blockIndex].name = name;
blocks[parentIndex].innerBlocks[blockIndex].attributes = sanitizeBlockAttributes(name, blocks[parentIndex].innerBlocks[blockIndex].attributes)
}
else {
blocks[blockIndex].name = name;
if (blocks[blockIndex].name === 'group' && size(blocks[blockIndex].innerBlocks) === 0) {
blocks[blockIndex].innerBlocks = [{
name: 'short-text',
id: generateBlockId(),
attributes: {
}
}]
}
blocks[blockIndex].attributes = sanitizeBlockAttributes(name, blocks[blockIndex]?.attributes);
}
return {
...state,
blocks: sortBlocks(blocks),
};
}
// SET BLOCK ATTRIBUTES
case SET_BLOCK_ATTRIBUTES: {
const { blockId, attributes, parentId } = action;
let parentIndex;
// Get block index within its category.
let $blocks = [...state.blocks] as FormBlocks | undefined;
if (!$blocks) {
return state;
}
if (typeof parentId !== 'undefined' && parentId !== blockId) {
parentIndex = $blocks.findIndex((block) => {
return block.id === parentId;
});
$blocks = [...state.blocks][parentIndex]?.innerBlocks;
}
if (!$blocks) {
return state;
}
const blockIndex = $blocks.findIndex((block) => {
return block.id === blockId;
});
// Ignore updates if block isn't known.
if (blockIndex === -1) {
return state;
}
// Consider as updates only changed values
// const nextAttributes = reduce(
// { ...attributes },
// ( result, value, key ) => {
// if ( value !== result[ key ] ) {
// result = getMutateSafeObject(
// state.blocks[ blockIndex ],
// result
// );
// result[ key ] = value;
// }
// return result;
// },
// state.blocks[ blockIndex ].attributes
// );
const nextAttributes = {
...cloneDeep($blocks[blockIndex].attributes),
...cloneDeep(attributes),
};
// // Skip update if nothing has been changed. The reference will
// // match the original block if `reduce` had no changed values.
// if ( nextAttributes === state.blocks[ blockIndex ].attributes ) {
// return state;
// }
$blocks[blockIndex].attributes = nextAttributes;
const blocks = [...state.blocks];
if (typeof parentIndex !== 'undefined') {
blocks[parentIndex].innerBlocks = $blocks;
$blocks = blocks;
}
return {
...state,
blocks: $blocks,
};
}
case REORDER_BLOCKS: {
const {
sourceIndex,
destinationIndex,
parentSourceIndex,
parentDestIndex,
} = action;
// Create deep copy of blocks
const newBlocks = JSON.parse(JSON.stringify(state.blocks));
// Get source block and remove it from original position
let sourceBlock;
if (typeof parentSourceIndex === 'undefined') {
sourceBlock = { ...newBlocks[sourceIndex] };
newBlocks.splice(sourceIndex, 1);
} else {
sourceBlock = { ...newBlocks[parentSourceIndex].innerBlocks[sourceIndex] };
newBlocks[parentSourceIndex].innerBlocks.splice(sourceIndex, 1);
}
// Insert block at new position
if (typeof parentDestIndex === 'undefined') {
// Insert at root level
newBlocks.splice(destinationIndex, 0, sourceBlock);
} else {
// Insert into group
if (!newBlocks[parentDestIndex].innerBlocks) {
newBlocks[parentDestIndex].innerBlocks = [];
}
newBlocks[parentDestIndex].innerBlocks.splice(destinationIndex, 0, sourceBlock);
}
return {
...state,
blocks: newBlocks,
};
}
// INSERT NEW FORM BLOCK
case INSERT_BLOCK: {
const { block, destinationIndex, parent } = action;
const blocks = [...state.blocks];
const index = destinationIndex;
let parentBlock = undefined as FormBlock | undefined;
if (index === undefined || index < 0) {
return state;
}
if (!parent) {
blocks.splice(index, 0, {
...block,
});
} else {
const parentIndex = blocks.findIndex(
($block) => $block.id === parent
);
parentBlock = blocks[parentIndex];
if (!blocks[parentIndex].innerBlocks) {
blocks[parentIndex].innerBlocks = [];
}
blocks[parentIndex]?.innerBlocks?.splice(index, 0, {
...block,
});
}
return {
...state,
blocks: sortBlocks(blocks),
currentBlockId: parentBlock ? parentBlock.id : block.id,
currentChildBlockId: parentBlock ? block.id : undefined,
};
}
// DELETE FORM BLOCK
case DELETE_BLOCK: {
const { blockId, parentId } = action;
const originalBlocks = [...state.blocks];
let parentIndex;
let blocks = originalBlocks;
if (!blocks) {
return state;
}
if (typeof parentId !== 'undefined') {
parentIndex = blocks.findIndex(
(item) => item.id === parentId
);
if (blocks)
blocks = blocks?.[parentIndex]?.innerBlocks ?? [];
}
// Get block index.
const blockIndex = blocks.findIndex(
(item) => item.id === blockId
);
// If block isn't found.
if (blockIndex === -1) {
return state;
}
const nextBlock = blocks[blockIndex + 1];
const prevBlock = blocks[blockIndex - 1];
blocks.splice(blockIndex, 1);
const newCurrentBlockId = nextBlock
? nextBlock.id
: prevBlock
? prevBlock.id
: undefined;
if (typeof parentIndex !== 'undefined') {
const $blocks = originalBlocks;
$blocks[parentIndex].innerBlocks = blocks;
blocks = $blocks;
}
const newState = {
...state,
currentBlockId:
typeof parentIndex === 'undefined'
? newCurrentBlockId
: state.currentBlockId,
currentChildBlockId:
typeof parentIndex === 'undefined'
? undefined
: newCurrentBlockId,
blocks,
};
return newState;
}
// SET CURRENT BLOCK
case SET_CURRENT_BLOCK: {
const { blockId } = action;
const blockIndex = state.blocks.findIndex(
(item) => item.id === blockId
);
// If block isn't found.
if (blockIndex === -1) {
return state;
}
if (blockId === state.currentBlockId)
return state;
return {
...state,
currentBlockId: blockId,
currentChildBlockId: undefined,
};
}
// SET CURRENT CHILD BLOCK
case SET_CURRENT_CHILD_BLOCK: {
const { blockId } = action;
if (blockId === state.currentChildBlockId) {
return state;
}
// const parentblockIndex = state.blocks.findIndex(
// ( item ) => item.id === state.currentBlockId
// );
// // If block isn't found.
// if (
// parentblockIndex === -1 ||
// typeof parentblockIndex === 'undefined' ||
// state.blocks.length === 0 ||
// ! state.blocks[ parentblockIndex ]
// ) {
// return state;
// }
// const childBlockIndex = state.blocks[
// parentblockIndex
// ]?.innerBlocks?.findIndex(
// ( item ) => item.id === state.currentBlockId
// );
// if ( childBlockIndex === -1 ) {
// return state;
// }
return {
...state,
currentChildBlockId: blockId,
};
}
}
return state;
};
const BlockEditorReducerWithHigherOrder = BlockEditorReducer;
export type State = ReturnType<typeof BlockEditorReducerWithHigherOrder>;
export default BlockEditorReducerWithHigherOrder;