-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Editor: Implement meta as custom source #16402
Changes from all commits
e33523c
81bdcb1
6f58b3e
df30e18
6bfb9b0
2a6ef11
9b99c78
db51f77
e1786a1
c2e0c1f
ba1a8cd
f4f7403
19618fd
b5b0990
9100f39
de45deb
d318a82
eab1ee5
6472638
7263e11
d643af0
d9c95e0
54c1f48
2247600
814e04a
c237ccb
3e097ef
c12bee4
ac2acaf
075ffab
6f66257
8516fd1
37a7ba9
468f908
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
BlockEditorProvider | ||
=================== | ||
|
||
BlockEditorProvider is a component which establishes a new block editing context, and serves as the entry point for a new block editor. It is implemented as a [controlled input](https://reactjs.org/docs/forms.html#controlled-components), expected to receive a value of a blocks array, calling `onChange` and/or `onInput` when the user interacts to change blocks in the editor. It is intended to be used as a wrapper component, where its children comprise the user interface through which a user modifies the blocks value, notably via other components made available from this `block-editor` module. | ||
|
||
## Props | ||
|
||
### `value` | ||
|
||
* **Type:** `Array` | ||
* **Required** `no` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The component will try to use all of these props (except for "children") and throw errors. We should mark them as required. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I was actually thinking we should make them optional. At least in the case of I agree that the documentation here is not accurate per the current implementation. I didn't want to document an undesirable requirement, however. Maybe we should seek to make it optional as a separate task to this pull request. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense 👍 |
||
|
||
The current array of blocks. | ||
|
||
### `onChange` | ||
|
||
* **Type:** `Function` | ||
* **Required** `no` | ||
|
||
A callback invoked when the blocks have been modified in a persistent manner. Contrasted with `onInput`, a "persistent" change is one which is not an extension of a composed input. Any update to a distinct block or block attribute is treated as persistent. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a bit hard to grasp. Maybe an example would help. |
||
|
||
The distinction between these two callbacks is akin to the [differences between `input` and `change` events](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event) in the DOM API: | ||
|
||
>The input event is fired every time the value of the element changes. **This is unlike the change event, which only fires when the value is committed**, such as by pressing the enter key, selecting a value from a list of options, and the like. | ||
|
||
In the context of an editor, an example usage of this distinction is for managing a history of blocks values (an "Undo"/"Redo" mechanism). While value updates should always be reflected immediately (`onInput`), you may only want history entries to reflect change milestones (`onChange`). | ||
|
||
### `onInput` | ||
|
||
* **Type:** `Function` | ||
* **Required** `no` | ||
|
||
A callback invoked when the blocks have been modified in a non-persistent manner. Contrasted with `onChange`, a "non-persistent" change is one which is part of a composed input. Any sequence of updates to the same block attribute are treated as non-persistent, except for the first. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why is this desired? If I edit a color twice, the second edit wouldn't persist? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think the choice of "persistent" as the term here can be potentially misleading. The intent was for it to be akin to the distinction between
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event In the context of the editor, the use-case is for Undo levels. You don't want Cmd+Z to undo paragraph changes one character at a time, but rather as units (in our case, reflected as sequences of updates to the same block attribute). When I was first thinking about this documentation, I had in mind to explicitly mention how it's used for Undo/Redo in the editor, but neglected to write it when I'd returned to my computer 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, that's much clearer now. I remember running into that code, but forgot about it when reading this. |
||
|
||
### `children` | ||
|
||
* **Type:** `WPElement` | ||
* **Required** `no` | ||
|
||
Children elements for which the BlockEditorProvider context should apply. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,10 +34,21 @@ class BlockEditorProvider extends Component { | |
this.attachChangeObserver( registry ); | ||
} | ||
|
||
if ( this.isSyncingOutcomingValue ) { | ||
this.isSyncingOutcomingValue = false; | ||
if ( this.isSyncingOutcomingValue !== null && this.isSyncingOutcomingValue === value ) { | ||
// 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. | ||
this.isSyncingOutcomingValue = null; | ||
} else if ( value !== prevProps.value ) { | ||
this.isSyncingIncomingValue = true; | ||
// 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. | ||
this.isSyncingOutcomingValue = null; | ||
this.isSyncingIncomingValue = value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know these changes are really tricky and fragile. The good news is that we have e2e tests for most of the use-cases and you can endup fixing one but breaking others :P |
||
resetBlocks( value ); | ||
} | ||
} | ||
|
@@ -79,15 +90,17 @@ class BlockEditorProvider extends Component { | |
onChange, | ||
onInput, | ||
} = this.props; | ||
|
||
const newBlocks = getBlocks(); | ||
const newIsPersistent = isLastBlockChangePersistent(); | ||
|
||
if ( | ||
newBlocks !== blocks && ( | ||
this.isSyncingIncomingValue || | ||
__unstableIsLastBlockChangeIgnored() | ||
) | ||
) { | ||
this.isSyncingIncomingValue = false; | ||
this.isSyncingIncomingValue = null; | ||
blocks = newBlocks; | ||
isPersistent = newIsPersistent; | ||
return; | ||
|
@@ -101,7 +114,7 @@ class BlockEditorProvider extends Component { | |
// When knowing the blocks value is changing, assign instance | ||
// value to skip reset in subsequent `componentDidUpdate`. | ||
if ( newBlocks !== blocks ) { | ||
this.isSyncingOutcomingValue = true; | ||
this.isSyncingOutcomingValue = newBlocks; | ||
} | ||
|
||
blocks = newBlocks; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -195,29 +195,6 @@ export function isUpdatingSameBlockAttribute( action, lastAction ) { | |
); | ||
} | ||
|
||
/** | ||
* Higher-order reducer intended to reset the cache key of all blocks | ||
* whenever the post meta values change. | ||
* | ||
* @param {Function} reducer Original reducer function. | ||
* | ||
* @return {Function} Enhanced reducer function. | ||
*/ | ||
const withPostMetaUpdateCacheReset = ( reducer ) => ( state, action ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
||
const newState = reducer( state, action ); | ||
const previousMetaValues = get( state, [ 'settings', '__experimentalMetaSource', 'value' ] ); | ||
const nextMetaValues = get( newState.settings.__experimentalMetaSource, [ 'value' ] ); | ||
// If post meta values change, reset the cache key for all blocks | ||
if ( previousMetaValues !== nextMetaValues ) { | ||
newState.blocks = { | ||
...newState.blocks, | ||
cache: mapValues( newState.blocks.cache, () => ( {} ) ), | ||
}; | ||
} | ||
|
||
return newState; | ||
}; | ||
|
||
/** | ||
* Utility returning an object with an empty object value for each key. | ||
* | ||
|
@@ -1228,17 +1205,44 @@ export const blockListSettings = ( state = {}, action ) => { | |
return state; | ||
}; | ||
|
||
export default withPostMetaUpdateCacheReset( | ||
combineReducers( { | ||
blocks, | ||
isTyping, | ||
isCaretWithinFormattedText, | ||
blockSelection, | ||
blocksMode, | ||
blockListSettings, | ||
insertionPoint, | ||
template, | ||
settings, | ||
preferences, | ||
} ) | ||
); | ||
/** | ||
* 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.clientId ]: action.attributes }; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens for other actions that change multiple blocks, like RESET_BLOCKS... (Thinking about a third-party plugin triggering that action and not the block editor provider), should we try to list the changes or do we just trigger a full sync? What happens now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It wouldn't try to sync meta attributes from the updates, which is the same as how it behaves today in master. I think it might be a bit difficult to infer from the reset whether those meta values should be considered canonical, or whether the source should be applied again by the editor. The latter is what I expect happens in the current state of this branch. |
||
|
||
return null; | ||
} | ||
|
||
export default combineReducers( { | ||
blocks, | ||
isTyping, | ||
isCaretWithinFormattedText, | ||
blockSelection, | ||
blocksMode, | ||
blockListSettings, | ||
insertionPoint, | ||
template, | ||
settings, | ||
preferences, | ||
lastBlockAttributesChange, | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❤️ I love this README :)