-
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
Links: do not reposition container on selection change #14921
Changes from all commits
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 |
---|---|---|
|
@@ -9,7 +9,6 @@ import { | |
omit, | ||
pickBy, | ||
} from 'lodash'; | ||
import memize from 'memize'; | ||
|
||
/** | ||
* WordPress dependencies | ||
|
@@ -40,7 +39,6 @@ import { | |
__UNSTABLE_LINE_SEPARATOR as LINE_SEPARATOR, | ||
__unstableIndentListItems as indentListItems, | ||
__unstableGetActiveFormats as getActiveFormats, | ||
__unstableUpdateFormats as updateFormats, | ||
} from '@wordpress/rich-text'; | ||
import { decodeEntities } from '@wordpress/html-entities'; | ||
import { withFilters, IsolatedEventContainer } from '@wordpress/components'; | ||
|
@@ -147,12 +145,8 @@ export class RichText extends Component { | |
this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this ); | ||
this.onPointerDown = this.onPointerDown.bind( this ); | ||
|
||
this.formatToValue = memize( | ||
this.formatToValue.bind( this ), | ||
{ maxSize: 1 } | ||
); | ||
|
||
this.savedContent = value; | ||
this.rawValue = this.formatToValue( value ); | ||
this.patterns = getPatterns( { | ||
onReplace, | ||
valueToFormat: this.valueToFormat, | ||
|
@@ -199,7 +193,7 @@ export class RichText extends Component { | |
* @return {Object} The current record (value and selection). | ||
*/ | ||
getRecord() { | ||
const { formats, replacements, text } = this.formatToValue( this.props.value ); | ||
const { formats, replacements, text } = this.rawValue; | ||
const { start, end, activeFormats } = this.state; | ||
|
||
return { formats, replacements, text, start, end, activeFormats }; | ||
|
@@ -231,7 +225,7 @@ export class RichText extends Component { | |
} | ||
|
||
isEmpty() { | ||
return isEmpty( this.formatToValue( this.props.value ) ); | ||
return isEmpty( this.rawValue ); | ||
} | ||
|
||
/** | ||
|
@@ -380,6 +374,7 @@ export class RichText extends Component { | |
} | ||
|
||
this.recalculateBoundaryStyle(); | ||
this.onSelectionChange(); | ||
|
||
document.addEventListener( 'selectionchange', this.onSelectionChange ); | ||
} | ||
|
@@ -419,16 +414,33 @@ export class RichText extends Component { | |
} | ||
} | ||
|
||
const record = this.getRecord(); | ||
const { activeFormats = [], start, end } = record; | ||
const formats = createPrepareEditableTree( this.props )( record ); | ||
const value = this.createRecord(); | ||
const { activeFormats = [], start } = this.state; | ||
|
||
let newFormats; | ||
|
||
// Update the formats between the last and new caret position. | ||
const change = updateFormats( { | ||
value, | ||
start, | ||
end: value.start, | ||
formats: activeFormats, | ||
} ); | ||
if ( value.start <= start ) { // Deletion | ||
const beforeFormats = formats.slice( 0, value.start ); | ||
const afterFormats = formats.slice( end ); | ||
|
||
newFormats = beforeFormats.concat( afterFormats ); | ||
} else { // Insertion | ||
const length = value.start - start; | ||
const formatsToInsert = Array.from( Array( length ), () => activeFormats ); | ||
const beforeFormats = formats.slice( 0, start ); | ||
const afterFormats = formats.slice( end ); | ||
|
||
newFormats = beforeFormats.concat( formatsToInsert, afterFormats ); | ||
} | ||
|
||
const change = { | ||
...value, | ||
formats: newFormats, | ||
activeFormats, | ||
}; | ||
|
||
this.onChange( change, { withoutHistory: true } ); | ||
|
||
|
@@ -459,12 +471,18 @@ export class RichText extends Component { | |
* Handles the `selectionchange` event: sync the selection to local state. | ||
*/ | ||
onSelectionChange() { | ||
const value = this.createRecord(); | ||
const { start, end } = value; | ||
const { start, end } = this.createRecord(); | ||
|
||
if ( start !== this.state.start || end !== this.state.end ) { | ||
const { isCaretWithinFormattedText } = this.props; | ||
const activeFormats = getActiveFormats( value ); | ||
const value = this.getRecord(); | ||
const newValue = { | ||
...value, | ||
start, | ||
end, | ||
}; | ||
delete newValue.activeFormats; | ||
const activeFormats = getActiveFormats( newValue ); | ||
|
||
if ( ! isCaretWithinFormattedText && activeFormats.length ) { | ||
this.props.onEnterFormattedText(); | ||
|
@@ -473,7 +491,7 @@ export class RichText extends Component { | |
} | ||
|
||
this.setState( { start, end, activeFormats } ); | ||
this.applyRecord( { ...value, activeFormats }, { domOnly: true } ); | ||
this.applyRecord( { ...newValue, activeFormats }, { domOnly: true } ); | ||
|
||
if ( activeFormats.length > 0 ) { | ||
this.recalculateBoundaryStyle(); | ||
|
@@ -517,6 +535,7 @@ export class RichText extends Component { | |
changeHandler( record.formats, record.text ); | ||
} ); | ||
|
||
this.rawValue = record; | ||
this.savedContent = this.valueToFormat( record ); | ||
this.props.onChange( this.savedContent ); | ||
this.setState( { start, end, activeFormats } ); | ||
|
@@ -733,8 +752,8 @@ export class RichText extends Component { | |
* @param {SyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
handleHorizontalNavigation( event ) { | ||
const value = this.createRecord(); | ||
const { formats, text, start, end } = value; | ||
const value = this.getRecord(); | ||
const { text, start, end } = value; | ||
const { activeFormats = [] } = this.state; | ||
const collapsed = isCollapsed( value ); | ||
const isReverse = event.keyCode === LEFT; | ||
|
@@ -763,6 +782,7 @@ export class RichText extends Component { | |
// In all other cases, prevent default behaviour. | ||
event.preventDefault(); | ||
|
||
const formats = createPrepareEditableTree( this.props )( 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 don't understand why this change is here. 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'll comment, it's to make sure the boundaries work for annotations. |
||
const formatsBefore = formats[ start - 1 ] || []; | ||
const formatsAfter = formats[ start ] || []; | ||
|
||
|
@@ -915,6 +935,7 @@ export class RichText extends Component { | |
} | ||
|
||
this.applyRecord( record ); | ||
this.rawValue = record; | ||
this.savedContent = value; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ import { | |
applyFormat, | ||
getTextContent, | ||
slice, | ||
getActiveFormat, | ||
} from '@wordpress/rich-text'; | ||
import { URLInput, URLPopover } from '@wordpress/block-editor'; | ||
|
||
|
@@ -108,23 +109,36 @@ class InlineLinkUI extends Component { | |
this.state = { | ||
opensInNewWindow: false, | ||
inputValue: '', | ||
key: 0, | ||
}; | ||
} | ||
|
||
static getDerivedStateFromProps( props, state ) { | ||
const { activeAttributes: { url, target } } = props; | ||
const { activeAttributes: { url, target }, value } = props; | ||
const opensInNewWindow = target === '_blank'; | ||
const activeFormat = getActiveFormat( value, 'core/link' ); | ||
|
||
const newState = {}; | ||
|
||
if ( ! isShowingInput( props, state ) ) { | ||
if ( url !== state.inputValue ) { | ||
return { inputValue: url }; | ||
newState.inputValue = url; | ||
} | ||
|
||
if ( opensInNewWindow !== state.opensInNewWindow ) { | ||
return { opensInNewWindow }; | ||
newState.opensInNewWindow = opensInNewWindow; | ||
} | ||
} | ||
|
||
if ( activeFormat && activeFormat !== state.activeFormat ) { | ||
newState.activeFormat = activeFormat; | ||
newState.key = state.key + 1; | ||
} | ||
|
||
if ( Object.keys( newState ).length ) { | ||
return newState; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
|
@@ -211,7 +225,7 @@ class InlineLinkUI extends Component { | |
} | ||
|
||
render() { | ||
const { isActive, activeAttributes: { url }, addingLink, value } = this.props; | ||
const { isActive, activeAttributes: { url }, addingLink } = this.props; | ||
|
||
if ( ! isActive && ! addingLink ) { | ||
return null; | ||
|
@@ -222,7 +236,12 @@ class InlineLinkUI extends Component { | |
|
||
return ( | ||
<PositionedAtSelection | ||
key={ `${ value.start }${ value.end }` /* Used to force rerender on selection change */ } | ||
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. Possible dumb question: Couldn't a minimal solution just be to remove 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 if you move the caret from one link to another? 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.
That's true. I wonder then if it could either act more like the I'm not totally sure the ramifications of either of these alternatives. Just trying to consider in terms of the least potential for impact. |
||
// When adding a new link, set the container at the caret, | ||
// otherwise set it at the link element. | ||
selector={ addingLink ? null : 'a' } | ||
// Since the key cannot be the format object, we have to keep it | ||
// in the state and bump it when the object reference changes. | ||
key={ this.state.key } | ||
> | ||
<URLPopover | ||
onClickOutside={ this.onClickOutside } | ||
|
This file was deleted.
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.
If we're working with a shallow clone, seems we might as well assign
activeFormats
directly, rather than create another shallow clone below?