-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Rich text: combine all ref effects #60936
Merged
+1,369
−1,431
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
92 changes: 92 additions & 0 deletions
92
packages/block-editor/src/components/rich-text/event-listeners/before-input-rules.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { insert, isCollapsed } from '@wordpress/rich-text'; | ||
import { applyFilters } from '@wordpress/hooks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { store as blockEditorStore } from '../../../store'; | ||
|
||
/** | ||
* When typing over a selection, the selection will we wrapped by a matching | ||
* character pair. The second character is optional, it defaults to the first | ||
* character. | ||
* | ||
* @type {string[]} Array of character pairs. | ||
*/ | ||
const wrapSelectionSettings = [ '`', '"', "'", '“”', '‘’' ]; | ||
|
||
export default ( props ) => ( element ) => { | ||
function onInput( event ) { | ||
const { inputType, data } = event; | ||
const { value, onChange, registry } = props.current; | ||
|
||
// Only run the rules when inserting text. | ||
if ( inputType !== 'insertText' ) { | ||
return; | ||
} | ||
|
||
if ( isCollapsed( value ) ) { | ||
return; | ||
} | ||
|
||
const pair = applyFilters( | ||
'blockEditor.wrapSelectionSettings', | ||
wrapSelectionSettings | ||
).find( | ||
( [ startChar, endChar ] ) => startChar === data || endChar === data | ||
); | ||
|
||
if ( ! pair ) { | ||
return; | ||
} | ||
|
||
const [ startChar, endChar = startChar ] = pair; | ||
const start = value.start; | ||
const end = value.end + startChar.length; | ||
|
||
let newValue = insert( value, startChar, start, start ); | ||
newValue = insert( newValue, endChar, end, end ); | ||
|
||
const { | ||
__unstableMarkLastChangeAsPersistent, | ||
__unstableMarkAutomaticChange, | ||
} = registry.dispatch( blockEditorStore ); | ||
|
||
__unstableMarkLastChangeAsPersistent(); | ||
onChange( newValue ); | ||
__unstableMarkAutomaticChange(); | ||
|
||
const init = {}; | ||
|
||
for ( const key in event ) { | ||
init[ key ] = event[ key ]; | ||
} | ||
|
||
init.data = endChar; | ||
|
||
const { ownerDocument } = element; | ||
const { defaultView } = ownerDocument; | ||
const newEvent = new defaultView.InputEvent( 'input', init ); | ||
|
||
// Dispatch an `input` event with the new data. This will trigger the | ||
// input rules. | ||
// Postpone the `input` to the next event loop tick so that the dispatch | ||
// doesn't happen synchronously in the middle of `beforeinput` dispatch. | ||
// This is closer to how native `input` event would be timed, and also | ||
// makes sure that the `input` event is dispatched only after the `onChange` | ||
// call few lines above has fully updated the data store state and rerendered | ||
// all affected components. | ||
window.queueMicrotask( () => { | ||
event.target.dispatchEvent( newEvent ); | ||
} ); | ||
event.preventDefault(); | ||
} | ||
|
||
element.addEventListener( 'beforeinput', onInput ); | ||
return () => { | ||
element.removeEventListener( 'beforeinput', onInput ); | ||
}; | ||
}; |
53 changes: 53 additions & 0 deletions
53
packages/block-editor/src/components/rich-text/event-listeners/delete.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { DELETE, BACKSPACE } from '@wordpress/keycodes'; | ||
import { isCollapsed, isEmpty } from '@wordpress/rich-text'; | ||
|
||
export default ( props ) => ( element ) => { | ||
function onKeyDown( event ) { | ||
const { keyCode } = event; | ||
|
||
if ( event.defaultPrevented ) { | ||
return; | ||
} | ||
|
||
const { value, onMerge, onRemove } = props.current; | ||
|
||
if ( keyCode === DELETE || keyCode === BACKSPACE ) { | ||
const { start, end, text } = value; | ||
const isReverse = keyCode === BACKSPACE; | ||
const hasActiveFormats = | ||
value.activeFormats && !! value.activeFormats.length; | ||
|
||
// Only process delete if the key press occurs at an uncollapsed edge. | ||
if ( | ||
! isCollapsed( value ) || | ||
hasActiveFormats || | ||
( isReverse && start !== 0 ) || | ||
( ! isReverse && end !== text.length ) | ||
) { | ||
return; | ||
} | ||
|
||
if ( onMerge ) { | ||
onMerge( ! isReverse ); | ||
} | ||
|
||
// Only handle remove on Backspace. This serves dual-purpose of being | ||
// an intentional user interaction distinguishing between Backspace and | ||
// Delete to remove the empty field, but also to avoid merge & remove | ||
// causing destruction of two fields (merge, then removed merged). | ||
else if ( onRemove && isEmpty( value ) && isReverse ) { | ||
onRemove( ! isReverse ); | ||
} | ||
|
||
event.preventDefault(); | ||
} | ||
} | ||
|
||
element.addEventListener( 'keydown', onKeyDown ); | ||
return () => { | ||
element.removeEventListener( 'keydown', onKeyDown ); | ||
}; | ||
}; |
104 changes: 104 additions & 0 deletions
104
packages/block-editor/src/components/rich-text/event-listeners/enter.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { ENTER } from '@wordpress/keycodes'; | ||
import { insert, remove } from '@wordpress/rich-text'; | ||
import { getBlockTransforms, findTransform } from '@wordpress/blocks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { store as blockEditorStore } from '../../../store'; | ||
import { splitValue } from '../split-value'; | ||
|
||
export default ( props ) => ( element ) => { | ||
function onKeyDown( event ) { | ||
if ( event.target.contentEditable !== 'true' ) { | ||
return; | ||
} | ||
|
||
if ( event.defaultPrevented ) { | ||
return; | ||
} | ||
|
||
if ( event.keyCode !== ENTER ) { | ||
return; | ||
} | ||
|
||
const { | ||
removeEditorOnlyFormats, | ||
value, | ||
onReplace, | ||
onSplit, | ||
onChange, | ||
disableLineBreaks, | ||
onSplitAtEnd, | ||
onSplitAtDoubleLineEnd, | ||
registry, | ||
} = props.current; | ||
|
||
event.preventDefault(); | ||
|
||
const _value = { ...value }; | ||
_value.formats = removeEditorOnlyFormats( value ); | ||
const canSplit = onReplace && onSplit; | ||
|
||
if ( onReplace ) { | ||
const transforms = getBlockTransforms( 'from' ).filter( | ||
( { type } ) => type === 'enter' | ||
); | ||
const transformation = findTransform( transforms, ( item ) => { | ||
return item.regExp.test( _value.text ); | ||
} ); | ||
|
||
if ( transformation ) { | ||
onReplace( [ | ||
transformation.transform( { | ||
content: _value.text, | ||
} ), | ||
] ); | ||
registry | ||
.dispatch( blockEditorStore ) | ||
.__unstableMarkAutomaticChange(); | ||
return; | ||
} | ||
} | ||
|
||
const { text, start, end } = _value; | ||
|
||
if ( event.shiftKey ) { | ||
if ( ! disableLineBreaks ) { | ||
onChange( insert( _value, '\n' ) ); | ||
} | ||
} else if ( canSplit ) { | ||
splitValue( { | ||
value: _value, | ||
onReplace, | ||
onSplit, | ||
} ); | ||
} else if ( onSplitAtEnd && start === end && end === text.length ) { | ||
onSplitAtEnd(); | ||
} else if ( | ||
// For some blocks it's desirable to split at the end of the | ||
// block when there are two line breaks at the end of the | ||
// block, so triple Enter exits the block. | ||
onSplitAtDoubleLineEnd && | ||
start === end && | ||
end === text.length && | ||
text.slice( -2 ) === '\n\n' | ||
) { | ||
registry.batch( () => { | ||
_value.start = _value.end - 2; | ||
onChange( remove( _value ) ); | ||
onSplitAtDoubleLineEnd(); | ||
} ); | ||
} else if ( ! disableLineBreaks ) { | ||
onChange( insert( _value, '\n' ) ); | ||
} | ||
} | ||
|
||
element.addEventListener( 'keydown', onKeyDown ); | ||
return () => { | ||
element.removeEventListener( 'keydown', onKeyDown ); | ||
}; | ||
}; |
31 changes: 31 additions & 0 deletions
31
packages/block-editor/src/components/rich-text/event-listeners/firefox-compat.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { store as blockEditorStore } from '../../../store'; | ||
|
||
export default ( props ) => ( element ) => { | ||
function onFocus() { | ||
const { registry } = props.current; | ||
if ( ! registry.select( blockEditorStore ).isMultiSelecting() ) { | ||
return; | ||
} | ||
|
||
// This is a little hack to work around focus issues with nested | ||
// editable elements in Firefox. For some reason the editable child | ||
// element sometimes regains focus, while it should not be focusable | ||
// and focus should remain on the editable parent element. | ||
// To do: try to find the cause of the shifting focus. | ||
const parentEditable = element.parentElement.closest( | ||
'[contenteditable="true"]' | ||
); | ||
|
||
if ( parentEditable ) { | ||
parentEditable.focus(); | ||
} | ||
} | ||
|
||
element.addEventListener( 'focus', onFocus ); | ||
return () => { | ||
element.removeEventListener( 'focus', onFocus ); | ||
}; | ||
}; |
56 changes: 56 additions & 0 deletions
56
packages/block-editor/src/components/rich-text/event-listeners/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useMemo, useRef } from '@wordpress/element'; | ||
import { useRefEffect } from '@wordpress/compose'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import beforeInputRules from './before-input-rules'; | ||
import inputRules from './input-rules'; | ||
import insertReplacementText from './insert-replacement-text'; | ||
import removeBrowserShortcuts from './remove-browser-shortcuts'; | ||
import shortcuts from './shortcuts'; | ||
import inputEvents from './input-events'; | ||
import undoAutomaticChange from './undo-automatic-change'; | ||
import pasteHandler from './paste-handler'; | ||
import _delete from './delete'; | ||
import enter from './enter'; | ||
import firefoxCompat from './firefox-compat'; | ||
|
||
const allEventListeners = [ | ||
beforeInputRules, | ||
inputRules, | ||
insertReplacementText, | ||
removeBrowserShortcuts, | ||
shortcuts, | ||
inputEvents, | ||
undoAutomaticChange, | ||
pasteHandler, | ||
_delete, | ||
enter, | ||
firefoxCompat, | ||
]; | ||
|
||
export function useEventListeners( props ) { | ||
const propsRef = useRef( props ); | ||
propsRef.current = props; | ||
const refEffects = useMemo( | ||
() => allEventListeners.map( ( refEffect ) => refEffect( propsRef ) ), | ||
[ propsRef ] | ||
); | ||
|
||
return useRefEffect( | ||
( element ) => { | ||
if ( ! props.isSelected ) { | ||
return; | ||
} | ||
const cleanups = refEffects.map( ( effect ) => effect( element ) ); | ||
return () => { | ||
cleanups.forEach( ( cleanup ) => cleanup() ); | ||
}; | ||
}, | ||
[ refEffects, props.isSelected ] | ||
); | ||
} |
13 changes: 13 additions & 0 deletions
13
packages/block-editor/src/components/rich-text/event-listeners/input-events.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export default ( props ) => ( element ) => { | ||
const { inputEvents } = props.current; | ||
function onInput( event ) { | ||
for ( const keyboardShortcut of inputEvents.current ) { | ||
keyboardShortcut( event ); | ||
} | ||
} | ||
|
||
element.addEventListener( 'input', onInput ); | ||
return () => { | ||
element.removeEventListener( 'input', onInput ); | ||
}; | ||
}; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Additionally this PR avoids attaching all these listeners for non selected blocks.