diff --git a/.eslintrc.js b/.eslintrc.js index b5a22b0..88dfc81 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ diff --git a/LICENSE.md b/LICENSE.md index ed3848d..a461d84 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,7 +2,7 @@ Software License Agreement ========================== **CKEditor 5 Highlight Feature** – https://github.com/ckeditor/ckeditor5-highlight
-Copyright (c) 2003-2017, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. +Copyright (c) 2003-2018, [CKSource](http://cksource.com) Frederico Knabben. All rights reserved. Licensed under the terms of any of the following licenses at your choice: diff --git a/docs/_snippets/features/build-highlight-source.html b/docs/_snippets/features/build-highlight-source.html new file mode 100644 index 0000000..a12a59a --- /dev/null +++ b/docs/_snippets/features/build-highlight-source.html @@ -0,0 +1,23 @@ + diff --git a/docs/_snippets/features/build-highlight-source.js b/docs/_snippets/features/build-highlight-source.js new file mode 100644 index 0000000..278e78c --- /dev/null +++ b/docs/_snippets/features/build-highlight-source.js @@ -0,0 +1,14 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals window */ + +import ClassicEditor from '@ckeditor/ckeditor5-build-classic/src/ckeditor'; + +import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; + +ClassicEditor.build.plugins.push( Highlight ); + +window.ClassicEditor = ClassicEditor; diff --git a/docs/_snippets/features/custom-highlight-options.html b/docs/_snippets/features/custom-highlight-options.html new file mode 100644 index 0000000..50a4976 --- /dev/null +++ b/docs/_snippets/features/custom-highlight-options.html @@ -0,0 +1,5 @@ +
+

+ Here are defined highlighters: green one and blue one. +

+
diff --git a/docs/_snippets/features/custom-highlight-options.js b/docs/_snippets/features/custom-highlight-options.js new file mode 100644 index 0000000..f47964e --- /dev/null +++ b/docs/_snippets/features/custom-highlight-options.js @@ -0,0 +1,27 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document */ +ClassicEditor + .create( document.querySelector( '#snippet-custom-highlight-options' ), { + toolbar: { + items: [ + 'headings', '|', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' + ], + viewportTopOffset: 60 + }, + highlight: { + options: [ + { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#66ff00', type: 'marker' }, + { model: 'bluePen', class: 'pen-blue', title: 'Blue pen', color: '#0091ff', type: 'pen' } + ] + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/highlight-buttons.html b/docs/_snippets/features/highlight-buttons.html new file mode 100644 index 0000000..eb8d483 --- /dev/null +++ b/docs/_snippets/features/highlight-buttons.html @@ -0,0 +1,12 @@ +
+

Highlight feature sample.

+ +

+ Here are some markers: + yellow one, pink one and green one. +

+

+ Here are some pens: + red pen and blue one. +

+
diff --git a/docs/_snippets/features/highlight-buttons.js b/docs/_snippets/features/highlight-buttons.js new file mode 100644 index 0000000..0293d49 --- /dev/null +++ b/docs/_snippets/features/highlight-buttons.js @@ -0,0 +1,23 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document */ + +ClassicEditor + .create( document.querySelector( '#snippet-highlight-buttons' ), { + toolbar: { + items: [ + 'headings', '|', 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:bluePen', + 'highlight:redPen', 'removeHighlight', '|', 'undo', 'redo' + ], + viewportTopOffset: 60 + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/_snippets/features/highlight.html b/docs/_snippets/features/highlight.html new file mode 100644 index 0000000..fe3d1e4 --- /dev/null +++ b/docs/_snippets/features/highlight.html @@ -0,0 +1,12 @@ +
+

Highlight feature sample.

+ +

+ Here are some markers: + yellow one, pink one and green one. +

+

+ Here are some pens: + red pen and blue one. +

+
diff --git a/docs/_snippets/features/highlight.js b/docs/_snippets/features/highlight.js new file mode 100644 index 0000000..e04e6ef --- /dev/null +++ b/docs/_snippets/features/highlight.js @@ -0,0 +1,22 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals ClassicEditor, console, window, document */ + +ClassicEditor + .create( document.querySelector( '#snippet-highlight' ), { + toolbar: { + items: [ + 'headings', '|', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' + ], + viewportTopOffset: 60 + } + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/docs/api/highlight.md b/docs/api/highlight.md new file mode 100644 index 0000000..665f6b3 --- /dev/null +++ b/docs/api/highlight.md @@ -0,0 +1,30 @@ +--- +category: api-reference +--- + +# CKEditor 5 highlight feature + +[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-highlight.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-highlight) + +This package implements the highlight feature for CKEditor 5. + +## Documentation + +See the {@link features/highlight Highlight feature} guide and the {@link module:highlight/highlight~Highlight} plugin documentation. + +## Installation + +```bash +npm install --save @ckeditor/ckeditor5-highlight +``` + +## Contribute + +The source code of this package is available on GitHub in https://github.com/ckeditor/ckeditor5-highlight. + +## External links + +* [`@ckeditor/ckeditor5-highlight` on npm](https://www.npmjs.com/package/@ckeditor/ckeditor5-highlight) +* [`ckeditor/ckeditor5-highlight` on GitHub](https://github.com/ckeditor/ckeditor5-highlight) +* [Issue tracker](https://github.com/ckeditor/ckeditor5-highlight/issues) +* [Changelog](https://github.com/ckeditor/ckeditor5-highlight/blob/master/CHANGELOG.md) diff --git a/docs/features/highlight.md b/docs/features/highlight.md new file mode 100644 index 0000000..b4cbb29 --- /dev/null +++ b/docs/features/highlight.md @@ -0,0 +1,138 @@ +--- +title: Highlight +category: features +--- + +{@snippet features/build-highlight-source} + +The {@link module:highlight/highlight~Highlight} feature offers a text marking tools that help content authors speed up their work, e.g. reviewing content or marking it for the future reference. It uses inline `` elements in the view, supports both markers (background color) and pens (text color), and comes with a flexible configuration. + +## Demo + +{@snippet features/highlight} + +## Configuring the highlight options + +It is possible to configure which highlight options are supported by the editor. +You can use the {@link module:highlight/highlight~HighlightConfig#options `highlight.options`} configuration and define your own highlight styles. + +For example, the following editor supports only two styles (a green marker and a blue pen): + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + highlight: { + options: [ + { + model: 'greenMarker', + class: 'marker-green', + title: 'Green marker', + color: '#66ff00', + type: 'marker' + }, + { + model: 'bluePen', + class: 'pen-blue', + title: 'Blue pen', + color: '#0091ff', + type: 'pen' + } + ] + }, + toolbar: [ + 'headings', '|', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' + ] + } ) + .then( ... ) + .catch( ... ); +``` + +{@snippet features/custom-highlight-options} + +Instead of using the (default) `highlightDropdown`, the feature also supports a configuration with separate buttons directly in the toolbar: + +```js +ClassicEditor + .create( document.querySelector( '#editor' ), { + toolbar: { + items: [ + 'headings', '|', 'highlight:marker', 'highlight:greenMarker', + 'highlight:pinkMarker', 'highlight:bluePen', + 'highlight:redPen', 'removeHighlight', 'undo', 'redo' + ] + } + } ) + .then( ... ) + .catch( ... ); +``` + +{@snippet features/highlight-buttons} + +## Installation + +To add this feature to your editor install the [`@ckeditor/ckeditor5-highlight`](https://www.npmjs.com/package/@ckeditor/ckeditor5-highlight) package: + +``` +npm install --save @ckeditor/ckeditor5-highlight +``` + +And add it to your plugin list and the toolbar configuration: + +```js +import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Highlight, ... ], + toolbar: [ 'highlightDropdown', ... ] + } ) + .then( ... ) + .catch( ... ); +``` + + + Read more about {@link builds/guides/development/installing-plugins installing plugins}. + + +## Common API + +The {@link module:highlight/highlight~Highlight} plugin registers: + +* The `'highlightDropdown'` dropdown, +* The {@link module:highlight/highlightcommand~HighlightCommand `'highlight'`} command. + + The number of options and their names correspond to the {@link module:highlight/highlight~HighlightConfig#options `highlight.options`} configuration option. + + You can change the highlight of the current selection by executing the command with a desired value: + + ```js + editor.execute( 'highlight', { value: 'marker' } ); + ``` + + The `value` corresponds to the `model` property in configuration object. For the default configuration: + ```js + highlight.options = [ + { model: 'marker', class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, + { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#66ff00', type: 'marker' }, + { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#ff6fff', type: 'marker' }, + { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#ff2929', type: 'pen' }, + { model: 'bluePen', class: 'pen-blue', title: 'Blue pen', color: '#0091ff', type: 'pen' } + ] + ``` + + the `highlight` command will accept the corresponding strings as values: + - `'marker'` – available as a `'highlight:marker'` button. + - `'greenMarker'` – available as a `'highlight:greenMarker'` button. + - `'pinkMarker'` – available as a `'highlight:pinkMarker'` button. + - `'redPen'` – available as a `'highlight:redPen'` button. + - `'bluePen'` – available as a `'highlight:bluePen'` button. + + passing an empty `value` will remove any `highlight` from the selection: + + ```js + editor.execute( 'highlight' ); + ``` + +## Contribute + +The source code of the feature is available on GitHub in https://github.com/ckeditor/ckeditor5-highlight. diff --git a/package.json b/package.json index cc80704..344dd9f 100644 --- a/package.json +++ b/package.json @@ -7,22 +7,24 @@ "ckeditor5-feature" ], "dependencies": { - "@ckeditor/ckeditor5-core": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-engine": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-ui": "^1.0.0-alpha.1" + "@ckeditor/ckeditor5-core": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-engine": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-ui": "^1.0.0-alpha.2" }, "devDependencies": { - "@ckeditor/ckeditor5-block-quote": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-enter": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-heading": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-image": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-list": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.1", - "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.1", - "eslint": "^4.8.0", - "eslint-config-ckeditor5": "^1.0.6", + "@ckeditor/ckeditor5-block-quote": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-editor-classic": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-enter": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-heading": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-image": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-list": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.2", + "@ckeditor/ckeditor5-utils": "^1.0.0-alpha.2", + "eslint": "^4.15.0", + "eslint-config-ckeditor5": "^1.0.7", "husky": "^0.14.3", - "lint-staged": "^4.2.3" + "lint-staged": "^6.0.0" }, "engines": { "node": ">=6.0.0", diff --git a/src/highlight.js b/src/highlight.js index c1ddaf6..5c266e9 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ @@ -15,7 +15,10 @@ import HighlightUI from './highlightui'; /** * The highlight plugin. * - * It requires {@link module:highlight/highlightediting~HighlightEditing} and {@link module:highlight/highlightui~HighlightUI} plugins. + * It loads the {@link module:highlight/highlightediting~HighlightEditing} and + * {@link module:highlight/highlightui~HighlightUI} plugins. + * + * Read more about the feature in the {@glink api/highlight highlight package} page. * * @extends module:core/plugin~Plugin */ @@ -34,3 +37,96 @@ export default class Highlight extends Plugin { return 'Highlight'; } } + +/** + * The highlight option descriptor. See the {@link module:highlight/highlight~HighlightConfig} to learn more. + * + * { + * model: 'pinkMarker', + * class: 'marker-pink', + * title: 'Pink Marker', + * color: '#ff6fff', + * type: 'marker' + * } + * + * @typedef {Object} module:highlight/highlight~HighlightOption + * @property {String} title The user-readable title of the option. + * @property {String} model The unique attribute value in the model. + * @property {String} color The color used for the highlighter. It should match the `class` CSS definition. + * The color is used in the user interface to represent the highlighter. + * @property {String} class The CSS class used on the `` element in the view. It should match the `color` setting. + * @property {'marker'|'pen'} type The type of highlighter: + * - `'marker'` – uses the `color` as a `background-color` style, + * - `'pen'` – uses the `color` as a font `color` style. + */ + +/** + * The configuration of the {@link module:highlight/highlight~Highlight} feature. + * + * Read more in {@link module:highlight/highlight~HighlightConfig}. + * + * @member {module:highlight/highlight~HighlightConfig} module:core/editor/editorconfig~EditorConfig#highlight + */ + +/** + * The configuration of the {@link module:highlight/highlight~Highlight Highlight feature}. + * + * ClassicEditor + * .create( editorElement, { + * highlight: ... // Highlight feature config. + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * + * @interface HighlightConfig + */ + +/** + * The available highlighters options. The default value is: + * + * options: [ + * { model: 'marker', class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, + * { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#66ff00', type: 'marker' }, + * { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#ff6fff', type: 'marker' }, + * { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#ff2929', type: 'pen' }, + * { model: 'bluePen', class: 'pen-blue', title: 'Blue pen', color: '#0091ff', type: 'pen' } + * ] + * + * There are two types of highlighters available: + * - `'marker'` - rendered as a `` element, styled with the `background-color`, + * - `'pen'` - rendered as a `` element, styled with the font `color`. + * + * **Note**: A style sheet with CSS classes is required for the configuration to work properly. + * The highlight feature does not provide the actual styles by itself. + * + * **Note**: It is recommended that the `color` value should correspond to the class in the content + * style sheet. It represents the highlighter in the user interface of the editor. + * + * ClassicEditor + * .create( editorElement, { + * highlight: { + * options: [ + * { + * model: 'pinkMarker', + * class: 'marker-pink', + * title: 'Pink Marker', + * color: '#ff6fff', + * type: 'marker' + * }, + * { + * model: 'redPen', + * class: 'pen-red', + * title: 'Red Pen', + * color: '#ff2929', + * type: 'pen' + * }, + * ] + * } + * } ) + * .then( ... ) + * .catch( ... ); + * + * @member {Array.} module:highlight/highlight~HighlightConfig#options + */ diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 2ad2bde..4a7aece 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ @@ -8,10 +8,17 @@ */ import Command from '@ckeditor/ckeditor5-core/src/command'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; /** * The highlight command. It is used by the {@link module:highlight/highlightediting~HighlightEditing highlight feature} - * to apply text highlighting. + * to apply the text highlighting. + * + * editor.execute( 'highlight', { value: 'greenMarker' } ); + * + * **Note**: Executing the command without the value removes the attribute from the model. If the selection is collapsed + * inside a text with the highlight attribute, the command will remove the attribute from the entire range + * of that text. * * @extends module:core/command~Command */ @@ -20,10 +27,19 @@ export default class HighlightCommand extends Command { * @inheritDoc */ refresh() { - const doc = this.editor.document; + const model = this.editor.model; + const doc = model.document; + /** + * A value indicating whether the command is active. If the selection has some highlight attribute, + * it corresponds to the value of that attribute. + * + * @observable + * @readonly + * @member {undefined|String} module:highlight/highlightcommand~HighlightCommand#value + */ this.value = doc.selection.getAttribute( 'highlight' ); - this.isEnabled = doc.schema.checkAttributeInSelection( doc.selection, 'highlight' ); + this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); } /** @@ -31,38 +47,56 @@ export default class HighlightCommand extends Command { * * @protected * @param {Object} [options] Options for the executed command. - * @param {String} options.class Name of highlighter class. - * @param {module:engine/model/batch~Batch} [options.batch] A batch to collect all the change steps. - * A new batch will be created if this option is not set. + * @param {String} [options.value] a value to apply. + * + * @fires execute */ execute( options = {} ) { - const doc = this.editor.document; - const selection = doc.selection; + const model = this.editor.model; + const document = model.document; + const selection = document.selection; + + const highlighter = options.value; + + model.change( writer => { + const ranges = model.schema.getValidRanges( selection.getRanges(), 'highlight' ); + + if ( selection.isCollapsed ) { + const position = selection.getFirstPosition(); - // Do not apply highlight no collapsed selection. - if ( selection.isCollapsed ) { - return; - } + // When selection is inside text with `highlight` attribute. + if ( selection.hasAttribute( 'highlight' ) ) { + // Find the full highlighted range. + const isSameHighlight = value => { + return value.item.hasAttribute( 'highlight' ) && value.item.getAttribute( 'highlight' ) === this.value; + }; - doc.enqueueChanges( () => { - const ranges = doc.schema.getValidRanges( selection.getRanges(), 'highlight' ); - const batch = options.batch || doc.batch(); + const highlightStart = position.getLastMatchingPosition( isSameHighlight, { direction: 'backward' } ); + const highlightEnd = position.getLastMatchingPosition( isSameHighlight ); - for ( const range of ranges ) { - if ( options.class ) { - batch.setAttribute( range, 'highlight', options.class ); - } else { - batch.removeAttribute( range, 'highlight' ); + const highlightRange = new Range( highlightStart, highlightEnd ); + + // Then depending on current value... + if ( !highlighter || this.value === highlighter ) { + // ...remove attribute when passing highlighter different then current or executing "eraser". + writer.removeAttribute( 'highlight', highlightRange ); + writer.removeSelectionAttribute( 'highlight' ); + } else { + // ...update `highlight` value. + writer.setAttribute( 'highlight', highlighter, highlightRange ); + } + } else if ( highlighter ) { + writer.setSelectionAttribute( 'highlight', highlighter ); + } + } else { + for ( const range of ranges ) { + if ( highlighter ) { + writer.setAttribute( 'highlight', highlighter, range ); + } else { + writer.removeAttribute( 'highlight', range ); + } } } } ); } } - -/** - * Holds current highlight class. If there is no highlight in selection then value will be undefined. - * - * @observable - * @readonly - * @member {undefined|String} module:highlight/highlightcommand~HighlightCommand#value - */ diff --git a/src/highlightediting.js b/src/highlightediting.js index 1293657..8fbd0aa 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ @@ -8,16 +8,15 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; - -import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter'; -import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; - -import AttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeelement'; +import { attributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/two-way-converters'; import HighlightCommand from './highlightcommand'; /** - * The highlight editing feature. It introduces `highlight` command which allow to highlight selected text with defined 'marker' or 'pen'. + * The highlight editing feature. It introduces the {@link module:highlight/highlightcommand~HighlightCommand command} and the `highlight` + * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view} + * as a `` element with the class attribute (`...`) depending + * on the {@link module:highlight/highlight~HighlightConfig configuration}. * * @extends module:core/plugin~Plugin */ @@ -28,13 +27,15 @@ export default class HighlightEditing extends Plugin { constructor( editor ) { super( editor ); - editor.config.define( 'highlight', [ - { class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, - { class: 'marker-green', title: 'Green Marker', color: '#66ff00', type: 'marker' }, - { class: 'marker-pink', title: 'Pink Marker', color: '#ff6fff', type: 'marker' }, - { class: 'pen-red', title: 'Red Pen', color: '#ff0000', type: 'pen' }, - { class: 'pen-blue', title: 'Blue Pen', color: '#0000ff', type: 'pen' } - ] ); + editor.config.define( 'highlight', { + options: [ + { model: 'marker', class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, + { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#66ff00', type: 'marker' }, + { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#ff6fff', type: 'marker' }, + { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#ff2929', type: 'pen' }, + { model: 'bluePen', class: 'pen-blue', title: 'Blue pen', color: '#0091ff', type: 'pen' } + ] + } ); } /** @@ -42,82 +43,29 @@ export default class HighlightEditing extends Plugin { */ init() { const editor = this.editor; - const data = editor.data; - const editing = editor.editing; - - // Allow highlight attribute on all elements - editor.document.schema.allow( { name: '$inline', attributes: 'highlight', inside: '$block' } ); - // Temporary workaround. See https://github.com/ckeditor/ckeditor5/issues/477. - editor.document.schema.allow( { name: '$inline', attributes: 'highlight', inside: '$clipboardHolder' } ); - // Convert highlight attribute to a mark element with associated class. - buildModelConverter() - .for( data.modelToView, editing.modelToView ) - .fromAttribute( 'highlight' ) - .toElement( data => new AttributeElement( 'mark', { class: data } ) ); + // Allow highlight attribute on text nodes. + editor.model.schema.extend( '$text', { allowAttributes: 'highlight' } ); - const configuredClasses = editor.config.get( 'highlight' ).map( config => config.class ); + const options = editor.config.get( 'highlight.options' ); - // Convert `mark` attribute with class name to model's highlight attribute. - buildViewConverter() - .for( data.viewToModel ) - .fromElement( 'mark' ) - .toAttribute( viewElement => { - for ( const className of viewElement.getClassNames() ) { - if ( configuredClasses.indexOf( className ) > -1 ) { - return { key: 'highlight', value: className }; - } - } - } ); + attributeToElement( editor.conversion, 'highlight', options.map( _getConverterDefinition ) ); editor.commands.add( 'highlight', new HighlightCommand( editor ) ); } } -/** - * Highlight option descriptor. - * - * @typedef {Object} module:highlight/highlightediting~HeadingOption - * @property {String} class The class which is used to differentiate highlighters. - * @property {String} title The user-readable title of the option. - * @property {String} color Color used for highlighter. Should be coherent with CSS class definition. - * @property {'marker'|'pen'} type The type of highlighter: - * - "marker" - will use #color as background, - * - "pen" - will use #color as font color. - */ - -/** - * The configuration of the {@link module:highlight/highlightediting~HighlightEditing Highlight feature}. - * - * Read more in {@link module:highlight/highlightediting~HighlightEditingConfig}. - * - * @member {module:highlight/highlightediting~HighlightEditingConfig} module:core/editor/editorconfig~EditorConfig#highlight - */ - -/** - * The configuration of the {@link module:highlight/highlightediting~HighlightEditing Highlight feature}. - * - * ClassicEditor - * .create( editorElement, { - * highlight: ... // Highlight feature config. - * } ) - * .then( ... ) - * .catch( ... ); - * - * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. - * - * @interface HighlightEditingConfig - */ - -/** - * Available highlighters options. - * - * There are two types of highlighters: - * - 'marker' - rendered as `` element with defined background color. - * - 'pen' - rendered as `` element with defined foreground (font) color. - * - * Note: Each highlighter must have it's own CSS class defined to properly match content data. Also it is advised - * that color value should match the values defined in content CSS stylesheet. - * - * @member {Array.} module:heading/heading~HeadingConfig#options - */ +// Converts {@link module:highlight/highlight~HighlightOption} +// to {@link module:engine/conversion/definition-based-converters~ConverterDefinition} +// +// @param {module:highlight/highlight~HighlightOption} option +// @returns {module:engine/conversion/definition-based-converters~ConverterDefinition} +function _getConverterDefinition( option ) { + return { + model: option.model, + view: { + name: 'mark', + class: option.class + } + }; +} diff --git a/src/highlightui.js b/src/highlightui.js index 92b796f..de95d9f 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ @@ -10,13 +10,63 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import HighlightEditing from './highlightediting'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +import markerIcon from './../theme/icons/marker.svg'; +import penIcon from './../theme/icons/pen.svg'; +import eraserIcon from './../theme/icons/eraser.svg'; + +import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; +import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview'; +import { createDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; + +import './../theme/highlight.css'; /** - * The default Highlight UI plugin. + * The default Highlight UI plugin. It introduces: + * * the `'highlightDropdown'` drop-down, + * * `'removeHighlight'` and `'highlight:*'` buttons. + * + * The default configuration includes the following buttons: + * * `'highlight:marker'` + * * `'highlight:pinkMarker'` + * * `'highlight:greenMarker'` + * * `'highlight:redPen'` + * * `'highlight:bluePen'` + * + * See the {@link module:highlight/highlight~HighlightConfig#options configuration} to learn more + * about the defaults. * * @extends module:core/plugin~Plugin */ export default class HighlightUI extends Plugin { + /** + * Returns the localized option titles provided by the plugin. + * + * The following localized titles corresponding with default + * {@link module:highlight/highlight~HighlightConfig#options} are available: + * + * * `'Marker'`, + * * `'Green marker'`, + * * `'Pink marker'`, + * * `'Blue pen'`. + * * `'Red pen'`. + * + * @readonly + * @type {Object.} + */ + get localizedOptionTitles() { + const t = this.editor.t; + + return { + 'Marker': t( 'Marker' ), + 'Green marker': t( 'Green marker' ), + 'Pink marker': t( 'Pink marker' ), + 'Red pen': t( 'Red pen' ), + 'Blue pen': t( 'Blue pen' ) + }; + } + /** * @inheritDoc */ @@ -30,4 +80,207 @@ export default class HighlightUI extends Plugin { static get pluginName() { return 'HighlightUI'; } + + /** + * @inheritDoc + */ + init() { + const options = this.editor.config.get( 'highlight.options' ); + + for ( const option of options ) { + this._addHighlighterButton( option ); + } + + this._addRemoveHighlightButton(); + + this._addDropdown( options ); + } + + /** + * Creates remove highlight button. + * + * @private + */ + _addRemoveHighlightButton() { + const t = this.editor.t; + + this._addButton( 'removeHighlight', t( 'Remove highlighting' ), eraserIcon ); + } + + /** + * Creates toolbar button from provided highlight option. + * + * @param {module:highlight/highlight~HighlightOption} option + * @private + */ + _addHighlighterButton( option ) { + const command = this.editor.commands.get( 'highlight' ); + + // TODO: change naming + this._addButton( 'highlight:' + option.model, option.title, getIconForType( option.type ), option.model, decorateHighlightButton ); + + function decorateHighlightButton( button ) { + button.bind( 'isEnabled' ).to( command, 'isEnabled' ); + button.bind( 'isOn' ).to( command, 'value', value => value === option.model ); + + button.extendTemplate( { + attributes: { + style: `color: ${ option.color }`, + class: 'ck-highlight-button' + } + } ); + } + } + + /** + * Internal method for creating highlight buttons. + * + * @param {String} name Name of a button. + * @param {String} label Label for button. + * @param {String} icon Button's icon. + * @param {Function} [decorateButton=()=>{}] Additional method for extending button. + * @private + */ + _addButton( name, label, icon, value, decorateButton = () => {} ) { + const editor = this.editor; + + editor.ui.componentFactory.add( name, locale => { + const buttonView = new ButtonView( locale ); + + const localized = this.localizedOptionTitles[ label ] ? this.localizedOptionTitles[ label ] : label; + + buttonView.set( { + label: localized, + icon, + tooltip: true + } ); + + buttonView.on( 'execute', () => { + editor.execute( 'highlight', { value } ); + editor.editing.view.focus(); + } ); + + // Add additional behavior for buttonView. + decorateButton( buttonView ); + + return buttonView; + } ); + } + + /** + * Creates split button dropdown UI from provided highlight options. + * + * @param {Array.} options + * @private + */ + _addDropdown( options ) { + const editor = this.editor; + const t = editor.t; + const componentFactory = editor.ui.componentFactory; + + const startingHighlighter = options[ 0 ]; + + const optionsMap = options.reduce( ( retVal, option ) => { + retVal[ option.model ] = option; + + return retVal; + }, {} ); + + componentFactory.add( 'highlightDropdown', locale => { + const command = editor.commands.get( 'highlight' ); + const dropdownView = createDropdown( locale, SplitButtonView ); + const splitButtonView = dropdownView.buttonView; + + splitButtonView.set( { + tooltip: t( 'Highlight' ), + // Holds last executed highlighter. + lastExecuted: startingHighlighter.model, + // Holds current highlighter to execute (might be different then last used). + commandValue: startingHighlighter.model + } ); + + // Dropdown button changes to selection (command.value): + // - If selection is in highlight it get active highlight appearance (icon, color) and is activated. + // - Otherwise it gets appearance (icon, color) of last executed highlight. + splitButtonView.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); + splitButtonView.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); + splitButtonView.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); + splitButtonView.bind( 'isOn' ).to( command, 'value', value => !!value ); + + splitButtonView.delegate( 'execute' ).to( dropdownView ); + + splitButtonView.extendTemplate( { + attributes: { + class: 'ck-highlight-button' + } + } ); + + // Create buttons array. + const buttons = options.map( option => { + // Get existing highlighter button. + const buttonView = componentFactory.create( 'highlight:' + option.model ); + + // Update lastExecutedHighlight on execute. + this.listenTo( buttonView, 'execute', () => dropdownView.buttonView.set( { lastExecuted: option.model } ) ); + + return buttonView; + } ); + + // Make toolbar button enabled when any button in dropdown is enabled before adding separator and eraser. + dropdownView.bind( 'isEnabled' ).toMany( buttons, 'isEnabled', ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); + + // Add separator and eraser buttons to dropdown. + buttons.push( new ToolbarSeparatorView() ); + buttons.push( componentFactory.create( 'removeHighlight' ) ); + + addToolbarToDropdown( dropdownView, buttons ); + + bindIconStyleToColor( dropdownView ); + + dropdownView.extendTemplate( { + attributes: { + class: [ 'ck-highlight-dropdown' ] + } + } ); + + // Execute current action from dropdown's split button action button. + splitButtonView.on( 'execute', () => { + editor.execute( 'highlight', { value: splitButtonView.commandValue } ); + editor.editing.view.focus(); + } ); + + // Returns active highlighter option depending on current command value. + // If current is not set or it is the same as last execute this method will return the option key (like icon or color) + // of last executed highlighter. Otherwise it will return option key for current one. + function getActiveOption( current, key ) { + const whichHighlighter = !current || + current === splitButtonView.lastExecuted ? splitButtonView.lastExecuted : current; + + return optionsMap[ whichHighlighter ][ key ]; + } + + return dropdownView; + } ); + } +} + +// Extends split button icon style to reflect last used button style. +function bindIconStyleToColor( dropdownView ) { + const actionView = dropdownView.buttonView.actionView; + + const bind = actionView.bindTemplate; + + // Color will propagate to iconView. + actionView.extendTemplate( { + attributes: { + style: bind.to( 'color', color => `color:${ color }` ) + } + } ); + + actionView.bind( 'color' ).to( dropdownView.buttonView, 'color' ); +} + +// Returns icon for given highlighter type. +function getIconForType( type ) { + return type === 'marker' ? markerIcon : penIcon; } diff --git a/tests/highlight.js b/tests/highlight.js index 1f1daea..db84159 100644 --- a/tests/highlight.js +++ b/tests/highlight.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index df20344..a1badc6 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ @@ -8,22 +8,30 @@ import HighlightCommand from './../src/highlightcommand'; import Command from '@ckeditor/ckeditor5-core/src/command'; import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import { getData, setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; describe( 'HighlightCommand', () => { - let editor, doc, command; + let editor, model, doc, root, command; beforeEach( () => { return ModelTestEditor.create() .then( newEditor => { - doc = newEditor.document; - command = new HighlightCommand( newEditor ); editor = newEditor; + model = editor.model; + doc = model.document; + root = doc.getRoot(); + command = new HighlightCommand( newEditor ); editor.commands.add( 'highlight', command ); - doc.schema.registerItem( 'paragraph', '$block' ); - - doc.schema.allow( { name: '$inline', attributes: 'highlight', inside: '$block' } ); + model.schema.register( 'p', { inheritAllFrom: '$block' } ); + model.schema.addAttributeCheck( ( ctx, attributeName ) => { + // Allow 'highlight' on p>$text. + if ( ctx.endsWith( 'p $text' ) && attributeName == 'highlight' ) { + return true; + } + } ); } ); } ); @@ -38,94 +46,210 @@ describe( 'HighlightCommand', () => { describe( 'value', () => { it( 'is set to highlight attribute value when selection is in text with highlight attribute', () => { - setData( doc, '<$text highlight="marker">fo[]o' ); + setData( model, '

<$text highlight="marker">fo[o]

' ); expect( command ).to.have.property( 'value', 'marker' ); } ); it( 'is undefined when selection is not in text with highlight attribute', () => { - setData( doc, 'fo[]o' ); + setData( model, '

fo[]o

' ); expect( command ).to.have.property( 'value', undefined ); } ); } ); describe( 'isEnabled', () => { + beforeEach( () => { + model.schema.register( 'x', { inheritAllFrom: '$block' } ); + } ); + it( 'is true when selection is on text which can have highlight added', () => { - setData( doc, 'fo[]o' ); + setData( model, '

fo[]o

' ); expect( command ).to.have.property( 'isEnabled', true ); } ); + + it( 'is false when selection is on text which can not have highlight added', () => { + setData( model, 'fo[]o' ); + expect( command.isEnabled ).to.be.false; + } ); } ); describe( 'execute()', () => { - it( 'should add highlight attribute on selected nodes nodes when passed as parameter', () => { - setData( doc, 'a[bc<$text highlight="marker">fo]obarxyz' ); + describe( 'with option.value set', () => { + describe( 'on collapsed range', () => { + it( 'should change entire highlight when inside highlighted text', () => { + setData( model, '

abc<$text highlight="marker">foo[]barxyz

' ); - expect( command.value ).to.be.undefined; + expect( command.value ).to.equal( 'marker' ); - command.execute( { class: 'marker' } ); + command.execute( { value: 'greenMarker' } ); - expect( command.value ).to.equal( 'marker' ); + expect( getData( model ) ).to.equal( '

abc<$text highlight="greenMarker">foo[]barxyz

' ); - expect( getData( doc ) ).to.equal( 'a[<$text highlight="marker">bcfo]obarxyz' ); - } ); + expect( command.value ).to.equal( 'greenMarker' ); + } ); - it( 'should add highlight attribute on selected nodes nodes when passed as parameter (multiple nodes)', () => { - setData( - doc, - 'abcabc[abc' + - 'foofoofoo' + - 'barbar]bar' - ); + it( 'should remove entire highlight when inside highlighted text of the same value', () => { + setData( model, '

abc<$text highlight="marker">foo[]barxyz

' ); - command.execute( { class: 'marker' } ); + expect( command.value ).to.equal( 'marker' ); - expect( command.value ).to.equal( 'marker' ); + command.execute( { value: 'marker' } ); - expect( getData( doc ) ).to.equal( - 'abcabc[<$text highlight="marker">abc' + - '<$text highlight="marker">foofoofoo' + - '<$text highlight="marker">barbar]bar' - ); - } ); + expect( getData( model ) ).to.equal( '

abcfoo[]barxyz

' ); - it( 'should set highlight attribute on selected nodes when passed as parameter', () => { - setData( doc, 'abc[<$text highlight="marker">foo]barxyz' ); + expect( command.value ).to.be.undefined; + } ); - expect( command.value ).to.equal( 'marker' ); + it( 'should change selection attribute in non-empty parent', () => { + setData( model, '

a[]bc<$text highlight="marker">foobarxyz

' ); + expect( command.value ).to.be.undefined; - command.execute( { class: 'foo' } ); + command.execute( { value: 'foo' } ); + expect( command.value ).to.equal( 'foo' ); - expect( getData( doc ) ).to.equal( - 'abc[<$text highlight="foo">foo]<$text highlight="marker">barxyz' - ); + expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.true; - expect( command.value ).to.equal( 'foo' ); - } ); + command.execute(); + + expect( command.value ).to.be.undefined; + expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.false; + + // Execute remove highlight on selection without 'highlight' attribute should do nothing. + command.execute(); + + expect( command.value ).to.be.undefined; + expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.false; + } ); + + it( 'should not store attribute change on selection if selection is collapsed in non-empty parent', () => { + setData( model, '

a[]bc<$text highlight="marker">foobarxyz

' ); + + command.execute( { value: 'foo' } ); + + // It should not save that bold was executed at position ( root, [ 0, 1 ] ). + + model.change( writer => { + // Simulate clicking right arrow key by changing selection ranges. + writer.setSelection( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + + // Get back to previous selection. + writer.setSelection( [ new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 1 ] ) ) ] ); + } ); + + expect( command.value ).to.be.undefined; + } ); + + it( 'should change selection attribute and store it if selection is collapsed in empty parent', () => { + setData( model, '

abc<$text highlight="marker">foobarxyz

[]

' ); + + expect( command.value ).to.be.undefined; + + command.execute( { value: 'foo' } ); + + expect( command.value ).to.equal( 'foo' ); + expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.true; + + // Attribute should be stored. + // Simulate clicking somewhere else in the editor. + model.change( writer => { + writer.setSelection( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + } ); - it( 'should remove highlight attribute on selected nodes nodes when undefined passed as parameter', () => { - setData( doc, 'abc[<$text highlight="marker">foo]barxyz' ); + expect( command.value ).to.be.undefined; - expect( command.value ).to.equal( 'marker' ); + // Go back to where attribute was stored. + model.change( writer => { + writer.setSelection( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] ); + } ); - command.execute(); + // Attribute should be restored. + expect( command.value ).to.equal( 'foo' ); - expect( getData( doc ) ).to.equal( 'abc[foo]<$text highlight="marker">barxyz' ); + command.execute(); - expect( command.value ).to.be.undefined; + expect( command.value ).to.be.undefined; + expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.false; + } ); + } ); + + describe( 'on not collapsed range', () => { + it( 'should set highlight attribute on selected node when passed as parameter', () => { + setData( model, '

a[bc<$text highlight="marker">fo]obarxyz

' ); + + expect( command.value ).to.be.undefined; + + command.execute( { value: 'marker' } ); + + expect( command.value ).to.equal( 'marker' ); + + expect( getData( model ) ).to.equal( '

a[<$text highlight="marker">bcfo]obarxyz

' ); + } ); + + it( 'should set highlight attribute on selected node when passed as parameter (multiple nodes)', () => { + setData( + model, + '

abcabc[abc

' + + '

foofoofoo

' + + '

barbar]bar

' + ); + + command.execute( { value: 'marker' } ); + + expect( command.value ).to.equal( 'marker' ); + + expect( getData( model ) ).to.equal( + '

abcabc[<$text highlight="marker">abc

' + + '

<$text highlight="marker">foofoofoo

' + + '

<$text highlight="marker">barbar]bar

' + ); + } ); + + it( 'should set highlight attribute on selected nodes when passed as parameter only on selected characters', () => { + setData( model, '

abc[<$text highlight="marker">foo]barxyz

' ); + + expect( command.value ).to.equal( 'marker' ); + + command.execute( { value: 'foo' } ); + + expect( getData( model ) ).to.equal( + '

abc[<$text highlight="foo">foo]<$text highlight="marker">barxyz

' + ); + + expect( command.value ).to.equal( 'foo' ); + } ); + } ); } ); - it( 'should do nothing on collapsed range', () => { - setData( doc, 'abc<$text highlight="marker">foo[]barxyz' ); + describe( 'with undefined option.value', () => { + describe( 'on collapsed range', () => { + it( 'should remove entire highlight when inside highlighted text', () => { + setData( model, '

abc<$text highlight="marker">foo[]barxyz

' ); + + expect( command.value ).to.equal( 'marker' ); - expect( command.value ).to.equal( 'marker' ); + command.execute(); - command.execute(); + expect( getData( model ) ).to.equal( '

abcfoo[]barxyz

' ); - expect( getData( doc ) ).to.equal( 'abc<$text highlight="marker">foo[]barxyz' ); + expect( command.value ).to.be.undefined; + } ); + } ); - expect( command.value ).to.equal( 'marker' ); + describe( 'on not collapsed range', () => { + it( 'should remove highlight attribute on selected node when undefined passed as parameter', () => { + setData( model, '

abc[<$text highlight="marker">foo]barxyz

' ); + + expect( command.value ).to.equal( 'marker' ); + + command.execute(); + + expect( getData( model ) ).to.equal( '

abc[foo]<$text highlight="marker">barxyz

' ); + + expect( command.value ).to.be.undefined; + } ); + } ); } ); } ); } ); diff --git a/tests/highlightediting.js b/tests/highlightediting.js index 3c32090..8de135e 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ @@ -12,7 +12,7 @@ import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtest import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'HighlightEditing', () => { - let editor, doc; + let editor, model; beforeEach( () => { return VirtualTestEditor @@ -22,7 +22,7 @@ describe( 'HighlightEditing', () => { .then( newEditor => { editor = newEditor; - doc = editor.document; + model = editor.model; } ); } ); @@ -31,48 +31,50 @@ describe( 'HighlightEditing', () => { } ); it( 'should set proper schema rules', () => { - expect( doc.schema.check( { name: '$inline', attributes: 'highlight', inside: '$block' } ) ).to.be.true; - expect( doc.schema.check( { name: '$inline', attributes: 'highlight', inside: '$clipboardHolder' } ) ).to.be.true; + expect( editor.model.schema.checkAttribute( [ '$block', '$text' ], 'highlight' ) ).to.be.true; + expect( editor.model.schema.checkAttribute( [ '$clipboardHolder', '$text' ], 'highlight' ) ).to.be.true; + + expect( editor.model.schema.checkAttribute( [ '$block' ], 'highlight' ) ).to.be.false; } ); - it( 'adds highlight commands', () => { + it( 'adds highlight command', () => { expect( editor.commands.get( 'highlight' ) ).to.be.instanceOf( HighlightCommand ); } ); describe( 'data pipeline conversions', () => { it( 'should convert defined marker classes', () => { const data = '

foo

'; - editor.setData( data ); - expect( getModelData( doc ) ).to.equal( '[]f<$text highlight="marker">oo' ); + expect( getModelData( model ) ).to.equal( '[]f<$text highlight="marker">oo' ); expect( editor.getData() ).to.equal( data ); } ); + it( 'should convert only one defined marker classes', () => { editor.setData( '

foo

' ); - expect( getModelData( doc ) ).to.equal( '[]f<$text highlight="marker-green">oo' ); - expect( editor.getData() ).to.equal( '

foo

' ); + expect( getModelData( model ) ).to.equal( '[]f<$text highlight="marker">oo' ); + expect( editor.getData() ).to.equal( '

foo

' ); } ); it( 'should not convert undefined marker classes', () => { editor.setData( '

foo

' ); - expect( getModelData( doc ) ).to.equal( '[]foo' ); + expect( getModelData( model ) ).to.equal( '[]foo' ); expect( editor.getData() ).to.equal( '

foo

' ); } ); it( 'should not convert marker without class', () => { editor.setData( '

foo

' ); - expect( getModelData( doc ) ).to.equal( '[]foo' ); + expect( getModelData( model ) ).to.equal( '[]foo' ); expect( editor.getData() ).to.equal( '

foo

' ); } ); } ); describe( 'editing pipeline conversion', () => { it( 'should convert mark element with defined class', () => { - setModelData( doc, 'f<$text highlight="marker">oo' ); + setModelData( model, 'f<$text highlight="marker">oo' ); expect( editor.getData() ).to.equal( '

foo

' ); } ); @@ -81,13 +83,15 @@ describe( 'HighlightEditing', () => { describe( 'config', () => { describe( 'default value', () => { it( 'should be set', () => { - expect( editor.config.get( 'highlight' ) ).to.deep.equal( [ - { class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, - { class: 'marker-green', title: 'Green Marker', color: '#66ff00', type: 'marker' }, - { class: 'marker-pink', title: 'Pink Marker', color: '#ff6fff', type: 'marker' }, - { class: 'pen-red', title: 'Red Pen', color: '#ff0000', type: 'pen' }, - { class: 'pen-blue', title: 'Blue Pen', color: '#0000ff', type: 'pen' } - ] ); + expect( editor.config.get( 'highlight' ) ).to.deep.equal( { + options: [ + { model: 'marker', class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, + { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#66ff00', type: 'marker' }, + { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#ff6fff', type: 'marker' }, + { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#ff2929', type: 'pen' }, + { model: 'bluePen', class: 'pen-blue', title: 'Blue pen', color: '#0091ff', type: 'pen' } + ] + } ); } ); } ); } ); diff --git a/tests/highlightui.js b/tests/highlightui.js new file mode 100644 index 0000000..a44ad8d --- /dev/null +++ b/tests/highlightui.js @@ -0,0 +1,228 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* global document */ + +import HighlightEditing from '../src/highlightediting'; +import HighlightUI from '../src/highlightui'; + +import markerIcon from '../theme/icons/marker.svg'; +import penIcon from '../theme/icons/pen.svg'; +import eraserIcon from '../theme/icons/eraser.svg'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { _clear as clearTranslations, add as addTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; + +testUtils.createSinonSandbox(); + +describe( 'HighlightUI', () => { + let editor, command, element; + + before( () => { + addTranslations( 'en', { + 'Highlight': 'Highlight', + 'Marker': 'Marker', + 'Green marker': 'Green marker', + 'Pink marker': 'Pink marker', + 'Red pen': 'Red pen', + 'Blue pen': 'Blue pen', + 'Remove highlighting': 'Remove highlighting' + } ); + + addTranslations( 'pl', { + 'Highlight': 'Zakreślacz', + 'Marker': 'Marker', + 'Green marker': 'Zielony marker', + 'Pink marker': 'Różowy marker', + 'Red pen': 'Czerwony długopis', + 'Blue pen': 'Niebieski długopis', + 'Remove highlighting': 'Usuń zaznaczenie' + } ); + } ); + + after( () => { + clearTranslations(); + } ); + + beforeEach( () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + return ClassicTestEditor + .create( element, { + plugins: [ HighlightEditing, HighlightUI ] + } ) + .then( newEditor => { + editor = newEditor; + } ); + } ); + + afterEach( () => { + element.remove(); + + return editor.destroy(); + } ); + + describe( 'highlight Dropdown', () => { + let dropdown; + + beforeEach( () => { + command = editor.commands.get( 'highlight' ); + dropdown = editor.ui.componentFactory.create( 'highlightDropdown' ); + } ); + + it( 'button has the base properties', () => { + const button = dropdown.buttonView; + + expect( button ).to.have.property( 'tooltip', 'Highlight' ); + expect( button ).to.have.property( 'icon', markerIcon ); + } ); + + it( 'should add custom CSS class to dropdown and dropdown buttons', () => { + dropdown.render(); + + expect( dropdown.element.classList.contains( 'ck-highlight-dropdown' ) ).to.be.true; + expect( dropdown.buttonView.element.classList.contains( 'ck-highlight-button' ) ).to.be.true; + // There should be 5 highlight buttons, one separator and highlight remove button in toolbar. + expect( dropdown.toolbarView.items.map( button => button.element.classList.contains( 'ck-highlight-button' ) ) ) + .to.deep.equal( [ true, true, true, true, true, false, false ] ); + } ); + + it( 'should have proper icons in dropdown', () => { + const toolbar = dropdown.toolbarView; + + // Not in a selection with highlight. + command.value = undefined; + + expect( toolbar.items.map( item => item.icon ) ) + .to.deep.equal( [ markerIcon, markerIcon, markerIcon, penIcon, penIcon, undefined, eraserIcon ] ); + } ); + + it( 'should activate current option in dropdown', () => { + const toolbar = dropdown.toolbarView; + + // Not in a selection with highlight. + command.value = undefined; + + expect( toolbar.items.map( item => item.isOn ) ) + .to.deep.equal( [ false, false, false, false, false, undefined, false ] ); + + // Inside a selection with highlight. + command.value = 'greenMarker'; + + // The second item is 'greenMarker' highlighter. + expect( toolbar.items.map( item => item.isOn ) ).to.deep.equal( [ false, true, false, false, false, undefined, false ] ); + } ); + + describe( 'toolbar button behavior', () => { + let button, buttons, options; + + beforeEach( () => { + button = dropdown.buttonView; + buttons = dropdown.toolbarView.items.map( b => b ); + options = editor.config.get( 'highlight.options' ); + } ); + + function validateButton( which ) { + expect( button.icon ).to.equal( buttons[ which ].icon ); + expect( button.actionView.color ).to.equal( options[ which ].color ); + } + + it( 'should have properties of first defined highlighter', () => { + validateButton( 0 ); + } ); + + it( 'should change button on selection', () => { + command.value = 'redPen'; + + validateButton( 3 ); + + command.value = undefined; + + validateButton( 0 ); + } ); + + it( 'should change button on execute option', () => { + command.value = 'marker'; + validateButton( 0 ); + + buttons[ 4 ].fire( 'execute' ); + command.value = 'bluePen'; + + // Simulate selection moved to not highlighted text. + command.value = undefined; + + validateButton( 4 ); + } ); + + it( 'should focus view after command execution', () => { + const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); + + dropdown.buttonView.commandName = 'highlight'; + dropdown.buttonView.fire( 'execute' ); + + sinon.assert.calledOnce( focusSpy ); + } ); + } ); + + describe( 'model to command binding', () => { + it( 'isEnabled', () => { + command.isEnabled = false; + + expect( dropdown.buttonView.isEnabled ).to.be.false; + + command.isEnabled = true; + expect( dropdown.buttonView.isEnabled ).to.be.true; + } ); + } ); + + describe( 'localization', () => { + beforeEach( () => { + return localizedEditor(); + } ); + + it( 'works for the #buttonView', () => { + const buttonView = dropdown.buttonView; + + expect( buttonView.tooltip ).to.equal( 'Zakreślacz' ); + } ); + + it( 'works for the listView#items in the panel', () => { + const listView = dropdown.toolbarView; + + expect( listView.items.map( item => item.label ).filter( label => !!label ) ).to.deep.equal( [ + 'Marker', + 'Zielony marker', + 'Różowy marker', + 'Czerwony długopis', + 'Niebieski długopis', + 'Usuń zaznaczenie' + ] ); + } ); + + function localizedEditor() { + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ HighlightEditing, HighlightUI ], + toolbar: [ 'highlight' ], + language: 'pl' + } ) + .then( newEditor => { + editor = newEditor; + dropdown = editor.ui.componentFactory.create( 'highlightDropdown' ); + command = editor.commands.get( 'highlight' ); + + editorElement.remove(); + + return editor.destroy(); + } ); + } + } ); + } ); +} ); diff --git a/tests/integration.js b/tests/integration.js index 1fd3f40..3a18367 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -1,5 +1,5 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ @@ -19,7 +19,7 @@ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictest import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'Highlight', () => { - let editor, doc, element; + let editor, model, element; beforeEach( () => { element = document.createElement( 'div' ); @@ -31,7 +31,7 @@ describe( 'Highlight', () => { } ) .then( newEditor => { editor = newEditor; - doc = editor.document; + model = editor.model; } ); } ); @@ -42,24 +42,24 @@ describe( 'Highlight', () => { } ); describe( 'compatibility with images', () => { - it( 'does not work inside image caption', () => { - setModelData( doc, 'foo[bar]baz' ); + it( 'does work inside image caption', () => { + setModelData( model, 'foo[bar]baz' ); - editor.execute( 'highlight', { class: 'marker' } ); + editor.execute( 'highlight', { value: 'marker' } ); - expect( getModelData( doc ) ) + expect( getModelData( model ) ) .to.equal( 'foo[<$text highlight="marker">bar]baz' ); } ); - it( 'does not work on selection with image', () => { + it( 'does work on selection with image', () => { setModelData( - doc, + model, 'foo[fooabcbar]bar' ); - editor.execute( 'highlight', { class: 'marker' } ); + editor.execute( 'highlight', { value: 'marker' } ); - expect( getModelData( doc ) ).to.equal( + expect( getModelData( model ) ).to.equal( 'foo[<$text highlight="marker">foo' + '<$text highlight="marker">abc' + '<$text highlight="marker">bar]bar' diff --git a/tests/manual/highlight-buttons.html b/tests/manual/highlight-buttons.html new file mode 100644 index 0000000..8c99f1e --- /dev/null +++ b/tests/manual/highlight-buttons.html @@ -0,0 +1,39 @@ + + +
+

Highlight feature example.

+

+ Here are some markers: + yellow one, pink one and green one. +

+

+ Here are some pens: + red pen and blue one. +

+
+ CKEditor logo +
Some image with caption and highlighted text.
+
+
diff --git a/tests/manual/highlight-buttons.js b/tests/manual/highlight-buttons.js new file mode 100644 index 0000000..5aba8b8 --- /dev/null +++ b/tests/manual/highlight-buttons.js @@ -0,0 +1,25 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; +import Highlight from '../../src/highlight'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ ArticlePluginSet, Highlight ], + toolbar: [ + 'highlight:marker', 'highlight:pinkMarker', 'highlight:greenMarker', 'highlight:redPen', 'highlight:bluePen', 'removeHighlight', + '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/highlight-buttons.md b/tests/manual/highlight-buttons.md new file mode 100644 index 0000000..a39d5bd --- /dev/null +++ b/tests/manual/highlight-buttons.md @@ -0,0 +1,19 @@ +### Loading + +1. The data should be loaded with different markers and pens. +2. The toolbar should have 5 highlight buttons and one remove highlight button. + +### Testing + +You should be able to: +- see different markers class +- manually invoke highlight command in console: + +``` +editor.execute( 'highlight', { class: 'marker' } ); +editor.execute( 'highlight', { class: 'marker-green' } ); +editor.execute( 'highlight', { class: 'marker-pink' } ); + +editor.execute( 'highlight', { class: 'pen-red' } ); +editor.execute( 'highlight', { class: 'pen-blue' } ); +``` diff --git a/tests/manual/highlight.html b/tests/manual/highlight.html index f9de287..8c99f1e 100644 --- a/tests/manual/highlight.html +++ b/tests/manual/highlight.html @@ -13,21 +13,27 @@ .pen-red { background-color: transparent; - color: #ff0000; + color: #ff2929; } .pen-blue { background-color: transparent; - color: #0000ff; + color: #0091ff; } - +

Highlight feature example.

-

Here ares some markers: +

+ Here are some markers: yellow one, pink one and green one.

-

Here ares some pens: +

+ Here are some pens: red pen and blue one.

+
+ CKEditor logo +
Some image with caption and highlighted text.
+
diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 2884081..422e491 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -1,19 +1,19 @@ /** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ /* globals console, window, document */ -import ClassicEditor from '../../../ckeditor5-editor-classic/src/classiceditor'; -import ArticlePluginSet from '../../../ckeditor5-core/tests/_utils/articlepluginset'; +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset'; import Highlight from '../../src/highlight'; ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ ArticlePluginSet, Highlight ], toolbar: [ - 'headings', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + 'headings', '|', 'highlightDropdown', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) .then( editor => { diff --git a/tests/manual/sample.jpg b/tests/manual/sample.jpg new file mode 100644 index 0000000..b77d07e Binary files /dev/null and b/tests/manual/sample.jpg differ diff --git a/theme/highlight.css b/theme/highlight.css new file mode 100644 index 0000000..4a06722 --- /dev/null +++ b/theme/highlight.css @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +.ck-highlight-button svg path:not(.ck-icon__fill) { + /* Do not inherit color from parent. */ + fill: initial; +} diff --git a/theme/icons/eraser.svg b/theme/icons/eraser.svg new file mode 100644 index 0000000..397657a --- /dev/null +++ b/theme/icons/eraser.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/theme/icons/marker.svg b/theme/icons/marker.svg new file mode 100644 index 0000000..ca6b76b --- /dev/null +++ b/theme/icons/marker.svg @@ -0,0 +1 @@ + diff --git a/theme/icons/pen.svg b/theme/icons/pen.svg new file mode 100644 index 0000000..e77bdc6 --- /dev/null +++ b/theme/icons/pen.svg @@ -0,0 +1 @@ +