From 0f4ab42318c2e043f112e83a769a86d61fe1a84d Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Tue, 11 Sep 2018 16:44:09 +0200 Subject: [PATCH 01/20] Implement annotations Integrate Annotation API with Format API Keep annotations when applying formatting Change parameters for toTree after rebasing on master Fix unit test Add files after `npm run docs:build` Only re-render one block when adding annotation Do this by optimizing the annotations state to keep track of which annotations belongs to which block. This information can then be used in the selector to return the same reference if the annotations for a block haven't changed. Add unit tests for annotations reducer Also remove MOVE_ANNOTATION action. Rename block to blockClientId Remove block ID from RichText documentation It is not relevant at that level. Change block annotation to use selector property Build docs Fix low hanging fruit Add annotation support for block attributes Rename blockAttribute to richTextIdentifier Simplify annotations to only `start` and `end` Revert changes to to-tree Add annotation support to lists and headings Build docs Move the applyAnnotations call into formatToValue Refactor applyAnnotation to only loop once Add basic e2e test for annotation API Disallow invalid annotation ranges in state Drop general block annotation className Add assertions for annotations behavior Add selector to retrieve currently selected text Move removeAnnotations to valueToFormat Move applyAnnotations and removeAnnotations They don't use `this`, so it is only fair. Fix failing tests Build docs Move annotations state to @wordpress/annotations --- docs/data/README.md | 1 + docs/data/data-core-annotations.md | 84 +++++++++ docs/data/data-core-editor.md | 26 +++ docs/extensibility/annotations.md | 62 +++++++ docs/manifest.json | 12 ++ docs/tool/config.js | 5 + lib/client-assets.php | 8 + package-lock.json | 8 + package.json | 1 + packages/annotations/.npmrc | 1 + packages/annotations/CHANGELOG.md | 5 + packages/annotations/README.md | 15 ++ packages/annotations/package.json | 30 ++++ packages/annotations/src/index.js | 4 + packages/annotations/src/store/actions.js | 69 ++++++++ packages/annotations/src/store/index.js | 24 +++ packages/annotations/src/store/reducer.js | 106 ++++++++++++ packages/annotations/src/store/selectors.js | 62 +++++++ .../annotations/src/store/test/reducer.js | 163 ++++++++++++++++++ packages/block-library/src/heading/edit.js | 1 + packages/block-library/src/list/index.js | 1 + packages/block-library/src/paragraph/edit.js | 1 + packages/block-library/src/quote/index.js | 2 + .../editor/src/components/block-list/block.js | 11 +- .../editor/src/components/rich-text/README.md | 19 ++ .../src/components/rich-text/annotations.js | 45 +++++ .../editor/src/components/rich-text/index.js | 58 +++++-- packages/editor/src/store/actions.js | 20 +++ packages/editor/src/store/reducer.js | 11 ++ packages/editor/src/store/selectors.js | 15 ++ packages/editor/src/store/test/reducer.js | 5 + .../format-library/src/annotation/index.js | 19 ++ packages/format-library/src/index.js | 2 + .../__snapshots__/plugins-api.test.js.snap | 2 +- test/e2e/specs/plugins-api.test.js | 40 +++++ test/e2e/test-plugins/plugins-api/sidebar.js | 20 +++ webpack.config.js | 1 + 37 files changed, 938 insertions(+), 21 deletions(-) create mode 100644 docs/data/data-core-annotations.md create mode 100644 docs/extensibility/annotations.md create mode 100644 packages/annotations/.npmrc create mode 100644 packages/annotations/CHANGELOG.md create mode 100644 packages/annotations/README.md create mode 100644 packages/annotations/package.json create mode 100644 packages/annotations/src/index.js create mode 100644 packages/annotations/src/store/actions.js create mode 100644 packages/annotations/src/store/index.js create mode 100644 packages/annotations/src/store/reducer.js create mode 100644 packages/annotations/src/store/selectors.js create mode 100644 packages/annotations/src/store/test/reducer.js create mode 100644 packages/editor/src/components/rich-text/annotations.js create mode 100644 packages/format-library/src/annotation/index.js diff --git a/docs/data/README.md b/docs/data/README.md index 1f99bcb017f5b9..ac44230651976e 100644 --- a/docs/data/README.md +++ b/docs/data/README.md @@ -1,6 +1,7 @@ # Data Module Reference - [**core**: WordPress Core Data](../../docs/data/data-core.md) + - [**core/annotations**: Annotations](../../docs/data/data-core-annotations.md) - [**core/blocks**: Block Types Data](../../docs/data/data-core-blocks.md) - [**core/editor**: The Editor’s Data](../../docs/data/data-core-editor.md) - [**core/edit-post**: The Editor’s UI Data](../../docs/data/data-core-edit-post.md) diff --git a/docs/data/data-core-annotations.md b/docs/data/data-core-annotations.md new file mode 100644 index 00000000000000..7c4efc606318e6 --- /dev/null +++ b/docs/data/data-core-annotations.md @@ -0,0 +1,84 @@ +# **core/annotations**: Annotations + +## Selectors + +### getAnnotationsForBlock + +Returns the annotations for a specific client ID. + +*Parameters* + + * state: Editor state. + * clientId: The ID of the block to get the annotations for. + +### getAnnotationsForRichText + +Returns the annotations that apply to the given RichText instance. + +Both a blockClientId and a richTextIdentifier are required. This is because +a block might have multiple `RichText` components. This does mean that every +block needs to implement annotations itself. + +*Parameters* + + * state: Editor state. + * blockClientId: The client ID for the block. + * richTextIdentifier: Unique identifier that identifies the given RichText. + +*Returns* + +All the annotations relevant for the `RichText`. + +### getAnnotations + +Returns all annotations in the editor state. + +*Parameters* + + * state: Editor state. + +*Returns* + +All annotations currently applied. + +## Actions + +### addAnnotation + +Adds an annotation to a block. + +The `block` attribute refers to a block ID that needs to be annotated. +`isBlockAnnotation` controls whether or not the annotation is a block +annotation. The `source` is the source of the annotation, this will be used +to identity groups of annotations. + +The `range` property is only relevant if the selector is 'range'. + +*Parameters* + + * annotation: The annotation to add. + * blockClientId: The blockClientId to add the annotation to. + * richTextIdentifier: Identifier for the RichText instance the annotation applies to. + * range: The range at which to apply this annotation. + * range.start: The offset where the annotation should start. + * range.end: The offset where the annotation should end. + * string: [selector="range"] The way to apply this annotation. + * string: [source="default"] The source that added the annotation. + * string: [id=uuid()] The ID the annotation should have. + Generates a UUID by default. + +### removeAnnotation + +Removes an annotation with a specific ID. + +*Parameters* + + * annotationId: The annotation to remove. + +### removeAnnotationsBySource + +Removes all annotations of a specific source. + +*Parameters* + + * source: The source to remove. \ No newline at end of file diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 1517f5b5317cc0..582b4a5e3bf844 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1393,6 +1393,22 @@ or skipped when the user clicks the "publish" button. Whether the pre-publish panel should be shown or not. +### getCurrentRichTextSelection + +Returns the properties for the currently selected text inside the active +RichText. + +Can be used together with the addAnnotation action to annotate the selected +text. + +*Parameters* + + * state: Editor state. + +*Returns* + +RichText information about the currently selected text. + ## Actions ### setupEditor @@ -1804,6 +1820,16 @@ Returns an action object used to signal that post saving is unlocked. * lockName: The lock name. +### setRichTextSelection + +Sets the rich text selection to a certain value. + +*Parameters* + + * blockClientId: The block that text is selected in. + * identifier: The RichText identifier that text is selected in. + * range: The start and end that text is selected in. + ### createNotice ### fetchReusableBlocks \ No newline at end of file diff --git a/docs/extensibility/annotations.md b/docs/extensibility/annotations.md new file mode 100644 index 00000000000000..d62ef15451a526 --- /dev/null +++ b/docs/extensibility/annotations.md @@ -0,0 +1,62 @@ +# Annotations + +Annotations are a way to highlight a specific piece in a Gutenberg post. Examples of this include commenting on a piece of text and spellchecking. Both can use the annotations API to mark a piece of text. + +## API + +To see the API for yourself the easiest way is to have a block that is at least 200 characters long without formatting and putting the following in the console: + +```js +wp.data.dispatch( 'core/annotations' ).addAnnotation( { + source: "my-annotations-plugin", + blockClientId: wp.data.select( 'core/editor' ).getBlockOrder()[0], + richTextIdentifier: "content", + range: { + start: 50, + end: 100, + }, +} ); +``` + +The start and the end of the range should be calculated based only on the text of the relevant `RichText`. For example, in the following HTML position 0 will refer to the position before the capital S: + +```html +Strong text +``` + +To help with determining the correct positions, the `wp.richText.create` method can be used. This will split a piece of HTML into text and formats. + +All available properties can be found in the API documentation of the `addAnnotation` action. + +## Block annotation + +It is also possible to annotate a block completely. In that case just provide the `selector` property and set it to `block`. The default `selector` is `range`, which can be used for text annotation. + +```js +wp.data.dispatch( 'core/annotations' ).addAnnotation( { + source: "my-annotations-plugin", + blockClientId: wp.data.select( 'core/editor' ).getBlockOrder()[0], + selector: "block", +} ); +``` + +This doesn't provide any styling out of the box, so you have to provide some CSS to make sure your annotation is shown: + +```css +.is-annotated-by-my-annotations-plugin { + outline: 1px solid black; +} +``` + +## Text annotation + +The text annotation is controlled by the `start` and `end` properties. Simple `start` and `end` properties don't work for HTML, so these properties are assumed to be offsets within the `rich-text` internal structure. For simplicity you can think about this as if all HTML would be stripped out and then you calculate the `start` and the `end` of the annotation. + +If you simply want to annotate the currently selected text you can use the following code: + +```js +wp.data.dispatch( 'core/annotations' ).addAnnotation( { + source: "my-annotations-plugin", + ...wp.data.select( 'core/editor' ).getCurrentRichTextSelection(), +} ); +``` diff --git a/docs/manifest.json b/docs/manifest.json index efab957c872c6c..522cee09fd1738 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -257,6 +257,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/a11y/README.md", "parent": "packages" }, + { + "title": "@wordpress/annotations", + "slug": "packages-annotations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/annotations/README.md", + "parent": "packages" + }, { "title": "@wordpress/api-fetch", "slug": "packages-api-fetch", @@ -929,6 +935,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core.md", "parent": "data" }, + { + "title": "Annotations", + "slug": "data-core-annotations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/data/data-core-annotations.md", + "parent": "data" + }, { "title": "Block Types Data", "slug": "data-core-blocks", diff --git a/docs/tool/config.js b/docs/tool/config.js index b7b0fa3ef4ad66..6758ac592a72f7 100644 --- a/docs/tool/config.js +++ b/docs/tool/config.js @@ -15,6 +15,11 @@ module.exports = { selectors: [ path.resolve( root, 'packages/core-data/src/selectors.js' ) ], actions: [ path.resolve( root, 'packages/core-data/src/actions.js' ) ], }, + 'core/annotations': { + title: 'Annotations', + selectors: [ path.resolve( root, 'packages/annotations/src/store/selectors.js' ) ], + actions: [ path.resolve( root, 'packages/annotations/src/store/actions.js' ) ], + }, 'core/blocks': { title: 'Block Types Data', selectors: [ path.resolve( root, 'packages/blocks/src/store/selectors.js' ) ], diff --git a/lib/client-assets.php b/lib/client-assets.php index 9bb0180c929638..fc1d89d074d5d0 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -316,6 +316,13 @@ function gutenberg_register_scripts_and_styles() { ) ) ); + gutenberg_override_script( + 'wp-annotations', + gutenberg_url( 'build/annotations/index.js' ), + array( 'wp-polyfill', 'wp-data' ), + filemtime( gutenberg_dir_path() . 'build/annotations/index.js' ), + true + ); gutenberg_override_script( 'wp-core-data', gutenberg_url( 'build/core-data/index.js' ), @@ -670,6 +677,7 @@ function gutenberg_register_scripts_and_styles() { 'lodash', 'tinymce-latest-lists', 'wp-a11y', + 'wp-annotations', 'wp-api-fetch', 'wp-blob', 'wp-blocks', diff --git a/package-lock.json b/package-lock.json index 4353dd993bee93..812b0930a4fc4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2002,6 +2002,14 @@ "@wordpress/dom-ready": "file:packages/dom-ready" } }, + "@wordpress/annotations": { + "version": "file:packages/annotations", + "requires": { + "@babel/runtime": "^7.0.0", + "@wordpress/data": "file:packages/data", + "rememo": "^3.0.0" + } + }, "@wordpress/api-fetch": { "version": "file:packages/api-fetch", "requires": { diff --git a/package.json b/package.json index 0f8afe18111e67..b61319f6a4a2cb 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@wordpress/a11y": "file:packages/a11y", + "@wordpress/annotations": "file:packages/annotations", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/autop": "file:packages/autop", "@wordpress/blob": "file:packages/blob", diff --git a/packages/annotations/.npmrc b/packages/annotations/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/annotations/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md new file mode 100644 index 00000000000000..95745945dd46e2 --- /dev/null +++ b/packages/annotations/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0 (unreleased) + +### New Features + +- Implement annotations API in the editor. diff --git a/packages/annotations/README.md b/packages/annotations/README.md new file mode 100644 index 00000000000000..5a6ef603633c42 --- /dev/null +++ b/packages/annotations/README.md @@ -0,0 +1,15 @@ +# Annotations + + + +## Installation + +Install the module + +```bash +npm install @wordpress/annotations --save +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ + +## Usage diff --git a/packages/annotations/package.json b/packages/annotations/package.json new file mode 100644 index 00000000000000..e57906f3820148 --- /dev/null +++ b/packages/annotations/package.json @@ -0,0 +1,30 @@ +{ + "name": "@wordpress/annotations", + "version": "1.0.0-beta1", + "description": "Annotate content in the Gutenberg editor.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "annotations" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/annotations/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "dependencies": { + "@babel/runtime": "^7.0.0", + "@wordpress/data": "file:../data", + "rememo": "^3.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/annotations/src/index.js b/packages/annotations/src/index.js new file mode 100644 index 00000000000000..a7ff64df3ef2a3 --- /dev/null +++ b/packages/annotations/src/index.js @@ -0,0 +1,4 @@ +/** + * Internal dependencies + */ +import './store'; diff --git a/packages/annotations/src/store/actions.js b/packages/annotations/src/store/actions.js new file mode 100644 index 00000000000000..8d83f06bbb1bbb --- /dev/null +++ b/packages/annotations/src/store/actions.js @@ -0,0 +1,69 @@ +import uuid from 'uuid/v4'; + +/** + * Adds an annotation to a block. + * + * The `block` attribute refers to a block ID that needs to be annotated. + * `isBlockAnnotation` controls whether or not the annotation is a block + * annotation. The `source` is the source of the annotation, this will be used + * to identity groups of annotations. + * + * The `range` property is only relevant if the selector is 'range'. + * + * @param {Object} annotation The annotation to add. + * @param {string} blockClientId The blockClientId to add the annotation to. + * @param {string} richTextIdentifier Identifier for the RichText instance the annotation applies to. + * @param {Object} range The range at which to apply this annotation. + * @param {number} range.start The offset where the annotation should start. + * @param {number} range.end The offset where the annotation should end. + * @param {string} [selector="range"] The way to apply this annotation. + * @param {string} [source="default"] The source that added the annotation. + * @param {string} [id=uuid()] The ID the annotation should have. + * Generates a UUID by default. + * + * @return {Object} Action object. + */ +export function addAnnotation( { blockClientId, richTextIdentifier = null, range = null, selector = 'range', source = 'default', id = uuid() } ) { + const action = { + type: 'ANNOTATION_ADD', + id, + blockClientId, + richTextIdentifier, + source, + selector, + }; + + if ( selector === 'range' ) { + action.range = range; + } + + return action; +} + +/** + * Removes an annotation with a specific ID. + * + * @param {string} annotationId The annotation to remove. + * + * @return {Object} Action object. + */ +export function removeAnnotation( annotationId ) { + return { + type: 'ANNOTATION_REMOVE', + annotationId, + }; +} + +/** + * Removes all annotations of a specific source. + * + * @param {string} source The source to remove. + * + * @return {Object} Action object. + */ +export function removeAnnotationsBySource( source ) { + return { + type: 'ANNOTATION_REMOVE_SOURCE', + source, + }; +} diff --git a/packages/annotations/src/store/index.js b/packages/annotations/src/store/index.js new file mode 100644 index 00000000000000..917a342ad9f49d --- /dev/null +++ b/packages/annotations/src/store/index.js @@ -0,0 +1,24 @@ +/** + * WordPress Dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import reducer from './reducer'; +import * as selectors from './selectors'; +import * as actions from './actions'; + +/** + * Module Constants + */ +const MODULE_KEY = 'core/annotations'; + +const store = registerStore( MODULE_KEY, { + reducer, + selectors, + actions, +} ); + +export default store; diff --git a/packages/annotations/src/store/reducer.js b/packages/annotations/src/store/reducer.js new file mode 100644 index 00000000000000..624cebcd2ca290 --- /dev/null +++ b/packages/annotations/src/store/reducer.js @@ -0,0 +1,106 @@ +import { isNumber, mapValues } from 'lodash'; + +/** + * Filters an array based on the predicate, but keeps the reference the same if + * the array hasn't changed. + * + * @param {Array} collection The collection to filter. + * @param {Function} predicate Function that determines if the item should stay + * in the array. + * @return {Array} Filtered array. + */ +function filterWithReference( collection, predicate ) { + const filteredCollection = collection.filter( predicate ); + + return collection.length === filteredCollection.length ? collection : filteredCollection; +} + +/** + * Verifies whether the given annotations is a valid annotation. + * + * @param {Object} annotation The annotation to verify. + * @return {boolean} Whether the given annotation is valid. + */ +function isValidAnnotationRange( annotation ) { + return isNumber( annotation.start ) && + isNumber( annotation.end ) && + annotation.start <= annotation.end; +} + +/** + * Reducer managing annotations. + * + * @param {Array} state The annotations currently shown in the editor. + * @param {Object} action Dispatched action. + * + * @return {Array} Updated state. + */ +export function annotations( state = { all: [], byBlockClientId: {} }, action ) { + switch ( action.type ) { + case 'ANNOTATION_ADD': + const blockClientId = action.blockClientId; + const newAnnotation = { + id: action.id, + blockClientId, + richTextIdentifier: action.richTextIdentifier, + source: action.source, + selector: action.selector, + range: action.range, + }; + + if ( newAnnotation.selector === 'range' && ! isValidAnnotationRange( newAnnotation.range ) ) { + return state; + } + + const previousAnnotationsForBlock = state.byBlockClientId[ blockClientId ] || []; + + return { + all: [ + ...state.all, + newAnnotation, + ], + byBlockClientId: { + ...state.byBlockClientId, + [ blockClientId ]: [ ...previousAnnotationsForBlock, action.id ], + }, + }; + + case 'ANNOTATION_REMOVE': + return { + all: state.all.filter( ( annotation ) => annotation.id !== action.annotationId ), + + // We use filterWithReference to not refresh the reference if a block still has + // the same annotations. + byBlockClientId: mapValues( state.byBlockClientId, ( annotationForBlock ) => { + return filterWithReference( annotationForBlock, ( annotationId ) => { + return annotationId !== action.annotationId; + } ); + } ), + }; + + case 'ANNOTATION_REMOVE_SOURCE': + const idsToRemove = []; + + const allAnnotations = state.all.filter( ( annotation ) => { + if ( annotation.source === action.source ) { + idsToRemove.push( annotation.id ); + return false; + } + + return true; + } ); + + return { + all: allAnnotations, + byBlockClientId: mapValues( state.byBlockClientId, ( annotationForBlock ) => { + return filterWithReference( annotationForBlock, ( annotationId ) => { + return ! idsToRemove.includes( annotationId ); + } ); + } ), + }; + } + + return state; +} + +export default annotations; diff --git a/packages/annotations/src/store/selectors.js b/packages/annotations/src/store/selectors.js new file mode 100644 index 00000000000000..a0089976e66740 --- /dev/null +++ b/packages/annotations/src/store/selectors.js @@ -0,0 +1,62 @@ +import createSelector from 'rememo'; + +/** + * Returns the annotations for a specific client ID. + * + * @param {Object} state Editor state. + * @param {string} clientId The ID of the block to get the annotations for. + * + * @return {Array} The annotations applicable to this block. + */ +export const getAnnotationsForBlock = createSelector( + ( state, blockClientId ) => { + return state.all.filter( ( annotation ) => { + return annotation.selector === 'block' && annotation.blockClientId === blockClientId; + } ); + }, + ( state, blockClientId ) => [ + state.byBlockClientId[ blockClientId ], + ] +); + +/** + * Returns the annotations that apply to the given RichText instance. + * + * Both a blockClientId and a richTextIdentifier are required. This is because + * a block might have multiple `RichText` components. This does mean that every + * block needs to implement annotations itself. + * + * @param {Object} state Editor state. + * @param {string} blockClientId The client ID for the block. + * @param {string} richTextIdentifier Unique identifier that identifies the given RichText. + * @return {Array} All the annotations relevant for the `RichText`. + */ +export const getAnnotationsForRichText = createSelector( + ( state, blockClientId, richTextIdentifier ) => { + return state.all.filter( ( annotation ) => { + return annotation.selector === 'range' && + annotation.blockClientId === blockClientId && + richTextIdentifier === annotation.richTextIdentifier; + } ).map( ( annotation ) => { + const { range, ...other } = annotation; + + return { + ...range, + ...other, + }; + } ); + }, + ( state, blockClientId ) => [ + state.byBlockClientId[ blockClientId ], + ] +); + +/** + * Returns all annotations in the editor state. + * + * @param {Object} state Editor state. + * @return {Array} All annotations currently applied. + */ +export function getAnnotations( state ) { + return state.all; +} diff --git a/packages/annotations/src/store/test/reducer.js b/packages/annotations/src/store/test/reducer.js new file mode 100644 index 00000000000000..ec88a062842199 --- /dev/null +++ b/packages/annotations/src/store/test/reducer.js @@ -0,0 +1,163 @@ +import { annotations } from '../reducer'; + +describe( 'annotations', () => { + const initialState = { all: [], byBlockClientId: {} }; + + it( 'returns all annotations and annotation IDs per block', () => { + const state = annotations( undefined, {} ); + + expect( state ).toEqual( { all: [], byBlockClientId: {} } ); + } ); + + it( 'returns a state with an annotation that has been added', () => { + const state = annotations( undefined, { + type: 'ANNOTATION_ADD', + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + } ); + + expect( state ).toEqual( { + all: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + }, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + }, + } ); + } ); + + it( 'allows an annotation to be removed', () => { + const state = annotations( { + all: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + }, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + }, + }, { + type: 'ANNOTATION_REMOVE', + annotationId: 'annotationId', + } ); + + expect( state ).toEqual( { all: [], byBlockClientId: { blockClientId: [] } } ); + } ); + + it( 'allows an annotation to be removed by its source', () => { + const annotation1 = { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'block', + }; + const annotation2 = { + id: 'annotationId2', + blockClientId: 'blockClientId2', + richTextIdentifier: 'identifier2', + source: 'other-source', + selector: 'block', + }; + const state = annotations( { + all: [ + annotation1, + annotation2, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + blockClientId2: [ 'annotationId2' ], + }, + }, { + type: 'ANNOTATION_REMOVE_SOURCE', + source: 'default', + } ); + + expect( state ).toEqual( { + all: [ annotation2 ], + byBlockClientId: { + blockClientId: [], + blockClientId2: [ 'annotationId2' ], + }, + } ); + } ); + + it( 'allows a range selector', () => { + const state = annotations( undefined, { + type: 'ANNOTATION_ADD', + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'range', + range: { + start: 0, + end: 100, + }, + } ); + + expect( state ).toEqual( { + all: [ + { + id: 'annotationId', + blockClientId: 'blockClientId', + richTextIdentifier: 'identifier', + source: 'default', + selector: 'range', + range: { + start: 0, + end: 100, + }, + }, + ], + byBlockClientId: { + blockClientId: [ 'annotationId' ], + }, + } ); + } ); + + it( 'rejects invalid annotations', () => { + let state = annotations( undefined, { + type: 'ANNOTATION_ADD', + source: 'default', + selector: 'range', + range: { + start: 5, + end: 4, + }, + } ); + state = annotations( state, { + type: 'ANNOTATION_ADD', + source: 'default', + selector: 'range', + range: { + start: 'not a number', + end: 100, + }, + } ); + state = annotations( state, { + type: 'ANNOTATION_ADD', + source: 'default', + selector: 'range', + range: { + start: 100, + end: 'not a number', + }, + } ); + + expect( state ).toEqual( initialState ); + } ); +} ); diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index 711f0c100e4317..6f067493315349 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -44,6 +44,7 @@ export default function HeadingEdit( { setAttributes( { content: value } ) } diff --git a/packages/block-library/src/list/index.js b/packages/block-library/src/list/index.js index 9fe52078e89937..8091b138328078 100644 --- a/packages/block-library/src/list/index.js +++ b/packages/block-library/src/list/index.js @@ -335,6 +335,7 @@ export const settings = { { ( ! RichText.isEmpty( citation ) || isSelected ) && ( ) } diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 8b656f0616944e..61207b822ec88b 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -383,6 +383,7 @@ export class BlockListBlock extends Component { isPreviousBlockADefaultEmptyBlock, isParentOfSelectedBlock, isDraggable, + annotations, } = this.props; const isHovered = this.state.isHovered && ! isMultiSelecting; const { name: blockName, isValid } = block; @@ -412,6 +413,10 @@ export class BlockListBlock extends Component { const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirstMultiSelected ) || ! isPartOfMultiSelection; const canShowInBetweenInserter = ! isEmptyDefaultBlock && ! isPreviousBlockADefaultEmptyBlock; + const annotationsClassNames = annotations.map( ( annotation ) => { + return 'is-annotated-by-' + annotation.source; + } ); + // The wp-block className is important for editor styles. // Generate the wrapper class names handling the different states of the block. const wrapperClassName = classnames( 'wp-block editor-block-list__block', { @@ -424,7 +429,7 @@ export class BlockListBlock extends Component { 'is-typing': isTypingWithinBlock, 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ), 'is-focus-mode': isFocusMode, - } ); + }, annotationsClassNames ); const { onReplace } = this.props; @@ -601,6 +606,9 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV hasSelectedInnerBlock, getTemplateLock, } = select( 'core/editor' ); + const { + getAnnotationsForBlock, + } = select( 'core/annotations' ); const isSelected = isBlockSelected( clientId ); const { hasFixedToolbar, focusMode } = getEditorSettings(); const block = getBlock( clientId ); @@ -633,6 +641,7 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV block, isSelected, isParentOfSelectedBlock, + annotations: getAnnotationsForBlock( clientId ), }; } ); diff --git a/packages/editor/src/components/rich-text/README.md b/packages/editor/src/components/rich-text/README.md index b5449fbe4ec9e0..c46a611893a400 100644 --- a/packages/editor/src/components/rich-text/README.md +++ b/packages/editor/src/components/rich-text/README.md @@ -57,6 +57,25 @@ Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs *Optional.* A list of autocompleters to use instead of the default. +### `annotations: Array` + +*Optional.* A list of annotations that should be applied to the content. + +An annotation has the following shape: + +```js +{ + // The source of the annotation, will be used for the HTML class. + "source": "annotations-tester", + + // The starting offset within the rich-text structure. + "start": 106, + + // The ending offset within the rich-text structure. + "end": 75 +} +``` + ## RichText.Content `RichText.Content` should be used in the `save` function of your block to correctly save rich text content. diff --git a/packages/editor/src/components/rich-text/annotations.js b/packages/editor/src/components/rich-text/annotations.js new file mode 100644 index 00000000000000..e0a15c5e842ce4 --- /dev/null +++ b/packages/editor/src/components/rich-text/annotations.js @@ -0,0 +1,45 @@ +/* WordPress dependencies */ +import { applyFormat, removeFormat } from '@wordpress/rich-text'; + +/** + * Applies given annotations to the given record. + * + * @param {Object} record The record to apply annotations to. + * @param {Array} annotations The annotation to apply. + * @return {Object} A record with the annotations applied. + */ + +export function applyAnnotations( record, annotations = [] ) { + annotations.forEach( ( annotation ) => { + let { start, end } = annotation; + + if ( start > record.text.length ) { + start = record.text.length; + } + + if ( end > record.text.length ) { + end = record.text.length; + } + + const className = 'annotation-text-' + annotation.source; + + record = applyFormat( + record, + { type: 'core/annotation', attributes: { className } }, + start, + end + ); + } ); + + return record; +} + +/** + * Removes annotations from the given record. + * + * @param {Object} record Record to remove annotations from. + * @return {Object} The cleaned record. + */ +export function removeAnnotations( record ) { + return removeFormat( record, 'core/annotation', 0, record.text.length ); +} diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index a59fc64bf28b6c..ede9bb5d372498 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -57,6 +57,7 @@ import TinyMCE, { TINYMCE_ZWSP } from './tinymce'; import { pickAriaProps } from './aria'; import { getPatterns } from './patterns'; import { withBlockEditContext } from '../block-edit/context'; +import { applyAnnotations, removeAnnotations } from './annotations'; /** * Browser dependencies @@ -221,7 +222,7 @@ export class RichText extends Component { * @return {Object} The current record (value and selection). */ getRecord() { - const { formats, text } = this.formatToValue( this.props.value ); + const { formats, text } = this.formatToValue( this.props.value, this.props.annotations ); const { start, end } = this.state; return { formats, text, start, end }; @@ -259,7 +260,7 @@ export class RichText extends Component { } isEmpty() { - return isEmpty( this.formatToValue( this.props.value ) ); + return isEmpty( this.formatToValue( this.props.value, this.props.annotations ) ); } /** @@ -439,6 +440,7 @@ export class RichText extends Component { } this.setState( { start, end } ); + this.props.onSelectionChange( this.props.clientId, this.props.identifier, { start, end } ); } } @@ -739,12 +741,15 @@ export class RichText extends Component { } componentDidUpdate( prevProps ) { - const { tagName, value, isSelected } = this.props; + const { tagName, value, isSelected, annotations } = this.props; + + const shouldApplyAnnotations = annotations !== prevProps.annotations; if ( - tagName === prevProps.tagName && + ( tagName === prevProps.tagName && value !== prevProps.value && - value !== this.savedContent + value !== this.savedContent ) || + shouldApplyAnnotations ) { // Handle deprecated `children` and `node` sources. // The old way of passing a value with the `node` matcher required @@ -756,10 +761,10 @@ export class RichText extends Component { return; } - const record = this.formatToValue( value ); + const record = this.formatToValue( value, annotations ); if ( isSelected ) { - const prevRecord = this.formatToValue( prevProps.value ); + const prevRecord = this.formatToValue( prevProps.value, prevProps.annotations ); const length = getTextContent( prevRecord ).length; record.start = length; record.end = length; @@ -773,8 +778,8 @@ export class RichText extends Component { // an empty paragraph into another, then also set the selection to the // end. if ( isSelected && ! prevProps.isSelected && ! this.isActive() ) { - const record = this.formatToValue( value ); - const prevRecord = this.formatToValue( prevProps.value ); + const record = this.formatToValue( value, annotations ); + const prevRecord = this.formatToValue( prevProps.value, prevProps.annotations ); const length = getTextContent( prevRecord ).length; record.start = length; record.end = length; @@ -798,10 +803,12 @@ export class RichText extends Component { } } - formatToValue( value ) { + formatToValue( value, annotations = [] ) { + let record; + // Handle deprecated `children` and `node` sources. if ( Array.isArray( value ) ) { - return create( { + record = create( { html: children.toHTML( value ), multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, @@ -809,7 +816,7 @@ export class RichText extends Component { } if ( this.props.format === 'string' ) { - return create( { + record = create( { html: value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, @@ -819,10 +826,10 @@ export class RichText extends Component { // Guard for blocks passing `null` in onSplit callbacks. May be removed // if onSplit is revised to not pass a `null` value. if ( value === null ) { - return create(); + record = create(); } - return value; + return applyAnnotations( record, annotations ); } valueToEditableHTML( value ) { @@ -840,10 +847,12 @@ export class RichText extends Component { } valueToFormat( { formats, text } ) { + const value = removeAnnotations( { formats, text } ); + // Handle deprecated `children` and `node` sources. if ( this.usedDeprecatedChildrenSource ) { return children.fromDOM( unstableToDom( { - value: { formats, text }, + value: value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, } ).body.childNodes ); @@ -851,13 +860,13 @@ export class RichText extends Component { if ( this.props.format === 'string' ) { return toHTMLString( { - value: { formats, text }, + value: value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, } ); } - return { formats, text }; + return value; } render() { @@ -978,15 +987,24 @@ const RichTextContainer = compose( [ clientId: context.clientId, }; } ), - withSelect( ( select ) => { + withSelect( ( select, props ) => { const { isViewportMatch } = select( 'core/viewport' ); const { canUserUseUnfilteredHTML, isCaretWithinFormattedText } = select( 'core/editor' ); + const { getAnnotationsForRichText } = select( 'core/annotations' ); - return { + const selectProps = { isViewportSmall: isViewportMatch( '< small' ), canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), isCaretWithinFormattedText: isCaretWithinFormattedText(), }; + + // Allow explicit annotations to be passed in. + // When an identifier is passed in we can retrieve the annotations for this RichText. + if ( ! props.annotations && props.identifier ) { + selectProps.annotations = getAnnotationsForRichText( props.clientId, props.identifier ); + } + + return selectProps; } ), withDispatch( ( dispatch ) => { const { @@ -995,6 +1013,7 @@ const RichTextContainer = compose( [ undo, enterFormattedText, exitFormattedText, + setRichTextSelection, } = dispatch( 'core/editor' ); return { @@ -1003,6 +1022,7 @@ const RichTextContainer = compose( [ onUndo: undo, onEnterFormattedText: enterFormattedText, onExitFormattedText: exitFormattedText, + onSelectionChange: setRichTextSelection, }; } ), withSafeTimeout, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index a77b27add51c28..2687e2f844dde5 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -788,6 +788,26 @@ export function unlockPostSaving( lockName ) { }; } +/** + * Sets the rich text selection to a certain value. + * + * @param {string} blockClientId The block that text is selected in. + * @param {string} identifier The RichText identifier that text is selected in. + * @param {Object} range The start and end that text is selected in. + * + * @return {Object} Action object. + */ +export function setRichTextSelection( blockClientId, identifier, range ) { + return { + type: 'SET_RICH_TEXT_SELECTION', + selection: { + blockClientId, + richTextIdentifier: identifier, + range, + }, + }; +} + // // Deprecated // diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 6bfb468cd71578..4a4fb8bebdb4ba 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -653,6 +653,11 @@ export function blockSelection( state = { isMultiSelecting: false, isEnabled: true, initialPosition: null, + richText: { + blockClientId: null, + identifier: null, + range: { start: null, end: null }, + }, }, action ) { switch ( action.type ) { case 'CLEAR_SELECTED_BLOCK': @@ -748,6 +753,12 @@ export function blockSelection( state = { ...state, isEnabled: action.isSelectionEnabled, }; + + case 'SET_RICH_TEXT_SELECTION': + return { + ...state, + richText: action.selection, + }; } return state; diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 619c0e381712a5..b49efd188819c3 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -2162,6 +2162,21 @@ export function isPublishSidebarEnabled( state ) { return PREFERENCES_DEFAULTS.isPublishSidebarEnabled; } +/** + * Returns the properties for the currently selected text inside the active + * RichText. + * + * Can be used together with the addAnnotation action to annotate the selected + * text. + * + * @param {Object} state Editor state. + * + * @return {Object} RichText information about the currently selected text. + */ +export function getCurrentRichTextSelection( state ) { + return state.blockSelection.richText; +} + // // Deprecated // diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index d40b07b0b36bd7..ffe88b378adf50 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1427,6 +1427,11 @@ describe( 'state', () => { initialPosition: -1, isMultiSelecting: false, isEnabled: true, + richText: { + blockClientId: null, + identifier: null, + range: { start: null, end: null }, + }, } ); } ); diff --git a/packages/format-library/src/annotation/index.js b/packages/format-library/src/annotation/index.js new file mode 100644 index 00000000000000..5891a6073d87e1 --- /dev/null +++ b/packages/format-library/src/annotation/index.js @@ -0,0 +1,19 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +const name = 'core/annotation'; + +export const annotation = { + name, + title: __( 'Annotation' ), + tagName: 'mark', + className: 'annotation-text', + attributes: { + className: 'class', + }, + edit() { + return null; + }, +}; diff --git a/packages/format-library/src/index.js b/packages/format-library/src/index.js index 387c2666f7e0e6..df8c877a721fd0 100644 --- a/packages/format-library/src/index.js +++ b/packages/format-library/src/index.js @@ -7,6 +7,7 @@ import { image } from './image'; import { italic } from './italic'; import { link } from './link'; import { strikethrough } from './strikethrough'; +import { annotation } from './annotation'; /** * WordPress dependencies @@ -22,4 +23,5 @@ import { italic, link, strikethrough, + annotation, ].forEach( ( { name, ...settings } ) => registerFormatType( name, settings ) ); diff --git a/test/e2e/specs/__snapshots__/plugins-api.test.js.snap b/test/e2e/specs/__snapshots__/plugins-api.test.js.snap index 0b62fc522bf7ef..5916648d1dd2b5 100644 --- a/test/e2e/specs/__snapshots__/plugins-api.test.js.snap +++ b/test/e2e/specs/__snapshots__/plugins-api.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Using Plugins API Sidebar Should open plugins sidebar using More Menu item and render content 1`] = `"
Sidebar title plugin
"`; +exports[`Using Plugins API Sidebar Should open plugins sidebar using More Menu item and render content 1`] = `"
Sidebar title plugin
"`; diff --git a/test/e2e/specs/plugins-api.test.js b/test/e2e/specs/plugins-api.test.js index d149cf492a5a33..bc841eb9e52f67 100644 --- a/test/e2e/specs/plugins-api.test.js +++ b/test/e2e/specs/plugins-api.test.js @@ -11,6 +11,14 @@ import { } from '../support/utils'; import { activatePlugin, deactivatePlugin } from '../support/plugins'; +const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => { + await expect( page ).toClick( '.editor-block-settings-menu__toggle' ); + const itemButton = ( await page.$x( `//*[contains(@class, "editor-block-settings-menu__popover")]//button[contains(text(), '${ buttonLabel }')]` ) )[ 0 ]; + await itemButton.click(); +}; + +const ANNOTATIONS_SELECTOR = '.annotation-text-e2e-tests'; + describe( 'Using Plugins API', () => { beforeAll( async () => { await activatePlugin( 'gutenberg-test-plugin-plugins-api' ); @@ -75,4 +83,36 @@ describe( 'Using Plugins API', () => { expect( pluginSidebarClosed ).toBeNull(); } ); } ); + + describe( 'Annotations', () => { + it( 'Allows a block to be annotated', async () => { + await page.keyboard.type( 'Title' + '\n' + 'Paragraph to annotate' ); + await clickOnMoreMenuItem( 'Sidebar title plugin' ); + + let annotations = await page.$$( ANNOTATIONS_SELECTOR ); + expect( annotations ).toHaveLength( 0 ); + + // Click add annotation button. + const addAnnotationButton = ( await page.$x( "//button[contains(text(), 'Add annotation')]" ) )[ 0 ]; + await addAnnotationButton.click(); + + annotations = await page.$$( ANNOTATIONS_SELECTOR ); + expect( annotations ).toHaveLength( 1 ); + + const annotation = annotations[ 0 ]; + + const text = await page.evaluate( ( el ) => el.innerText, annotation ); + expect( text ).toBe( ' to ' ); + + await clickOnBlockSettingsMenuItem( 'Edit as HTML' ); + + const htmlContent = await page.$$( '.editor-block-list__block-html-textarea' ); + const html = await page.evaluate( ( el ) => { + return el.innerHTML; + }, htmlContent[ 0 ] ); + + // There should be no tags in the raw content. + expect( html ).toBe( '<p>Paragraph to annotate</p>' ); + } ); + } ); } ); diff --git a/test/e2e/test-plugins/plugins-api/sidebar.js b/test/e2e/test-plugins/plugins-api/sidebar.js index 10112e3770155c..d0b15b218af6f7 100644 --- a/test/e2e/test-plugins/plugins-api/sidebar.js +++ b/test/e2e/test-plugins/plugins-api/sidebar.js @@ -5,6 +5,8 @@ var compose = wp.compose.compose; var withDispatch = wp.data.withDispatch; var withSelect = wp.data.withSelect; + var select = wp.data.select; + var dispatch = wp.data.dispatch; var PlainText = wp.editor.PlainText; var Fragment = wp.element.Fragment; var el = wp.element.createElement; @@ -48,6 +50,24 @@ }, __( 'Reset' ) ) + ), + el( + Button, + { + isPrimary: true, + onClick: () => { + dispatch( 'core/annotations' ).addAnnotation( { + source: 'e2e-tests', + blockClientId: select( 'core/editor' ).getBlockOrder()[ 0 ], + richTextIdentifier: 'content', + range: { + start: 9, + end: 13, + }, + } ); + }, + }, + __( 'Add annotation' ) ) ); } diff --git a/webpack.config.js b/webpack.config.js index dedeac0858017c..afd5c17c28c937 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -35,6 +35,7 @@ function camelCaseDash( string ) { const gutenbergPackages = [ 'a11y', + 'annotations', 'api-fetch', 'autop', 'blob', From efea71c4d20d5fb3a4ec9c3b45c61e0e04f089e4 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Thu, 8 Nov 2018 17:33:47 +0100 Subject: [PATCH 02/20] Mark Annotation API as experimental --- docs/data/data-core-annotations.md | 12 ++++++------ docs/extensibility/annotations.md | 2 ++ packages/annotations/src/store/actions.js | 6 +++--- packages/annotations/src/store/selectors.js | 6 +++--- packages/editor/src/components/block-list/block.js | 4 ++-- packages/editor/src/components/rich-text/index.js | 4 ++-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/data/data-core-annotations.md b/docs/data/data-core-annotations.md index 7c4efc606318e6..f4f7d8cb5ff072 100644 --- a/docs/data/data-core-annotations.md +++ b/docs/data/data-core-annotations.md @@ -2,7 +2,7 @@ ## Selectors -### getAnnotationsForBlock +### __experimentalGetAnnotationsForBlock Returns the annotations for a specific client ID. @@ -11,7 +11,7 @@ Returns the annotations for a specific client ID. * state: Editor state. * clientId: The ID of the block to get the annotations for. -### getAnnotationsForRichText +### __experimentalGetAnnotationsForRichText Returns the annotations that apply to the given RichText instance. @@ -29,7 +29,7 @@ block needs to implement annotations itself. All the annotations relevant for the `RichText`. -### getAnnotations +### __experimentalGetAnnotations Returns all annotations in the editor state. @@ -43,7 +43,7 @@ All annotations currently applied. ## Actions -### addAnnotation +### __experimentalAddAnnotation Adds an annotation to a block. @@ -67,7 +67,7 @@ The `range` property is only relevant if the selector is 'range'. * string: [id=uuid()] The ID the annotation should have. Generates a UUID by default. -### removeAnnotation +### __experimentalRemoveAnnotation Removes an annotation with a specific ID. @@ -75,7 +75,7 @@ Removes an annotation with a specific ID. * annotationId: The annotation to remove. -### removeAnnotationsBySource +### __experimentalRemoveAnnotationsBySource Removes all annotations of a specific source. diff --git a/docs/extensibility/annotations.md b/docs/extensibility/annotations.md index d62ef15451a526..377cfaec619659 100644 --- a/docs/extensibility/annotations.md +++ b/docs/extensibility/annotations.md @@ -1,5 +1,7 @@ # Annotations +**Note: This API is experimental, that means it is subject to non-backward compatible changes or removal in any future version.** + Annotations are a way to highlight a specific piece in a Gutenberg post. Examples of this include commenting on a piece of text and spellchecking. Both can use the annotations API to mark a piece of text. ## API diff --git a/packages/annotations/src/store/actions.js b/packages/annotations/src/store/actions.js index 8d83f06bbb1bbb..fa6fbc00d60749 100644 --- a/packages/annotations/src/store/actions.js +++ b/packages/annotations/src/store/actions.js @@ -23,7 +23,7 @@ import uuid from 'uuid/v4'; * * @return {Object} Action object. */ -export function addAnnotation( { blockClientId, richTextIdentifier = null, range = null, selector = 'range', source = 'default', id = uuid() } ) { +export function __experimentalAddAnnotation( { blockClientId, richTextIdentifier = null, range = null, selector = 'range', source = 'default', id = uuid() } ) { const action = { type: 'ANNOTATION_ADD', id, @@ -47,7 +47,7 @@ export function addAnnotation( { blockClientId, richTextIdentifier = null, range * * @return {Object} Action object. */ -export function removeAnnotation( annotationId ) { +export function __experimentalRemoveAnnotation( annotationId ) { return { type: 'ANNOTATION_REMOVE', annotationId, @@ -61,7 +61,7 @@ export function removeAnnotation( annotationId ) { * * @return {Object} Action object. */ -export function removeAnnotationsBySource( source ) { +export function __experimentalRemoveAnnotationsBySource( source ) { return { type: 'ANNOTATION_REMOVE_SOURCE', source, diff --git a/packages/annotations/src/store/selectors.js b/packages/annotations/src/store/selectors.js index a0089976e66740..ec3af680d8f7d9 100644 --- a/packages/annotations/src/store/selectors.js +++ b/packages/annotations/src/store/selectors.js @@ -8,7 +8,7 @@ import createSelector from 'rememo'; * * @return {Array} The annotations applicable to this block. */ -export const getAnnotationsForBlock = createSelector( +export const __experimentalGetAnnotationsForBlock = createSelector( ( state, blockClientId ) => { return state.all.filter( ( annotation ) => { return annotation.selector === 'block' && annotation.blockClientId === blockClientId; @@ -31,7 +31,7 @@ export const getAnnotationsForBlock = createSelector( * @param {string} richTextIdentifier Unique identifier that identifies the given RichText. * @return {Array} All the annotations relevant for the `RichText`. */ -export const getAnnotationsForRichText = createSelector( +export const __experimentalGetAnnotationsForRichText = createSelector( ( state, blockClientId, richTextIdentifier ) => { return state.all.filter( ( annotation ) => { return annotation.selector === 'range' && @@ -57,6 +57,6 @@ export const getAnnotationsForRichText = createSelector( * @param {Object} state Editor state. * @return {Array} All annotations currently applied. */ -export function getAnnotations( state ) { +export function __experimentalGetAnnotations( state ) { return state.all; } diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 61207b822ec88b..6ee520b0aa942b 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -607,7 +607,7 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV getTemplateLock, } = select( 'core/editor' ); const { - getAnnotationsForBlock, + __experimentalGetAnnotationsForBlock, } = select( 'core/annotations' ); const isSelected = isBlockSelected( clientId ); const { hasFixedToolbar, focusMode } = getEditorSettings(); @@ -641,7 +641,7 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV block, isSelected, isParentOfSelectedBlock, - annotations: getAnnotationsForBlock( clientId ), + annotations: __experimentalGetAnnotationsForBlock( clientId ), }; } ); diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index ede9bb5d372498..5a8872ce2f7058 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -990,7 +990,7 @@ const RichTextContainer = compose( [ withSelect( ( select, props ) => { const { isViewportMatch } = select( 'core/viewport' ); const { canUserUseUnfilteredHTML, isCaretWithinFormattedText } = select( 'core/editor' ); - const { getAnnotationsForRichText } = select( 'core/annotations' ); + const { __experimentalGetAnnotationsForRichText } = select( 'core/annotations' ); const selectProps = { isViewportSmall: isViewportMatch( '< small' ), @@ -1001,7 +1001,7 @@ const RichTextContainer = compose( [ // Allow explicit annotations to be passed in. // When an identifier is passed in we can retrieve the annotations for this RichText. if ( ! props.annotations && props.identifier ) { - selectProps.annotations = getAnnotationsForRichText( props.clientId, props.identifier ); + selectProps.annotations = __experimentalGetAnnotationsForRichText( props.clientId, props.identifier ); } return selectProps; From d96b7c5314a8b9538ab34821ef43d35535a51051 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 09:56:11 +0100 Subject: [PATCH 03/20] Move annotation format to annotation package --- .../src/format/annotation.js} | 0 packages/annotations/src/format/index.js | 15 +++++++++++++++ packages/annotations/src/index.js | 1 + packages/format-library/src/index.js | 2 -- 4 files changed, 16 insertions(+), 2 deletions(-) rename packages/{format-library/src/annotation/index.js => annotations/src/format/annotation.js} (100%) create mode 100644 packages/annotations/src/format/index.js diff --git a/packages/format-library/src/annotation/index.js b/packages/annotations/src/format/annotation.js similarity index 100% rename from packages/format-library/src/annotation/index.js rename to packages/annotations/src/format/annotation.js diff --git a/packages/annotations/src/format/index.js b/packages/annotations/src/format/index.js new file mode 100644 index 00000000000000..5c953391bc76d4 --- /dev/null +++ b/packages/annotations/src/format/index.js @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ +import { annotation } from './annotation'; + +/** + * WordPress dependencies + */ +import { + registerFormatType, +} from '@wordpress/rich-text'; + +const { name, ...settings } = annotation; + +registerFormatType( name, settings ); diff --git a/packages/annotations/src/index.js b/packages/annotations/src/index.js index a7ff64df3ef2a3..d4dadefa17ae15 100644 --- a/packages/annotations/src/index.js +++ b/packages/annotations/src/index.js @@ -2,3 +2,4 @@ * Internal dependencies */ import './store'; +import './format'; diff --git a/packages/format-library/src/index.js b/packages/format-library/src/index.js index df8c877a721fd0..387c2666f7e0e6 100644 --- a/packages/format-library/src/index.js +++ b/packages/format-library/src/index.js @@ -7,7 +7,6 @@ import { image } from './image'; import { italic } from './italic'; import { link } from './link'; import { strikethrough } from './strikethrough'; -import { annotation } from './annotation'; /** * WordPress dependencies @@ -23,5 +22,4 @@ import { italic, link, strikethrough, - annotation, ].forEach( ( { name, ...settings } ) => registerFormatType( name, settings ) ); From 254b0f959f46ae31b2dbd754812ab0cb644f0719 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 10:22:30 +0100 Subject: [PATCH 04/20] Move annotation application to annotation package --- packages/annotations/src/format/annotation.js | 62 +++++++++++++++++++ .../src/components/rich-text/annotations.js | 45 -------------- .../editor/src/components/rich-text/index.js | 55 ++++++---------- 3 files changed, 81 insertions(+), 81 deletions(-) delete mode 100644 packages/editor/src/components/rich-text/annotations.js diff --git a/packages/annotations/src/format/annotation.js b/packages/annotations/src/format/annotation.js index 5891a6073d87e1..025ba184051f86 100644 --- a/packages/annotations/src/format/annotation.js +++ b/packages/annotations/src/format/annotation.js @@ -5,6 +5,52 @@ import { __ } from '@wordpress/i18n'; const name = 'core/annotation'; +/* WordPress dependencies */ +import { applyFormat, removeFormat } from '@wordpress/rich-text'; + +/** + * Applies given annotations to the given record. + * + * @param {Object} record The record to apply annotations to. + * @param {Array} annotations The annotation to apply. + * @return {Object} A record with the annotations applied. + */ + +export function applyAnnotations( record, annotations = [] ) { + annotations.forEach( ( annotation ) => { + let { start, end } = annotation; + + if ( start > record.text.length ) { + start = record.text.length; + } + + if ( end > record.text.length ) { + end = record.text.length; + } + + const className = 'annotation-text-' + annotation.source; + + record = applyFormat( + record, + { type: 'core/annotation', attributes: { className } }, + start, + end + ); + } ); + + return record; +} + +/** + * Removes annotations from the given record. + * + * @param {Object} record Record to remove annotations from. + * @return {Object} The cleaned record. + */ +export function removeAnnotations( record ) { + return removeFormat( record, 'core/annotation', 0, record.text.length ); +} + export const annotation = { name, title: __( 'Annotation' ), @@ -16,4 +62,20 @@ export const annotation = { edit() { return null; }, + __experimentalGetPropsForEditableTreePreparation( select, { richTextIdentifier, blockClientId } ) { + return { + annotations: select( 'core/annotations' ).__experimentalGetAnnotationsForRichText( blockClientId, richTextIdentifier ), + }; + }, + __experimentalCreatePrepareEditableTree( props ) { + return ( formats, text ) => { + if ( props.annotations.length === 0 ) { + return formats; + } + + let record = { formats, text }; + record = applyAnnotations( record, props.annotations ); + return record.formats; + }; + }, }; diff --git a/packages/editor/src/components/rich-text/annotations.js b/packages/editor/src/components/rich-text/annotations.js deleted file mode 100644 index e0a15c5e842ce4..00000000000000 --- a/packages/editor/src/components/rich-text/annotations.js +++ /dev/null @@ -1,45 +0,0 @@ -/* WordPress dependencies */ -import { applyFormat, removeFormat } from '@wordpress/rich-text'; - -/** - * Applies given annotations to the given record. - * - * @param {Object} record The record to apply annotations to. - * @param {Array} annotations The annotation to apply. - * @return {Object} A record with the annotations applied. - */ - -export function applyAnnotations( record, annotations = [] ) { - annotations.forEach( ( annotation ) => { - let { start, end } = annotation; - - if ( start > record.text.length ) { - start = record.text.length; - } - - if ( end > record.text.length ) { - end = record.text.length; - } - - const className = 'annotation-text-' + annotation.source; - - record = applyFormat( - record, - { type: 'core/annotation', attributes: { className } }, - start, - end - ); - } ); - - return record; -} - -/** - * Removes annotations from the given record. - * - * @param {Object} record Record to remove annotations from. - * @return {Object} The cleaned record. - */ -export function removeAnnotations( record ) { - return removeFormat( record, 'core/annotation', 0, record.text.length ); -} diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 5a8872ce2f7058..9929c48f6e8610 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -57,7 +57,6 @@ import TinyMCE, { TINYMCE_ZWSP } from './tinymce'; import { pickAriaProps } from './aria'; import { getPatterns } from './patterns'; import { withBlockEditContext } from '../block-edit/context'; -import { applyAnnotations, removeAnnotations } from './annotations'; /** * Browser dependencies @@ -222,7 +221,7 @@ export class RichText extends Component { * @return {Object} The current record (value and selection). */ getRecord() { - const { formats, text } = this.formatToValue( this.props.value, this.props.annotations ); + const { formats, text } = this.formatToValue( this.props.value ); const { start, end } = this.state; return { formats, text, start, end }; @@ -260,7 +259,7 @@ export class RichText extends Component { } isEmpty() { - return isEmpty( this.formatToValue( this.props.value, this.props.annotations ) ); + return isEmpty( this.formatToValue( this.props.value ) ); } /** @@ -741,15 +740,12 @@ export class RichText extends Component { } componentDidUpdate( prevProps ) { - const { tagName, value, isSelected, annotations } = this.props; - - const shouldApplyAnnotations = annotations !== prevProps.annotations; + const { tagName, value, isSelected } = this.props; if ( - ( tagName === prevProps.tagName && + tagName === prevProps.tagName && value !== prevProps.value && - value !== this.savedContent ) || - shouldApplyAnnotations + value !== this.savedContent ) { // Handle deprecated `children` and `node` sources. // The old way of passing a value with the `node` matcher required @@ -761,10 +757,10 @@ export class RichText extends Component { return; } - const record = this.formatToValue( value, annotations ); + const record = this.formatToValue( value ); if ( isSelected ) { - const prevRecord = this.formatToValue( prevProps.value, prevProps.annotations ); + const prevRecord = this.formatToValue( prevProps.value ); const length = getTextContent( prevRecord ).length; record.start = length; record.end = length; @@ -778,8 +774,8 @@ export class RichText extends Component { // an empty paragraph into another, then also set the selection to the // end. if ( isSelected && ! prevProps.isSelected && ! this.isActive() ) { - const record = this.formatToValue( value, annotations ); - const prevRecord = this.formatToValue( prevProps.value, prevProps.annotations ); + const record = this.formatToValue( value ); + const prevRecord = this.formatToValue( prevProps.value ); const length = getTextContent( prevRecord ).length; record.start = length; record.end = length; @@ -803,12 +799,10 @@ export class RichText extends Component { } } - formatToValue( value, annotations = [] ) { - let record; - + formatToValue( value ) { // Handle deprecated `children` and `node` sources. if ( Array.isArray( value ) ) { - record = create( { + return create( { html: children.toHTML( value ), multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, @@ -816,7 +810,7 @@ export class RichText extends Component { } if ( this.props.format === 'string' ) { - record = create( { + return create( { html: value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, @@ -826,10 +820,10 @@ export class RichText extends Component { // Guard for blocks passing `null` in onSplit callbacks. May be removed // if onSplit is revised to not pass a `null` value. if ( value === null ) { - record = create(); + return create(); } - return applyAnnotations( record, annotations ); + return value; } valueToEditableHTML( value ) { @@ -846,13 +840,11 @@ export class RichText extends Component { } ).body.innerHTML; } - valueToFormat( { formats, text } ) { - const value = removeAnnotations( { formats, text } ); - + valueToFormat( value ) { // Handle deprecated `children` and `node` sources. if ( this.usedDeprecatedChildrenSource ) { return children.fromDOM( unstableToDom( { - value: value, + value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, } ).body.childNodes ); @@ -860,7 +852,7 @@ export class RichText extends Component { if ( this.props.format === 'string' ) { return toHTMLString( { - value: value, + value, multilineTag: this.multilineTag, multilineWrapperTags: this.multilineWrapperTags, } ); @@ -987,24 +979,15 @@ const RichTextContainer = compose( [ clientId: context.clientId, }; } ), - withSelect( ( select, props ) => { + withSelect( ( select ) => { const { isViewportMatch } = select( 'core/viewport' ); const { canUserUseUnfilteredHTML, isCaretWithinFormattedText } = select( 'core/editor' ); - const { __experimentalGetAnnotationsForRichText } = select( 'core/annotations' ); - const selectProps = { + return { isViewportSmall: isViewportMatch( '< small' ), canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), isCaretWithinFormattedText: isCaretWithinFormattedText(), }; - - // Allow explicit annotations to be passed in. - // When an identifier is passed in we can retrieve the annotations for this RichText. - if ( ! props.annotations && props.identifier ) { - selectProps.annotations = __experimentalGetAnnotationsForRichText( props.clientId, props.identifier ); - } - - return selectProps; } ), withDispatch( ( dispatch ) => { const { From c82b80957ef9ed8a8432964174b23a17ae43ba2f Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 10:36:50 +0100 Subject: [PATCH 05/20] Remove getCurrentRichTextSelection selector --- docs/data/data-core-editor.md | 26 ------------------- docs/extensibility/annotations.md | 9 ------- .../editor/src/components/rich-text/index.js | 3 --- packages/editor/src/store/actions.js | 20 -------------- packages/editor/src/store/reducer.js | 6 ----- packages/editor/src/store/selectors.js | 15 ----------- 6 files changed, 79 deletions(-) diff --git a/docs/data/data-core-editor.md b/docs/data/data-core-editor.md index 582b4a5e3bf844..1517f5b5317cc0 100644 --- a/docs/data/data-core-editor.md +++ b/docs/data/data-core-editor.md @@ -1393,22 +1393,6 @@ or skipped when the user clicks the "publish" button. Whether the pre-publish panel should be shown or not. -### getCurrentRichTextSelection - -Returns the properties for the currently selected text inside the active -RichText. - -Can be used together with the addAnnotation action to annotate the selected -text. - -*Parameters* - - * state: Editor state. - -*Returns* - -RichText information about the currently selected text. - ## Actions ### setupEditor @@ -1820,16 +1804,6 @@ Returns an action object used to signal that post saving is unlocked. * lockName: The lock name. -### setRichTextSelection - -Sets the rich text selection to a certain value. - -*Parameters* - - * blockClientId: The block that text is selected in. - * identifier: The RichText identifier that text is selected in. - * range: The start and end that text is selected in. - ### createNotice ### fetchReusableBlocks \ No newline at end of file diff --git a/docs/extensibility/annotations.md b/docs/extensibility/annotations.md index 377cfaec619659..70df5b61e5d187 100644 --- a/docs/extensibility/annotations.md +++ b/docs/extensibility/annotations.md @@ -53,12 +53,3 @@ This doesn't provide any styling out of the box, so you have to provide some CSS ## Text annotation The text annotation is controlled by the `start` and `end` properties. Simple `start` and `end` properties don't work for HTML, so these properties are assumed to be offsets within the `rich-text` internal structure. For simplicity you can think about this as if all HTML would be stripped out and then you calculate the `start` and the `end` of the annotation. - -If you simply want to annotate the currently selected text you can use the following code: - -```js -wp.data.dispatch( 'core/annotations' ).addAnnotation( { - source: "my-annotations-plugin", - ...wp.data.select( 'core/editor' ).getCurrentRichTextSelection(), -} ); -``` diff --git a/packages/editor/src/components/rich-text/index.js b/packages/editor/src/components/rich-text/index.js index 9929c48f6e8610..64161f64210c30 100644 --- a/packages/editor/src/components/rich-text/index.js +++ b/packages/editor/src/components/rich-text/index.js @@ -439,7 +439,6 @@ export class RichText extends Component { } this.setState( { start, end } ); - this.props.onSelectionChange( this.props.clientId, this.props.identifier, { start, end } ); } } @@ -996,7 +995,6 @@ const RichTextContainer = compose( [ undo, enterFormattedText, exitFormattedText, - setRichTextSelection, } = dispatch( 'core/editor' ); return { @@ -1005,7 +1003,6 @@ const RichTextContainer = compose( [ onUndo: undo, onEnterFormattedText: enterFormattedText, onExitFormattedText: exitFormattedText, - onSelectionChange: setRichTextSelection, }; } ), withSafeTimeout, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 2687e2f844dde5..a77b27add51c28 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -788,26 +788,6 @@ export function unlockPostSaving( lockName ) { }; } -/** - * Sets the rich text selection to a certain value. - * - * @param {string} blockClientId The block that text is selected in. - * @param {string} identifier The RichText identifier that text is selected in. - * @param {Object} range The start and end that text is selected in. - * - * @return {Object} Action object. - */ -export function setRichTextSelection( blockClientId, identifier, range ) { - return { - type: 'SET_RICH_TEXT_SELECTION', - selection: { - blockClientId, - richTextIdentifier: identifier, - range, - }, - }; -} - // // Deprecated // diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 4a4fb8bebdb4ba..c593614773e0fe 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -753,12 +753,6 @@ export function blockSelection( state = { ...state, isEnabled: action.isSelectionEnabled, }; - - case 'SET_RICH_TEXT_SELECTION': - return { - ...state, - richText: action.selection, - }; } return state; diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index b49efd188819c3..619c0e381712a5 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -2162,21 +2162,6 @@ export function isPublishSidebarEnabled( state ) { return PREFERENCES_DEFAULTS.isPublishSidebarEnabled; } -/** - * Returns the properties for the currently selected text inside the active - * RichText. - * - * Can be used together with the addAnnotation action to annotate the selected - * text. - * - * @param {Object} state Editor state. - * - * @return {Object} RichText information about the currently selected text. - */ -export function getCurrentRichTextSelection( state ) { - return state.blockSelection.richText; -} - // // Deprecated // From 467580a64a34401a102cb6529f13359997a71e21 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 10:44:43 +0100 Subject: [PATCH 06/20] Remove richText selection remnants --- packages/editor/src/store/reducer.js | 5 ----- packages/editor/src/store/test/reducer.js | 5 ----- 2 files changed, 10 deletions(-) diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index c593614773e0fe..6bfb468cd71578 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -653,11 +653,6 @@ export function blockSelection( state = { isMultiSelecting: false, isEnabled: true, initialPosition: null, - richText: { - blockClientId: null, - identifier: null, - range: { start: null, end: null }, - }, }, action ) { switch ( action.type ) { case 'CLEAR_SELECTED_BLOCK': diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index ffe88b378adf50..d40b07b0b36bd7 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -1427,11 +1427,6 @@ describe( 'state', () => { initialPosition: -1, isMultiSelecting: false, isEnabled: true, - richText: { - blockClientId: null, - identifier: null, - range: { start: null, end: null }, - }, } ); } ); From 30e67cf9ab9619d99ad3c46aea8f8570727cbd39 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 10:45:04 +0100 Subject: [PATCH 07/20] Remove annotations from rich-text docs --- .../editor/src/components/rich-text/README.md | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/packages/editor/src/components/rich-text/README.md b/packages/editor/src/components/rich-text/README.md index c46a611893a400..b5449fbe4ec9e0 100644 --- a/packages/editor/src/components/rich-text/README.md +++ b/packages/editor/src/components/rich-text/README.md @@ -57,25 +57,6 @@ Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs *Optional.* A list of autocompleters to use instead of the default. -### `annotations: Array` - -*Optional.* A list of annotations that should be applied to the content. - -An annotation has the following shape: - -```js -{ - // The source of the annotation, will be used for the HTML class. - "source": "annotations-tester", - - // The starting offset within the rich-text structure. - "start": 106, - - // The ending offset within the rich-text structure. - "end": 75 -} -``` - ## RichText.Content `RichText.Content` should be used in the `save` function of your block to correctly save rich text content. From 1d9ab4b4bf9e1af8bbd67157ce6225cfa50edaaf Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 11:05:50 +0100 Subject: [PATCH 08/20] Remove duplicate props --- packages/block-library/src/heading/edit.js | 1 - packages/block-library/src/list/index.js | 1 - packages/block-library/src/paragraph/edit.js | 1 - packages/block-library/src/quote/index.js | 2 -- 4 files changed, 5 deletions(-) diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index 6f067493315349..711f0c100e4317 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -44,7 +44,6 @@ export default function HeadingEdit( { setAttributes( { content: value } ) } diff --git a/packages/block-library/src/list/index.js b/packages/block-library/src/list/index.js index 8091b138328078..9fe52078e89937 100644 --- a/packages/block-library/src/list/index.js +++ b/packages/block-library/src/list/index.js @@ -335,7 +335,6 @@ export const settings = { { ( ! RichText.isEmpty( citation ) || isSelected ) && ( ) } From 833ceabb66e2e657985c556611a6ce5573dad5cb Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 11:17:27 +0100 Subject: [PATCH 09/20] Move block annotation to annotations package --- lib/client-assets.php | 2 +- packages/annotations/src/block/index.js | 23 +++++++++++++++++++ packages/annotations/src/index.js | 2 ++ .../editor/src/components/block-list/block.js | 12 ++-------- 4 files changed, 28 insertions(+), 11 deletions(-) create mode 100644 packages/annotations/src/block/index.js diff --git a/lib/client-assets.php b/lib/client-assets.php index fc1d89d074d5d0..5b63d56af532c3 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -319,7 +319,7 @@ function gutenberg_register_scripts_and_styles() { gutenberg_override_script( 'wp-annotations', gutenberg_url( 'build/annotations/index.js' ), - array( 'wp-polyfill', 'wp-data' ), + array( 'wp-polyfill', 'wp-data', 'wp-rich-text', 'wp-hooks' ), filemtime( gutenberg_dir_path() . 'build/annotations/index.js' ), true ); diff --git a/packages/annotations/src/block/index.js b/packages/annotations/src/block/index.js new file mode 100644 index 00000000000000..3ee5a5e97df83d --- /dev/null +++ b/packages/annotations/src/block/index.js @@ -0,0 +1,23 @@ +/* WordPress dependencies */ +import { addFilter } from '@wordpress/hooks'; +import { withSelect } from '@wordpress/data'; + +/** + * Adds annotation className to the block-list-block component. + * + * @param {Object} OriginalComponent The original BlockListBlock component. + * @return {Object} The enhanced component. + */ +const addAnnotationClassName = ( OriginalComponent ) => { + return withSelect( ( select, { clientId } ) => { + const annotations = select( 'core/annotations' ).__experimentalGetAnnotationsForBlock( clientId ); + + return { + className: annotations.map( ( annotation ) => { + return 'is-annotated-by-' + annotation.source; + } ), + }; + } )( OriginalComponent ); +}; + +addFilter( 'editor.BlockListBlock', 'core/annotations', addAnnotationClassName ); diff --git a/packages/annotations/src/index.js b/packages/annotations/src/index.js index d4dadefa17ae15..ce64106bf903cf 100644 --- a/packages/annotations/src/index.js +++ b/packages/annotations/src/index.js @@ -3,3 +3,5 @@ */ import './store'; import './format'; +import './block'; + diff --git a/packages/editor/src/components/block-list/block.js b/packages/editor/src/components/block-list/block.js index 6ee520b0aa942b..60912d294c6257 100644 --- a/packages/editor/src/components/block-list/block.js +++ b/packages/editor/src/components/block-list/block.js @@ -383,7 +383,7 @@ export class BlockListBlock extends Component { isPreviousBlockADefaultEmptyBlock, isParentOfSelectedBlock, isDraggable, - annotations, + className, } = this.props; const isHovered = this.state.isHovered && ! isMultiSelecting; const { name: blockName, isValid } = block; @@ -413,10 +413,6 @@ export class BlockListBlock extends Component { const shouldShowInsertionPoint = ( isPartOfMultiSelection && isFirstMultiSelected ) || ! isPartOfMultiSelection; const canShowInBetweenInserter = ! isEmptyDefaultBlock && ! isPreviousBlockADefaultEmptyBlock; - const annotationsClassNames = annotations.map( ( annotation ) => { - return 'is-annotated-by-' + annotation.source; - } ); - // The wp-block className is important for editor styles. // Generate the wrapper class names handling the different states of the block. const wrapperClassName = classnames( 'wp-block editor-block-list__block', { @@ -429,7 +425,7 @@ export class BlockListBlock extends Component { 'is-typing': isTypingWithinBlock, 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ), 'is-focus-mode': isFocusMode, - }, annotationsClassNames ); + }, className ); const { onReplace } = this.props; @@ -606,9 +602,6 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV hasSelectedInnerBlock, getTemplateLock, } = select( 'core/editor' ); - const { - __experimentalGetAnnotationsForBlock, - } = select( 'core/annotations' ); const isSelected = isBlockSelected( clientId ); const { hasFixedToolbar, focusMode } = getEditorSettings(); const block = getBlock( clientId ); @@ -641,7 +634,6 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId, isLargeV block, isSelected, isParentOfSelectedBlock, - annotations: __experimentalGetAnnotationsForBlock( clientId ), }; } ); From 64acd0c40ffb298c38829388f17d84834bb97139 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 11:23:33 +0100 Subject: [PATCH 10/20] Properly define dependencies --- lib/client-assets.php | 2 +- package-lock.json | 3 +++ packages/annotations/package.json | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 5b63d56af532c3..16dc5404f1b3e2 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -319,7 +319,7 @@ function gutenberg_register_scripts_and_styles() { gutenberg_override_script( 'wp-annotations', gutenberg_url( 'build/annotations/index.js' ), - array( 'wp-polyfill', 'wp-data', 'wp-rich-text', 'wp-hooks' ), + array( 'wp-polyfill', 'wp-data', 'wp-rich-text', 'wp-hooks', 'wp-i18n' ), filemtime( gutenberg_dir_path() . 'build/annotations/index.js' ), true ); diff --git a/package-lock.json b/package-lock.json index 812b0930a4fc4b..43d43e0bb9ba9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2007,6 +2007,9 @@ "requires": { "@babel/runtime": "^7.0.0", "@wordpress/data": "file:packages/data", + "@wordpress/hooks": "file:packages/hooks", + "@wordpress/i18n": "file:packages/i18n", + "@wordpress/rich-text": "file:packages/rich-text", "rememo": "^3.0.0" } }, diff --git a/packages/annotations/package.json b/packages/annotations/package.json index e57906f3820148..95025c3e905952 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -22,6 +22,9 @@ "dependencies": { "@babel/runtime": "^7.0.0", "@wordpress/data": "file:../data", + "@wordpress/hooks": "file:../hooks", + "@wordpress/i18n": "file:../i18n", + "@wordpress/rich-text": "file:../rich-text", "rememo": "^3.0.0" }, "publishConfig": { From 399f9bc9e5d5a4b00c6aae4f398a7b557eb493e8 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 11:36:29 +0100 Subject: [PATCH 11/20] Add _experimental flag to e2e tests --- test/e2e/test-plugins/plugins-api/sidebar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/test-plugins/plugins-api/sidebar.js b/test/e2e/test-plugins/plugins-api/sidebar.js index d0b15b218af6f7..c97d29c754f23a 100644 --- a/test/e2e/test-plugins/plugins-api/sidebar.js +++ b/test/e2e/test-plugins/plugins-api/sidebar.js @@ -56,7 +56,7 @@ { isPrimary: true, onClick: () => { - dispatch( 'core/annotations' ).addAnnotation( { + dispatch( 'core/annotations' ).__experimentalAddAnnotation( { source: 'e2e-tests', blockClientId: select( 'core/editor' ).getBlockOrder()[ 0 ], richTextIdentifier: 'content', From f5eabaa5eb17a608595ed1efcd58d82130140407 Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 12:14:11 +0100 Subject: [PATCH 12/20] Remove wp-annotations as a dependency of wp-editor --- lib/client-assets.php | 1 - test/e2e/test-plugins/plugins-api.php | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client-assets.php b/lib/client-assets.php index 16dc5404f1b3e2..51a6054f01cfd5 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -677,7 +677,6 @@ function gutenberg_register_scripts_and_styles() { 'lodash', 'tinymce-latest-lists', 'wp-a11y', - 'wp-annotations', 'wp-api-fetch', 'wp-blob', 'wp-blocks', diff --git a/test/e2e/test-plugins/plugins-api.php b/test/e2e/test-plugins/plugins-api.php index fcd9fb04b6a2f6..d219eab684f955 100644 --- a/test/e2e/test-plugins/plugins-api.php +++ b/test/e2e/test-plugins/plugins-api.php @@ -45,6 +45,7 @@ 'wp-element', 'wp-i18n', 'wp-plugins', + 'wp-annotations', ), filemtime( plugin_dir_path( __FILE__ ) . 'plugins-api/sidebar.js' ), true From fc29ca440fb628b3d62c72d03121efca399b305d Mon Sep 17 00:00:00 2001 From: Anton Timmermans Date: Fri, 9 Nov 2018 12:14:32 +0100 Subject: [PATCH 13/20] Fix spacing --- packages/annotations/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/annotations/README.md b/packages/annotations/README.md index 5a6ef603633c42..89c8b2f83adbef 100644 --- a/packages/annotations/README.md +++ b/packages/annotations/README.md @@ -1,7 +1,5 @@ # Annotations - - ## Installation Install the module From 6f84e3a52ff7dfab2796b326f154a49e48f2fd95 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 9 Nov 2018 11:32:02 -0500 Subject: [PATCH 14/20] Annotations: Add missing uuid dependency --- package-lock.json | 3 ++- packages/annotations/package.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43d43e0bb9ba9d..f3f9f9a306f466 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2010,7 +2010,8 @@ "@wordpress/hooks": "file:packages/hooks", "@wordpress/i18n": "file:packages/i18n", "@wordpress/rich-text": "file:packages/rich-text", - "rememo": "^3.0.0" + "rememo": "^3.0.0", + "uuid": "^3.3.2" } }, "@wordpress/api-fetch": { diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 95025c3e905952..e1a743bed50def 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -25,7 +25,8 @@ "@wordpress/hooks": "file:../hooks", "@wordpress/i18n": "file:../i18n", "@wordpress/rich-text": "file:../rich-text", - "rememo": "^3.0.0" + "rememo": "^3.0.0", + "uuid": "^3.3.2" }, "publishConfig": { "access": "public" From 47749437189d8aa784a4777307ce7102d4d4d4e1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 9 Nov 2018 11:36:23 -0500 Subject: [PATCH 15/20] Annotations: Fix DocBlock inconsistency --- packages/annotations/src/block/index.js | 4 +++- packages/annotations/src/format/index.js | 10 +++++----- packages/annotations/src/store/actions.js | 3 +++ packages/annotations/src/store/reducer.js | 3 +++ packages/annotations/src/store/selectors.js | 3 +++ packages/annotations/src/store/test/reducer.js | 3 +++ 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/annotations/src/block/index.js b/packages/annotations/src/block/index.js index 3ee5a5e97df83d..5095fc473d67e6 100644 --- a/packages/annotations/src/block/index.js +++ b/packages/annotations/src/block/index.js @@ -1,4 +1,6 @@ -/* WordPress dependencies */ +/** + * WordPress dependencies + */ import { addFilter } from '@wordpress/hooks'; import { withSelect } from '@wordpress/data'; diff --git a/packages/annotations/src/format/index.js b/packages/annotations/src/format/index.js index 5c953391bc76d4..1dccbbd5012a0c 100644 --- a/packages/annotations/src/format/index.js +++ b/packages/annotations/src/format/index.js @@ -1,8 +1,3 @@ -/** - * Internal dependencies - */ -import { annotation } from './annotation'; - /** * WordPress dependencies */ @@ -10,6 +5,11 @@ import { registerFormatType, } from '@wordpress/rich-text'; +/** + * Internal dependencies + */ +import { annotation } from './annotation'; + const { name, ...settings } = annotation; registerFormatType( name, settings ); diff --git a/packages/annotations/src/store/actions.js b/packages/annotations/src/store/actions.js index fa6fbc00d60749..73f8c9e1fe381c 100644 --- a/packages/annotations/src/store/actions.js +++ b/packages/annotations/src/store/actions.js @@ -1,3 +1,6 @@ +/** + * External dependencies + */ import uuid from 'uuid/v4'; /** diff --git a/packages/annotations/src/store/reducer.js b/packages/annotations/src/store/reducer.js index 624cebcd2ca290..cb14165a5d6bdd 100644 --- a/packages/annotations/src/store/reducer.js +++ b/packages/annotations/src/store/reducer.js @@ -1,3 +1,6 @@ +/** + * External dependencies + */ import { isNumber, mapValues } from 'lodash'; /** diff --git a/packages/annotations/src/store/selectors.js b/packages/annotations/src/store/selectors.js index ec3af680d8f7d9..659b83e83e30d1 100644 --- a/packages/annotations/src/store/selectors.js +++ b/packages/annotations/src/store/selectors.js @@ -1,3 +1,6 @@ +/** + * External dependencies + */ import createSelector from 'rememo'; /** diff --git a/packages/annotations/src/store/test/reducer.js b/packages/annotations/src/store/test/reducer.js index ec88a062842199..a1dba8db8c8ac4 100644 --- a/packages/annotations/src/store/test/reducer.js +++ b/packages/annotations/src/store/test/reducer.js @@ -1,3 +1,6 @@ +/** + * Internal dependencies + */ import { annotations } from '../reducer'; describe( 'annotations', () => { From 807571badb143240c695c2f9ac811948291c0f79 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 9 Nov 2018 11:37:29 -0500 Subject: [PATCH 16/20] Annotations: Add missing Lodash dependency --- package-lock.json | 1 + packages/annotations/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package-lock.json b/package-lock.json index f3f9f9a306f466..b568c0ee77e33d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2010,6 +2010,7 @@ "@wordpress/hooks": "file:packages/hooks", "@wordpress/i18n": "file:packages/i18n", "@wordpress/rich-text": "file:packages/rich-text", + "lodash": "^4.17.11", "rememo": "^3.0.0", "uuid": "^3.3.2" } diff --git a/packages/annotations/package.json b/packages/annotations/package.json index e1a743bed50def..40816778f4ebec 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -25,6 +25,7 @@ "@wordpress/hooks": "file:../hooks", "@wordpress/i18n": "file:../i18n", "@wordpress/rich-text": "file:../rich-text", + "lodash": "^4.17.11", "rememo": "^3.0.0", "uuid": "^3.3.2" }, From c7e0e52903a2513533e7ab59fe61761afb08f12c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 9 Nov 2018 11:39:07 -0500 Subject: [PATCH 17/20] Annotations: Fix up a few more DocBlocks --- packages/annotations/src/format/annotation.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/annotations/src/format/annotation.js b/packages/annotations/src/format/annotation.js index 025ba184051f86..b052de27f335fd 100644 --- a/packages/annotations/src/format/annotation.js +++ b/packages/annotations/src/format/annotation.js @@ -5,7 +5,9 @@ import { __ } from '@wordpress/i18n'; const name = 'core/annotation'; -/* WordPress dependencies */ +/** + * WordPress dependencies + */ import { applyFormat, removeFormat } from '@wordpress/rich-text'; /** @@ -15,7 +17,6 @@ import { applyFormat, removeFormat } from '@wordpress/rich-text'; * @param {Array} annotations The annotation to apply. * @return {Object} A record with the annotations applied. */ - export function applyAnnotations( record, annotations = [] ) { annotations.forEach( ( annotation ) => { let { start, end } = annotation; From f7d0f1463a83e6589a2ffd15d2b230648dd3c6cc Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 9 Nov 2018 11:39:20 -0500 Subject: [PATCH 18/20] Annotations: Add a basic description for package --- packages/annotations/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/annotations/README.md b/packages/annotations/README.md index 89c8b2f83adbef..a1585de3106cb1 100644 --- a/packages/annotations/README.md +++ b/packages/annotations/README.md @@ -1,5 +1,7 @@ # Annotations +Annotate content in the Gutenberg editor. + ## Installation Install the module From ebf62c90974ec47fb56ef02ce067165c516adddd Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 9 Nov 2018 11:44:32 -0500 Subject: [PATCH 19/20] Docs: Include Annotations document in root manifest --- docs/manifest.json | 6 ++++++ docs/root-manifest.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/docs/manifest.json b/docs/manifest.json index 522cee09fd1738..075d922f2cad93 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -89,6 +89,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/autocomplete.md", "parent": "extensibility" }, + { + "title": "Annotations", + "slug": "annotations", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/extensibility/annotations.md", + "parent": "extensibility" + }, { "title": "Design", "slug": "design", diff --git a/docs/root-manifest.json b/docs/root-manifest.json index 759792a857b733..1202ab14eacb4a 100644 --- a/docs/root-manifest.json +++ b/docs/root-manifest.json @@ -89,6 +89,12 @@ "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/autocomplete.md", "parent": "extensibility" }, + { + "title": "Annotations", + "slug": "annotations", + "markdown_source": "https:\/\/raw.githubusercontent.com\/WordPress\/gutenberg\/master\/docs\/extensibility/annotations.md", + "parent": "extensibility" + }, { "title": "Design", "slug": "design", From 03c41cb57ba58dc91467c093d078a3ae5ab67f60 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Fri, 9 Nov 2018 18:12:42 +0100 Subject: [PATCH 20/20] Update dependencies --- package-lock.json | 18 ++++++++++++------ package.json | 2 +- packages/annotations/package.json | 2 +- packages/blocks/package.json | 2 +- packages/components/package.json | 2 +- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index b568c0ee77e33d..e09de35a5ca16d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2010,9 +2010,15 @@ "@wordpress/hooks": "file:packages/hooks", "@wordpress/i18n": "file:packages/i18n", "@wordpress/rich-text": "file:packages/rich-text", - "lodash": "^4.17.11", + "lodash": "^4.17.10", "rememo": "^3.0.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "bundled": true + } } }, "@wordpress/api-fetch": { @@ -2123,7 +2129,7 @@ "showdown": "^1.8.6", "simple-html-tokenizer": "^0.4.1", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" } }, "@wordpress/browserslist-config": { @@ -2158,7 +2164,7 @@ "react-dates": "^17.1.1", "rememo": "^3.0.0", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" } }, "@wordpress/compose": { @@ -20443,9 +20449,9 @@ "dev": true }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" }, "v8-compile-cache": { "version": "1.1.2", diff --git a/package.json b/package.json index b61319f6a4a2cb..11453090aa0941 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "stylelint": "9.5.0", "stylelint-config-wordpress": "13.1.0", "symlink-or-copy": "1.2.0", - "uuid": "3.1.0", + "uuid": "3.3.2", "webpack": "4.8.3", "webpack-bundle-analyzer": "3.0.2", "webpack-cli": "2.1.3", diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 40816778f4ebec..b68ce8f7efa25e 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -25,7 +25,7 @@ "@wordpress/hooks": "file:../hooks", "@wordpress/i18n": "file:../i18n", "@wordpress/rich-text": "file:../rich-text", - "lodash": "^4.17.11", + "lodash": "^4.17.10", "rememo": "^3.0.0", "uuid": "^3.3.2" }, diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 178ea4a5a97b4f..862a7b58a897e1 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -38,7 +38,7 @@ "showdown": "^1.8.6", "simple-html-tokenizer": "^0.4.1", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" }, "devDependencies": { "deep-freeze": "^0.0.1" diff --git a/packages/components/package.json b/packages/components/package.json index 7e7883ab2a7c2d..30269726a3f11b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -45,7 +45,7 @@ "react-dates": "^17.1.1", "rememo": "^3.0.0", "tinycolor2": "^1.4.1", - "uuid": "^3.1.0" + "uuid": "^3.3.2" }, "devDependencies": { "@wordpress/token-list": "file:../token-list",