Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotation API #7718

Merged
merged 20 commits into from
Nov 9, 2018
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/data/README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
84 changes: 84 additions & 0 deletions docs/data/data-core-annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# **core/annotations**: Annotations

## Selectors

### __experimentalGetAnnotationsForBlock

Returns the annotations for a specific client ID.

*Parameters*

* state: Editor state.
* clientId: The ID of the block to get the annotations for.

### __experimentalGetAnnotationsForRichText

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`.

### __experimentalGetAnnotations

Returns all annotations in the editor state.

*Parameters*

* state: Editor state.

*Returns*

All annotations currently applied.

## Actions

### __experimentalAddAnnotation

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.

### __experimentalRemoveAnnotation

Removes an annotation with a specific ID.

*Parameters*

* annotationId: The annotation to remove.

### __experimentalRemoveAnnotationsBySource

Removes all annotations of a specific source.

*Parameters*

* source: The source to remove.
55 changes: 55 additions & 0 deletions docs/extensibility/annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Annotations
aduth marked this conversation as resolved.
Show resolved Hide resolved

**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

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",
atimmer marked this conversation as resolved.
Show resolved Hide resolved
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>Strong text</strong>
```

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.
12 changes: 12 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions docs/tool/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' ) ],
Expand Down
7 changes: 7 additions & 0 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', 'wp-rich-text', 'wp-hooks', 'wp-i18n' ),
filemtime( gutenberg_dir_path() . 'build/annotations/index.js' ),
true
);
gutenberg_override_script(
'wp-core-data',
gutenberg_url( 'build/core-data/index.js' ),
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/annotations/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
5 changes: 5 additions & 0 deletions packages/annotations/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 1.0.0 (unreleased)

### New Features

- Implement annotations API in the editor.
13 changes: 13 additions & 0 deletions packages/annotations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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
34 changes: 34 additions & 0 deletions packages/annotations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@wordpress/annotations",
"version": "1.0.0-beta1",
"description": "Annotate content in the Gutenberg editor.",
aduth marked this conversation as resolved.
Show resolved Hide resolved
"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",
"@wordpress/hooks": "file:../hooks",
"@wordpress/i18n": "file:../i18n",
"@wordpress/rich-text": "file:../rich-text",
"rememo": "^3.0.0",
"uuid": "^3.3.2"
},
"publishConfig": {
"access": "public"
}
}
23 changes: 23 additions & 0 deletions packages/annotations/src/block/index.js
Original file line number Diff line number Diff line change
@@ -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 );
81 changes: 81 additions & 0 deletions packages/annotations/src/format/annotation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* WordPress dependencies
*/
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 ) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a perfect use-case for Array#reduce:

return annotations.reduce( ( recordResult, annotation ) => {
	// ...

	return applyFormat(
			recordResult,
			{ type: 'core/annotation', attributes: { className } },
			start,
			end
		);
}, record );

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd do the refactoring but there aren't any unit tests to verify the behavior so I don't feel confident.

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' ),
tagName: 'mark',
className: 'annotation-text',
attributes: {
className: 'class',
},
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;
};
},
};
Loading