-
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
Fix annotations text tracking #11861
Changes from all commits
2070eba
7b261c3
884fe9c
7433c14
6448073
28cd289
492ce7a
33c78b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,13 +2,12 @@ | |
* WordPress dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import { applyFormat, removeFormat } from '@wordpress/rich-text'; | ||
|
||
const name = 'core/annotation'; | ||
const FORMAT_NAME = 'core/annotation'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { applyFormat, removeFormat } from '@wordpress/rich-text'; | ||
const ANNOTATION_ATTRIBUTE_PREFIX = 'annotation-text-'; | ||
const STORE_KEY = 'core/annotations'; | ||
|
||
/** | ||
* Applies given annotations to the given record. | ||
|
@@ -29,11 +28,17 @@ export function applyAnnotations( record, annotations = [] ) { | |
end = record.text.length; | ||
} | ||
|
||
const className = 'annotation-text-' + annotation.source; | ||
const className = ANNOTATION_ATTRIBUTE_PREFIX + annotation.source; | ||
const id = ANNOTATION_ATTRIBUTE_PREFIX + annotation.id; | ||
|
||
record = applyFormat( | ||
record, | ||
{ type: 'core/annotation', attributes: { className } }, | ||
{ | ||
type: FORMAT_NAME, attributes: { | ||
className, | ||
id, | ||
}, | ||
}, | ||
start, | ||
end | ||
); | ||
|
@@ -52,31 +57,104 @@ export function removeAnnotations( record ) { | |
return removeFormat( record, 'core/annotation', 0, record.text.length ); | ||
} | ||
|
||
/** | ||
* Retrieves the positions of annotations inside an array of formats. | ||
* | ||
* @param {Array} formats Formats with annotations in there. | ||
* @return {Object} ID keyed positions of annotations. | ||
*/ | ||
function retrieveAnnotationPositions( formats ) { | ||
const positions = {}; | ||
|
||
formats.forEach( ( characterFormats, i ) => { | ||
characterFormats = characterFormats || []; | ||
characterFormats = characterFormats.filter( ( format ) => format.type === FORMAT_NAME ); | ||
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. Could these two loops be merged? |
||
characterFormats.forEach( ( format ) => { | ||
let { id } = format.attributes; | ||
id = id.replace( ANNOTATION_ATTRIBUTE_PREFIX, '' ); | ||
|
||
if ( ! positions.hasOwnProperty( id ) ) { | ||
positions[ id ] = { | ||
start: i, | ||
}; | ||
} | ||
|
||
// Annotations refer to positions between characters. | ||
// Formats refer to the character themselves. | ||
// So we need to adjust for that here. | ||
positions[ id ].end = i + 1; | ||
} ); | ||
} ); | ||
|
||
return positions; | ||
} | ||
|
||
/** | ||
* Updates annotations in the state based on positions retrieved from RichText. | ||
* | ||
* @param {Array} annotations The annotations that are currently applied. | ||
* @param {Array} positions The current positions of the given annotations. | ||
* @param {Function} removeAnnotation Function to remove an annotation from the state. | ||
* @param {Function} updateAnnotationRange Function to update an annotation range in the state. | ||
*/ | ||
function updateAnnotationsWithPositions( annotations, positions, { removeAnnotation, updateAnnotationRange } ) { | ||
annotations.forEach( ( currentAnnotation ) => { | ||
const position = positions[ currentAnnotation.id ]; | ||
// If we cannot find an annotation, delete it. | ||
if ( ! position ) { | ||
// Apparently the annotation has been removed, so remove it from the state: | ||
// Remove... | ||
removeAnnotation( currentAnnotation.id ); | ||
return; | ||
} | ||
|
||
const { start, end } = currentAnnotation; | ||
if ( start !== position.start || end !== position.end ) { | ||
updateAnnotationRange( currentAnnotation.id, position.start, position.end ); | ||
} | ||
} ); | ||
} | ||
|
||
export const annotation = { | ||
name, | ||
name: FORMAT_NAME, | ||
title: __( 'Annotation' ), | ||
tagName: 'mark', | ||
className: 'annotation-text', | ||
attributes: { | ||
className: 'class', | ||
id: 'id', | ||
}, | ||
edit() { | ||
return null; | ||
}, | ||
__experimentalGetPropsForEditableTreePreparation( select, { richTextIdentifier, blockClientId } ) { | ||
return { | ||
annotations: select( 'core/annotations' ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ), | ||
annotations: select( STORE_KEY ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ), | ||
}; | ||
}, | ||
__experimentalCreatePrepareEditableTree( props ) { | ||
__experimentalCreatePrepareEditableTree( { annotations } ) { | ||
return ( formats, text ) => { | ||
if ( props.annotations.length === 0 ) { | ||
if ( annotations.length === 0 ) { | ||
return formats; | ||
} | ||
|
||
let record = { formats, text }; | ||
record = applyAnnotations( record, props.annotations ); | ||
record = applyAnnotations( record, annotations ); | ||
return record.formats; | ||
}; | ||
}, | ||
__experimentalGetPropsForEditableTreeChangeHandler( dispatch ) { | ||
return { | ||
removeAnnotation: dispatch( STORE_KEY ).__experimentalRemoveAnnotation, | ||
updateAnnotationRange: dispatch( STORE_KEY ).__experimentalUpdateAnnotationRange, | ||
}; | ||
}, | ||
__experimentalCreateOnChangeEditableValue( props ) { | ||
return ( formats ) => { | ||
const positions = retrieveAnnotationPositions( formats ); | ||
const { removeAnnotation, updateAnnotationRange, annotations } = props; | ||
|
||
updateAnnotationsWithPositions( annotations, positions, { removeAnnotation, updateAnnotationRange } ); | ||
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 can 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 felt like these two functions were good to make the code more understandable, they can be rolled into one if that's better. |
||
}; | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
* External dependencies | ||
*/ | ||
import createSelector from 'rememo'; | ||
import { get, flatMap } from 'lodash'; | ||
|
||
/** | ||
* Returns the annotations for a specific client ID. | ||
|
@@ -13,15 +14,19 @@ import createSelector from 'rememo'; | |
*/ | ||
export const __experimentalGetAnnotationsForBlock = createSelector( | ||
( state, blockClientId ) => { | ||
return state.all.filter( ( annotation ) => { | ||
return annotation.selector === 'block' && annotation.blockClientId === blockClientId; | ||
return get( state, blockClientId, [] ).filter( ( annotation ) => { | ||
return annotation.selector === 'block'; | ||
} ); | ||
}, | ||
( state, blockClientId ) => [ | ||
state.byBlockClientId[ blockClientId ], | ||
get( state, blockClientId, [] ), | ||
] | ||
); | ||
|
||
export const __experimentalGetAllAnnotationsForBlock = function( state, blockClientId ) { | ||
return get( state, blockClientId, [] ); | ||
}; | ||
|
||
/** | ||
* Returns the annotations that apply to the given RichText instance. | ||
* | ||
|
@@ -36,9 +41,8 @@ export const __experimentalGetAnnotationsForBlock = createSelector( | |
*/ | ||
export const __experimentalGetAnnotationsForRichText = createSelector( | ||
( state, blockClientId, richTextIdentifier ) => { | ||
return state.all.filter( ( annotation ) => { | ||
return get( state, blockClientId, [] ).filter( ( annotation ) => { | ||
return annotation.selector === 'range' && | ||
annotation.blockClientId === blockClientId && | ||
richTextIdentifier === annotation.richTextIdentifier; | ||
} ).map( ( annotation ) => { | ||
const { range, ...other } = annotation; | ||
|
@@ -50,7 +54,7 @@ export const __experimentalGetAnnotationsForRichText = createSelector( | |
} ); | ||
}, | ||
( state, blockClientId ) => [ | ||
state.byBlockClientId[ blockClientId ], | ||
get( state, blockClientId, [] ), | ||
] | ||
); | ||
|
||
|
@@ -61,5 +65,7 @@ export const __experimentalGetAnnotationsForRichText = createSelector( | |
* @return {Array} All annotations currently applied. | ||
*/ | ||
export function __experimentalGetAnnotations( state ) { | ||
return state.all; | ||
return flatMap( state, ( annotations ) => { | ||
return annotations; | ||
} ); | ||
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 equivalent to |
||
} |
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.
This should always be an array, so the casting seems unnecessary?
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.
It can be
undefined
right, for when no formatting is present on that character?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.
It's a sparse array, there's no indices set as undefined, they're just not there.
forEach
will only loop over any indices that are set, which is only arrays.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.
Try: