@gechiui/block-editor
Version:
261 lines (228 loc) • 10.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useBlockSync;
var _lodash = require("lodash");
var _element = require("@gechiui/element");
var _data = require("@gechiui/data");
var _blocks = require("@gechiui/blocks");
var _store = require("../../store");
/**
* External dependencies
*/
/**
* GeChiUI dependencies
*/
/**
* Internal dependencies
*/
/**
* A function to call when the block value has been updated in the block-editor
* store.
*
* @callback onBlockUpdate
* @param {Object[]} blocks The updated blocks.
* @param {Object} options The updated block options, such as selectionStart
* and selectionEnd.
*/
/**
* useBlockSync is a side effect which handles bidirectional sync between the
* block-editor store and a controlling data source which provides blocks. This
* is most commonly used by the BlockEditorProvider to synchronize the contents
* of the block-editor store with the root entity, like a post.
*
* Another example would be the template part block, which provides blocks from
* a separate entity data source than a root entity. This hook syncs edits to
* the template part in the block editor back to the entity and vice-versa.
*
* Here are some of its basic functions:
* - Initalizes the block-editor store for the given clientID to the blocks
* given via props.
* - Adds incoming changes (like undo) to the block-editor store.
* - Adds outgoing changes (like editing content) to the controlling entity,
* determining if a change should be considered persistent or not.
* - Handles edge cases and race conditions which occur in those operations.
* - Ignores changes which happen to other entities (like nested inner block
* controllers.
* - Passes selection state from the block-editor store to the controlling entity.
*
* @param {Object} props Props for the block sync hook
* @param {string} props.clientId The client ID of the inner block controller.
* If none is passed, then it is assumed to be a
* root controller rather than an inner block
* controller.
* @param {Object[]} props.value The control value for the blocks. This value
* is used to initalize the block-editor store
* and for resetting the blocks to incoming
* changes like undo.
* @param {Object} props.selection The selection state responsible to restore the selection on undo/redo.
* @param {onBlockUpdate} props.onChange Function to call when a persistent
* change has been made in the block-editor blocks
* for the given clientId. For example, after
* this function is called, an entity is marked
* dirty because it has changes to save.
* @param {onBlockUpdate} props.onInput Function to call when a non-persistent
* change has been made in the block-editor blocks
* for the given clientId. When this is called,
* controlling sources do not become dirty.
*/
function useBlockSync(_ref) {
let {
clientId = null,
value: controlledBlocks,
selection: controlledSelection,
onChange = _lodash.noop,
onInput = _lodash.noop
} = _ref;
const registry = (0, _data.useRegistry)();
const {
resetBlocks,
resetSelection,
replaceInnerBlocks,
setHasControlledInnerBlocks,
__unstableMarkNextChangeAsNotPersistent
} = registry.dispatch(_store.store);
const {
getBlockName,
getBlocks
} = registry.select(_store.store);
const isControlled = (0, _data.useSelect)(select => {
return !clientId || select(_store.store).areInnerBlocksControlled(clientId);
}, [clientId]);
const pendingChanges = (0, _element.useRef)({
incoming: null,
outgoing: []
});
const subscribed = (0, _element.useRef)(false);
const setControlledBlocks = () => {
if (!controlledBlocks) {
return;
} // We don't need to persist this change because we only replace
// controlled inner blocks when the change was caused by an entity,
// and so it would already be persisted.
__unstableMarkNextChangeAsNotPersistent();
if (clientId) {
// It is important to batch here because otherwise,
// as soon as `setHasControlledInnerBlocks` is called
// the effect to restore might be triggered
// before the actual blocks get set properly in state.
registry.batch(() => {
setHasControlledInnerBlocks(clientId, true);
const storeBlocks = controlledBlocks.map(block => (0, _blocks.cloneBlock)(block));
if (subscribed.current) {
pendingChanges.current.incoming = storeBlocks;
}
__unstableMarkNextChangeAsNotPersistent();
replaceInnerBlocks(clientId, storeBlocks);
});
} else {
if (subscribed.current) {
pendingChanges.current.incoming = controlledBlocks;
}
resetBlocks(controlledBlocks);
}
}; // Add a subscription to the block-editor registry to detect when changes
// have been made. This lets us inform the data source of changes. This
// is an effect so that the subscriber can run synchronously without
// waiting for React renders for changes.
const onInputRef = (0, _element.useRef)(onInput);
const onChangeRef = (0, _element.useRef)(onChange);
(0, _element.useEffect)(() => {
onInputRef.current = onInput;
onChangeRef.current = onChange;
}, [onInput, onChange]); // Determine if blocks need to be reset when they change.
(0, _element.useEffect)(() => {
if (pendingChanges.current.outgoing.includes(controlledBlocks)) {
// Skip block reset if the value matches expected outbound sync
// triggered by this component by a preceding change detection.
// Only skip if the value matches expectation, since a reset should
// still occur if the value is modified (not equal by reference),
// to allow that the consumer may apply modifications to reflect
// back on the editor.
if ((0, _lodash.last)(pendingChanges.current.outgoing) === controlledBlocks) {
pendingChanges.current.outgoing = [];
}
} else if (getBlocks(clientId) !== controlledBlocks) {
// Reset changing value in all other cases than the sync described
// above. Since this can be reached in an update following an out-
// bound sync, unset the outbound value to avoid considering it in
// subsequent renders.
pendingChanges.current.outgoing = [];
setControlledBlocks();
if (controlledSelection) {
resetSelection(controlledSelection.selectionStart, controlledSelection.selectionEnd, controlledSelection.initialPosition);
}
}
}, [controlledBlocks, clientId]);
(0, _element.useEffect)(() => {
// When the block becomes uncontrolled, it means its inner state has been reset
// we need to take the blocks again from the external value property.
if (!isControlled) {
pendingChanges.current.outgoing = [];
setControlledBlocks();
}
}, [isControlled]);
(0, _element.useEffect)(() => {
const {
getSelectionStart,
getSelectionEnd,
getSelectedBlocksInitialCaretPosition,
isLastBlockChangePersistent,
__unstableIsLastBlockChangeIgnored,
areInnerBlocksControlled
} = registry.select(_store.store);
let blocks = getBlocks(clientId);
let isPersistent = isLastBlockChangePersistent();
let previousAreBlocksDifferent = false;
subscribed.current = true;
const unsubscribe = registry.subscribe(() => {
// Sometimes, when changing block lists, lingering subscriptions
// might trigger before they are cleaned up. If the block for which
// the subscription runs is no longer in the store, this would clear
// its parent entity's block list. To avoid this, we bail out if
// the subscription is triggering for a block (`clientId !== null`)
// and its block name can't be found because it's not on the list.
// (`getBlockName( clientId ) === null`).
if (clientId !== null && getBlockName(clientId) === null) return; // When RESET_BLOCKS on parent blocks get called, the controlled blocks
// can reset to uncontrolled, in these situations, it means we need to populate
// the blocks again from the external blocks (the value property here)
// and we should stop triggering onChange
const isStillControlled = !clientId || areInnerBlocksControlled(clientId);
if (!isStillControlled) {
return;
}
const newIsPersistent = isLastBlockChangePersistent();
const newBlocks = getBlocks(clientId);
const areBlocksDifferent = newBlocks !== blocks;
blocks = newBlocks;
if (areBlocksDifferent && (pendingChanges.current.incoming || __unstableIsLastBlockChangeIgnored())) {
pendingChanges.current.incoming = null;
isPersistent = newIsPersistent;
return;
} // Since we often dispatch an action to mark the previous action as
// persistent, we need to make sure that the blocks changed on the
// previous action before committing the change.
const didPersistenceChange = previousAreBlocksDifferent && !areBlocksDifferent && newIsPersistent && !isPersistent;
if (areBlocksDifferent || didPersistenceChange) {
isPersistent = newIsPersistent; // We know that onChange/onInput will update controlledBlocks.
// We need to be aware that it was caused by an outgoing change
// so that we do not treat it as an incoming change later on,
// which would cause a block reset.
pendingChanges.current.outgoing.push(blocks); // Inform the controlling entity that changes have been made to
// the block-editor store they should be aware about.
const updateParent = isPersistent ? onChangeRef.current : onInputRef.current;
updateParent(blocks, {
selection: {
selectionStart: getSelectionStart(),
selectionEnd: getSelectionEnd(),
initialPosition: getSelectedBlocksInitialCaretPosition()
}
});
}
previousAreBlocksDifferent = areBlocksDifferent;
});
return () => unsubscribe();
}, [registry, clientId]);
}
//# sourceMappingURL=use-block-sync.js.map