From d951359f0cfddebdf834ac392ff74a4ba929557d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 22 Nov 2017 13:01:59 +0100 Subject: [PATCH 01/80] Other: Initial split button dropdown for HighlightUI. --- src/highlightui.js | 99 +++++++++++++++++++++++++++++++++++++++ tests/manual/highlight.js | 4 +- 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/highlightui.js b/src/highlightui.js index 92b796f..624cff9 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -10,6 +10,13 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import HighlightEditing from './highlightediting'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; + +import highlightIcon from '@ckeditor/ckeditor5-core/theme/icons/input.svg'; +import highlightRemoveIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; + +import Model from '../../ckeditor5-ui/src/model'; +import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/button/createsplitbuttondropdown'; /** * The default Highlight UI plugin. @@ -30,4 +37,96 @@ export default class HighlightUI extends Plugin { static get pluginName() { return 'HighlightUI'; } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const highlighters = editor.config.get( 'highlight' ); + + for ( const highlighter of highlighters ) { + this._addButton( highlighter ); + } + + // Add rubber button + const componentFactory = editor.ui.componentFactory; + + componentFactory.add( 'highlightRemove', locale => { + const buttonView = new ButtonView( locale ); + + buttonView.set( { + label: 'Remove highlighting', + icon: highlightRemoveIcon, + tooltip: true + } ); + + this.listenTo( buttonView, 'execute', () => editor.execute( 'highlight' ) ); + + return buttonView; + } ); + + // Add highlight dropdown + + componentFactory.add( 'highlightDropdown', locale => { + const model = new Model( { + label: 'Highlight', + withText: false, + selected: highlighters[ 0 ].class, + icon: highlightIcon + } ); + + const buttons = highlighters.map( highlighter => componentFactory.create( 'highlight-' + highlighter.class ) ); + + buttons.push( componentFactory.create( 'highlightRemove' ) ); + + const buttonView = componentFactory.create( 'highlight-' + highlighters[ 0 ].class ); + + const dropdown = createSplitButtonDropdown( model, buttons, locale, buttonView ); + + buttons.map( buttonView => { + this.listenTo( buttonView, 'execute', () => { + if ( dropdown.buttonView.buttonView.class !== buttonView.class ) { + const newButton = componentFactory.create( 'highlight-' + buttonView.class ); + + dropdown.buttonView.swapButton( newButton ); + } + } ); + } ); + + return dropdown; + } ); + } + + _addButton( highlighter ) { + const editor = this.editor; + + const command = editor.commands.get( 'highlight' ); + + editor.ui.componentFactory.add( 'highlight-' + highlighter.class, locale => { + const buttonView = new ButtonView( locale ); + + buttonView.set( { + label: highlighter.title, + icon: highlightIcon, + tooltip: true, + class: highlighter.class + } ); + // Bind button model to command. + buttonView.bind( 'isEnabled' ).to( command, 'isEnabled' ); + + buttonView.bind( 'isOn' ).to( command, 'value', value => { + return value === highlighter.class; + } ); + + // Execute command. + this.listenTo( buttonView, 'execute', () => editor.execute( 'highlight', { class: highlighter.class } ) ); + + buttonView.iconView.extendTemplate( { + attributes: { style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } } + } ); + + return buttonView; + } ); + } } diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 2884081..293fc48 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -13,7 +13,9 @@ 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 => { From f74606775b90b9756807c09a2b5c290f8dd88e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 22 Nov 2017 14:25:46 +0100 Subject: [PATCH 02/80] Other: Make highlight dropdown hide after using rubber. --- src/highlightui.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 624cff9..77999eb 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -61,7 +61,11 @@ export default class HighlightUI extends Plugin { tooltip: true } ); - this.listenTo( buttonView, 'execute', () => editor.execute( 'highlight' ) ); + this.listenTo( buttonView, 'execute', () => { + // TODO: minor duplication of code + editor.execute( 'highlight' ); + editor.editing.view.focus(); + } ); return buttonView; } ); @@ -120,7 +124,10 @@ export default class HighlightUI extends Plugin { } ); // Execute command. - this.listenTo( buttonView, 'execute', () => editor.execute( 'highlight', { class: highlighter.class } ) ); + this.listenTo( buttonView, 'execute', () => { + editor.execute( 'highlight', { class: highlighter.class } ); + editor.editing.view.focus(); + } ); buttonView.iconView.extendTemplate( { attributes: { style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } } From 1c5b80e20bdffac3106e9a68968c1edc6ed49441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 22 Nov 2017 17:22:54 +0100 Subject: [PATCH 03/80] Other: Make createSplitButtonDropdown minimal. --- src/highlightui.js | 70 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 77999eb..8688813 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -15,8 +15,10 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import highlightIcon from '@ckeditor/ckeditor5-core/theme/icons/input.svg'; import highlightRemoveIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; -import Model from '../../ckeditor5-ui/src/model'; -import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/button/createsplitbuttondropdown'; +import Model from '@ckeditor/ckeditor5-ui/src/model'; +import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/createsplitbuttondropdown'; +import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownItemsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; +import ButtonGroupView from '@ckeditor/ckeditor5-ui/src/buttongroup/buttongroupview'; /** * The default Highlight UI plugin. @@ -70,8 +72,7 @@ export default class HighlightUI extends Plugin { return buttonView; } ); - // Add highlight dropdown - + // Add highlight dropdown: componentFactory.add( 'highlightDropdown', locale => { const model = new Model( { label: 'Highlight', @@ -86,19 +87,64 @@ export default class HighlightUI extends Plugin { const buttonView = componentFactory.create( 'highlight-' + highlighters[ 0 ].class ); - const dropdown = createSplitButtonDropdown( model, buttons, locale, buttonView ); + // TODO: move this out of this method (related to button only design) Make disabled when all buttons are disabled + model.bind( 'isEnabled' ).to( + // Bind to #isEnabled of each command... + ...getBindingTargets( buttons, 'isEnabled' ), + // ...and set it true if any command #isEnabled is true. + ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) + ); + + // TODO: Is this needed in UI at all? + const dropdownView = createSplitButtonDropdown( model, buttons, locale, buttonView ); + + const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: model.isVertical } ); + + buttonGroupView.bind( 'isVertical' ).to( model, 'isVertical' ); + + buttons.map( view => buttonGroupView.items.add( view ) ); + + dropdownView.extendTemplate( { + attributes: { + class: [ + 'ck-splitbutton-dropdown' + ] + } + } ); + + dropdownView.buttonView.extendTemplate( { + attributes: { + class: [ 'ck-button-dropdown' ] + } + } ); + + dropdownView.panelView.children.add( buttonGroupView ); + + closeDropdownOnBlur( dropdownView ); + closeDropdownOnExecute( dropdownView, buttonGroupView.items ); + focusDropdownItemsOnArrows( dropdownView, buttonGroupView ); + + // TODO: weak names buttonView.buttonView + // TODO: could be move to createSplitButtonDropdown + dropdownView.buttonView.arrowView.on( 'execute', () => { + if ( dropdownView.buttonView.buttonView.isEnabled && !dropdownView.isOpen ) { + dropdownView.isOpen = true; + buttonGroupView.focus(); + } + } ); + // TODO: A bit hack-ish: Swap the split button button to executed one. buttons.map( buttonView => { this.listenTo( buttonView, 'execute', () => { - if ( dropdown.buttonView.buttonView.class !== buttonView.class ) { - const newButton = componentFactory.create( 'highlight-' + buttonView.class ); + if ( dropdownView.buttonView.buttonView.class !== buttonView.class ) { + const newButton = componentFactory.create( buttonView.class ? 'highlight-' + buttonView.class : 'highlightRemove' ); - dropdown.buttonView.swapButton( newButton ); + dropdownView.buttonView.swapButton( newButton ); } } ); } ); - return dropdown; + return dropdownView; } ); } @@ -116,6 +162,7 @@ export default class HighlightUI extends Plugin { tooltip: true, class: highlighter.class } ); + // Bind button model to command. buttonView.bind( 'isEnabled' ).to( command, 'isEnabled' ); @@ -137,3 +184,8 @@ export default class HighlightUI extends Plugin { } ); } } + +// TODO: this is duplicated +function getBindingTargets( buttons, attribute ) { + return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) ); +} From ca89fc6bfe3d34d2d99b716d8b159dd4b97fc4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 23 Nov 2017 11:00:07 +0100 Subject: [PATCH 04/80] Other: Extract Highlight UI components factories to methods. --- src/highlightui.js | 86 ++++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 8688813..17bc949 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -44,38 +44,59 @@ export default class HighlightUI extends Plugin { * @inheritDoc */ init() { - const editor = this.editor; - const highlighters = editor.config.get( 'highlight' ); + const highlighters = this.editor.config.get( 'highlight' ); for ( const highlighter of highlighters ) { this._addButton( highlighter ); } - // Add rubber button - const componentFactory = editor.ui.componentFactory; + this._addRubberButton(); + + this._addDropdown( highlighters ); + } + + _addButton( highlighter ) { + const editor = this.editor; + const command = editor.commands.get( 'highlight' ); - componentFactory.add( 'highlightRemove', locale => { + editor.ui.componentFactory.add( 'highlight-' + highlighter.class, locale => { const buttonView = new ButtonView( locale ); buttonView.set( { - label: 'Remove highlighting', - icon: highlightRemoveIcon, - tooltip: true + label: highlighter.title, + icon: highlightIcon, + tooltip: true, + // TODO: how to pass this & name + class: highlighter.class } ); + // Bind button model to command. + buttonView.bind( 'isEnabled' ).to( command, 'isEnabled' ); + buttonView.bind( 'isOn' ).to( command, 'value', value => value === highlighter.class ); + + // Execute command. this.listenTo( buttonView, 'execute', () => { - // TODO: minor duplication of code - editor.execute( 'highlight' ); + editor.execute( 'highlight', { class: highlighter.class } ); editor.editing.view.focus(); } ); + // TODO: + buttonView.iconView.extendTemplate( { + attributes: { style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } } + } ); + return buttonView; } ); + } + + _addDropdown( highlighters ) { + const editor = this.editor; + const t = editor.t; + const componentFactory = editor.ui.componentFactory; - // Add highlight dropdown: componentFactory.add( 'highlightDropdown', locale => { const model = new Model( { - label: 'Highlight', + label: t( 'Highlight' ), withText: false, selected: highlighters[ 0 ].class, icon: highlightIcon @@ -87,7 +108,6 @@ export default class HighlightUI extends Plugin { const buttonView = componentFactory.create( 'highlight-' + highlighters[ 0 ].class ); - // TODO: move this out of this method (related to button only design) Make disabled when all buttons are disabled model.bind( 'isEnabled' ).to( // Bind to #isEnabled of each command... ...getBindingTargets( buttons, 'isEnabled' ), @@ -96,7 +116,7 @@ export default class HighlightUI extends Plugin { ); // TODO: Is this needed in UI at all? - const dropdownView = createSplitButtonDropdown( model, buttons, locale, buttonView ); + const dropdownView = createSplitButtonDropdown( model, locale, buttonView ); const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: model.isVertical } ); @@ -106,15 +126,7 @@ export default class HighlightUI extends Plugin { dropdownView.extendTemplate( { attributes: { - class: [ - 'ck-splitbutton-dropdown' - ] - } - } ); - - dropdownView.buttonView.extendTemplate( { - attributes: { - class: [ 'ck-button-dropdown' ] + class: 'ck-splitbutton-dropdown' } } ); @@ -148,38 +160,24 @@ export default class HighlightUI extends Plugin { } ); } - _addButton( highlighter ) { + _addRubberButton() { const editor = this.editor; + const t = editor.t; - const command = editor.commands.get( 'highlight' ); - - editor.ui.componentFactory.add( 'highlight-' + highlighter.class, locale => { + editor.ui.componentFactory.add( 'highlightRemove', locale => { const buttonView = new ButtonView( locale ); buttonView.set( { - label: highlighter.title, - icon: highlightIcon, - tooltip: true, - class: highlighter.class - } ); - - // Bind button model to command. - buttonView.bind( 'isEnabled' ).to( command, 'isEnabled' ); - - buttonView.bind( 'isOn' ).to( command, 'value', value => { - return value === highlighter.class; + label: t( 'Remove highlighting' ), + icon: highlightRemoveIcon, + tooltip: true } ); - // Execute command. this.listenTo( buttonView, 'execute', () => { - editor.execute( 'highlight', { class: highlighter.class } ); + editor.execute( 'highlight' ); editor.editing.view.focus(); } ); - buttonView.iconView.extendTemplate( { - attributes: { style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } } - } ); - return buttonView; } ); } From 5806fda1201800c5d4e3f6bbfd62cf4a8f9db2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 23 Nov 2017 11:00:43 +0100 Subject: [PATCH 05/80] Tests: Extend highlight manual tests with image. --- tests/manual/highlight.html | 4 ++++ tests/manual/sample.jpg | Bin 0 -> 114298 bytes 2 files changed, 4 insertions(+) create mode 100644 tests/manual/sample.jpg diff --git a/tests/manual/highlight.html b/tests/manual/highlight.html index f9de287..1d68fb5 100644 --- a/tests/manual/highlight.html +++ b/tests/manual/highlight.html @@ -30,4 +30,8 @@

Here ares some pens: red pen and blue one.

+
+ CKEditor logo +
Some image with caption
+
diff --git a/tests/manual/sample.jpg b/tests/manual/sample.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b77d07e7bfff7fed1563fe2cd190c46ade02905f GIT binary patch literal 114298 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 literal 0 HcmV?d00001 From 519d0aa8cd06c0d7b590b123cda97866b7f4bc0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 23 Nov 2017 15:58:31 +0100 Subject: [PATCH 06/80] Other: Refactor HighlightCommand and Highlight UI creation. --- src/highlightcommand.js | 16 ++--- src/highlightediting.js | 17 ++++-- src/highlightui.js | 111 +++++++++++++++++++--------------- src/removehighlightcommand.js | 64 ++++++++++++++++++++ 4 files changed, 147 insertions(+), 61 deletions(-) create mode 100644 src/removehighlightcommand.js diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 2ad2bde..82b6134 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -16,13 +16,19 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; * @extends module:core/command~Command */ export default class HighlightCommand extends Command { + constructor( editor, className ) { + super( editor ); + + this.className = className; + } + /** * @inheritDoc */ refresh() { const doc = this.editor.document; - this.value = doc.selection.getAttribute( 'highlight' ); + this.value = doc.selection.getAttribute( 'highlight' ) === this.className; this.isEnabled = doc.schema.checkAttributeInSelection( doc.selection, 'highlight' ); } @@ -39,7 +45,7 @@ export default class HighlightCommand extends Command { const doc = this.editor.document; const selection = doc.selection; - // Do not apply highlight no collapsed selection. + // Do not apply highlight on collapsed selection. if ( selection.isCollapsed ) { return; } @@ -49,11 +55,7 @@ export default class HighlightCommand extends Command { const batch = options.batch || doc.batch(); for ( const range of ranges ) { - if ( options.class ) { - batch.setAttribute( range, 'highlight', options.class ); - } else { - batch.removeAttribute( range, 'highlight' ); - } + batch.setAttribute( range, 'highlight', this.className ); } } ); } diff --git a/src/highlightediting.js b/src/highlightediting.js index 1293657..83e8049 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -15,6 +15,7 @@ import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/build import AttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeelement'; import HighlightCommand from './highlightcommand'; +import RemoveHighlightCommand from './removehighlightcommand'; /** * The highlight editing feature. It introduces `highlight` command which allow to highlight selected text with defined 'marker' or 'pen'. @@ -29,11 +30,11 @@ export default class HighlightEditing extends Plugin { 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' } + { name: 'marker', class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, + { name: 'greenMarker', class: 'marker-green', title: 'Green Marker', color: '#66ff00', type: 'marker' }, + { name: 'pinkMarker', class: 'marker-pink', title: 'Pink Marker', color: '#ff6fff', type: 'marker' }, + { name: 'redPen', class: 'pen-red', title: 'Red Pen', color: '#ff0000', type: 'pen' }, + { name: 'bluePen', class: 'pen-blue', title: 'Blue Pen', color: '#0000ff', type: 'pen' } ] ); } @@ -70,7 +71,11 @@ export default class HighlightEditing extends Plugin { } } ); - editor.commands.add( 'highlight', new HighlightCommand( editor ) ); + editor.config + .get( 'highlight' ) + .map( highlighter => editor.commands.add( highlighter.name, new HighlightCommand( editor, highlighter.class ) ) ); + + editor.commands.add( 'removeHighlight', new RemoveHighlightCommand( editor ) ); } } diff --git a/src/highlightui.js b/src/highlightui.js index 17bc949..99e9e54 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -47,43 +47,57 @@ export default class HighlightUI extends Plugin { const highlighters = this.editor.config.get( 'highlight' ); for ( const highlighter of highlighters ) { - this._addButton( highlighter ); + this._addHighlighterButton( highlighter ); } - this._addRubberButton(); + this._addRemoveHighlightButton(); this._addDropdown( highlighters ); } - _addButton( highlighter ) { + _addRemoveHighlightButton() { + const t = this.editor.t; + + this._addButton( 'removeHighlight', t( 'Remove highlighting' ), highlightRemoveIcon ); + } + + _addHighlighterButton( highlighter ) { + const name = highlighter.name; + const command = this.editor.commands.get( name ); + + this._addButton( name, highlighter.title, highlightIcon, decorateHighlightButton ); + + function decorateHighlightButton( button ) { + button.bind( 'isEnabled' ).to( command, 'isEnabled' ); + button.bind( 'isOn' ).to( command, 'value' ); + + // TODO: bind to + button.iconView.extendTemplate( { + attributes: { + style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } + } + } ); + } + } + + _addButton( name, label, icon, decorateButton = () => {} ) { const editor = this.editor; - const command = editor.commands.get( 'highlight' ); - editor.ui.componentFactory.add( 'highlight-' + highlighter.class, locale => { + editor.ui.componentFactory.add( name, locale => { const buttonView = new ButtonView( locale ); buttonView.set( { - label: highlighter.title, - icon: highlightIcon, - tooltip: true, - // TODO: how to pass this & name - class: highlighter.class + label, + icon, + tooltip: true } ); - // Bind button model to command. - buttonView.bind( 'isEnabled' ).to( command, 'isEnabled' ); - buttonView.bind( 'isOn' ).to( command, 'value', value => value === highlighter.class ); - - // Execute command. this.listenTo( buttonView, 'execute', () => { - editor.execute( 'highlight', { class: highlighter.class } ); + editor.execute( name ); editor.editing.view.focus(); } ); - // TODO: - buttonView.iconView.extendTemplate( { - attributes: { style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } } - } ); + decorateButton( buttonView ); return buttonView; } ); @@ -102,11 +116,11 @@ export default class HighlightUI extends Plugin { icon: highlightIcon } ); - const buttons = highlighters.map( highlighter => componentFactory.create( 'highlight-' + highlighter.class ) ); + const buttons = highlighters.map( highlighter => componentFactory.create( highlighter.name ) ); - buttons.push( componentFactory.create( 'highlightRemove' ) ); + buttons.push( componentFactory.create( 'removeHighlight' ) ); - const buttonView = componentFactory.create( 'highlight-' + highlighters[ 0 ].class ); + const initialButton = componentFactory.create( highlighters[ 0 ].name ); model.bind( 'isEnabled' ).to( // Bind to #isEnabled of each command... @@ -116,7 +130,7 @@ export default class HighlightUI extends Plugin { ); // TODO: Is this needed in UI at all? - const dropdownView = createSplitButtonDropdown( model, locale, buttonView ); + const dropdownView = createSplitButtonDropdown( model, locale, initialButton ); const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: model.isVertical } ); @@ -145,13 +159,36 @@ export default class HighlightUI extends Plugin { } } ); + const bind = dropdownView.buttonView.buttonView.iconView.bindTemplate; + + // const bind = Template.bind( observable, emitter ); + + // TODO: check binding + + dropdownView.buttonView.buttonView.iconView.extendTemplate( { + attributes: { + style: bind.to( 'style' ) + } + } ); + + dropdownView.buttonView.buttonView.iconView.bind( 'style' ).to( model, 'type', model, 'color', ( type, color ) => { + if ( type === 'pen' ) { + return 'color:' + color; + } else { + return 'background-color:' + color; + } + } ); + // TODO: A bit hack-ish: Swap the split button button to executed one. buttons.map( buttonView => { this.listenTo( buttonView, 'execute', () => { if ( dropdownView.buttonView.buttonView.class !== buttonView.class ) { - const newButton = componentFactory.create( buttonView.class ? 'highlight-' + buttonView.class : 'highlightRemove' ); + // TODO: const newButton = + // componentFactory.create( buttonView.class ? 'highlight-' + buttonView.class : 'highlightRemove' ); - dropdownView.buttonView.swapButton( newButton ); + model.type = ''; + model.color = ''; + model.command = ''; } } ); } ); @@ -159,28 +196,6 @@ export default class HighlightUI extends Plugin { return dropdownView; } ); } - - _addRubberButton() { - const editor = this.editor; - const t = editor.t; - - editor.ui.componentFactory.add( 'highlightRemove', locale => { - const buttonView = new ButtonView( locale ); - - buttonView.set( { - label: t( 'Remove highlighting' ), - icon: highlightRemoveIcon, - tooltip: true - } ); - - this.listenTo( buttonView, 'execute', () => { - editor.execute( 'highlight' ); - editor.editing.view.focus(); - } ); - - return buttonView; - } ); - } } // TODO: this is duplicated diff --git a/src/removehighlightcommand.js b/src/removehighlightcommand.js new file mode 100644 index 0000000..d08eb02 --- /dev/null +++ b/src/removehighlightcommand.js @@ -0,0 +1,64 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module highlight/removehighlightcommand + */ + +import Command from '@ckeditor/ckeditor5-core/src/command'; + +/** + * The highlight command. It is used by the {@link module:highlight/highlightediting~HighlightEditing highlight feature} + * to apply text highlighting. + * + * @extends module:core/command~Command + */ +export default class RemoveHighlightCommand extends Command { + /** + * @inheritDoc + */ + refresh() { + const doc = this.editor.document; + + this.value = false; + this.isEnabled = doc.schema.checkAttributeInSelection( doc.selection, 'highlight' ); + } + + /** + * Executes the 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. + */ + execute( options = {} ) { + const doc = this.editor.document; + const selection = doc.selection; + + // Do nothing on collapsed selection. + if ( selection.isCollapsed ) { + return; + } + + doc.enqueueChanges( () => { + const ranges = doc.schema.getValidRanges( selection.getRanges(), 'highlight' ); + const batch = options.batch || doc.batch(); + + for ( const range of ranges ) { + batch.removeAttribute( range, 'highlight' ); + } + } ); + } +} + +/** + * 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 + */ From a56fee8b3603cd3a45c39295e5b430b9b3f506ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 23 Nov 2017 18:53:29 +0100 Subject: [PATCH 07/80] Other: Change how split button is bound to model. --- src/highlightui.js | 96 +++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 99e9e54..e619d2b 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -71,7 +71,6 @@ export default class HighlightUI extends Plugin { button.bind( 'isEnabled' ).to( command, 'isEnabled' ); button.bind( 'isOn' ).to( command, 'value' ); - // TODO: bind to button.iconView.extendTemplate( { attributes: { style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } @@ -108,19 +107,62 @@ export default class HighlightUI extends Plugin { const t = editor.t; const componentFactory = editor.ui.componentFactory; + const firstHighlighter = highlighters[ 0 ]; + componentFactory.add( 'highlightDropdown', locale => { const model = new Model( { label: t( 'Highlight' ), withText: false, - selected: highlighters[ 0 ].class, - icon: highlightIcon + selected: firstHighlighter.name, + icon: highlightIcon, + type: firstHighlighter.type, + color: firstHighlighter.color, + command: firstHighlighter.name + } ); + + // TODO: bind this in model + const dropdownView = createSplitButtonDropdown( model, locale ); + + const iconView = dropdownView.buttonView.buttonView.iconView; + const bind = iconView.bindTemplate; + + // TODO: forward event: + dropdownView.buttonView.buttonView.on( 'execute', () => { + editor.execute( model.command ); + editor.editing.view.focus(); } ); - const buttons = highlighters.map( highlighter => componentFactory.create( highlighter.name ) ); + iconView.extendTemplate( { + attributes: { + style: bind.to( 'style' ) + } + } ); - buttons.push( componentFactory.create( 'removeHighlight' ) ); + iconView.bind( 'style' ) + .to( model, 'type', model, 'color', ( type, color ) => type === 'pen' ? 'color:' + color : 'background-color:' + color ); - const initialButton = componentFactory.create( highlighters[ 0 ].name ); + const buttons = highlighters.map( highlighter => { + const buttonView = componentFactory.create( highlighter.name ); + + this.listenTo( buttonView, 'execute', () => { + model.type = highlighter.type; + model.color = highlighter.color; + model.command = highlighter.name; + model.icon = highlightIcon; + } ); + + return buttonView; + } ); + + // TODO: bind + const removeButton = componentFactory.create( 'removeHighlight' ); + buttons.push( removeButton ); + this.listenTo( removeButton, 'execute', () => { + model.type = false; + model.color = false; + model.command = 'removeHighlight'; + model.icon = highlightRemoveIcon; + } ); model.bind( 'isEnabled' ).to( // Bind to #isEnabled of each command... @@ -129,14 +171,14 @@ export default class HighlightUI extends Plugin { ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); - // TODO: Is this needed in UI at all? - const dropdownView = createSplitButtonDropdown( model, locale, initialButton ); - const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: model.isVertical } ); buttonGroupView.bind( 'isVertical' ).to( model, 'isVertical' ); - buttons.map( view => buttonGroupView.items.add( view ) ); + // TODO: A bit hack-ish: Swap the split button button to executed one. + buttons.map( buttonView => { + buttonGroupView.items.add( buttonView ); + } ); dropdownView.extendTemplate( { attributes: { @@ -159,40 +201,6 @@ export default class HighlightUI extends Plugin { } } ); - const bind = dropdownView.buttonView.buttonView.iconView.bindTemplate; - - // const bind = Template.bind( observable, emitter ); - - // TODO: check binding - - dropdownView.buttonView.buttonView.iconView.extendTemplate( { - attributes: { - style: bind.to( 'style' ) - } - } ); - - dropdownView.buttonView.buttonView.iconView.bind( 'style' ).to( model, 'type', model, 'color', ( type, color ) => { - if ( type === 'pen' ) { - return 'color:' + color; - } else { - return 'background-color:' + color; - } - } ); - - // TODO: A bit hack-ish: Swap the split button button to executed one. - buttons.map( buttonView => { - this.listenTo( buttonView, 'execute', () => { - if ( dropdownView.buttonView.buttonView.class !== buttonView.class ) { - // TODO: const newButton = - // componentFactory.create( buttonView.class ? 'highlight-' + buttonView.class : 'highlightRemove' ); - - model.type = ''; - model.color = ''; - model.command = ''; - } - } ); - } ); - return dropdownView; } ); } From 157b1e6d135db18552c9a89645b80e97f8336b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 24 Nov 2017 13:51:48 +0100 Subject: [PATCH 08/80] Other: Move some SplitButtonDropdown functionality to SplitButtonDropdown. --- src/highlightui.js | 86 ++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index e619d2b..0a249db 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -69,11 +69,10 @@ export default class HighlightUI extends Plugin { function decorateHighlightButton( button ) { button.bind( 'isEnabled' ).to( command, 'isEnabled' ); - button.bind( 'isOn' ).to( command, 'value' ); button.iconView.extendTemplate( { attributes: { - style: highlighter.type === 'pen' ? { color: highlighter.color } : { backgroundColor: highlighter.color } + style: getIconStyleForHighlighter( highlighter.type, highlighter.color ) } } ); } @@ -91,7 +90,7 @@ export default class HighlightUI extends Plugin { tooltip: true } ); - this.listenTo( buttonView, 'execute', () => { + buttonView.on( 'execute', () => { editor.execute( name ); editor.editing.view.focus(); } ); @@ -120,48 +119,59 @@ export default class HighlightUI extends Plugin { command: firstHighlighter.name } ); + model.bind( 'isOn' ).to( editor.commands.get( firstHighlighter.name ), 'value' ); + // TODO: bind this in model const dropdownView = createSplitButtonDropdown( model, locale ); + // Extend split button icon style to reflect last used button style const iconView = dropdownView.buttonView.buttonView.iconView; const bind = iconView.bindTemplate; - // TODO: forward event: - dropdownView.buttonView.buttonView.on( 'execute', () => { - editor.execute( model.command ); - editor.editing.view.focus(); - } ); - iconView.extendTemplate( { attributes: { style: bind.to( 'style' ) } } ); - iconView.bind( 'style' ) - .to( model, 'type', model, 'color', ( type, color ) => type === 'pen' ? 'color:' + color : 'background-color:' + color ); + iconView.bind( 'style' ).to( model, 'type', model, 'color', getIconStyleForHighlighter ); + + // TODO: forward event ?: + // TODO: lame names buttonView/buttonView + dropdownView.buttonView.on( 'execute', () => { + editor.execute( model.command ); + editor.editing.view.focus(); + } ); const buttons = highlighters.map( highlighter => { const buttonView = componentFactory.create( highlighter.name ); this.listenTo( buttonView, 'execute', () => { - model.type = highlighter.type; - model.color = highlighter.color; - model.command = highlighter.name; - model.icon = highlightIcon; + model.set( { + type: highlighter.type, + color: highlighter.color, + command: highlighter.name, + icon: highlightIcon + } ); + + model.unbind( 'isOn' ); + model.bind( 'isOn' ).to( editor.commands.get( highlighter.name ), 'value' ); } ); return buttonView; } ); - // TODO: bind const removeButton = componentFactory.create( 'removeHighlight' ); buttons.push( removeButton ); + this.listenTo( removeButton, 'execute', () => { - model.type = false; - model.color = false; + model.type = 'remove'; + model.color = undefined; model.command = 'removeHighlight'; model.icon = highlightRemoveIcon; + + model.unbind( 'isOn' ); + model.bind( 'isOn' ).to( editor.commands.get( 'removeHighlight' ), 'value' ); } ); model.bind( 'isEnabled' ).to( @@ -171,20 +181,10 @@ export default class HighlightUI extends Plugin { ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); - const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: model.isVertical } ); - - buttonGroupView.bind( 'isVertical' ).to( model, 'isVertical' ); - - // TODO: A bit hack-ish: Swap the split button button to executed one. - buttons.map( buttonView => { - buttonGroupView.items.add( buttonView ); - } ); - - dropdownView.extendTemplate( { - attributes: { - class: 'ck-splitbutton-dropdown' - } - } ); + // TODO: This duplicates buttonDropdown + // Group buttons for dropdown. + const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: false } ); + buttons.map( buttonView => buttonGroupView.items.add( buttonView ) ); dropdownView.panelView.children.add( buttonGroupView ); @@ -192,14 +192,12 @@ export default class HighlightUI extends Plugin { closeDropdownOnExecute( dropdownView, buttonGroupView.items ); focusDropdownItemsOnArrows( dropdownView, buttonGroupView ); - // TODO: weak names buttonView.buttonView - // TODO: could be move to createSplitButtonDropdown - dropdownView.buttonView.arrowView.on( 'execute', () => { - if ( dropdownView.buttonView.buttonView.isEnabled && !dropdownView.isOpen ) { - dropdownView.isOpen = true; + // Focus button group upon opening dropdown view + dropdownView.buttonView.on( 'select', () => { + if ( dropdownView.buttonView.buttonView.isEnabled && dropdownView.isOpen ) { buttonGroupView.focus(); } - } ); + }, { priority: 'low' } ); return dropdownView; } ); @@ -210,3 +208,15 @@ export default class HighlightUI extends Plugin { function getBindingTargets( buttons, attribute ) { return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) ); } + +// Returns style definition for highlighter button +// @param {String} type Type of highlighter. One of: "marker", "pen", "remove". +// @param {String} color Color of highlighter. +function getIconStyleForHighlighter( type, color ) { + // Only return type for defined types "marker"/"pen". Return empty style otherwise (ie. for "remove" type). + if ( type === 'pen' ) { + return 'color:' + color; + } else if ( type === 'marker' ) { + return 'background-color:' + color; + } +} From 4a7cb30f6146c5d77b34751a62f41b786b84a4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 27 Nov 2017 16:13:48 +0100 Subject: [PATCH 09/80] Other: Refactor Highlight UI methods and add more documentation. --- src/highlightcommand.js | 20 +++--- src/highlightediting.js | 4 +- src/highlightui.js | 129 ++++++++++++++++++++++------------ src/removehighlightcommand.js | 12 +--- 4 files changed, 98 insertions(+), 67 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 82b6134..b352881 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -19,7 +19,18 @@ export default class HighlightCommand extends Command { constructor( editor, className ) { super( editor ); + /** + * Name of marker class that is used by associated highlighter. + */ this.className = className; + + /** + * A flag indicating whether the command is active, which means that the selection has highlight attribute set. + * + * @observable + * @readonly + * @member {undefined|String} module:highlight/highlightcommand~HighlightCommand#value + */ } /** @@ -37,7 +48,6 @@ 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. */ @@ -60,11 +70,3 @@ export default class HighlightCommand extends Command { } ); } } - -/** - * 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 83e8049..937a0d4 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -82,7 +82,7 @@ export default class HighlightEditing extends Plugin { /** * Highlight option descriptor. * - * @typedef {Object} module:highlight/highlightediting~HeadingOption + * @typedef {Object} module:highlight/highlightediting~HighlightOption * @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. @@ -124,5 +124,5 @@ export default class HighlightEditing extends Plugin { * 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 + * @member {Array.} module:heading/heading~HeadingConfig#options */ diff --git a/src/highlightui.js b/src/highlightui.js index 0a249db..5a97697 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -55,12 +55,23 @@ export default class HighlightUI extends Plugin { this._addDropdown( highlighters ); } + /** + * Creates remove highlight button. + * + * @private + */ _addRemoveHighlightButton() { const t = this.editor.t; this._addButton( 'removeHighlight', t( 'Remove highlighting' ), highlightRemoveIcon ); } + /** + * Creates toolbar button from provided highlight option. + * + * @param {module:highlight/highlightediting~HighlightOption} highlighter + * @private + */ _addHighlighterButton( highlighter ) { const name = highlighter.name; const command = this.editor.commands.get( name ); @@ -78,6 +89,15 @@ export default class HighlightUI extends Plugin { } } + /** + * 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, decorateButton = () => {} ) { const editor = this.editor; @@ -95,85 +115,76 @@ export default class HighlightUI extends Plugin { editor.editing.view.focus(); } ); + // Add additional behavior for buttonView. decorateButton( buttonView ); return buttonView; } ); } + /** + * Creates split button drop down UI from provided highlight options. + * + * @param {Array.} highlighters + * @private + */ _addDropdown( highlighters ) { const editor = this.editor; const t = editor.t; const componentFactory = editor.ui.componentFactory; - const firstHighlighter = highlighters[ 0 ]; + const startingHighlighter = highlighters[ 0 ]; componentFactory.add( 'highlightDropdown', locale => { + const commandName = startingHighlighter.name; + const model = new Model( { label: t( 'Highlight' ), withText: false, - selected: firstHighlighter.name, icon: highlightIcon, - type: firstHighlighter.type, - color: firstHighlighter.color, - command: firstHighlighter.name + type: startingHighlighter.type, + color: startingHighlighter.color, + command: commandName } ); - model.bind( 'isOn' ).to( editor.commands.get( firstHighlighter.name ), 'value' ); + bindModelToCommand( model, editor, commandName ); - // TODO: bind this in model const dropdownView = createSplitButtonDropdown( model, locale ); - // Extend split button icon style to reflect last used button style - const iconView = dropdownView.buttonView.buttonView.iconView; - const bind = iconView.bindTemplate; + bindIconStyle( dropdownView, model ); - iconView.extendTemplate( { - attributes: { - style: bind.to( 'style' ) - } - } ); - - iconView.bind( 'style' ).to( model, 'type', model, 'color', getIconStyleForHighlighter ); - - // TODO: forward event ?: - // TODO: lame names buttonView/buttonView dropdownView.buttonView.on( 'execute', () => { editor.execute( model.command ); editor.editing.view.focus(); } ); + // Add highlighters buttons to dropdown const buttons = highlighters.map( highlighter => { const buttonView = componentFactory.create( highlighter.name ); + const commandName = highlighter.name; - this.listenTo( buttonView, 'execute', () => { - model.set( { - type: highlighter.type, - color: highlighter.color, - command: highlighter.name, - icon: highlightIcon - } ); - - model.unbind( 'isOn' ); - model.bind( 'isOn' ).to( editor.commands.get( highlighter.name ), 'value' ); - } ); + this.listenTo( buttonView, 'execute', () => changeToolbarButton( editor, model, { + type: highlighter.type, + color: highlighter.color, + command: commandName, + icon: highlightIcon + } ) ); return buttonView; } ); - const removeButton = componentFactory.create( 'removeHighlight' ); - buttons.push( removeButton ); + // Add rubber button to dropdown. + const rubberButton = componentFactory.create( 'removeHighlight' ); + buttons.push( rubberButton ); - this.listenTo( removeButton, 'execute', () => { - model.type = 'remove'; - model.color = undefined; - model.command = 'removeHighlight'; - model.icon = highlightRemoveIcon; - - model.unbind( 'isOn' ); - model.bind( 'isOn' ).to( editor.commands.get( 'removeHighlight' ), 'value' ); - } ); + this.listenTo( rubberButton, 'execute', () => changeToolbarButton( editor, model, { + type: 'remove', + color: undefined, + command: 'removeHighlight', + icon: highlightRemoveIcon + } ) ); + // Make toolbar button enabled when any button in dropdown is enabled. model.bind( 'isEnabled' ).to( // Bind to #isEnabled of each command... ...getBindingTargets( buttons, 'isEnabled' ), @@ -181,7 +192,7 @@ export default class HighlightUI extends Plugin { ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); - // TODO: This duplicates buttonDropdown + // TODO: Temporary group as UI not fully defined yet. Also duplicates button dropdown // Group buttons for dropdown. const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: false } ); buttons.map( buttonView => buttonGroupView.items.add( buttonView ) ); @@ -198,13 +209,11 @@ export default class HighlightUI extends Plugin { buttonGroupView.focus(); } }, { priority: 'low' } ); - - return dropdownView; } ); } } -// TODO: this is duplicated +// TODO: this is duplicated in various places (dropdowns) function getBindingTargets( buttons, attribute ) { return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) ); } @@ -220,3 +229,31 @@ function getIconStyleForHighlighter( type, color ) { return 'background-color:' + color; } } + +// Rebinds model values to a new command. +function bindModelToCommand( model, editor, commandName ) { + model.unbind( 'isOn' ); + model.bind( 'isOn' ).to( editor.commands.get( commandName ), 'value' ); +} + +// Updates toolbar dropdown button with last selected highlighter. +function changeToolbarButton( editor, model, iconData ) { + model.set( iconData ); + + bindModelToCommand( model, editor, iconData.command ); +} + +// Extends split button icon style to reflect last used button style. +function bindIconStyle( dropdownView, model ) { + const iconView = dropdownView.buttonView.buttonView.iconView; + + const bind = iconView.bindTemplate; + + iconView.extendTemplate( { + attributes: { + style: bind.to( 'style' ) + } + } ); + + iconView.bind( 'style' ).to( model, 'type', model, 'color', getIconStyleForHighlighter ); +} diff --git a/src/removehighlightcommand.js b/src/removehighlightcommand.js index d08eb02..070634c 100644 --- a/src/removehighlightcommand.js +++ b/src/removehighlightcommand.js @@ -10,8 +10,8 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; /** - * The highlight command. It is used by the {@link module:highlight/highlightediting~HighlightEditing highlight feature} - * to apply text highlighting. + * The remove highlight command. It is used by the {@link module:highlight/highlightediting~HighlightEditing highlight feature} + * to remove text highlighting. * * @extends module:core/command~Command */ @@ -54,11 +54,3 @@ export default class RemoveHighlightCommand extends Command { } ); } } - -/** - * 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 - */ From 3523d5ff15e3890a180c780dd275f34e0bc3eff0 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Thu, 21 Dec 2017 12:03:58 +0100 Subject: [PATCH 10/80] Added pen and marker icons with dynamic fill color. --- theme/icons/marker.svg | 1 + theme/icons/pen.svg | 1 + 2 files changed, 2 insertions(+) create mode 100644 theme/icons/marker.svg create mode 100644 theme/icons/pen.svg diff --git a/theme/icons/marker.svg b/theme/icons/marker.svg new file mode 100644 index 0000000..aef73a3 --- /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..f592d2f --- /dev/null +++ b/theme/icons/pen.svg @@ -0,0 +1 @@ + From 7d54644ab34fe10a4ab53b063f3a7d28bcd0bf0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 12 Jan 2018 11:43:25 +0100 Subject: [PATCH 11/80] Other: Update code to changes in schema API. --- src/highlightcommand.js | 22 +++++++++----------- src/highlightediting.js | 6 ++---- src/highlightui.js | 27 +++++++++++++++++------- src/removehighlightcommand.js | 23 +++++++++------------ tests/highlightcommand.js | 39 +++++++++++++++++------------------ tests/highlightediting.js | 29 ++++++++++++++------------ tests/integration.js | 18 ++++++++-------- 7 files changed, 85 insertions(+), 79 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index b352881..b1c192c 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -37,35 +37,33 @@ export default class HighlightCommand extends Command { * @inheritDoc */ refresh() { - const doc = this.editor.document; + const model = this.editor.model; + const doc = model.document; this.value = doc.selection.getAttribute( 'highlight' ) === this.className; - this.isEnabled = doc.schema.checkAttributeInSelection( doc.selection, 'highlight' ); + this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); } /** * Executes the command. * * @protected - * @param {Object} [options] Options for the executed command. - * @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. */ - execute( options = {} ) { - const doc = this.editor.document; - const selection = doc.selection; + execute() { + const model = this.editor.model; + const document = model.document; + const selection = document.selection; // Do not apply highlight on collapsed selection. if ( selection.isCollapsed ) { return; } - doc.enqueueChanges( () => { - const ranges = doc.schema.getValidRanges( selection.getRanges(), 'highlight' ); - const batch = options.batch || doc.batch(); + model.change( writer => { + const ranges = model.schema.getValidRanges( selection.getRanges(), 'highlight' ); for ( const range of ranges ) { - batch.setAttribute( range, 'highlight', this.className ); + writer.setAttribute( 'highlight', this.className, range ); } } ); } diff --git a/src/highlightediting.js b/src/highlightediting.js index 937a0d4..867a655 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -46,10 +46,8 @@ export default class HighlightEditing extends Plugin { 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' } ); + // Allow fontSize attribute on text nodes. + editor.model.schema.extend( '$text', { allowAttributes: 'highlight' } ); // Convert highlight attribute to a mark element with associated class. buildModelConverter() diff --git a/src/highlightui.js b/src/highlightui.js index 5a97697..8505e3f 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -17,8 +17,8 @@ import highlightRemoveIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision import Model from '@ckeditor/ckeditor5-ui/src/model'; import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/createsplitbuttondropdown'; -import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownItemsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; -import ButtonGroupView from '@ckeditor/ckeditor5-ui/src/buttongroup/buttongroupview'; +import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownContentsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; +import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; /** * The default Highlight UI plugin. @@ -192,21 +192,32 @@ export default class HighlightUI extends Plugin { ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); + model.set( 'buttons', buttons ); + // TODO: Temporary group as UI not fully defined yet. Also duplicates button dropdown // Group buttons for dropdown. - const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView( { isVertical: false } ); - buttons.map( buttonView => buttonGroupView.items.add( buttonView ) ); + const toolbarView = dropdownView.toolbarView = new ToolbarView(); + + toolbarView.bind( 'isVertical', 'className' ).to( model, 'isVertical', 'toolbarClassName' ); + + model.buttons.map( view => toolbarView.items.add( view ) ); + + dropdownView.extendTemplate( { + attributes: { + class: [ 'ck-buttondropdown' ] + } + } ); - dropdownView.panelView.children.add( buttonGroupView ); + dropdownView.panelView.children.add( toolbarView ); closeDropdownOnBlur( dropdownView ); - closeDropdownOnExecute( dropdownView, buttonGroupView.items ); - focusDropdownItemsOnArrows( dropdownView, buttonGroupView ); + closeDropdownOnExecute( dropdownView, toolbarView.items ); + focusDropdownContentsOnArrows( dropdownView, toolbarView ); // Focus button group upon opening dropdown view dropdownView.buttonView.on( 'select', () => { if ( dropdownView.buttonView.buttonView.isEnabled && dropdownView.isOpen ) { - buttonGroupView.focus(); + // buttonGroupView.focus(); } }, { priority: 'low' } ); } ); diff --git a/src/removehighlightcommand.js b/src/removehighlightcommand.js index 070634c..1ff7073 100644 --- a/src/removehighlightcommand.js +++ b/src/removehighlightcommand.js @@ -20,36 +20,33 @@ export default class RemoveHighlightCommand extends Command { * @inheritDoc */ refresh() { - const doc = this.editor.document; + const model = this.editor.model; + const doc = model.document; this.value = false; - this.isEnabled = doc.schema.checkAttributeInSelection( doc.selection, 'highlight' ); + this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); } /** * Executes the 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. */ - execute( options = {} ) { - const doc = this.editor.document; - const selection = doc.selection; + execute() { + const model = this.editor.model; + const document = model.document; + const selection = document.selection; // Do nothing on collapsed selection. if ( selection.isCollapsed ) { return; } - doc.enqueueChanges( () => { - const ranges = doc.schema.getValidRanges( selection.getRanges(), 'highlight' ); - const batch = options.batch || doc.batch(); + model.change( writer => { + const ranges = model.schema.getValidRanges( selection.getRanges(), 'highlight' ); for ( const range of ranges ) { - batch.removeAttribute( range, 'highlight' ); + writer.removeAttribute( range, 'highlight' ); } } ); } diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index df20344..259331c 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -9,21 +9,20 @@ 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'; -describe( 'HighlightCommand', () => { - let editor, doc, command; +describe.skip( 'HighlightCommand', () => { + let editor, model, command; beforeEach( () => { return ModelTestEditor.create() .then( newEditor => { - doc = newEditor.document; - command = new HighlightCommand( newEditor ); editor = newEditor; + model = editor.model; + 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( 'paragraph', { inheritAllFrom: '$block' } ); + model.schema.extend( '$text', { allowAttributes: 'highlight' } ); } ); } ); @@ -38,13 +37,13 @@ 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 ); } ); @@ -52,7 +51,7 @@ describe( 'HighlightCommand', () => { describe( 'isEnabled', () => { 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 ); } ); @@ -60,7 +59,7 @@ describe( 'HighlightCommand', () => { describe( 'execute()', () => { it( 'should add highlight attribute on selected nodes nodes when passed as parameter', () => { - setData( doc, 'a[bc<$text highlight="marker">fo]obarxyz' ); + setData( model, 'a[bc<$text highlight="marker">fo]obarxyz' ); expect( command.value ).to.be.undefined; @@ -68,12 +67,12 @@ describe( 'HighlightCommand', () => { expect( command.value ).to.equal( 'marker' ); - expect( getData( doc ) ).to.equal( 'a[<$text highlight="marker">bcfo]obarxyz' ); + expect( getData( model ) ).to.equal( 'a[<$text highlight="marker">bcfo]obarxyz' ); } ); it( 'should add highlight attribute on selected nodes nodes when passed as parameter (multiple nodes)', () => { setData( - doc, + model, 'abcabc[abc' + 'foofoofoo' + 'barbar]bar' @@ -83,7 +82,7 @@ describe( 'HighlightCommand', () => { expect( command.value ).to.equal( 'marker' ); - expect( getData( doc ) ).to.equal( + expect( getData( model ) ).to.equal( 'abcabc[<$text highlight="marker">abc' + '<$text highlight="marker">foofoofoo' + '<$text highlight="marker">barbar]bar' @@ -91,13 +90,13 @@ describe( 'HighlightCommand', () => { } ); it( 'should set highlight attribute on selected nodes when passed as parameter', () => { - setData( doc, 'abc[<$text highlight="marker">foo]barxyz' ); + setData( model, 'abc[<$text highlight="marker">foo]barxyz' ); expect( command.value ).to.equal( 'marker' ); command.execute( { class: 'foo' } ); - expect( getData( doc ) ).to.equal( + expect( getData( model ) ).to.equal( 'abc[<$text highlight="foo">foo]<$text highlight="marker">barxyz' ); @@ -105,25 +104,25 @@ describe( 'HighlightCommand', () => { } ); it( 'should remove highlight attribute on selected nodes nodes when undefined passed as parameter', () => { - setData( doc, 'abc[<$text highlight="marker">foo]barxyz' ); + setData( model, 'abc[<$text highlight="marker">foo]barxyz' ); expect( command.value ).to.equal( 'marker' ); command.execute(); - expect( getData( doc ) ).to.equal( 'abc[foo]<$text highlight="marker">barxyz' ); + expect( getData( model ) ).to.equal( 'abc[foo]<$text highlight="marker">barxyz' ); expect( command.value ).to.be.undefined; } ); it( 'should do nothing on collapsed range', () => { - setData( doc, 'abc<$text highlight="marker">foo[]barxyz' ); + setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); expect( command.value ).to.equal( 'marker' ); command.execute(); - expect( getData( doc ) ).to.equal( 'abc<$text highlight="marker">foo[]barxyz' ); + expect( getData( model ) ).to.equal( 'abc<$text highlight="marker">foo[]barxyz' ); expect( command.value ).to.equal( 'marker' ); } ); diff --git a/tests/highlightediting.js b/tests/highlightediting.js index 3c32090..da3a603 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -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,51 @@ 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.skip( 'adds highlight commands', () => { expect( editor.commands.get( 'highlight' ) ).to.be.instanceOf( HighlightCommand ); } ); - describe( 'data pipeline conversions', () => { + describe.skip( '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( getModelData( model ) ).to.equal( '[]f<$text highlight="marker-green">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', () => { + describe.skip( '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

' ); } ); @@ -80,7 +83,7 @@ describe( 'HighlightEditing', () => { describe( 'config', () => { describe( 'default value', () => { - it( 'should be set', () => { + it.skip( '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' }, diff --git a/tests/integration.js b/tests/integration.js index 1fd3f40..1508f42 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -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; } ); } ); @@ -41,25 +41,25 @@ describe( 'Highlight', () => { return editor.destroy(); } ); - describe( 'compatibility with images', () => { + describe.skip( 'compatibility with images', () => { it( 'does not work inside image caption', () => { - setModelData( doc, 'foo[bar]baz' ); + setModelData( model, 'foo[bar]baz' ); - editor.execute( 'highlight', { class: 'marker' } ); + editor.execute( 'marker' ); - expect( getModelData( doc ) ) + expect( getModelData( model ) ) .to.equal( 'foo[<$text highlight="marker">bar]baz' ); } ); it( 'does not work on selection with image', () => { setModelData( - doc, + model, 'foo[fooabcbar]bar' ); - editor.execute( 'highlight', { class: 'marker' } ); + editor.execute( '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' From cb3832840516d5fb8be0b1bbbe1f6041348f9de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 12 Jan 2018 14:22:01 +0100 Subject: [PATCH 12/80] Other: Remove RemoveHighlightCommand. --- src/highlightcommand.js | 38 ++++++++++++------------- src/highlightediting.js | 4 +-- src/removehighlightcommand.js | 53 ----------------------------------- tests/highlightcommand.js | 10 +++---- 4 files changed, 26 insertions(+), 79 deletions(-) delete mode 100644 src/removehighlightcommand.js diff --git a/src/highlightcommand.js b/src/highlightcommand.js index b1c192c..f18d030 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -16,13 +16,12 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; * @extends module:core/command~Command */ export default class HighlightCommand extends Command { - constructor( editor, className ) { - super( editor ); - - /** - * Name of marker class that is used by associated highlighter. - */ - this.className = className; + /** + * @inheritDoc + */ + refresh() { + const model = this.editor.model; + const doc = model.document; /** * A flag indicating whether the command is active, which means that the selection has highlight attribute set. @@ -31,16 +30,7 @@ export default class HighlightCommand extends Command { * @readonly * @member {undefined|String} module:highlight/highlightcommand~HighlightCommand#value */ - } - - /** - * @inheritDoc - */ - refresh() { - const model = this.editor.model; - const doc = model.document; - - this.value = doc.selection.getAttribute( 'highlight' ) === this.className; + this.value = doc.selection.getAttribute( 'highlight' ); this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); } @@ -48,8 +38,12 @@ export default class HighlightCommand extends Command { * Executes the command. * * @protected + * @param {Object} [options] Options for the executed command. + * @param {String} [options.value] a value to apply. + * + * @fires execute */ - execute() { + execute( options = {} ) { const model = this.editor.model; const document = model.document; const selection = document.selection; @@ -59,11 +53,17 @@ export default class HighlightCommand extends Command { return; } + const value = options.value; + model.change( writer => { const ranges = model.schema.getValidRanges( selection.getRanges(), 'highlight' ); for ( const range of ranges ) { - writer.setAttribute( 'highlight', this.className, range ); + if ( value ) { + writer.setAttribute( 'highlight', value, range ); + } else { + writer.removeAttribute( 'highlight', range ); + } } } ); } diff --git a/src/highlightediting.js b/src/highlightediting.js index 867a655..7603126 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -15,7 +15,6 @@ import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/build import AttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeelement'; import HighlightCommand from './highlightcommand'; -import RemoveHighlightCommand from './removehighlightcommand'; /** * The highlight editing feature. It introduces `highlight` command which allow to highlight selected text with defined 'marker' or 'pen'. @@ -71,9 +70,10 @@ export default class HighlightEditing extends Plugin { editor.config .get( 'highlight' ) + // TODO: change as in Font .map( highlighter => editor.commands.add( highlighter.name, new HighlightCommand( editor, highlighter.class ) ) ); - editor.commands.add( 'removeHighlight', new RemoveHighlightCommand( editor ) ); + editor.commands.add( 'removeHighlight', new HighlightCommand( editor ) ); } } diff --git a/src/removehighlightcommand.js b/src/removehighlightcommand.js deleted file mode 100644 index 1ff7073..0000000 --- a/src/removehighlightcommand.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module highlight/removehighlightcommand - */ - -import Command from '@ckeditor/ckeditor5-core/src/command'; - -/** - * The remove highlight command. It is used by the {@link module:highlight/highlightediting~HighlightEditing highlight feature} - * to remove text highlighting. - * - * @extends module:core/command~Command - */ -export default class RemoveHighlightCommand extends Command { - /** - * @inheritDoc - */ - refresh() { - const model = this.editor.model; - const doc = model.document; - - this.value = false; - this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); - } - - /** - * Executes the command. - * - * @protected - */ - execute() { - const model = this.editor.model; - const document = model.document; - const selection = document.selection; - - // Do nothing on collapsed selection. - if ( selection.isCollapsed ) { - return; - } - - model.change( writer => { - const ranges = model.schema.getValidRanges( selection.getRanges(), 'highlight' ); - - for ( const range of ranges ) { - writer.removeAttribute( range, 'highlight' ); - } - } ); - } -} diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index 259331c..e38cf0c 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -9,7 +9,7 @@ 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'; -describe.skip( 'HighlightCommand', () => { +describe( 'HighlightCommand', () => { let editor, model, command; beforeEach( () => { @@ -37,7 +37,7 @@ describe.skip( 'HighlightCommand', () => { describe( 'value', () => { it( 'is set to highlight attribute value when selection is in text with highlight attribute', () => { - setData( model, '<$text highlight="marker">fo[]o' ); + setData( model, '<$text highlight="marker">fo[o]' ); expect( command ).to.have.property( 'value', 'marker' ); } ); @@ -63,7 +63,7 @@ describe.skip( 'HighlightCommand', () => { expect( command.value ).to.be.undefined; - command.execute( { class: 'marker' } ); + command.execute( { value: 'marker' } ); expect( command.value ).to.equal( 'marker' ); @@ -78,7 +78,7 @@ describe.skip( 'HighlightCommand', () => { 'barbar]bar' ); - command.execute( { class: 'marker' } ); + command.execute( { value: 'marker' } ); expect( command.value ).to.equal( 'marker' ); @@ -94,7 +94,7 @@ describe.skip( 'HighlightCommand', () => { expect( command.value ).to.equal( 'marker' ); - command.execute( { class: 'foo' } ); + command.execute( { value: 'foo' } ); expect( getData( model ) ).to.equal( 'abc[<$text highlight="foo">foo]<$text highlight="marker">barxyz' From 6903aee2b019c31f236621f363a5c5bee4c91964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 15 Jan 2018 13:13:21 +0100 Subject: [PATCH 13/80] Other: Use `definition-based-converters` for defining highlighters. --- src/highlightediting.js | 64 +++++++++++++++++---------------------- tests/highlightediting.js | 11 +++---- 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/highlightediting.js b/src/highlightediting.js index 7603126..cd981e2 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -8,11 +8,10 @@ */ 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 { + modelAttributeToViewAttributeElement, + viewToModelAttribute +} from '@ckeditor/ckeditor5-engine/src/conversion/definition-based-converters'; import HighlightCommand from './highlightcommand'; @@ -29,11 +28,17 @@ export default class HighlightEditing extends Plugin { super( editor ); editor.config.define( 'highlight', [ - { name: 'marker', class: 'marker', title: 'Marker', color: '#ffff66', type: 'marker' }, - { name: 'greenMarker', class: 'marker-green', title: 'Green Marker', color: '#66ff00', type: 'marker' }, - { name: 'pinkMarker', class: 'marker-pink', title: 'Pink Marker', color: '#ff6fff', type: 'marker' }, - { name: 'redPen', class: 'pen-red', title: 'Red Pen', color: '#ff0000', type: 'pen' }, - { name: 'bluePen', class: 'pen-blue', title: 'Blue Pen', color: '#0000ff', type: 'pen' } + { model: 'marker', view: { name: 'mark', class: 'marker' }, title: 'Marker', color: '#ffff66', type: 'marker' }, + { + model: 'greenMarker', + view: { name: 'mark', class: 'marker-green' }, + title: 'Green Marker', + color: '#66ff00', + type: 'marker' + }, + { model: 'pinkMarker', view: { name: 'mark', class: 'marker-pink' }, title: 'Pink Marker', color: '#ff6fff', type: 'marker' }, + { model: 'redPen', view: { name: 'mark', class: 'pen-red' }, title: 'Red Pen', color: '#ff0000', type: 'pen' }, + { model: 'bluePen', view: { name: 'mark', class: 'pen-blue' }, title: 'Blue Pen', color: '#0000ff', type: 'pen' } ] ); } @@ -45,45 +50,30 @@ export default class HighlightEditing extends Plugin { const data = editor.data; const editing = editor.editing; - // Allow fontSize attribute on text nodes. + // Allow highlight attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: 'highlight' } ); - // 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 } ) ); - - const configuredClasses = editor.config.get( 'highlight' ).map( config => config.class ); + const options = editor.config.get( 'highlight' ); - // 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 }; - } - } - } ); + // Define view to model conversion. + for ( const option of options ) { + viewToModelAttribute( 'highlight', option, [ data.viewToModel ] ); + } - editor.config - .get( 'highlight' ) - // TODO: change as in Font - .map( highlighter => editor.commands.add( highlighter.name, new HighlightCommand( editor, highlighter.class ) ) ); + // Define model to view conversion. + modelAttributeToViewAttributeElement( 'highlight', options, [ data.modelToView, editing.modelToView ] ); - editor.commands.add( 'removeHighlight', new HighlightCommand( editor ) ); + editor.commands.add( 'highlight', new HighlightCommand( editor ) ); } } /** - * Highlight option descriptor. + * Highlight option descriptor. Compatible with {@link module:engine/conversion/definition-based-converters~ConverterDefinition}. * * @typedef {Object} module:highlight/highlightediting~HighlightOption - * @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 {String} model Attribute's unique value in the model. + * @property {String} color Color used for highlighter. Should be coherent with view definition. * @property {'marker'|'pen'} type The type of highlighter: * - "marker" - will use #color as background, * - "pen" - will use #color as font color. diff --git a/tests/highlightediting.js b/tests/highlightediting.js index da3a603..4468222 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -37,14 +37,13 @@ describe( 'HighlightEditing', () => { expect( editor.model.schema.checkAttribute( [ '$block' ], 'highlight' ) ).to.be.false; } ); - it.skip( 'adds highlight commands', () => { + it( 'adds highlight command', () => { expect( editor.commands.get( 'highlight' ) ).to.be.instanceOf( HighlightCommand ); } ); - describe.skip( 'data pipeline conversions', () => { + describe( 'data pipeline conversions', () => { it( 'should convert defined marker classes', () => { const data = '

foo

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

foo

' ); - expect( getModelData( model ) ).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', () => { @@ -73,7 +72,7 @@ describe( 'HighlightEditing', () => { } ); } ); - describe.skip( 'editing pipeline conversion', () => { + describe( 'editing pipeline conversion', () => { it( 'should convert mark element with defined class', () => { setModelData( model, 'f<$text highlight="marker">oo' ); From 5a4f9fd8b7b5ffab59f1db01e1864d865576e68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 15 Jan 2018 15:34:25 +0100 Subject: [PATCH 14/80] Other: Update button creation. --- src/highlightediting.js | 4 +-- src/highlightui.js | 64 +++++++++++++++++++------------------ tests/manual/highlight.html | 12 ++++--- tests/manual/highlight.js | 7 +++- theme/highlight.css | 9 ++++++ 5 files changed, 57 insertions(+), 39 deletions(-) create mode 100644 theme/highlight.css diff --git a/src/highlightediting.js b/src/highlightediting.js index cd981e2..a7ac765 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -37,8 +37,8 @@ export default class HighlightEditing extends Plugin { type: 'marker' }, { model: 'pinkMarker', view: { name: 'mark', class: 'marker-pink' }, title: 'Pink Marker', color: '#ff6fff', type: 'marker' }, - { model: 'redPen', view: { name: 'mark', class: 'pen-red' }, title: 'Red Pen', color: '#ff0000', type: 'pen' }, - { model: 'bluePen', view: { name: 'mark', class: 'pen-blue' }, title: 'Blue Pen', color: '#0000ff', type: 'pen' } + { model: 'redPen', view: { name: 'mark', class: 'pen-red' }, title: 'Red Pen', color: '#ff2929', type: 'pen' }, + { model: 'bluePen', view: { name: 'mark', class: 'pen-blue' }, title: 'Blue Pen', color: '#0091ff', type: 'pen' } ] ); } diff --git a/src/highlightui.js b/src/highlightui.js index 8505e3f..732169a 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -12,14 +12,17 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import HighlightEditing from './highlightediting'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; -import highlightIcon from '@ckeditor/ckeditor5-core/theme/icons/input.svg'; -import highlightRemoveIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; +import markerIcon from './../theme/icons/marker.svg'; +import penIcon from './../theme/icons/pen.svg'; +import rubberIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/createsplitbuttondropdown'; import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownContentsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; +import './../theme/highlight.css'; + /** * The default Highlight UI plugin. * @@ -44,15 +47,15 @@ export default class HighlightUI extends Plugin { * @inheritDoc */ init() { - const highlighters = this.editor.config.get( 'highlight' ); + const options = this.editor.config.get( 'highlight' ); - for ( const highlighter of highlighters ) { - this._addHighlighterButton( highlighter ); + for ( const option of options ) { + this._addHighlighterButton( option ); } this._addRemoveHighlightButton(); - this._addDropdown( highlighters ); + this._addDropdown( options ); } /** @@ -63,27 +66,34 @@ export default class HighlightUI extends Plugin { _addRemoveHighlightButton() { const t = this.editor.t; - this._addButton( 'removeHighlight', t( 'Remove highlighting' ), highlightRemoveIcon ); + this._addButton( 'removeHighlight', t( 'Remove highlighting' ), rubberIcon ); } /** * Creates toolbar button from provided highlight option. * - * @param {module:highlight/highlightediting~HighlightOption} highlighter + * @param {module:highlight/highlightediting~HighlightOption} option * @private */ - _addHighlighterButton( highlighter ) { - const name = highlighter.name; - const command = this.editor.commands.get( name ); + _addHighlighterButton( option ) { + const command = this.editor.commands.get( 'highlight' ); + + const icon = option.type === 'marker' ? markerIcon : penIcon; - this._addButton( name, highlighter.title, highlightIcon, decorateHighlightButton ); + this._addButton( 'highlight:' + option.model, option.title, icon, option.model, decorateHighlightButton ); function decorateHighlightButton( button ) { button.bind( 'isEnabled' ).to( command, 'isEnabled' ); + button.extendTemplate( { + attributes: { + class: 'ck-highlight_button' + } + } ); + button.iconView.extendTemplate( { attributes: { - style: getIconStyleForHighlighter( highlighter.type, highlighter.color ) + style: `color: ${ option.color }` } } ); } @@ -98,7 +108,7 @@ export default class HighlightUI extends Plugin { * @param {Function} [decorateButton=()=>{}] Additional method for extending button. * @private */ - _addButton( name, label, icon, decorateButton = () => {} ) { + _addButton( name, label, icon, value, decorateButton = () => {} ) { const editor = this.editor; editor.ui.componentFactory.add( name, locale => { @@ -111,7 +121,7 @@ export default class HighlightUI extends Plugin { } ); buttonView.on( 'execute', () => { - editor.execute( name ); + editor.execute( 'highlight', { value } ); editor.editing.view.focus(); } ); @@ -129,6 +139,10 @@ export default class HighlightUI extends Plugin { * @private */ _addDropdown( highlighters ) { + if ( true ) { + return; + } + const editor = this.editor; const t = editor.t; const componentFactory = editor.ui.componentFactory; @@ -141,7 +155,7 @@ export default class HighlightUI extends Plugin { const model = new Model( { label: t( 'Highlight' ), withText: false, - icon: highlightIcon, + icon: markerIcon, type: startingHighlighter.type, color: startingHighlighter.color, command: commandName @@ -167,7 +181,7 @@ export default class HighlightUI extends Plugin { type: highlighter.type, color: highlighter.color, command: commandName, - icon: highlightIcon + icon: markerIcon } ) ); return buttonView; @@ -181,7 +195,7 @@ export default class HighlightUI extends Plugin { type: 'remove', color: undefined, command: 'removeHighlight', - icon: highlightRemoveIcon + icon: rubberIcon } ) ); // Make toolbar button enabled when any button in dropdown is enabled. @@ -229,18 +243,6 @@ function getBindingTargets( buttons, attribute ) { return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) ); } -// Returns style definition for highlighter button -// @param {String} type Type of highlighter. One of: "marker", "pen", "remove". -// @param {String} color Color of highlighter. -function getIconStyleForHighlighter( type, color ) { - // Only return type for defined types "marker"/"pen". Return empty style otherwise (ie. for "remove" type). - if ( type === 'pen' ) { - return 'color:' + color; - } else if ( type === 'marker' ) { - return 'background-color:' + color; - } -} - // Rebinds model values to a new command. function bindModelToCommand( model, editor, commandName ) { model.unbind( 'isOn' ); @@ -266,5 +268,5 @@ function bindIconStyle( dropdownView, model ) { } } ); - iconView.bind( 'style' ).to( model, 'type', model, 'color', getIconStyleForHighlighter ); + iconView.bind( 'style' ).to( model, 'color', color => `color:${ color }` ); } diff --git a/tests/manual/highlight.html b/tests/manual/highlight.html index 1d68fb5..70c639a 100644 --- a/tests/manual/highlight.html +++ b/tests/manual/highlight.html @@ -13,25 +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 ares some pens: red pen and blue one.

CKEditor logo -
Some image with caption
+
Some image with caption and highlighted text.
diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 293fc48..61777ad 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -14,7 +14,12 @@ ClassicEditor plugins: [ ArticlePluginSet, Highlight ], toolbar: [ 'headings', - 'highlightDropdown', + 'highlight:marker', + 'highlight:greenMarker', + 'highlight:pinkMarker', + 'highlight:bluePen', + 'highlight:redPen', + 'removeHighlight', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) diff --git a/theme/highlight.css b/theme/highlight.css new file mode 100644 index 0000000..3133aa6 --- /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; +} From 8edc6f5d633f862a40e98ebb0347428ceb119c25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 15 Jan 2018 15:36:01 +0100 Subject: [PATCH 15/80] Tests: Fix HighlightEditing config tests. --- tests/highlightediting.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/highlightediting.js b/tests/highlightediting.js index 4468222..5b21169 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -82,13 +82,25 @@ describe( 'HighlightEditing', () => { describe( 'config', () => { describe( 'default value', () => { - it.skip( 'should be set', () => { + 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' } + { model: 'marker', view: { name: 'mark', class: 'marker' }, title: 'Marker', color: '#ffff66', type: 'marker' }, + { + model: 'greenMarker', + view: { name: 'mark', class: 'marker-green' }, + title: 'Green Marker', + color: '#66ff00', + type: 'marker' + }, + { + model: 'pinkMarker', + view: { name: 'mark', class: 'marker-pink' }, + title: 'Pink Marker', + color: '#ff6fff', + type: 'marker' + }, + { model: 'redPen', view: { name: 'mark', class: 'pen-red' }, title: 'Red Pen', color: '#ff2929', type: 'pen' }, + { model: 'bluePen', view: { name: 'mark', class: 'pen-blue' }, title: 'Blue Pen', color: '#0091ff', type: 'pen' } ] ); } ); } ); From 2bbf7f7f0564ea6588086b97612b5b7bf0cab795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 15 Jan 2018 18:33:18 +0100 Subject: [PATCH 16/80] Other: Fix highlight dropdown UI. --- src/highlightui.js | 44 ++++++++++++++------------------------- tests/manual/highlight.js | 13 ++++++------ 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 732169a..4cb2f12 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -135,69 +135,56 @@ export default class HighlightUI extends Plugin { /** * Creates split button drop down UI from provided highlight options. * - * @param {Array.} highlighters + * @param {Array.} options * @private */ - _addDropdown( highlighters ) { - if ( true ) { - return; - } - + _addDropdown( options ) { const editor = this.editor; const t = editor.t; const componentFactory = editor.ui.componentFactory; - const startingHighlighter = highlighters[ 0 ]; + const startingHighlighter = options[ 0 ]; componentFactory.add( 'highlightDropdown', locale => { - const commandName = startingHighlighter.name; - const model = new Model( { label: t( 'Highlight' ), withText: false, + isVertical: false, icon: markerIcon, type: startingHighlighter.type, color: startingHighlighter.color, - command: commandName + commandValue: startingHighlighter.model } ); - bindModelToCommand( model, editor, commandName ); + bindModelToCommand( model, editor, 'highlight' ); const dropdownView = createSplitButtonDropdown( model, locale ); bindIconStyle( dropdownView, model ); dropdownView.buttonView.on( 'execute', () => { - editor.execute( model.command ); + editor.execute( 'highlight', { value: model.commandValue } ); editor.editing.view.focus(); } ); // Add highlighters buttons to dropdown - const buttons = highlighters.map( highlighter => { - const buttonView = componentFactory.create( highlighter.name ); - const commandName = highlighter.name; + const buttons = options.map( option => { + const buttonView = componentFactory.create( 'highlight:' + option.model ); this.listenTo( buttonView, 'execute', () => changeToolbarButton( editor, model, { - type: highlighter.type, - color: highlighter.color, - command: commandName, + type: option.type, + color: option.color, + command: 'highlight', + commandValue: option.model, icon: markerIcon } ) ); return buttonView; } ); - // Add rubber button to dropdown. const rubberButton = componentFactory.create( 'removeHighlight' ); buttons.push( rubberButton ); - this.listenTo( rubberButton, 'execute', () => changeToolbarButton( editor, model, { - type: 'remove', - color: undefined, - command: 'removeHighlight', - icon: rubberIcon - } ) ); - // Make toolbar button enabled when any button in dropdown is enabled. model.bind( 'isEnabled' ).to( // Bind to #isEnabled of each command... @@ -218,10 +205,9 @@ export default class HighlightUI extends Plugin { dropdownView.extendTemplate( { attributes: { - class: [ 'ck-buttondropdown' ] + class: [ 'ck-highlight_button', 'ck-buttondropdown' ] } } ); - dropdownView.panelView.children.add( toolbarView ); closeDropdownOnBlur( dropdownView ); @@ -234,6 +220,8 @@ export default class HighlightUI extends Plugin { // buttonGroupView.focus(); } }, { priority: 'low' } ); + + return dropdownView; } ); } } diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 61777ad..a99f93c 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -14,12 +14,13 @@ ClassicEditor plugins: [ ArticlePluginSet, Highlight ], toolbar: [ 'headings', - 'highlight:marker', - 'highlight:greenMarker', - 'highlight:pinkMarker', - 'highlight:bluePen', - 'highlight:redPen', - 'removeHighlight', + 'highlightDropdown', + // 'highlight:marker', + // 'highlight:greenMarker', + // 'highlight:pinkMarker', + // 'highlight:bluePen', + // 'highlight:redPen', + // 'removeHighlight', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) From 8eefab835af3d5043bcf6d97e9fb711e89bcff00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 16 Jan 2018 11:47:13 +0100 Subject: [PATCH 17/80] Code style: Make default options indented the same way. --- src/highlightediting.js | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/highlightediting.js b/src/highlightediting.js index a7ac765..e52e4bd 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -28,7 +28,13 @@ export default class HighlightEditing extends Plugin { super( editor ); editor.config.define( 'highlight', [ - { model: 'marker', view: { name: 'mark', class: 'marker' }, title: 'Marker', color: '#ffff66', type: 'marker' }, + { + model: 'marker', + view: { name: 'mark', class: 'marker' }, + title: 'Marker', + color: '#ffff66', + type: 'marker' + }, { model: 'greenMarker', view: { name: 'mark', class: 'marker-green' }, @@ -36,9 +42,27 @@ export default class HighlightEditing extends Plugin { color: '#66ff00', type: 'marker' }, - { model: 'pinkMarker', view: { name: 'mark', class: 'marker-pink' }, title: 'Pink Marker', color: '#ff6fff', type: 'marker' }, - { model: 'redPen', view: { name: 'mark', class: 'pen-red' }, title: 'Red Pen', color: '#ff2929', type: 'pen' }, - { model: 'bluePen', view: { name: 'mark', class: 'pen-blue' }, title: 'Blue Pen', color: '#0091ff', type: 'pen' } + { + model: 'pinkMarker', + view: { name: 'mark', class: 'marker-pink' }, + title: 'Pink Marker', + color: '#ff6fff', + type: 'marker' + }, + { + model: 'redPen', + view: { name: 'mark', class: 'pen-red' }, + title: 'Red Pen', + color: '#ff2929', + type: 'pen' + }, + { + model: 'bluePen', + view: { name: 'mark', class: 'pen-blue' }, + title: 'Blue Pen', + color: '#0091ff', + type: 'pen' + } ] ); } From 6130804f4410ac8bcae4528490330cd9637cdd9f Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 16 Jan 2018 12:43:18 +0100 Subject: [PATCH 18/80] Added eraser icon. --- src/highlightui.js | 10 +++++----- theme/icons/eraser.svg | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 theme/icons/eraser.svg diff --git a/src/highlightui.js b/src/highlightui.js index 4cb2f12..8db01ea 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -14,7 +14,7 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import markerIcon from './../theme/icons/marker.svg'; import penIcon from './../theme/icons/pen.svg'; -import rubberIcon from '@ckeditor/ckeditor5-core/theme/icons/low-vision.svg'; +import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/createsplitbuttondropdown'; @@ -66,7 +66,7 @@ export default class HighlightUI extends Plugin { _addRemoveHighlightButton() { const t = this.editor.t; - this._addButton( 'removeHighlight', t( 'Remove highlighting' ), rubberIcon ); + this._addButton( 'removeHighlight', t( 'Remove highlighting' ), eraserIcon ); } /** @@ -181,9 +181,9 @@ export default class HighlightUI extends Plugin { return buttonView; } ); - // Add rubber button to dropdown. - const rubberButton = componentFactory.create( 'removeHighlight' ); - buttons.push( rubberButton ); + // Add eraser button to dropdown. + const eraserButton = componentFactory.create( 'removeHighlight' ); + buttons.push( eraserButton ); // Make toolbar button enabled when any button in dropdown is enabled. model.bind( 'isEnabled' ).to( diff --git a/theme/icons/eraser.svg b/theme/icons/eraser.svg new file mode 100644 index 0000000..669082b --- /dev/null +++ b/theme/icons/eraser.svg @@ -0,0 +1 @@ + \ No newline at end of file From 950eea82886e712b16cbb5e135872df71a3aecdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 16 Jan 2018 14:17:44 +0100 Subject: [PATCH 19/80] Added: Changing the style on collapsed selection changes the entire highlighter. --- src/findattributerange.js | 47 +++++++++++++ src/highlightcommand.js | 30 +++++++-- tests/findattributerange.js | 127 ++++++++++++++++++++++++++++++++++++ tests/highlightcommand.js | 8 +-- 4 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 src/findattributerange.js create mode 100644 tests/findattributerange.js diff --git a/src/findattributerange.js b/src/findattributerange.js new file mode 100644 index 0000000..332c989 --- /dev/null +++ b/src/findattributerange.js @@ -0,0 +1,47 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module highlight/findattributerange + */ + +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; + +/** + * Walks backward and forward from the start position, node by node, as long as they have the same `attribute` value and return + * a {@link module:engine/model/range~Range Range} with the found attribute. + * + * @param {module:engine/model/position~Position} position The start position. + * @param {String} attribute The `attribute` key to search. + * @param {String} value The `attribute` value. + * @returns {module:engine/model/range~Range} The attribute range. + */ +export default function findAttributeRange( position, attribute, value ) { + return new Range( _findBound( position, attribute, value, true ), _findBound( position, attribute, value, false ) ); +} + +// Walks forward or backward (depends on the `lookBack` flag), node by node, as long as they have the same `linkHref` attribute value +// and returns a position just before or after (depends on the `lookBack` flag) the last matched node. +// +// @param {module:engine/model/position~Position} position The start position. +// @param {String} attribute The `attribute` key to search. +// @param {String} value The `attribute` value. +// @param {Boolean} lookBack Whether the walk direction is forward (`false`) or backward (`true`). +// @returns {module:engine/model/position~Position} The position just before the last matched node. +function _findBound( position, attribute, value, lookBack ) { + // Get node before or after position (depends on `lookBack` flag). + // When position is inside text node then start searching from text node. + let node = position.textNode || ( lookBack ? position.nodeBefore : position.nodeAfter ); + + let lastNode = null; + + while ( node && node.getAttribute( attribute ) == value ) { + lastNode = node; + node = lookBack ? node.previousSibling : node.nextSibling; + } + + return lastNode ? Position.createAt( lastNode, lookBack ? 'before' : 'after' ) : position; +} diff --git a/src/highlightcommand.js b/src/highlightcommand.js index f18d030..d1e67d8 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -8,6 +8,7 @@ */ import Command from '@ckeditor/ckeditor5-core/src/command'; +import findAttributeRange from './findattributerange'; /** * The highlight command. It is used by the {@link module:highlight/highlightediting~HighlightEditing highlight feature} @@ -48,8 +49,8 @@ export default class HighlightCommand extends Command { const document = model.document; const selection = document.selection; - // Do not apply highlight on collapsed selection. - if ( selection.isCollapsed ) { + // Do not apply highlight on collapsed selection when not inside existing highlight. + if ( selection.isCollapsed && !this.value ) { return; } @@ -58,11 +59,28 @@ export default class HighlightCommand extends Command { model.change( writer => { const ranges = model.schema.getValidRanges( selection.getRanges(), 'highlight' ); - for ( const range of ranges ) { - if ( value ) { - writer.setAttribute( 'highlight', value, range ); + if ( selection.isCollapsed ) { + const position = selection.getFirstPosition(); + + // When selection is inside text with `linkHref` attribute. + if ( selection.hasAttribute( 'highlight' ) ) { + // Then update `highlight` value. + const linkRange = findAttributeRange( position, 'highlight', selection.getAttribute( 'highlight' ) ); + + writer.setAttribute( 'highlight', value, linkRange ); + + // Create new range wrapping changed link. + selection.setRanges( [ linkRange ] ); } else { - writer.removeAttribute( 'highlight', range ); + // TODO + } + } else { + for ( const range of ranges ) { + if ( value ) { + writer.setAttribute( 'highlight', value, range ); + } else { + writer.removeAttribute( 'highlight', range ); + } } } } ); diff --git a/tests/findattributerange.js b/tests/findattributerange.js new file mode 100644 index 0000000..24fe1da --- /dev/null +++ b/tests/findattributerange.js @@ -0,0 +1,127 @@ +/** + * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import findAttributeRange from '../src/findattributerange'; +import Model from '@ckeditor/ckeditor5-engine/src/model/model'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; +import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +describe( 'findAttributeRange', () => { + let model, document, root; + + beforeEach( () => { + model = new Model(); + document = model.document; + root = document.createRoot(); + model.schema.extend( '$text', { allowIn: '$root' } ); + model.schema.register( 'p', { inheritAllFrom: '$block' } ); + } ); + + it( 'should find attribute range searching from the center of the attribute #1', () => { + setData( model, '<$text foo="bar">foobar' ); + + const startPosition = new Position( root, [ 3 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 0, root, 6 ) ) ).to.true; + } ); + + it( 'should find attribute range searching from the center of the attribute #2', () => { + setData( model, 'abc <$text foo="bar">foobar abc' ); + + const startPosition = new Position( root, [ 7 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 4, root, 10 ) ) ).to.true; + } ); + + it( 'should find attribute range searching from the beginning of the attribute #1', () => { + setData( model, '<$text foo="bar">foobar' ); + + const startPosition = new Position( root, [ 0 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 0, root, 6 ) ) ).to.true; + } ); + + it( 'should find attribute range searching from the beginning of the attribute #2', () => { + setData( model, 'abc <$text foo="bar">foobar abc' ); + + const startPosition = new Position( root, [ 4 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 4, root, 10 ) ) ).to.true; + } ); + + it( 'should find attribute range searching from the end of the attribute #1', () => { + setData( model, '<$text foo="bar">foobar' ); + + const startPosition = new Position( root, [ 6 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 0, root, 6 ) ) ).to.true; + } ); + + it( 'should find attribute range searching from the end of the attribute #2', () => { + setData( model, 'abc <$text foo="bar">foobar abc' ); + + const startPosition = new Position( root, [ 10 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 4, root, 10 ) ) ).to.true; + } ); + + it( 'should find attribute range when attribute stick to other attribute searching from the center of the attribute', () => { + setData( model, '<$text foo="other">abc<$text foo="bar">foobar<$text foo="other">abc' ); + + const startPosition = new Position( root, [ 6 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 3, root, 9 ) ) ).to.true; + } ); + + it( 'should find attribute range when attribute stick to other attribute searching from the beginning of the attribute', () => { + setData( model, '<$text foo="other">abc<$text foo="bar">foobar<$text foo="other">abc' ); + + const startPosition = new Position( root, [ 3 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 3, root, 9 ) ) ).to.true; + } ); + + it( 'should find attribute range when attribute stick to other attribute searching from the end of the attribute', () => { + setData( model, '<$text foo="other">abc<$text foo="bar">foobar<$text foo="other">abc' ); + + const startPosition = new Position( root, [ 9 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( Range.createFromParentsAndOffsets( root, 3, root, 9 ) ) ).to.true; + } ); + + it( 'should find attribute range only inside current parent', () => { + setData( + model, + '

<$text foo="bar">foobar

' + + '

<$text foo="bar">foobar

' + + '

<$text foo="bar">foobar

' + ); + + const startPosition = new Position( root, [ 1, 3 ] ); + const result = findAttributeRange( startPosition, 'foo', 'bar' ); + + expect( result ).to.instanceOf( Range ); + expect( result.isEqual( new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 6 ] ) ) ) ).to.true; + } ); +} ); diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index e38cf0c..7ba79d8 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -115,16 +115,16 @@ describe( 'HighlightCommand', () => { expect( command.value ).to.be.undefined; } ); - it( 'should do nothing on collapsed range', () => { + it( 'should change entire highlight on collapsed range when inside highlighted text', () => { setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); expect( command.value ).to.equal( 'marker' ); - command.execute(); + command.execute( { value: 'greenMarker' } ); - expect( getData( model ) ).to.equal( 'abc<$text highlight="marker">foo[]barxyz' ); + expect( getData( model ) ).to.equal( 'abc[<$text highlight="greenMarker">foobar]xyz' ); - expect( command.value ).to.equal( 'marker' ); + expect( command.value ).to.equal( 'greenMarker' ); } ); } ); } ); From b924e4fe69eaa3f68e18c06f70671dd9902d9bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 16 Jan 2018 15:03:04 +0100 Subject: [PATCH 20/80] Changed: Use `Position.getLastMatchingPosition()` instead of findAttributeRange(). --- src/findattributerange.js | 47 ------------- src/highlightcommand.js | 19 ++++-- tests/findattributerange.js | 127 ------------------------------------ 3 files changed, 14 insertions(+), 179 deletions(-) delete mode 100644 src/findattributerange.js delete mode 100644 tests/findattributerange.js diff --git a/src/findattributerange.js b/src/findattributerange.js deleted file mode 100644 index 332c989..0000000 --- a/src/findattributerange.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module highlight/findattributerange - */ - -import Range from '@ckeditor/ckeditor5-engine/src/model/range'; -import Position from '@ckeditor/ckeditor5-engine/src/model/position'; - -/** - * Walks backward and forward from the start position, node by node, as long as they have the same `attribute` value and return - * a {@link module:engine/model/range~Range Range} with the found attribute. - * - * @param {module:engine/model/position~Position} position The start position. - * @param {String} attribute The `attribute` key to search. - * @param {String} value The `attribute` value. - * @returns {module:engine/model/range~Range} The attribute range. - */ -export default function findAttributeRange( position, attribute, value ) { - return new Range( _findBound( position, attribute, value, true ), _findBound( position, attribute, value, false ) ); -} - -// Walks forward or backward (depends on the `lookBack` flag), node by node, as long as they have the same `linkHref` attribute value -// and returns a position just before or after (depends on the `lookBack` flag) the last matched node. -// -// @param {module:engine/model/position~Position} position The start position. -// @param {String} attribute The `attribute` key to search. -// @param {String} value The `attribute` value. -// @param {Boolean} lookBack Whether the walk direction is forward (`false`) or backward (`true`). -// @returns {module:engine/model/position~Position} The position just before the last matched node. -function _findBound( position, attribute, value, lookBack ) { - // Get node before or after position (depends on `lookBack` flag). - // When position is inside text node then start searching from text node. - let node = position.textNode || ( lookBack ? position.nodeBefore : position.nodeAfter ); - - let lastNode = null; - - while ( node && node.getAttribute( attribute ) == value ) { - lastNode = node; - node = lookBack ? node.previousSibling : node.nextSibling; - } - - return lastNode ? Position.createAt( lastNode, lookBack ? 'before' : 'after' ) : position; -} diff --git a/src/highlightcommand.js b/src/highlightcommand.js index d1e67d8..9bec195 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -8,7 +8,7 @@ */ import Command from '@ckeditor/ckeditor5-core/src/command'; -import findAttributeRange from './findattributerange'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; /** * The highlight command. It is used by the {@link module:highlight/highlightediting~HighlightEditing highlight feature} @@ -64,13 +64,22 @@ export default class HighlightCommand extends Command { // When selection is inside text with `linkHref` attribute. if ( selection.hasAttribute( 'highlight' ) ) { - // Then update `highlight` value. - const linkRange = findAttributeRange( position, 'highlight', selection.getAttribute( 'highlight' ) ); + const highlightStart = position.getLastMatchingPosition( + val => val.item.hasAttribute( 'highlight' ) && val.item.getAttribute( 'highlight' ) === value, + { direction: 'backward' } + ); + + const highlightEnd = position.getLastMatchingPosition( + val => val.item.hasAttribute( 'highlight' ) && val.item.getAttribute( 'highlight' ) === value + ); - writer.setAttribute( 'highlight', value, linkRange ); + const highlightRange = new Range( highlightStart, highlightEnd ); + + // Then update `highlight` value. + writer.setAttribute( 'highlight', value, highlightRange ); // Create new range wrapping changed link. - selection.setRanges( [ linkRange ] ); + selection.setRanges( [ highlightRange ] ); } else { // TODO } diff --git a/tests/findattributerange.js b/tests/findattributerange.js deleted file mode 100644 index 24fe1da..0000000 --- a/tests/findattributerange.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -import findAttributeRange from '../src/findattributerange'; -import Model from '@ckeditor/ckeditor5-engine/src/model/model'; -import Range from '@ckeditor/ckeditor5-engine/src/model/range'; -import Position from '@ckeditor/ckeditor5-engine/src/model/position'; -import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; - -describe( 'findAttributeRange', () => { - let model, document, root; - - beforeEach( () => { - model = new Model(); - document = model.document; - root = document.createRoot(); - model.schema.extend( '$text', { allowIn: '$root' } ); - model.schema.register( 'p', { inheritAllFrom: '$block' } ); - } ); - - it( 'should find attribute range searching from the center of the attribute #1', () => { - setData( model, '<$text foo="bar">foobar' ); - - const startPosition = new Position( root, [ 3 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 0, root, 6 ) ) ).to.true; - } ); - - it( 'should find attribute range searching from the center of the attribute #2', () => { - setData( model, 'abc <$text foo="bar">foobar abc' ); - - const startPosition = new Position( root, [ 7 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 4, root, 10 ) ) ).to.true; - } ); - - it( 'should find attribute range searching from the beginning of the attribute #1', () => { - setData( model, '<$text foo="bar">foobar' ); - - const startPosition = new Position( root, [ 0 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 0, root, 6 ) ) ).to.true; - } ); - - it( 'should find attribute range searching from the beginning of the attribute #2', () => { - setData( model, 'abc <$text foo="bar">foobar abc' ); - - const startPosition = new Position( root, [ 4 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 4, root, 10 ) ) ).to.true; - } ); - - it( 'should find attribute range searching from the end of the attribute #1', () => { - setData( model, '<$text foo="bar">foobar' ); - - const startPosition = new Position( root, [ 6 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 0, root, 6 ) ) ).to.true; - } ); - - it( 'should find attribute range searching from the end of the attribute #2', () => { - setData( model, 'abc <$text foo="bar">foobar abc' ); - - const startPosition = new Position( root, [ 10 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 4, root, 10 ) ) ).to.true; - } ); - - it( 'should find attribute range when attribute stick to other attribute searching from the center of the attribute', () => { - setData( model, '<$text foo="other">abc<$text foo="bar">foobar<$text foo="other">abc' ); - - const startPosition = new Position( root, [ 6 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 3, root, 9 ) ) ).to.true; - } ); - - it( 'should find attribute range when attribute stick to other attribute searching from the beginning of the attribute', () => { - setData( model, '<$text foo="other">abc<$text foo="bar">foobar<$text foo="other">abc' ); - - const startPosition = new Position( root, [ 3 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 3, root, 9 ) ) ).to.true; - } ); - - it( 'should find attribute range when attribute stick to other attribute searching from the end of the attribute', () => { - setData( model, '<$text foo="other">abc<$text foo="bar">foobar<$text foo="other">abc' ); - - const startPosition = new Position( root, [ 9 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( Range.createFromParentsAndOffsets( root, 3, root, 9 ) ) ).to.true; - } ); - - it( 'should find attribute range only inside current parent', () => { - setData( - model, - '

<$text foo="bar">foobar

' + - '

<$text foo="bar">foobar

' + - '

<$text foo="bar">foobar

' - ); - - const startPosition = new Position( root, [ 1, 3 ] ); - const result = findAttributeRange( startPosition, 'foo', 'bar' ); - - expect( result ).to.instanceOf( Range ); - expect( result.isEqual( new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 6 ] ) ) ) ).to.true; - } ); -} ); From 0d36487543fa5c0f952146a632fa8df8b6a6ff16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 13:30:25 +0100 Subject: [PATCH 21/80] Other: Fix find highlighter range from collapsed selection method. --- src/highlightcommand.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 9bec195..9c3486a 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -54,7 +54,7 @@ export default class HighlightCommand extends Command { return; } - const value = options.value; + const highlighter = options.value; model.change( writer => { const ranges = model.schema.getValidRanges( selection.getRanges(), 'highlight' ); @@ -64,19 +64,17 @@ export default class HighlightCommand extends Command { // When selection is inside text with `linkHref` attribute. if ( selection.hasAttribute( 'highlight' ) ) { - const highlightStart = position.getLastMatchingPosition( - val => val.item.hasAttribute( 'highlight' ) && val.item.getAttribute( 'highlight' ) === value, - { direction: 'backward' } - ); + const isSameHighlight = value => { + return value.item.hasAttribute( 'highlight' ) && value.item.getAttribute( 'highlight' ) === this.value; + }; - const highlightEnd = position.getLastMatchingPosition( - val => val.item.hasAttribute( 'highlight' ) && val.item.getAttribute( 'highlight' ) === value - ); + const highlightStart = position.getLastMatchingPosition( isSameHighlight, { direction: 'backward' } ); + const highlightEnd = position.getLastMatchingPosition( isSameHighlight ); const highlightRange = new Range( highlightStart, highlightEnd ); // Then update `highlight` value. - writer.setAttribute( 'highlight', value, highlightRange ); + writer.setAttribute( 'highlight', highlighter, highlightRange ); // Create new range wrapping changed link. selection.setRanges( [ highlightRange ] ); @@ -85,8 +83,8 @@ export default class HighlightCommand extends Command { } } else { for ( const range of ranges ) { - if ( value ) { - writer.setAttribute( 'highlight', value, range ); + if ( highlighter ) { + writer.setAttribute( 'highlight', highlighter, range ); } else { writer.removeAttribute( 'highlight', range ); } From 78a039144989473cda13345f52c401d67421640f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 14:03:23 +0100 Subject: [PATCH 22/80] Added: remove entire highlight on collapsed range when inside highlighted text of the same value. --- src/highlightcommand.js | 17 ++++++++++++----- tests/highlightcommand.js | 12 ++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 9c3486a..77af238 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -62,8 +62,9 @@ export default class HighlightCommand extends Command { if ( selection.isCollapsed ) { const position = selection.getFirstPosition(); - // When selection is inside text with `linkHref` attribute. + // 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; }; @@ -73,11 +74,17 @@ export default class HighlightCommand extends Command { const highlightRange = new Range( highlightStart, highlightEnd ); - // Then update `highlight` value. - writer.setAttribute( 'highlight', highlighter, highlightRange ); + // Then depending on current value... + if ( this.value === highlighter ) { + // ...remove attribute. + writer.removeAttribute( 'highlight', highlightRange ); + } else { + // ...update `highlight` value. + writer.setAttribute( 'highlight', highlighter, highlightRange ); - // Create new range wrapping changed link. - selection.setRanges( [ highlightRange ] ); + // And create new range wrapping changed highlighter. + selection.setRanges( [ highlightRange ] ); + } } else { // TODO } diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index 7ba79d8..d7476c5 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -126,5 +126,17 @@ describe( 'HighlightCommand', () => { expect( command.value ).to.equal( 'greenMarker' ); } ); + + it( 'should remove entire highlight on collapsed range when inside highlighted text of the same value', () => { + setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); + + expect( command.value ).to.equal( 'marker' ); + + command.execute( { value: 'marker' } ); + + expect( getData( model ) ).to.equal( 'abcfoo[]barxyz' ); + + expect( command.value ).to.be.undefined; + } ); } ); } ); From 18595eb93eff51753b4da5682bc06ce1b9c50b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 14:17:22 +0100 Subject: [PATCH 23/80] Added: Remove whole highlight when using eraser on collapsed range inside existing highlight. --- src/highlightcommand.js | 4 +- tests/highlightcommand.js | 136 ++++++++++++++++++++++++-------------- 2 files changed, 88 insertions(+), 52 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 77af238..f5db66f 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -75,8 +75,8 @@ export default class HighlightCommand extends Command { const highlightRange = new Range( highlightStart, highlightEnd ); // Then depending on current value... - if ( this.value === highlighter ) { - // ...remove attribute. + if ( !highlighter || this.value === highlighter ) { + // ...remove attribute when passing highlighter different then current or executing "eraser". writer.removeAttribute( 'highlight', highlightRange ); } else { // ...update `highlight` value. diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index d7476c5..e8e82fa 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -58,85 +58,121 @@ describe( 'HighlightCommand', () => { } ); describe( 'execute()', () => { - it( 'should add highlight attribute on selected nodes nodes when passed as parameter', () => { - setData( model, '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( { value: 'marker' } ); + command.execute( { value: 'greenMarker' } ); - expect( command.value ).to.equal( 'marker' ); + expect( getData( model ) ).to.equal( 'abc[<$text highlight="greenMarker">foobar]xyz' ); - expect( getData( model ) ).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( - model, - '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( { value: 'marker' } ); + expect( command.value ).to.equal( 'marker' ); - expect( command.value ).to.equal( 'marker' ); + command.execute( { value: 'marker' } ); - expect( getData( model ) ).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( model, 'abc[<$text highlight="marker">foo]barxyz' ); + expect( command.value ).to.be.undefined; + } ); - expect( command.value ).to.equal( 'marker' ); + it( 'should change entire highlight when inside highlighted text', () => { + setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); - command.execute( { value: 'foo' } ); + expect( command.value ).to.equal( 'marker' ); - expect( getData( model ) ).to.equal( - 'abc[<$text highlight="foo">foo]<$text highlight="marker">barxyz' - ); + command.execute( { value: 'greenMarker' } ); - expect( command.value ).to.equal( 'foo' ); - } ); + expect( getData( model ) ).to.equal( 'abc[<$text highlight="greenMarker">foobar]xyz' ); - it( 'should remove highlight attribute on selected nodes nodes when undefined passed as parameter', () => { - setData( model, 'abc[<$text highlight="marker">foo]barxyz' ); + expect( command.value ).to.equal( 'greenMarker' ); + } ); + } ); - expect( command.value ).to.equal( 'marker' ); + 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' ); - command.execute(); + expect( command.value ).to.be.undefined; - expect( getData( model ) ).to.equal( 'abc[foo]<$text highlight="marker">barxyz' ); + command.execute( { value: 'marker' } ); - expect( command.value ).to.be.undefined; - } ); + 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' ); - it( 'should change entire highlight on collapsed range when inside highlighted text', () => { - setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); + expect( getData( model ) ).to.equal( + 'abcabc[<$text highlight="marker">abc' + + '<$text highlight="marker">foofoofoo' + + '<$text highlight="marker">barbar]bar' + ); + } ); - expect( command.value ).to.equal( 'marker' ); + it( 'should set highlight attribute on selected nodes when passed as parameter only on selected characters', () => { + setData( model, 'abc[<$text highlight="marker">foo]barxyz' ); - command.execute( { value: 'greenMarker' } ); + expect( command.value ).to.equal( 'marker' ); - expect( getData( model ) ).to.equal( 'abc[<$text highlight="greenMarker">foobar]xyz' ); + command.execute( { value: 'foo' } ); - expect( command.value ).to.equal( 'greenMarker' ); + expect( getData( model ) ).to.equal( + 'abc[<$text highlight="foo">foo]<$text highlight="marker">barxyz' + ); + + expect( command.value ).to.equal( 'foo' ); + } ); + } ); } ); - it( 'should remove entire highlight on collapsed range when inside highlighted text of the same value', () => { - setData( model, '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( { value: 'marker' } ); + expect( getData( model ) ).to.equal( 'abcfoo[]barxyz' ); + + expect( command.value ).to.be.undefined; + } ); + } ); - expect( getData( model ) ).to.equal( 'abcfoo[]barxyz' ); + 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.be.undefined; + 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; + } ); + } ); } ); } ); } ); From 1cff774329c7edd7d4786be8347cc4bba400517e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 15:28:43 +0100 Subject: [PATCH 24/80] Changed: Use hardcoded view element and add `options` namespace to config. --- src/highlightediting.js | 76 ++++++++++++++++++--------------------- src/highlightui.js | 2 +- tests/highlightediting.js | 28 +++++---------- 3 files changed, 44 insertions(+), 62 deletions(-) diff --git a/src/highlightediting.js b/src/highlightediting.js index e52e4bd..4098f82 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -27,43 +27,15 @@ export default class HighlightEditing extends Plugin { constructor( editor ) { super( editor ); - editor.config.define( 'highlight', [ - { - model: 'marker', - view: { name: 'mark', class: 'marker' }, - title: 'Marker', - color: '#ffff66', - type: 'marker' - }, - { - model: 'greenMarker', - view: { name: 'mark', class: 'marker-green' }, - title: 'Green Marker', - color: '#66ff00', - type: 'marker' - }, - { - model: 'pinkMarker', - view: { name: 'mark', class: 'marker-pink' }, - title: 'Pink Marker', - color: '#ff6fff', - type: 'marker' - }, - { - model: 'redPen', - view: { name: 'mark', class: 'pen-red' }, - title: 'Red Pen', - color: '#ff2929', - type: 'pen' - }, - { - model: 'bluePen', - view: { name: 'mark', class: 'pen-blue' }, - title: 'Blue Pen', - color: '#0091ff', - 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' } + ] + } ); } /** @@ -77,27 +49,32 @@ export default class HighlightEditing extends Plugin { // Allow highlight attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: 'highlight' } ); - const options = editor.config.get( 'highlight' ); + const options = editor.config.get( 'highlight.options' ); // Define view to model conversion. for ( const option of options ) { - viewToModelAttribute( 'highlight', option, [ data.viewToModel ] ); + viewToModelAttribute( 'highlight', _getConverterDefinition( option ), [ data.viewToModel ] ); } // Define model to view conversion. - modelAttributeToViewAttributeElement( 'highlight', options, [ data.modelToView, editing.modelToView ] ); + modelAttributeToViewAttributeElement( + 'highlight', + options.map( _getConverterDefinition ), + [ data.modelToView, editing.modelToView ] + ); editor.commands.add( 'highlight', new HighlightCommand( editor ) ); } } /** - * Highlight option descriptor. Compatible with {@link module:engine/conversion/definition-based-converters~ConverterDefinition}. + * Highlight option descriptor. * * @typedef {Object} module:highlight/highlightediting~HighlightOption * @property {String} title The user-readable title of the option. * @property {String} model Attribute's unique value in the model. - * @property {String} color Color used for highlighter. Should be coherent with view definition. + * @property {String} color Color used for highlighter. Should be coherent with `class` CSS setting. + * @property {String} class CSS Class used on `mark` element in view. Should be coherent with `color` setting. * @property {'marker'|'pen'} type The type of highlighter: * - "marker" - will use #color as background, * - "pen" - will use #color as font color. @@ -138,3 +115,18 @@ export default class HighlightEditing extends Plugin { * * @member {Array.} module:heading/heading~HeadingConfig#options */ + +// Converts {@link module:highlight/highlightediting~HighlightOption} +// to {@link module:engine/conversion/definition-based-converters~ConverterDefinition} +// +// @param {module:highlight/highlightediting~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 8db01ea..9185c84 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -47,7 +47,7 @@ export default class HighlightUI extends Plugin { * @inheritDoc */ init() { - const options = this.editor.config.get( 'highlight' ); + const options = this.editor.config.get( 'highlight.options' ); for ( const option of options ) { this._addHighlighterButton( option ); diff --git a/tests/highlightediting.js b/tests/highlightediting.js index 5b21169..94e5d56 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -83,25 +83,15 @@ describe( 'HighlightEditing', () => { describe( 'config', () => { describe( 'default value', () => { it( 'should be set', () => { - expect( editor.config.get( 'highlight' ) ).to.deep.equal( [ - { model: 'marker', view: { name: 'mark', class: 'marker' }, title: 'Marker', color: '#ffff66', type: 'marker' }, - { - model: 'greenMarker', - view: { name: 'mark', class: 'marker-green' }, - title: 'Green Marker', - color: '#66ff00', - type: 'marker' - }, - { - model: 'pinkMarker', - view: { name: 'mark', class: 'marker-pink' }, - title: 'Pink Marker', - color: '#ff6fff', - type: 'marker' - }, - { model: 'redPen', view: { name: 'mark', class: 'pen-red' }, title: 'Red Pen', color: '#ff2929', type: 'pen' }, - { model: 'bluePen', view: { name: 'mark', class: 'pen-blue' }, title: 'Blue Pen', color: '#0091ff', 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' } + ] + } ); } ); } ); } ); From 5662d932894686c0f2f54249adc9e1d556ec5280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 15:59:10 +0100 Subject: [PATCH 25/80] Changed: Make highlight toolbar look decent. --- src/highlightui.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/highlightui.js b/src/highlightui.js index 9185c84..393b5e0 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -22,6 +22,8 @@ import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownContentsOnArr import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; import './../theme/highlight.css'; +// TODO: dirty hack to make toolbar look nice in dropdown. +import '@ckeditor/ckeditor5-ui/theme/components/dropdown/buttondropdown.css'; /** * The default Highlight UI plugin. From c80ea78d04ab541bdc78c33e76d2aa12545f1f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 17:09:08 +0100 Subject: [PATCH 26/80] Added: Add separator before eraser in highlight dropdown. --- src/highlightui.js | 2 ++ tests/manual/highlight.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/highlightui.js b/src/highlightui.js index 393b5e0..3c689b3 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -20,6 +20,7 @@ import Model from '@ckeditor/ckeditor5-ui/src/model'; import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/createsplitbuttondropdown'; import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownContentsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; +import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import './../theme/highlight.css'; // TODO: dirty hack to make toolbar look nice in dropdown. @@ -185,6 +186,7 @@ export default class HighlightUI extends Plugin { } ); // Add eraser button to dropdown. const eraserButton = componentFactory.create( 'removeHighlight' ); + buttons.push( new ToolbarSeparatorView() ); buttons.push( eraserButton ); // Make toolbar button enabled when any button in dropdown is enabled. diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index a99f93c..4f780c8 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -21,7 +21,7 @@ ClassicEditor // 'highlight:bluePen', // 'highlight:redPen', // 'removeHighlight', - 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + 'bold', '|', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) .then( editor => { From b9c28b62c31326a97bdb19388f048933745d1be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 19:27:24 +0100 Subject: [PATCH 27/80] Changed: Make toolbar split button react to highlight in selection. --- src/highlightui.js | 58 +++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 3c689b3..d083ed6 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -148,18 +148,31 @@ export default class HighlightUI extends Plugin { 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 model = new Model( { label: t( 'Highlight' ), withText: false, isVertical: false, - icon: markerIcon, - type: startingHighlighter.type, - color: startingHighlighter.color, + // Holds last executed highlighter. + lastExecuted: startingHighlighter.model, + // Holds current highlighter to execute (might be different then last used). commandValue: startingHighlighter.model } ); - bindModelToCommand( model, editor, 'highlight' ); + // Dropdown button changes to selection (command.value). + // If selection is in highlight it get active highlight appearance (icon, color). + // Otherwise it gets appearance (icon, color) of last executed highlight. + model.bind( 'icon' ).to( command, 'value', value => getActiveOption( value, 'type' ) === 'marker' ? markerIcon : penIcon ); + model.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); + model.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); const dropdownView = createSplitButtonDropdown( model, locale ); @@ -170,22 +183,19 @@ export default class HighlightUI extends Plugin { editor.editing.view.focus(); } ); - // Add highlighters buttons to dropdown + // Add highlighters buttons to dropdown. const buttons = options.map( option => { const buttonView = componentFactory.create( 'highlight:' + option.model ); - this.listenTo( buttonView, 'execute', () => changeToolbarButton( editor, model, { - type: option.type, - color: option.color, - command: 'highlight', - commandValue: option.model, - icon: markerIcon - } ) ); + // Update lastExecutedHighlight on execute. + this.listenTo( buttonView, 'execute', () => model.set( { lastExecuted: option.model } ) ); return buttonView; } ); + // Add eraser button to dropdown. const eraserButton = componentFactory.create( 'removeHighlight' ); + buttons.push( new ToolbarSeparatorView() ); buttons.push( eraserButton ); @@ -221,10 +231,19 @@ export default class HighlightUI extends Plugin { // Focus button group upon opening dropdown view dropdownView.buttonView.on( 'select', () => { if ( dropdownView.buttonView.buttonView.isEnabled && dropdownView.isOpen ) { - // buttonGroupView.focus(); + toolbarView.focus(); } }, { priority: 'low' } ); + // 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 === model.lastExecuted ? model.lastExecuted : current; + + return optionsMap[ whichHighlighter ][ key ]; + } + return dropdownView; } ); } @@ -235,19 +254,6 @@ function getBindingTargets( buttons, attribute ) { return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) ); } -// Rebinds model values to a new command. -function bindModelToCommand( model, editor, commandName ) { - model.unbind( 'isOn' ); - model.bind( 'isOn' ).to( editor.commands.get( commandName ), 'value' ); -} - -// Updates toolbar dropdown button with last selected highlighter. -function changeToolbarButton( editor, model, iconData ) { - model.set( iconData ); - - bindModelToCommand( model, editor, iconData.command ); -} - // Extends split button icon style to reflect last used button style. function bindIconStyle( dropdownView, model ) { const iconView = dropdownView.buttonView.buttonView.iconView; From 5d9f012e161f50f307aa1f34c328ace988ae5484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 17 Jan 2018 19:30:30 +0100 Subject: [PATCH 28/80] Changed: Extract getIconForType() internal method in HighlightUI. --- src/highlightui.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index d083ed6..f2154ac 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -81,9 +81,7 @@ export default class HighlightUI extends Plugin { _addHighlighterButton( option ) { const command = this.editor.commands.get( 'highlight' ); - const icon = option.type === 'marker' ? markerIcon : penIcon; - - this._addButton( 'highlight:' + option.model, option.title, icon, option.model, decorateHighlightButton ); + this._addButton( 'highlight:' + option.model, option.title, getIconForType( option.type ), option.model, decorateHighlightButton ); function decorateHighlightButton( button ) { button.bind( 'isEnabled' ).to( command, 'isEnabled' ); @@ -170,7 +168,7 @@ export default class HighlightUI extends Plugin { // Dropdown button changes to selection (command.value). // If selection is in highlight it get active highlight appearance (icon, color). // Otherwise it gets appearance (icon, color) of last executed highlight. - model.bind( 'icon' ).to( command, 'value', value => getActiveOption( value, 'type' ) === 'marker' ? markerIcon : penIcon ); + model.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); model.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); model.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); @@ -268,3 +266,8 @@ function bindIconStyle( dropdownView, model ) { iconView.bind( 'style' ).to( model, 'color', color => `color:${ color }` ); } + +// Returns icon for given highlighter type. +function getIconForType( type ) { + return type === 'marker' ? markerIcon : penIcon; +} From 5cbc000474940a63b0883aec040af2ae811761b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jan 2018 10:58:08 +0100 Subject: [PATCH 29/80] Tests: Update manual tests. --- tests/manual/highlight.html | 2 +- tests/manual/highlight.js | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/manual/highlight.html b/tests/manual/highlight.html index 70c639a..4f05c53 100644 --- a/tests/manual/highlight.html +++ b/tests/manual/highlight.html @@ -29,7 +29,7 @@ yellow one, pink one and green one.

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

diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 4f780c8..a3eebf8 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -13,15 +13,7 @@ ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ ArticlePluginSet, Highlight ], toolbar: [ - 'headings', - 'highlightDropdown', - // 'highlight:marker', - // 'highlight:greenMarker', - // 'highlight:pinkMarker', - // 'highlight:bluePen', - // 'highlight:redPen', - // 'removeHighlight', - 'bold', '|', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + 'headings', 'highlightDropdown', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) .then( editor => { From 03ecb5422b79c017c5d174337547531c5ae6de04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jan 2018 12:50:24 +0100 Subject: [PATCH 30/80] Tests: Add initial HighlightUI tests. --- src/highlightcommand.js | 4 + src/highlightui.js | 16 ++-- tests/highlightui.js | 183 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 tests/highlightui.js diff --git a/src/highlightcommand.js b/src/highlightcommand.js index f5db66f..1d5adb8 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -33,6 +33,10 @@ export default class HighlightCommand extends Command { */ this.value = doc.selection.getAttribute( 'highlight' ); this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); + // Alternate: + // const selection = doc.selection; + // this.isEnabled = ( !selection.isCollapsed || selection.hasAttribute( 'highlight' ) ) + // && model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); } /** diff --git a/src/highlightui.js b/src/highlightui.js index f2154ac..cfc70c7 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -85,6 +85,7 @@ export default class HighlightUI extends Plugin { function decorateHighlightButton( button ) { button.bind( 'isEnabled' ).to( command, 'isEnabled' ); + button.bind( 'isOn' ).to( command, 'value', value => value === option.model ); button.extendTemplate( { attributes: { @@ -134,7 +135,7 @@ export default class HighlightUI extends Plugin { } /** - * Creates split button drop down UI from provided highlight options. + * Creates split button dropdown UI from provided highlight options. * * @param {Array.} options * @private @@ -156,7 +157,7 @@ export default class HighlightUI extends Plugin { const command = editor.commands.get( 'highlight' ); const model = new Model( { - label: t( 'Highlight' ), + tooltip: t( 'Highlight' ), withText: false, isVertical: false, // Holds last executed highlighter. @@ -194,9 +195,6 @@ export default class HighlightUI extends Plugin { // Add eraser button to dropdown. const eraserButton = componentFactory.create( 'removeHighlight' ); - buttons.push( new ToolbarSeparatorView() ); - buttons.push( eraserButton ); - // Make toolbar button enabled when any button in dropdown is enabled. model.bind( 'isEnabled' ).to( // Bind to #isEnabled of each command... @@ -205,9 +203,12 @@ export default class HighlightUI extends Plugin { ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); + // Add those buttons after binding isEnabled to toolbar button. + buttons.push( new ToolbarSeparatorView() ); + buttons.push( eraserButton ); + model.set( 'buttons', buttons ); - // TODO: Temporary group as UI not fully defined yet. Also duplicates button dropdown // Group buttons for dropdown. const toolbarView = dropdownView.toolbarView = new ToolbarView(); @@ -215,9 +216,10 @@ export default class HighlightUI extends Plugin { model.buttons.map( view => toolbarView.items.add( view ) ); + // TODO: fix classes in dropdown dropdownView.extendTemplate( { attributes: { - class: [ 'ck-highlight_button', 'ck-buttondropdown' ] + class: [ 'ck-highlight_button', 'ck-buttondropdown', 'ck-highlight-dropdown' ] } } ); dropdownView.panelView.children.add( toolbarView ); diff --git a/tests/highlightui.js b/tests/highlightui.js new file mode 100644 index 0000000..5daf93d --- /dev/null +++ b/tests/highlightui.js @@ -0,0 +1,183 @@ +/** + * @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', { + 'Font Size': 'Font Size', + 'Normal': 'Normal', + 'Tiny': 'Tiny', + 'Small': 'Small', + 'Big': 'Big', + 'Huge': 'Huge' + } ); + + addTranslations( 'pl', { + 'Font Size': 'Rozmiar czcionki', + 'Normal': 'Normalny', + 'Tiny': 'Tyci', + 'Small': 'Mały', + 'Big': 'Duży', + 'Huge': 'Ogromny' + } ); + } ); + + 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 ); + expect( button ).to.have.property( 'withText', false ); + } ); + + it( 'should add custom CSS class to dropdown', () => { + dropdown.render(); + + expect( dropdown.element.classList.contains( 'ck-highlight-dropdown' ) ).to.be.true; + } ); + + it.skip( 'should focus view after command execution', () => { + const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); + + dropdown.commandName = 'highlight'; + dropdown.fire( 'execute' ); + + sinon.assert.calledOnce( focusSpy ); + } ); + + 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( '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.skip( 'localization', () => { + beforeEach( () => { + return localizedEditor( [ 'tiny', 'small', 'normal', 'big', 'huge' ] ); + } ); + + it( 'works for the #buttonView', () => { + const buttonView = dropdown.buttonView; + + expect( buttonView.tooltip ).to.equal( 'Rozmiar czcionki' ); + } ); + + it( 'works for the listView#items in the panel', () => { + const listView = dropdown.listView; + + expect( listView.items.map( item => item.label ) ).to.deep.equal( [ + 'Tyci', + 'Mały', + 'Normalny', + 'Duży', + 'Ogromny' + ] ); + } ); + + function localizedEditor( options ) { + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ HighlightEditing, HighlightUI ], + toolbar: [ 'highlight' ], + language: 'pl', + highlight: { + options + } + } ) + .then( newEditor => { + editor = newEditor; + dropdown = editor.ui.componentFactory.create( 'highlightDropdown' ); + command = editor.commands.get( 'highlight' ); + + editorElement.remove(); + + return editor.destroy(); + } ); + } + } ); + } ); +} ); From dfda27f4813de87a1bb8650e848177ab3a992b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jan 2018 13:17:53 +0100 Subject: [PATCH 31/80] Tests: Add toolbar button behavior tests. --- src/highlightui.js | 4 ++-- tests/highlightui.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index cfc70c7..6938e4b 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -262,11 +262,11 @@ function bindIconStyle( dropdownView, model ) { iconView.extendTemplate( { attributes: { - style: bind.to( 'style' ) + style: bind.to( 'color', color => `color:${ color }` ) } } ); - iconView.bind( 'style' ).to( model, 'color', color => `color:${ color }` ); + iconView.bind( 'color' ).to( model, 'color' ); } // Returns icon for given highlighter type. diff --git a/tests/highlightui.js b/tests/highlightui.js index 5daf93d..9a496e8 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -121,6 +121,48 @@ describe( 'HighlightUI', () => { 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.buttonView.iconView.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 ); + } ); + } ); + describe( 'model to command binding', () => { it( 'isEnabled', () => { command.isEnabled = false; From 74fd242a55ace7540e6fadeaea45ff84dd9dba20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jan 2018 13:57:30 +0100 Subject: [PATCH 32/80] Other: Add missing dev dependency. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cc80704..0faeee8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@ckeditor/ckeditor5-list": "^1.0.0-alpha.1", "@ckeditor/ckeditor5-paragraph": "^1.0.0-alpha.1", "@ckeditor/ckeditor5-typing": "^1.0.0-alpha.1", + "@ckeditor/ckeditor5-utils": "^1.0.0-alpha.1", "eslint": "^4.8.0", "eslint-config-ckeditor5": "^1.0.6", "husky": "^0.14.3", From 2e35c2163131e9329ea846f505fb82e7bb45b653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 18 Jan 2018 14:01:24 +0100 Subject: [PATCH 33/80] Other: Update package.json dependencies. --- package.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 0faeee8..47d4d94 100644 --- a/package.json +++ b/package.json @@ -7,23 +7,23 @@ "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", - "@ckeditor/ckeditor5-utils": "^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-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", From 59368b069e9fc4dcb77aff63891dc4a2ee1b2e76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 19 Jan 2018 14:20:19 +0100 Subject: [PATCH 34/80] Other: Updated code to changes in dropdowns API. --- src/highlightui.js | 69 ++++++++++++++++------------------------------ 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 6938e4b..2792ea3 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -17,9 +17,12 @@ import penIcon from './../theme/icons/pen.svg'; import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; -import createSplitButtonDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/createsplitbuttondropdown'; -import { closeDropdownOnBlur, closeDropdownOnExecute, focusDropdownContentsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; -import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; +import { + addDefaultBehavior, + addToolbarToDropdown, + createSplitButtonDropdown, + enableModelIfOneIsEnabled +} from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import './../theme/highlight.css'; @@ -173,17 +176,9 @@ export default class HighlightUI extends Plugin { model.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); model.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); - const dropdownView = createSplitButtonDropdown( model, locale ); - - bindIconStyle( dropdownView, model ); - - dropdownView.buttonView.on( 'execute', () => { - editor.execute( 'highlight', { value: model.commandValue } ); - editor.editing.view.focus(); - } ); - - // Add highlighters buttons to dropdown. + // Create buttons array. const buttons = options.map( option => { + // Get existing highlighter button. const buttonView = componentFactory.create( 'highlight:' + option.model ); // Update lastExecutedHighlight on execute. @@ -192,48 +187,35 @@ export default class HighlightUI extends Plugin { return buttonView; } ); - // Add eraser button to dropdown. - const eraserButton = componentFactory.create( 'removeHighlight' ); + // Make toolbar button enabled when any button in dropdown is enabled before adding separator and eraser. + enableModelIfOneIsEnabled( model, buttons ); - // Make toolbar button enabled when any button in dropdown is enabled. - model.bind( 'isEnabled' ).to( - // Bind to #isEnabled of each command... - ...getBindingTargets( buttons, 'isEnabled' ), - // ...and set it true if any command #isEnabled is true. - ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) - ); - - // Add those buttons after binding isEnabled to toolbar button. + // Add separator and eraser buttons to dropdown. buttons.push( new ToolbarSeparatorView() ); - buttons.push( eraserButton ); + buttons.push( componentFactory.create( 'removeHighlight' ) ); model.set( 'buttons', buttons ); - // Group buttons for dropdown. - const toolbarView = dropdownView.toolbarView = new ToolbarView(); + const dropdownView = createSplitButtonDropdown( model, locale ); - toolbarView.bind( 'isVertical', 'className' ).to( model, 'isVertical', 'toolbarClassName' ); + bindIconStyle( dropdownView, model ); + + dropdownView.buttonView.on( 'execute', () => { + editor.execute( 'highlight', { value: model.commandValue } ); + // TODO: execute focus should be defined elsewhere + editor.editing.view.focus(); + } ); - model.buttons.map( view => toolbarView.items.add( view ) ); + addToolbarToDropdown( dropdownView, model ); // TODO: fix classes in dropdown dropdownView.extendTemplate( { attributes: { - class: [ 'ck-highlight_button', 'ck-buttondropdown', 'ck-highlight-dropdown' ] + class: [ 'ck-highlight_button', 'ck-highlight-dropdown' ] } } ); - dropdownView.panelView.children.add( toolbarView ); - - closeDropdownOnBlur( dropdownView ); - closeDropdownOnExecute( dropdownView, toolbarView.items ); - focusDropdownContentsOnArrows( dropdownView, toolbarView ); - // Focus button group upon opening dropdown view - dropdownView.buttonView.on( 'select', () => { - if ( dropdownView.buttonView.buttonView.isEnabled && dropdownView.isOpen ) { - toolbarView.focus(); - } - }, { priority: 'low' } ); + addDefaultBehavior( dropdownView ); // 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) @@ -249,11 +231,6 @@ export default class HighlightUI extends Plugin { } } -// TODO: this is duplicated in various places (dropdowns) -function getBindingTargets( buttons, attribute ) { - return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) ); -} - // Extends split button icon style to reflect last used button style. function bindIconStyle( dropdownView, model ) { const iconView = dropdownView.buttonView.buttonView.iconView; From c65e9a00b88c8653d29b0764f1e9aecd83d6e819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 19 Jan 2018 15:22:57 +0100 Subject: [PATCH 35/80] Changed: Rename parts of SplitButton buttons to action/select. --- src/highlightui.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/highlightui.js b/src/highlightui.js index 2792ea3..c259310 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -233,7 +233,7 @@ export default class HighlightUI extends Plugin { // Extends split button icon style to reflect last used button style. function bindIconStyle( dropdownView, model ) { - const iconView = dropdownView.buttonView.buttonView.iconView; + const iconView = dropdownView.buttonView.actionView.iconView; const bind = iconView.bindTemplate; From 1e46b0b10e436f79d4a09f3328d38089f4a8a418 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 22 Jan 2018 12:10:57 +0100 Subject: [PATCH 36/80] Updated marker, pen, and eraser icons for the updated theme (see ckeditor/ckeditor5#645). --- theme/icons/eraser.svg | 2 +- theme/icons/marker.svg | 2 +- theme/icons/pen.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/theme/icons/eraser.svg b/theme/icons/eraser.svg index 669082b..397657a 100644 --- a/theme/icons/eraser.svg +++ b/theme/icons/eraser.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/theme/icons/marker.svg b/theme/icons/marker.svg index aef73a3..ca6b76b 100644 --- a/theme/icons/marker.svg +++ b/theme/icons/marker.svg @@ -1 +1 @@ - + diff --git a/theme/icons/pen.svg b/theme/icons/pen.svg index f592d2f..e77bdc6 100644 --- a/theme/icons/pen.svg +++ b/theme/icons/pen.svg @@ -1 +1 @@ - + From cc659f15c08a2424409929b85d179fb8a160832d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 22 Jan 2018 16:51:49 +0100 Subject: [PATCH 37/80] Changed: update dropdowns to match the new API. --- src/highlightui.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index c259310..489342a 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -198,15 +198,10 @@ export default class HighlightUI extends Plugin { const dropdownView = createSplitButtonDropdown( model, locale ); - bindIconStyle( dropdownView, model ); - - dropdownView.buttonView.on( 'execute', () => { - editor.execute( 'highlight', { value: model.commandValue } ); - // TODO: execute focus should be defined elsewhere - editor.editing.view.focus(); - } ); - addToolbarToDropdown( dropdownView, model ); + addDefaultBehavior( dropdownView ); + + bindIconStyleToColor( dropdownView, model ); // TODO: fix classes in dropdown dropdownView.extendTemplate( { @@ -215,8 +210,6 @@ export default class HighlightUI extends Plugin { } } ); - addDefaultBehavior( dropdownView ); - // 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. @@ -232,7 +225,7 @@ export default class HighlightUI extends Plugin { } // Extends split button icon style to reflect last used button style. -function bindIconStyle( dropdownView, model ) { +function bindIconStyleToColor( dropdownView, model ) { const iconView = dropdownView.buttonView.actionView.iconView; const bind = iconView.bindTemplate; From 177f5d60543a7f2355cdad519c241b7582fc4e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 23 Jan 2018 10:13:01 +0100 Subject: [PATCH 38/80] Changed: Remove redundant class from highlight button. --- src/highlightui.js | 11 +---------- theme/highlight.css | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 489342a..502270d 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -26,8 +26,6 @@ import { import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import './../theme/highlight.css'; -// TODO: dirty hack to make toolbar look nice in dropdown. -import '@ckeditor/ckeditor5-ui/theme/components/dropdown/buttondropdown.css'; /** * The default Highlight UI plugin. @@ -90,12 +88,6 @@ export default class HighlightUI extends Plugin { button.bind( 'isEnabled' ).to( command, 'isEnabled' ); button.bind( 'isOn' ).to( command, 'value', value => value === option.model ); - button.extendTemplate( { - attributes: { - class: 'ck-highlight_button' - } - } ); - button.iconView.extendTemplate( { attributes: { style: `color: ${ option.color }` @@ -203,10 +195,9 @@ export default class HighlightUI extends Plugin { bindIconStyleToColor( dropdownView, model ); - // TODO: fix classes in dropdown dropdownView.extendTemplate( { attributes: { - class: [ 'ck-highlight_button', 'ck-highlight-dropdown' ] + class: [ 'ck-highlight-dropdown' ] } } ); diff --git a/theme/highlight.css b/theme/highlight.css index 3133aa6..4c928fd 100644 --- a/theme/highlight.css +++ b/theme/highlight.css @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -.ck-highlight_button svg path:not(.ck-icon__fill) { +.ck-highlight-dropdown svg path:not(.ck-icon__fill) { /* Do not inherit color from parent. */ fill: initial; } From 700e6a4fcaa51764d2952619f9b698a6ae0a60d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 23 Jan 2018 14:07:19 +0100 Subject: [PATCH 39/80] Changed: Removed `addDefaultBehavior()` from dropdown utils. --- src/highlightui.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 502270d..8d35230 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -18,10 +18,12 @@ import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import { - addDefaultBehavior, addToolbarToDropdown, + closeDropdownOnBlur, + closeDropdownOnExecute, createSplitButtonDropdown, - enableModelIfOneIsEnabled + enableModelIfOneIsEnabled, + focusDropdownContentsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; @@ -191,7 +193,9 @@ export default class HighlightUI extends Plugin { const dropdownView = createSplitButtonDropdown( model, locale ); addToolbarToDropdown( dropdownView, model ); - addDefaultBehavior( dropdownView ); + closeDropdownOnBlur( dropdownView ); + closeDropdownOnExecute( dropdownView ); + focusDropdownContentsOnArrows( dropdownView ); bindIconStyleToColor( dropdownView, model ); From e6a60ca59785e1f739bfea3f25f2cde43013d21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Tue, 23 Jan 2018 14:20:53 +0100 Subject: [PATCH 40/80] Changed: Removed `createSplitButtonDropdown()` from dropdown utils. --- src/highlightui.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 8d35230..52aa396 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -21,7 +21,8 @@ import { addToolbarToDropdown, closeDropdownOnBlur, closeDropdownOnExecute, - createSplitButtonDropdown, + createDropdownView, + createSplitButtonForDropdown, enableModelIfOneIsEnabled, focusDropdownContentsOnArrows } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; @@ -190,7 +191,15 @@ export default class HighlightUI extends Plugin { model.set( 'buttons', buttons ); - const dropdownView = createSplitButtonDropdown( model, locale ); + const splitButtonView = createSplitButtonForDropdown( model, locale ); + const dropdownView = createDropdownView( model, splitButtonView, locale ); + + // TODO: Extend template to hide arrow from dropdown. Remove me after changes in theme-lark. + dropdownView.extendTemplate( { + attributes: { + class: 'ck-splitbutton-dropdown' + } + } ); addToolbarToDropdown( dropdownView, model ); closeDropdownOnBlur( dropdownView ); From 1e68317d1922c12141c11dc67268ac2ec2e4518c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jan 2018 14:28:50 +0100 Subject: [PATCH 41/80] Changed: update dropdowns to match the new API. --- src/highlightui.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 52aa396..286bc98 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -17,15 +17,13 @@ import penIcon from './../theme/icons/pen.svg'; import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; -import { - addToolbarToDropdown, - closeDropdownOnBlur, - closeDropdownOnExecute, - createDropdownView, - createSplitButtonForDropdown, - enableModelIfOneIsEnabled, - focusDropdownContentsOnArrows -} from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; +import addToolbarToDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/addtoolbartodropdown'; +import closeDropdownOnBlur from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/closedropdownonblur'; +import closeDropdownOnExecute from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/closedropdownonexecute'; +import createDropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/createdropdownview'; +import createSplitButtonForDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/createsplitbuttonfordropdown'; +import enableModelIfOneIsEnabled from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/enablemodelifoneisenabled'; +import focusDropdownContentsOnArrows from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/focusdropdowncontentsonarrows'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import './../theme/highlight.css'; From a4b3cd4b91e4367d3faf653e8fcd3b2f7df72e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jan 2018 14:39:57 +0100 Subject: [PATCH 42/80] Other: Revert icon hack from ButtonView. --- src/highlightui.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 286bc98..9bc484e 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -89,7 +89,7 @@ export default class HighlightUI extends Plugin { button.bind( 'isEnabled' ).to( command, 'isEnabled' ); button.bind( 'isOn' ).to( command, 'value', value => value === option.model ); - button.iconView.extendTemplate( { + button.extendTemplate( { attributes: { style: `color: ${ option.color }` } @@ -228,17 +228,18 @@ export default class HighlightUI extends Plugin { // Extends split button icon style to reflect last used button style. function bindIconStyleToColor( dropdownView, model ) { - const iconView = dropdownView.buttonView.actionView.iconView; + const actionView = dropdownView.buttonView.actionView; - const bind = iconView.bindTemplate; + const bind = actionView.bindTemplate; - iconView.extendTemplate( { + // Color will propagate to iconView. + actionView.extendTemplate( { attributes: { style: bind.to( 'color', color => `color:${ color }` ) } } ); - iconView.bind( 'color' ).to( model, 'color' ); + actionView.bind( 'color' ).to( model, 'color' ); } // Returns icon for given highlighter type. From a35f73c815b188d050964c4169ef01e68edd03d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jan 2018 15:23:04 +0100 Subject: [PATCH 43/80] Added: Enable highlighter on collapsed selection. --- src/highlightcommand.js | 16 ++++----- tests/highlightcommand.js | 71 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 1d5adb8..b907a57 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -25,7 +25,8 @@ export default class HighlightCommand extends Command { const doc = model.document; /** - * A flag indicating whether the command is active, which means that the selection has highlight attribute set. + * A value indicating whether the command is active. If the selection has highlight attribute + * set the value will be set to highlight attribute value. * * @observable * @readonly @@ -33,10 +34,6 @@ export default class HighlightCommand extends Command { */ this.value = doc.selection.getAttribute( 'highlight' ); this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); - // Alternate: - // const selection = doc.selection; - // this.isEnabled = ( !selection.isCollapsed || selection.hasAttribute( 'highlight' ) ) - // && model.schema.checkAttributeInSelection( doc.selection, 'highlight' ); } /** @@ -53,8 +50,8 @@ export default class HighlightCommand extends Command { const document = model.document; const selection = document.selection; - // Do not apply highlight on collapsed selection when not inside existing highlight. - if ( selection.isCollapsed && !this.value ) { + // Do not apply highlight on wrong selection. + if ( !this.isEnabled ) { return; } @@ -82,6 +79,7 @@ export default class HighlightCommand extends Command { if ( !highlighter || this.value === highlighter ) { // ...remove attribute when passing highlighter different then current or executing "eraser". writer.removeAttribute( 'highlight', highlightRange ); + selection.removeAttribute( 'highlight' ); } else { // ...update `highlight` value. writer.setAttribute( 'highlight', highlighter, highlightRange ); @@ -89,8 +87,8 @@ export default class HighlightCommand extends Command { // And create new range wrapping changed highlighter. selection.setRanges( [ highlightRange ] ); } - } else { - // TODO + } else if ( highlighter ) { + selection.setAttribute( 'highlight', highlighter ); } } else { for ( const range of ranges ) { diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index e8e82fa..990696a 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -8,15 +8,19 @@ 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 '../../ckeditor5-engine/src/model/position'; +import Range from '../../ckeditor5-engine/src/model/range'; describe( 'HighlightCommand', () => { - let editor, model, command; + let editor, model, doc, root, command; beforeEach( () => { return ModelTestEditor.create() .then( newEditor => { editor = newEditor; model = editor.model; + doc = model.document; + root = doc.getRoot(); command = new HighlightCommand( newEditor ); editor.commands.add( 'highlight', command ); @@ -84,6 +88,71 @@ describe( 'HighlightCommand', () => { expect( command.value ).to.be.undefined; } ); + 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( { value: 'foo' } ); + expect( command.value ).to.equal( 'foo' ); + + expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.true; + + 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( () => { + // Simulate clicking right arrow key by changing selection ranges. + doc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + + // Get back to previous selection. + doc.selection.setRanges( [ 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( () => { + doc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + } ); + + expect( command.value ).to.be.undefined; + + // Go back to where attribute was stored. + model.change( () => { + doc.selection.setRanges( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] ); + } ); + + // Attribute should be restored. + expect( command.value ).to.equal( 'foo' ); + + command.execute(); + + expect( command.value ).to.be.undefined; + expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.false; + } ); + it( 'should change entire highlight when inside highlighted text', () => { setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); From e81e09981ea5bac5a5c93d53585c2eb782a16723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jan 2018 15:32:00 +0100 Subject: [PATCH 44/80] Added: Execute highight command on split button action button. --- src/highlightui.js | 7 +++++++ tests/highlightui.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/highlightui.js b/src/highlightui.js index 9bc484e..b43b85c 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -83,6 +83,7 @@ export default class HighlightUI extends Plugin { _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 ) { @@ -212,6 +213,12 @@ export default class HighlightUI extends Plugin { } } ); + // Execute current action from dropdown's split button action button. + dropdownView.buttonView.on( 'execute', () => { + editor.execute( 'highlight', { value: model.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. diff --git a/tests/highlightui.js b/tests/highlightui.js index 9a496e8..4f1fef5 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -132,7 +132,7 @@ describe( 'HighlightUI', () => { function validateButton( which ) { expect( button.icon ).to.equal( buttons[ which ].icon ); - expect( button.buttonView.iconView.color ).to.equal( options[ which ].color ); + expect( button.actionView.color ).to.equal( options[ which ].color ); } it( 'should have properties of first defined highlighter', () => { From 82010b1dbb5e7c60fd67cc64d6b8e787b6775990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Wed, 24 Jan 2018 15:39:23 +0100 Subject: [PATCH 45/80] Added: Bind model `isOn` to highlight command value. --- src/highlightui.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index b43b85c..53da27d 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -163,12 +163,13 @@ export default class HighlightUI extends Plugin { commandValue: startingHighlighter.model } ); - // Dropdown button changes to selection (command.value). - // If selection is in highlight it get active highlight appearance (icon, color). - // Otherwise it gets appearance (icon, color) of last executed highlight. + // 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. model.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); model.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); model.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); + model.bind( 'isOn' ).to( command, 'value', value => !!value ); // Create buttons array. const buttons = options.map( option => { From 43ea0d0e23f5c5a5cad1f57267f6757ef71c8893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 25 Jan 2018 11:21:43 +0100 Subject: [PATCH 46/80] Docs: move `HighlightOption` from `HighlightEditing` to `Highlight`. --- src/highlight.js | 79 ++++++++++++++++++++++++++++++++++++++++- src/highlightediting.js | 53 ++------------------------- src/highlightui.js | 4 +-- 3 files changed, 82 insertions(+), 54 deletions(-) diff --git a/src/highlight.js b/src/highlight.js index c1ddaf6..246a978 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -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,77 @@ export default class Highlight extends Plugin { return 'Highlight'; } } + +/** + * Highlight option descriptor. + * + * @typedef {Object} module:highlight/highlight~HighlightOption + * @property {String} title The user-readable title of the option. + * @property {String} model Attribute's unique value in the model. + * @property {String} color Color used for highlighter. Should be coherent with `class` CSS setting. + * @property {String} class CSS Class used on `mark` element in view. Should be coherent with `color` setting. + * @property {'marker'|'pen'} type The type of highlighter: + * - "marker" - will use #color as highlight background, + * - "pen" - will use #color as highlight font color. + */ + +/** + * 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 + */ + +/** + * 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. + * + * 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/highlightediting.js b/src/highlightediting.js index 4098f82..7967c89 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -67,59 +67,10 @@ export default class HighlightEditing extends Plugin { } } -/** - * Highlight option descriptor. - * - * @typedef {Object} module:highlight/highlightediting~HighlightOption - * @property {String} title The user-readable title of the option. - * @property {String} model Attribute's unique value in the model. - * @property {String} color Color used for highlighter. Should be coherent with `class` CSS setting. - * @property {String} class CSS Class used on `mark` element in view. Should be coherent with `color` setting. - * @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/highlightediting~HighlightOption} +// Converts {@link module:highlight/highlight~HighlightOption} // to {@link module:engine/conversion/definition-based-converters~ConverterDefinition} // -// @param {module:highlight/highlightediting~HighlightOption} option +// @param {module:highlight/highlight~HighlightOption} option // @returns {module:engine/conversion/definition-based-converters~ConverterDefinition} function _getConverterDefinition( option ) { return { diff --git a/src/highlightui.js b/src/highlightui.js index 53da27d..b48269d 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -77,7 +77,7 @@ export default class HighlightUI extends Plugin { /** * Creates toolbar button from provided highlight option. * - * @param {module:highlight/highlightediting~HighlightOption} option + * @param {module:highlight/highlight~HighlightOption} option * @private */ _addHighlighterButton( option ) { @@ -134,7 +134,7 @@ export default class HighlightUI extends Plugin { /** * Creates split button dropdown UI from provided highlight options. * - * @param {Array.} options + * @param {Array.} options * @private */ _addDropdown( options ) { From f3eb7dca3a4110234b505314cd35aef1f3c21573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 25 Jan 2018 12:46:31 +0100 Subject: [PATCH 47/80] Tests: Update localization tests. --- src/highlightediting.js | 8 +++--- src/highlightui.js | 35 +++++++++++++++++++++-- tests/highlightediting.js | 8 +++--- tests/highlightui.js | 58 +++++++++++++++++++-------------------- 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/src/highlightediting.js b/src/highlightediting.js index 7967c89..ff6faf3 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -30,10 +30,10 @@ export default class HighlightEditing extends Plugin { 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' } + { 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/src/highlightui.js b/src/highlightui.js index b48269d..64e0176 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -34,6 +34,33 @@ import './../theme/highlight.css'; * @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 */ @@ -113,8 +140,10 @@ export default class HighlightUI extends Plugin { editor.ui.componentFactory.add( name, locale => { const buttonView = new ButtonView( locale ); + const localized = this.localizedOptionTitles[ label ] ? this.localizedOptionTitles[ label ] : label; + buttonView.set( { - label, + label: localized, icon, tooltip: true } ); @@ -214,8 +243,10 @@ export default class HighlightUI extends Plugin { } } ); + splitButtonView.delegate( 'execute' ).to( dropdownView ); + // Execute current action from dropdown's split button action button. - dropdownView.buttonView.on( 'execute', () => { + dropdownView.on( 'execute', () => { editor.execute( 'highlight', { value: model.commandValue } ); editor.editing.view.focus(); } ); diff --git a/tests/highlightediting.js b/tests/highlightediting.js index 94e5d56..d3977ba 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -86,10 +86,10 @@ describe( 'HighlightEditing', () => { 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' } + { 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 index 4f1fef5..e62f970 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -23,21 +23,23 @@ describe( 'HighlightUI', () => { before( () => { addTranslations( 'en', { - 'Font Size': 'Font Size', - 'Normal': 'Normal', - 'Tiny': 'Tiny', - 'Small': 'Small', - 'Big': 'Big', - 'Huge': 'Huge' + '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', { - 'Font Size': 'Rozmiar czcionki', - 'Normal': 'Normalny', - 'Tiny': 'Tyci', - 'Small': 'Mały', - 'Big': 'Duży', - 'Huge': 'Ogromny' + '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' } ); } ); @@ -86,7 +88,7 @@ describe( 'HighlightUI', () => { expect( dropdown.element.classList.contains( 'ck-highlight-dropdown' ) ).to.be.true; } ); - it.skip( 'should focus view after command execution', () => { + it( 'should focus view after command execution', () => { const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); dropdown.commandName = 'highlight'; @@ -174,30 +176,31 @@ describe( 'HighlightUI', () => { } ); } ); - describe.skip( 'localization', () => { + describe( 'localization', () => { beforeEach( () => { - return localizedEditor( [ 'tiny', 'small', 'normal', 'big', 'huge' ] ); + return localizedEditor(); } ); it( 'works for the #buttonView', () => { const buttonView = dropdown.buttonView; - expect( buttonView.tooltip ).to.equal( 'Rozmiar czcionki' ); + expect( buttonView.tooltip ).to.equal( 'Zakreślacz' ); } ); it( 'works for the listView#items in the panel', () => { - const listView = dropdown.listView; - - expect( listView.items.map( item => item.label ) ).to.deep.equal( [ - 'Tyci', - 'Mały', - 'Normalny', - 'Duży', - 'Ogromny' + 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( options ) { + function localizedEditor() { const editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); @@ -205,10 +208,7 @@ describe( 'HighlightUI', () => { .create( editorElement, { plugins: [ HighlightEditing, HighlightUI ], toolbar: [ 'highlight' ], - language: 'pl', - highlight: { - options - } + language: 'pl' } ) .then( newEditor => { editor = newEditor; From a01c35865be0851985cc1f4d2ce4971b7a6236ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 25 Jan 2018 12:47:44 +0100 Subject: [PATCH 48/80] Other: Update copyright year. --- .eslintrc.js | 2 +- LICENSE.md | 2 +- src/highlight.js | 2 +- src/highlightcommand.js | 2 +- src/highlightediting.js | 2 +- src/highlightui.js | 2 +- tests/highlight.js | 2 +- tests/highlightcommand.js | 2 +- tests/highlightediting.js | 2 +- tests/integration.js | 2 +- tests/manual/highlight.js | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) 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/src/highlight.js b/src/highlight.js index 246a978..ff70518 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. */ diff --git a/src/highlightcommand.js b/src/highlightcommand.js index b907a57..784bbd1 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. */ diff --git a/src/highlightediting.js b/src/highlightediting.js index ff6faf3..cc2daf5 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. */ diff --git a/src/highlightui.js b/src/highlightui.js index 64e0176..b2ed6f2 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. */ 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 990696a..72cd0a6 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. */ diff --git a/tests/highlightediting.js b/tests/highlightediting.js index d3977ba..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. */ diff --git a/tests/integration.js b/tests/integration.js index 1508f42..95ef64c 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. */ diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index a3eebf8..8f74ce9 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/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. */ From 781cf1dc3aaaf21b463bc8ad6c4457359a417e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 25 Jan 2018 13:44:06 +0100 Subject: [PATCH 49/80] Tests: Update `HighlightCommand` tests. --- src/highlightcommand.js | 5 --- tests/highlightcommand.js | 76 ++++++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 784bbd1..2e3f8ff 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -50,11 +50,6 @@ export default class HighlightCommand extends Command { const document = model.document; const selection = document.selection; - // Do not apply highlight on wrong selection. - if ( !this.isEnabled ) { - return; - } - const highlighter = options.value; model.change( writer => { diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index 72cd0a6..961727c 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -25,8 +25,13 @@ describe( 'HighlightCommand', () => { command = new HighlightCommand( newEditor ); editor.commands.add( 'highlight', command ); - model.schema.register( 'paragraph', { inheritAllFrom: '$block' } ); - model.schema.extend( '$text', { allowAttributes: 'highlight' } ); + 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; + } + } ); } ); } ); @@ -41,55 +46,64 @@ describe( 'HighlightCommand', () => { describe( 'value', () => { it( 'is set to highlight attribute value when selection is in text with highlight attribute', () => { - setData( model, '<$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( model, '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( model, '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()', () => { 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' ); + setData( model, '

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

' ); expect( command.value ).to.equal( 'marker' ); command.execute( { value: 'greenMarker' } ); - expect( getData( model ) ).to.equal( 'abc[<$text highlight="greenMarker">foobar]xyz' ); + expect( getData( model ) ).to.equal( '

abc[<$text highlight="greenMarker">foobar]xyz

' ); expect( command.value ).to.equal( 'greenMarker' ); } ); it( 'should remove entire highlight when inside highlighted text of the same value', () => { - setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); + setData( model, '

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

' ); expect( command.value ).to.equal( 'marker' ); command.execute( { value: 'marker' } ); - expect( getData( model ) ).to.equal( 'abcfoo[]barxyz' ); + expect( getData( model ) ).to.equal( '

abcfoo[]barxyz

' ); expect( command.value ).to.be.undefined; } ); it( 'should change selection attribute in non-empty parent', () => { - setData( model, 'a[]bc<$text highlight="marker">foobarxyz' ); + setData( model, '

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

' ); expect( command.value ).to.be.undefined; command.execute( { value: 'foo' } ); @@ -101,10 +115,16 @@ describe( 'HighlightCommand', () => { 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' ); + setData( model, '

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

' ); command.execute( { value: 'foo' } ); @@ -122,7 +142,7 @@ describe( 'HighlightCommand', () => { } ); it( 'should change selection attribute and store it if selection is collapsed in empty parent', () => { - setData( model, 'abc<$text highlight="marker">foobarxyz[]' ); + setData( model, '

abc<$text highlight="marker">foobarxyz

[]

' ); expect( command.value ).to.be.undefined; @@ -154,13 +174,13 @@ describe( 'HighlightCommand', () => { } ); it( 'should change entire highlight when inside highlighted text', () => { - setData( model, 'abc<$text highlight="marker">foo[]barxyz' ); + setData( model, '

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

' ); expect( command.value ).to.equal( 'marker' ); command.execute( { value: 'greenMarker' } ); - expect( getData( model ) ).to.equal( 'abc[<$text highlight="greenMarker">foobar]xyz' ); + expect( getData( model ) ).to.equal( '

abc[<$text highlight="greenMarker">foobar]xyz

' ); expect( command.value ).to.equal( 'greenMarker' ); } ); @@ -168,7 +188,7 @@ describe( 'HighlightCommand', () => { 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' ); + setData( model, '

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

' ); expect( command.value ).to.be.undefined; @@ -176,15 +196,15 @@ describe( 'HighlightCommand', () => { expect( command.value ).to.equal( 'marker' ); - expect( getData( model ) ).to.equal( 'a[<$text highlight="marker">bcfo]obarxyz' ); + 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' + '

abcabc[abc

' + + '

foofoofoo

' + + '

barbar]bar

' ); command.execute( { value: 'marker' } ); @@ -192,21 +212,21 @@ describe( 'HighlightCommand', () => { 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' + '

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' ); + 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' + '

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

' ); expect( command.value ).to.equal( 'foo' ); @@ -217,13 +237,13 @@ describe( 'HighlightCommand', () => { 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' ); + setData( model, '

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

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

abcfoo[]barxyz

' ); expect( command.value ).to.be.undefined; } ); @@ -231,13 +251,13 @@ describe( 'HighlightCommand', () => { 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' ); + 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( getData( model ) ).to.equal( '

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

' ); expect( command.value ).to.be.undefined; } ); From 2add9c0ef6729d31af4fd6e1990e7ca2532f98c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 25 Jan 2018 14:12:40 +0100 Subject: [PATCH 50/80] Tests: Update compatibility tests. --- tests/integration.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration.js b/tests/integration.js index 95ef64c..3a18367 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -41,23 +41,23 @@ describe( 'Highlight', () => { return editor.destroy(); } ); - describe.skip( 'compatibility with images', () => { - it( 'does not work inside image caption', () => { + describe( 'compatibility with images', () => { + it( 'does work inside image caption', () => { setModelData( model, 'foo[bar]baz' ); - editor.execute( 'marker' ); + editor.execute( 'highlight', { value: 'marker' } ); 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( model, 'foo[fooabcbar]bar' ); - editor.execute( 'marker' ); + editor.execute( 'highlight', { value: 'marker' } ); expect( getModelData( model ) ).to.equal( 'foo[<$text highlight="marker">foo' + From e717306fa309b4de1673ed4b97fee96a603bb546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 26 Jan 2018 10:54:46 +0100 Subject: [PATCH 51/80] Docs: Add highlight api page. --- docs/api/highlight.md | 30 ++++++++++++++++++++++++++++++ src/highlight.js | 1 - 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 docs/api/highlight.md 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/src/highlight.js b/src/highlight.js index ff70518..65e06a5 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -110,4 +110,3 @@ export default class Highlight extends Plugin { * * @member {Array.} module:highlight/highlight~HighlightConfig#options */ - From bc1de94541b05b9ed02167cbd6a44f01ec65c1cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Sat, 27 Jan 2018 14:10:29 +0100 Subject: [PATCH 52/80] Docs: Add highlight feature guide. --- .../features/build-highlight-source.html | 23 +++ .../features/build-highlight-source.js | 14 ++ .../features/custom-highlight-options.html | 5 + .../features/custom-highlight-options.js | 27 ++++ .../_snippets/features/highlight-buttons.html | 12 ++ docs/_snippets/features/highlight-buttons.js | 23 +++ docs/_snippets/features/highlight.html | 12 ++ docs/_snippets/features/highlight.js | 22 +++ docs/features/highlight.md | 138 ++++++++++++++++++ tests/manual/highlight.html | 2 +- 10 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 docs/_snippets/features/build-highlight-source.html create mode 100644 docs/_snippets/features/build-highlight-source.js create mode 100644 docs/_snippets/features/custom-highlight-options.html create mode 100644 docs/_snippets/features/custom-highlight-options.js create mode 100644 docs/_snippets/features/highlight-buttons.html create mode 100644 docs/_snippets/features/highlight-buttons.js create mode 100644 docs/_snippets/features/highlight.html create mode 100644 docs/_snippets/features/highlight.js create mode 100644 docs/features/highlight.md 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..3844af8 --- /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..683c971 --- /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..6b7d6e0 --- /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/features/highlight.md b/docs/features/highlight.md new file mode 100644 index 0000000..ae866ed --- /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 enables support for setting highlight. + +## Demo + +{@snippet features/highlight} + +## Configuring highlight options + +It is, of course, possible to configure which highlight options the editor should support. +Use the {@link module:highlight/highlight~HighlightConfig#options `highlight.options`} configuration option to do so. + +For example, the following editor will support only two highlighters: + +```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', 'highlight', 'undo', 'redo' + ] + } ) + .then( ... ) + .catch( ... ); +``` + +{@snippet features/custom-highlight-options} + +Instead of using `highlightDropdown` the editor supports also button configuration: + +```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 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: + +* Dropdown: `'highlightDropdown'`. +* Command: `'highlight'`. + + The number of options and their names are based on the {@link module:highlight/highlight~HighlightConfig#options `highlight.options`} configuration option). + + You can change highlight of the current selection by executing command with proper value: + + ```js + editor.execute( 'highlight', { value: 'marker' } ); + ``` + + The Value passed to `highlight` corresponds to the `model` property in configuration object. For 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 strings below as value: + - `'marker'` - and will be available as `highligth:marker'` button. + - `'greenMarker'` - and will be available as `highligth:greenMarker'` button. + - `'pinkMarker'` - and will be available as `highligth:pinkMarker'` button. + - `'redPen'` - and will be available as `highligth:redPen'` button. + - `'bluePen'` - and will be available as `highligth:bluePen'` button. + + passing an empty value will remove any `highlight` set: + + ```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/tests/manual/highlight.html b/tests/manual/highlight.html index 4f05c53..8c99f1e 100644 --- a/tests/manual/highlight.html +++ b/tests/manual/highlight.html @@ -20,8 +20,8 @@ background-color: transparent; color: #0091ff; } - +

Highlight feature example.

From 691390d7de44af5fdfc3918308a83c70cbebfda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Sun, 28 Jan 2018 12:58:59 +0100 Subject: [PATCH 53/80] Changed: Change how dropdowns with `ButtonView` and `SplitButtonView` are created. --- src/highlightui.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index b2ed6f2..302fdce 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -17,14 +17,13 @@ import penIcon from './../theme/icons/pen.svg'; import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; +import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import addToolbarToDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/addtoolbartodropdown'; import closeDropdownOnBlur from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/closedropdownonblur'; import closeDropdownOnExecute from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/closedropdownonexecute'; -import createDropdownView from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/createdropdownview'; -import createSplitButtonForDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/createsplitbuttonfordropdown'; import enableModelIfOneIsEnabled from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/enablemodelifoneisenabled'; import focusDropdownContentsOnArrows from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/focusdropdowncontentsonarrows'; -import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; +import { createSplitButtonDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import './../theme/highlight.css'; @@ -220,8 +219,7 @@ export default class HighlightUI extends Plugin { model.set( 'buttons', buttons ); - const splitButtonView = createSplitButtonForDropdown( model, locale ); - const dropdownView = createDropdownView( model, splitButtonView, locale ); + const dropdownView = createSplitButtonDropdown( model, locale ); // TODO: Extend template to hide arrow from dropdown. Remove me after changes in theme-lark. dropdownView.extendTemplate( { @@ -243,8 +241,6 @@ export default class HighlightUI extends Plugin { } } ); - splitButtonView.delegate( 'execute' ).to( dropdownView ); - // Execute current action from dropdown's split button action button. dropdownView.on( 'execute', () => { editor.execute( 'highlight', { value: model.commandValue } ); From 94853632e5b9d1b61dc2d51b109c98143c97fa78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Sun, 28 Jan 2018 13:30:52 +0100 Subject: [PATCH 54/80] Changed: Remove dafult dropdown behavior helper methods from API. --- src/highlightui.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 302fdce..a4449ff 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -19,10 +19,7 @@ import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import addToolbarToDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/addtoolbartodropdown'; -import closeDropdownOnBlur from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/closedropdownonblur'; -import closeDropdownOnExecute from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/closedropdownonexecute'; import enableModelIfOneIsEnabled from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/enablemodelifoneisenabled'; -import focusDropdownContentsOnArrows from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/focusdropdowncontentsonarrows'; import { createSplitButtonDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import './../theme/highlight.css'; @@ -229,9 +226,6 @@ export default class HighlightUI extends Plugin { } ); addToolbarToDropdown( dropdownView, model ); - closeDropdownOnBlur( dropdownView ); - closeDropdownOnExecute( dropdownView ); - focusDropdownContentsOnArrows( dropdownView ); bindIconStyleToColor( dropdownView, model ); From f4f1ddca535ee638bf7efd23958d34e96278df45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Sun, 28 Jan 2018 13:48:18 +0100 Subject: [PATCH 55/80] Added: Introduce `bindOneToMany()` binding helper. --- src/highlightui.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index a4449ff..541c21a 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -19,7 +19,7 @@ import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import addToolbarToDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/addtoolbartodropdown'; -import enableModelIfOneIsEnabled from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/enablemodelifoneisenabled'; +import bindOneToMany from '@ckeditor/ckeditor5-ui/src/bindings/bindonetomany'; import { createSplitButtonDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import './../theme/highlight.css'; @@ -208,7 +208,9 @@ export default class HighlightUI extends Plugin { } ); // Make toolbar button enabled when any button in dropdown is enabled before adding separator and eraser. - enableModelIfOneIsEnabled( model, buttons ); + bindOneToMany( model, 'isEnabled', buttons, 'isEnabled', + ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) + ); // Add separator and eraser buttons to dropdown. buttons.push( new ToolbarSeparatorView() ); From 053decb39f2775537de535cbcb634314b4d3c073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 29 Jan 2018 10:43:10 +0100 Subject: [PATCH 56/80] Changed: Move `addToolbarToDropdown()` helper to ui/dropdown/utils. --- src/highlightui.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 541c21a..7f07b4d 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -18,9 +18,8 @@ import eraserIcon from './../theme/icons/eraser.svg'; import Model from '@ckeditor/ckeditor5-ui/src/model'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; -import addToolbarToDropdown from '@ckeditor/ckeditor5-ui/src/dropdown/helpers/addtoolbartodropdown'; import bindOneToMany from '@ckeditor/ckeditor5-ui/src/bindings/bindonetomany'; -import { createSplitButtonDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; +import { createSplitButtonDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import './../theme/highlight.css'; @@ -216,8 +215,6 @@ export default class HighlightUI extends Plugin { buttons.push( new ToolbarSeparatorView() ); buttons.push( componentFactory.create( 'removeHighlight' ) ); - model.set( 'buttons', buttons ); - const dropdownView = createSplitButtonDropdown( model, locale ); // TODO: Extend template to hide arrow from dropdown. Remove me after changes in theme-lark. @@ -227,7 +224,7 @@ export default class HighlightUI extends Plugin { } } ); - addToolbarToDropdown( dropdownView, model ); + addToolbarToDropdown( dropdownView, buttons, model ); bindIconStyleToColor( dropdownView, model ); From fdb76bc5e88cfcac9167471e9d633268338378be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 29 Jan 2018 11:43:01 +0100 Subject: [PATCH 57/80] Changed: Bind dropdown behavior to `DropdownView` instead of `Model`. --- src/highlightui.js | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 7f07b4d..bc4cc9c 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -16,7 +16,6 @@ import markerIcon from './../theme/icons/marker.svg'; import penIcon from './../theme/icons/pen.svg'; import eraserIcon from './../theme/icons/eraser.svg'; -import Model from '@ckeditor/ckeditor5-ui/src/model'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; import bindOneToMany from '@ckeditor/ckeditor5-ui/src/bindings/bindonetomany'; import { createSplitButtonDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; @@ -177,7 +176,9 @@ export default class HighlightUI extends Plugin { componentFactory.add( 'highlightDropdown', locale => { const command = editor.commands.get( 'highlight' ); - const model = new Model( { + const dropdownView = createSplitButtonDropdown( locale ); + + dropdownView.set( { tooltip: t( 'Highlight' ), withText: false, isVertical: false, @@ -190,10 +191,10 @@ export default class HighlightUI extends Plugin { // 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. - model.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); - model.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); - model.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); - model.bind( 'isOn' ).to( command, 'value', value => !!value ); + dropdownView.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); + dropdownView.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); + dropdownView.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); + dropdownView.bind( 'isOn' ).to( command, 'value', value => !!value ); // Create buttons array. const buttons = options.map( option => { @@ -201,13 +202,13 @@ export default class HighlightUI extends Plugin { const buttonView = componentFactory.create( 'highlight:' + option.model ); // Update lastExecutedHighlight on execute. - this.listenTo( buttonView, 'execute', () => model.set( { lastExecuted: option.model } ) ); + this.listenTo( buttonView, 'execute', () => dropdownView.set( { lastExecuted: option.model } ) ); return buttonView; } ); // Make toolbar button enabled when any button in dropdown is enabled before adding separator and eraser. - bindOneToMany( model, 'isEnabled', buttons, 'isEnabled', + bindOneToMany( dropdownView, 'isEnabled', buttons, 'isEnabled', ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); @@ -215,8 +216,6 @@ export default class HighlightUI extends Plugin { buttons.push( new ToolbarSeparatorView() ); buttons.push( componentFactory.create( 'removeHighlight' ) ); - const dropdownView = createSplitButtonDropdown( model, locale ); - // TODO: Extend template to hide arrow from dropdown. Remove me after changes in theme-lark. dropdownView.extendTemplate( { attributes: { @@ -224,9 +223,9 @@ export default class HighlightUI extends Plugin { } } ); - addToolbarToDropdown( dropdownView, buttons, model ); + addToolbarToDropdown( dropdownView, buttons ); - bindIconStyleToColor( dropdownView, model ); + bindIconStyleToColor( dropdownView ); dropdownView.extendTemplate( { attributes: { @@ -236,7 +235,7 @@ export default class HighlightUI extends Plugin { // Execute current action from dropdown's split button action button. dropdownView.on( 'execute', () => { - editor.execute( 'highlight', { value: model.commandValue } ); + editor.execute( 'highlight', { value: dropdownView.commandValue } ); editor.editing.view.focus(); } ); @@ -244,7 +243,7 @@ export default class HighlightUI extends Plugin { // 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 === model.lastExecuted ? model.lastExecuted : current; + const whichHighlighter = !current || current === dropdownView.lastExecuted ? dropdownView.lastExecuted : current; return optionsMap[ whichHighlighter ][ key ]; } @@ -255,7 +254,7 @@ export default class HighlightUI extends Plugin { } // Extends split button icon style to reflect last used button style. -function bindIconStyleToColor( dropdownView, model ) { +function bindIconStyleToColor( dropdownView ) { const actionView = dropdownView.buttonView.actionView; const bind = actionView.bindTemplate; @@ -267,7 +266,7 @@ function bindIconStyleToColor( dropdownView, model ) { } } ); - actionView.bind( 'color' ).to( model, 'color' ); + actionView.bind( 'color' ).to( dropdownView, 'color' ); } // Returns icon for given highlighter type. From 5c73bc1ec49d55bb3c7d618a4229d2fb8fae4254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 29 Jan 2018 17:34:14 +0100 Subject: [PATCH 58/80] Aligned code to the changes in DocumentSelection API. --- src/highlightcommand.js | 6 +++--- tests/highlightcommand.js | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 2e3f8ff..7d70e9d 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -74,16 +74,16 @@ export default class HighlightCommand extends Command { if ( !highlighter || this.value === highlighter ) { // ...remove attribute when passing highlighter different then current or executing "eraser". writer.removeAttribute( 'highlight', highlightRange ); - selection.removeAttribute( 'highlight' ); + writer.removeSelectionAttribute( 'highlight' ); } else { // ...update `highlight` value. writer.setAttribute( 'highlight', highlighter, highlightRange ); // And create new range wrapping changed highlighter. - selection.setRanges( [ highlightRange ] ); + writer.setSelection( [ highlightRange ] ); } } else if ( highlighter ) { - selection.setAttribute( 'highlight', highlighter ); + writer.setSelectionAttribute( 'highlight', highlighter ); } } else { for ( const range of ranges ) { diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index 961727c..a800f14 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -130,12 +130,12 @@ describe( 'HighlightCommand', () => { // It should not save that bold was executed at position ( root, [ 0, 1 ] ). - model.change( () => { + model.change( writer => { // Simulate clicking right arrow key by changing selection ranges. - doc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + writer.setSelection( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); // Get back to previous selection. - doc.selection.setRanges( [ new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 1 ] ) ) ] ); + writer.setSelection( [ new Range( new Position( root, [ 0, 1 ] ), new Position( root, [ 0, 1 ] ) ) ] ); } ); expect( command.value ).to.be.undefined; @@ -153,15 +153,15 @@ describe( 'HighlightCommand', () => { // Attribute should be stored. // Simulate clicking somewhere else in the editor. - model.change( () => { - doc.selection.setRanges( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); + model.change( writer => { + writer.setSelection( [ new Range( new Position( root, [ 0, 2 ] ), new Position( root, [ 0, 2 ] ) ) ] ); } ); expect( command.value ).to.be.undefined; // Go back to where attribute was stored. - model.change( () => { - doc.selection.setRanges( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] ); + model.change( writer => { + writer.setSelection( [ new Range( new Position( root, [ 1, 0 ] ), new Position( root, [ 1, 0 ] ) ) ] ); } ); // Attribute should be restored. From 346d509d626a0762ffa1c5533b4f92bf10deeec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 29 Jan 2018 18:16:45 +0100 Subject: [PATCH 59/80] Tests: Add manual test for highlight buttons in toolbar. --- tests/manual/highlight-buttons.html | 39 +++++++++++++++++++++++++++++ tests/manual/highlight-buttons.js | 25 ++++++++++++++++++ tests/manual/highlight-buttons.md | 19 ++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 tests/manual/highlight-buttons.html create mode 100644 tests/manual/highlight-buttons.js create mode 100644 tests/manual/highlight-buttons.md 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..f5d33fa --- /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 '../../../ckeditor5-editor-classic/src/classiceditor'; +import ArticlePluginSet from '../../../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' } ); +``` From d1e0d04706503bd04e507ac4c88fbf80d27ac35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 29 Jan 2018 18:19:48 +0100 Subject: [PATCH 60/80] Changed: Make CSS style to button rather then dropdown. --- src/highlightui.js | 9 ++++++++- tests/highlightui.js | 6 +++++- theme/highlight.css | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index bc4cc9c..4bd4588 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -113,7 +113,8 @@ export default class HighlightUI extends Plugin { button.extendTemplate( { attributes: { - style: `color: ${ option.color }` + style: `color: ${ option.color }`, + class: 'ck-highlight-button' } } ); } @@ -196,6 +197,12 @@ export default class HighlightUI extends Plugin { dropdownView.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); dropdownView.bind( 'isOn' ).to( command, 'value', value => !!value ); + dropdownView.buttonView.extendTemplate( { + attributes: { + class: 'ck-highlight-button' + } + } ); + // Create buttons array. const buttons = options.map( option => { // Get existing highlighter button. diff --git a/tests/highlightui.js b/tests/highlightui.js index e62f970..b89462c 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -82,10 +82,14 @@ describe( 'HighlightUI', () => { expect( button ).to.have.property( 'withText', false ); } ); - it( 'should add custom CSS class to dropdown', () => { + 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 focus view after command execution', () => { diff --git a/theme/highlight.css b/theme/highlight.css index 4c928fd..4a06722 100644 --- a/theme/highlight.css +++ b/theme/highlight.css @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -.ck-highlight-dropdown svg path:not(.ck-icon__fill) { +.ck-highlight-button svg path:not(.ck-icon__fill) { /* Do not inherit color from parent. */ fill: initial; } From abfbce370c3a1473627f149f596bc23973f7ac4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 1 Feb 2018 16:05:37 +0100 Subject: [PATCH 61/80] Changed: Use `bind().toMany()` binding chain from `ObservableMixin`. --- src/highlightui.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 4bd4588..216bb94 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -17,7 +17,6 @@ import penIcon from './../theme/icons/pen.svg'; import eraserIcon from './../theme/icons/eraser.svg'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; -import bindOneToMany from '@ckeditor/ckeditor5-ui/src/bindings/bindonetomany'; import { createSplitButtonDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import './../theme/highlight.css'; @@ -215,9 +214,7 @@ export default class HighlightUI extends Plugin { } ); // Make toolbar button enabled when any button in dropdown is enabled before adding separator and eraser. - bindOneToMany( dropdownView, 'isEnabled', buttons, 'isEnabled', - ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) - ); + dropdownView.bind( 'isEnabled' ).toMany( buttons, 'isEnabled', ( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled ) ); // Add separator and eraser buttons to dropdown. buttons.push( new ToolbarSeparatorView() ); From e55405540774574368c761ed87678f52755c0ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 5 Feb 2018 15:40:11 +0100 Subject: [PATCH 62/80] Changed: Make ButtonClass as a parameter of `createDropdown()`. --- src/highlightui.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 216bb94..1373061 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -17,7 +17,8 @@ import penIcon from './../theme/icons/pen.svg'; import eraserIcon from './../theme/icons/eraser.svg'; import ToolbarSeparatorView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarseparatorview'; -import { createSplitButtonDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; +import SplitButtonView from '@ckeditor/ckeditor5-ui/src/dropdown/button/splitbuttonview'; +import { createDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils'; import './../theme/highlight.css'; @@ -176,7 +177,7 @@ export default class HighlightUI extends Plugin { componentFactory.add( 'highlightDropdown', locale => { const command = editor.commands.get( 'highlight' ); - const dropdownView = createSplitButtonDropdown( locale ); + const dropdownView = createDropdown( locale, SplitButtonView ); dropdownView.set( { tooltip: t( 'Highlight' ), @@ -196,6 +197,8 @@ export default class HighlightUI extends Plugin { dropdownView.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); dropdownView.bind( 'isOn' ).to( command, 'value', value => !!value ); + dropdownView.buttonView.delegate( 'execute' ).to( dropdownView ); + dropdownView.buttonView.extendTemplate( { attributes: { class: 'ck-highlight-button' From 64eae363f853dc1886f6045b42690e7c70051f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 5 Feb 2018 15:44:38 +0100 Subject: [PATCH 63/80] Removed unnecessary code. --- src/highlightui.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 1373061..dddbb91 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -223,13 +223,6 @@ export default class HighlightUI extends Plugin { buttons.push( new ToolbarSeparatorView() ); buttons.push( componentFactory.create( 'removeHighlight' ) ); - // TODO: Extend template to hide arrow from dropdown. Remove me after changes in theme-lark. - dropdownView.extendTemplate( { - attributes: { - class: 'ck-splitbutton-dropdown' - } - } ); - addToolbarToDropdown( dropdownView, buttons ); bindIconStyleToColor( dropdownView ); From 219a7bc698d06f95e93d90c1ee15be06b111a5ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 5 Feb 2018 18:49:36 +0100 Subject: [PATCH 64/80] Changed: Remove `dropdown.buttonView` bindings to `dropdown`. --- src/highlightui.js | 19 +++++++++---------- tests/highlightui.js | 1 - 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index dddbb91..8b8e0f0 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -179,10 +179,8 @@ export default class HighlightUI extends Plugin { const dropdownView = createDropdown( locale, SplitButtonView ); - dropdownView.set( { + dropdownView.buttonView.set( { tooltip: t( 'Highlight' ), - withText: false, - isVertical: false, // Holds last executed highlighter. lastExecuted: startingHighlighter.model, // Holds current highlighter to execute (might be different then last used). @@ -192,10 +190,10 @@ export default class HighlightUI extends Plugin { // 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. - dropdownView.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); - dropdownView.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); - dropdownView.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); - dropdownView.bind( 'isOn' ).to( command, 'value', value => !!value ); + dropdownView.buttonView.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); + dropdownView.buttonView.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); + dropdownView.buttonView.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); + dropdownView.buttonView.bind( 'isOn' ).to( command, 'value', value => !!value ); dropdownView.buttonView.delegate( 'execute' ).to( dropdownView ); @@ -211,7 +209,7 @@ export default class HighlightUI extends Plugin { const buttonView = componentFactory.create( 'highlight:' + option.model ); // Update lastExecutedHighlight on execute. - this.listenTo( buttonView, 'execute', () => dropdownView.set( { lastExecuted: option.model } ) ); + this.listenTo( buttonView, 'execute', () => dropdownView.buttonView.set( { lastExecuted: option.model } ) ); return buttonView; } ); @@ -243,7 +241,8 @@ export default class HighlightUI extends Plugin { // 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 === dropdownView.lastExecuted ? dropdownView.lastExecuted : current; + const whichHighlighter = !current || + current === dropdownView.buttonView.lastExecuted ? dropdownView.buttonView.lastExecuted : current; return optionsMap[ whichHighlighter ][ key ]; } @@ -266,7 +265,7 @@ function bindIconStyleToColor( dropdownView ) { } } ); - actionView.bind( 'color' ).to( dropdownView, 'color' ); + actionView.bind( 'color' ).to( dropdownView.buttonView, 'color' ); } // Returns icon for given highlighter type. diff --git a/tests/highlightui.js b/tests/highlightui.js index b89462c..2aedc35 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -79,7 +79,6 @@ describe( 'HighlightUI', () => { expect( button ).to.have.property( 'tooltip', 'Highlight' ); expect( button ).to.have.property( 'icon', markerIcon ); - expect( button ).to.have.property( 'withText', false ); } ); it( 'should add custom CSS class to dropdown and dropdown buttons', () => { From 127a8933c12cff62baf4c39e8f1ab0733f23c110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 9 Feb 2018 10:04:05 +0100 Subject: [PATCH 65/80] Changed: Update dropdown API usage. --- src/highlightui.js | 22 +++++++++++----------- tests/highlightui.js | 18 +++++++++--------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/highlightui.js b/src/highlightui.js index 8b8e0f0..558fd30 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -176,10 +176,10 @@ export default class HighlightUI extends Plugin { componentFactory.add( 'highlightDropdown', locale => { const command = editor.commands.get( 'highlight' ); - const dropdownView = createDropdown( locale, SplitButtonView ); + const splitButtonView = dropdownView.buttonView; - dropdownView.buttonView.set( { + splitButtonView.set( { tooltip: t( 'Highlight' ), // Holds last executed highlighter. lastExecuted: startingHighlighter.model, @@ -190,14 +190,14 @@ export default class HighlightUI extends Plugin { // 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. - dropdownView.buttonView.bind( 'icon' ).to( command, 'value', value => getIconForType( getActiveOption( value, 'type' ) ) ); - dropdownView.buttonView.bind( 'color' ).to( command, 'value', value => getActiveOption( value, 'color' ) ); - dropdownView.buttonView.bind( 'commandValue' ).to( command, 'value', value => getActiveOption( value, 'model' ) ); - dropdownView.buttonView.bind( 'isOn' ).to( command, 'value', value => !!value ); + 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 ); - dropdownView.buttonView.delegate( 'execute' ).to( dropdownView ); + splitButtonView.delegate( 'execute' ).to( dropdownView ); - dropdownView.buttonView.extendTemplate( { + splitButtonView.extendTemplate( { attributes: { class: 'ck-highlight-button' } @@ -232,8 +232,8 @@ export default class HighlightUI extends Plugin { } ); // Execute current action from dropdown's split button action button. - dropdownView.on( 'execute', () => { - editor.execute( 'highlight', { value: dropdownView.commandValue } ); + splitButtonView.on( 'execute', () => { + editor.execute( 'highlight', { value: splitButtonView.commandValue } ); editor.editing.view.focus(); } ); @@ -242,7 +242,7 @@ export default class HighlightUI extends Plugin { // of last executed highlighter. Otherwise it will return option key for current one. function getActiveOption( current, key ) { const whichHighlighter = !current || - current === dropdownView.buttonView.lastExecuted ? dropdownView.buttonView.lastExecuted : current; + current === splitButtonView.lastExecuted ? splitButtonView.lastExecuted : current; return optionsMap[ whichHighlighter ][ key ]; } diff --git a/tests/highlightui.js b/tests/highlightui.js index 2aedc35..a44ad8d 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -91,15 +91,6 @@ describe( 'HighlightUI', () => { .to.deep.equal( [ true, true, true, true, true, false, false ] ); } ); - it( 'should focus view after command execution', () => { - const focusSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); - - dropdown.commandName = 'highlight'; - dropdown.fire( 'execute' ); - - sinon.assert.calledOnce( focusSpy ); - } ); - it( 'should have proper icons in dropdown', () => { const toolbar = dropdown.toolbarView; @@ -166,6 +157,15 @@ describe( 'HighlightUI', () => { 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', () => { From 86048d12bd39d99c96458b6306a45f659b87b5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 9 Feb 2018 10:19:25 +0100 Subject: [PATCH 66/80] Fix: Update imports to ckeditor5 plugins. --- tests/highlightcommand.js | 4 ++-- tests/manual/highlight-buttons.js | 4 ++-- tests/manual/highlight.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index a800f14..e9acfc1 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -8,8 +8,8 @@ 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 '../../ckeditor5-engine/src/model/position'; -import Range from '../../ckeditor5-engine/src/model/range'; +import Position from '@ckeditor/ckeditor5-engine/src/model/position'; +import Range from '@ckeditor/ckeditor5-engine/src/model/range'; describe( 'HighlightCommand', () => { let editor, model, doc, root, command; diff --git a/tests/manual/highlight-buttons.js b/tests/manual/highlight-buttons.js index f5d33fa..1e1138f 100644 --- a/tests/manual/highlight-buttons.js +++ b/tests/manual/highlight-buttons.js @@ -5,8 +5,8 @@ /* 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 diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 8f74ce9..228b124 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -5,8 +5,8 @@ /* 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 From 72bd8441b3e61fa770ce37e85007a107dcc8389d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Fri, 9 Feb 2018 10:53:41 +0100 Subject: [PATCH 67/80] Fix: Add missing dependencies to package.json. --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 47d4d94..344dd9f 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@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", From 9540a904d48bee7e5c78bbc90d5cbe2498456e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 15 Feb 2018 13:01:23 +0100 Subject: [PATCH 68/80] Updated the highlight feature's conversion to the newest API. --- src/highlightediting.js | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/highlightediting.js b/src/highlightediting.js index cc2daf5..cc6b065 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -8,10 +8,7 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import { - modelAttributeToViewAttributeElement, - viewToModelAttribute -} from '@ckeditor/ckeditor5-engine/src/conversion/definition-based-converters'; +import { attributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/two-way-converters'; import HighlightCommand from './highlightcommand'; @@ -43,25 +40,13 @@ export default class HighlightEditing extends Plugin { */ init() { const editor = this.editor; - const data = editor.data; - const editing = editor.editing; // Allow highlight attribute on text nodes. editor.model.schema.extend( '$text', { allowAttributes: 'highlight' } ); const options = editor.config.get( 'highlight.options' ); - // Define view to model conversion. - for ( const option of options ) { - viewToModelAttribute( 'highlight', _getConverterDefinition( option ), [ data.viewToModel ] ); - } - - // Define model to view conversion. - modelAttributeToViewAttributeElement( - 'highlight', - options.map( _getConverterDefinition ), - [ data.modelToView, editing.modelToView ] - ); + attributeToElement( editor.conversion, 'highlight', options.map( _getConverterDefinition ) ); editor.commands.add( 'highlight', new HighlightCommand( editor ) ); } From f703284355b7c3c7701d43d2f33ae5e27f1ddcc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 15 Feb 2018 13:34:48 +0100 Subject: [PATCH 69/80] Tests: Update toolbar in manual tests. --- tests/manual/highlight-buttons.js | 2 +- tests/manual/highlight.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/manual/highlight-buttons.js b/tests/manual/highlight-buttons.js index 1e1138f..5aba8b8 100644 --- a/tests/manual/highlight-buttons.js +++ b/tests/manual/highlight-buttons.js @@ -14,7 +14,7 @@ ClassicEditor plugins: [ ArticlePluginSet, Highlight ], toolbar: [ 'highlight:marker', 'highlight:pinkMarker', 'highlight:greenMarker', 'highlight:redPen', 'highlight:bluePen', 'removeHighlight', - 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) .then( editor => { diff --git a/tests/manual/highlight.js b/tests/manual/highlight.js index 228b124..422e491 100644 --- a/tests/manual/highlight.js +++ b/tests/manual/highlight.js @@ -13,7 +13,7 @@ ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ ArticlePluginSet, Highlight ], toolbar: [ - 'headings', 'highlightDropdown', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' + 'headings', '|', 'highlightDropdown', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) .then( editor => { From 2291aac0046df9992d671178d04a7f76bcadea79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Thu, 15 Feb 2018 14:19:58 +0100 Subject: [PATCH 70/80] Docs: Improve highlight documentation. --- src/highlightcommand.js | 5 +++++ src/highlightediting.js | 5 ++++- src/highlightui.js | 9 ++++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 7d70e9d..71bbcbe 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -14,6 +14,11 @@ 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. * + * editor.execute( 'highlight', { value: 'greenMarker' } ); + * + * **Note**: Executing the command without the value removes the attribute from the model. If selection is collapsed inside + * text with highlight attribute the whole range with that attribute will be removed from the model. + * * @extends module:core/command~Command */ export default class HighlightCommand extends Command { diff --git a/src/highlightediting.js b/src/highlightediting.js index cc6b065..094ba41 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -13,7 +13,10 @@ import { attributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/tw 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 {@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 a class attribute (`...`) depending + * on the {@link module:highlight/highlight~HighlightConfig configuration}. * * @extends module:core/plugin~Plugin */ diff --git a/src/highlightui.js b/src/highlightui.js index 558fd30..de36924 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -23,7 +23,14 @@ import { createDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src 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 provides below buttons: + * * `'highlight:marker'` + * * `'highlight:pinkMarker'` + * * `'highlight:greenMarker'` + * * `'highlight:redPen'` + * * `'highlight:bluePen'` * * @extends module:core/plugin~Plugin */ From aceca440fd48bbbd8c09f02f64140ff9d87f7b88 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 16 Feb 2018 16:44:36 +0100 Subject: [PATCH 71/80] Docs: Added separators to the toolbar configurations. --- docs/_snippets/features/custom-highlight-options.js | 2 +- docs/_snippets/features/highlight-buttons.js | 2 +- docs/_snippets/features/highlight.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_snippets/features/custom-highlight-options.js b/docs/_snippets/features/custom-highlight-options.js index 3844af8..f47964e 100644 --- a/docs/_snippets/features/custom-highlight-options.js +++ b/docs/_snippets/features/custom-highlight-options.js @@ -8,7 +8,7 @@ ClassicEditor .create( document.querySelector( '#snippet-custom-highlight-options' ), { toolbar: { items: [ - 'headings', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' + 'headings', '|', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' ], viewportTopOffset: 60 }, diff --git a/docs/_snippets/features/highlight-buttons.js b/docs/_snippets/features/highlight-buttons.js index 683c971..9cecdf8 100644 --- a/docs/_snippets/features/highlight-buttons.js +++ b/docs/_snippets/features/highlight-buttons.js @@ -9,7 +9,7 @@ ClassicEditor .create( document.querySelector( '#snippet-highlight-buttons' ), { toolbar: { items: [ - 'headings', 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:bluePen', + 'headings', '|', 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:bluePen', 'highlight:redPen', 'removeHighlight', 'undo', 'redo' ], viewportTopOffset: 60 diff --git a/docs/_snippets/features/highlight.js b/docs/_snippets/features/highlight.js index 6b7d6e0..e04e6ef 100644 --- a/docs/_snippets/features/highlight.js +++ b/docs/_snippets/features/highlight.js @@ -9,7 +9,7 @@ ClassicEditor .create( document.querySelector( '#snippet-highlight' ), { toolbar: { items: [ - 'headings', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' + 'headings', '|', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' ], viewportTopOffset: 60 } From 65808a79dff7a8e0b5f54de4aa658cfdf164b2da Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Fri, 16 Feb 2018 17:08:02 +0100 Subject: [PATCH 72/80] Docs: Improved the highlight documentation in the Features section. --- docs/_snippets/features/highlight-buttons.js | 2 +- docs/features/highlight.md | 56 ++++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/_snippets/features/highlight-buttons.js b/docs/_snippets/features/highlight-buttons.js index 9cecdf8..0293d49 100644 --- a/docs/_snippets/features/highlight-buttons.js +++ b/docs/_snippets/features/highlight-buttons.js @@ -10,7 +10,7 @@ ClassicEditor toolbar: { items: [ 'headings', '|', 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:bluePen', - 'highlight:redPen', 'removeHighlight', 'undo', 'redo' + 'highlight:redPen', 'removeHighlight', '|', 'undo', 'redo' ], viewportTopOffset: 60 } diff --git a/docs/features/highlight.md b/docs/features/highlight.md index ae866ed..b4cbb29 100644 --- a/docs/features/highlight.md +++ b/docs/features/highlight.md @@ -5,18 +5,18 @@ category: features {@snippet features/build-highlight-source} -The {@link module:highlight/highlight~Highlight} feature enables support for setting highlight. +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 highlight options +## Configuring the highlight options -It is, of course, possible to configure which highlight options the editor should support. -Use the {@link module:highlight/highlight~HighlightConfig#options `highlight.options`} configuration option to do so. +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 will support only two highlighters: +For example, the following editor supports only two styles (a green marker and a blue pen): ```js ClassicEditor @@ -28,19 +28,19 @@ ClassicEditor class: 'marker-green', title: 'Green marker', color: '#66ff00', - type: 'marker' + type: 'marker' }, - { + { model: 'bluePen', class: 'pen-blue', title: 'Blue pen', color: '#0091ff', - type: 'pen' + type: 'pen' } ] }, toolbar: [ - 'headings', 'bulletedList', 'numberedList', 'highlight', 'undo', 'redo' + 'headings', '|', 'bulletedList', 'numberedList', 'highlightDropdown', 'undo', 'redo' ] } ) .then( ... ) @@ -49,17 +49,17 @@ ClassicEditor {@snippet features/custom-highlight-options} -Instead of using `highlightDropdown` the editor supports also button configuration: +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', + 'headings', '|', 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:bluePen', 'highlight:redPen', 'removeHighlight', 'undo', 'redo' - ], + ] } } ) .then( ... ) @@ -76,7 +76,7 @@ To add this feature to your editor install the [`@ckeditor/ckeditor5-highlight`] npm install --save @ckeditor/ckeditor5-highlight ``` -And add it to your plugin list and toolbar configuration: +And add it to your plugin list and the toolbar configuration: ```js import Highlight from '@ckeditor/ckeditor5-highlight/src/highlight'; @@ -98,18 +98,18 @@ ClassicEditor The {@link module:highlight/highlight~Highlight} plugin registers: -* Dropdown: `'highlightDropdown'`. -* Command: `'highlight'`. +* The `'highlightDropdown'` dropdown, +* The {@link module:highlight/highlightcommand~HighlightCommand `'highlight'`} command. - The number of options and their names are based on the {@link module:highlight/highlight~HighlightConfig#options `highlight.options`} configuration option). + The number of options and their names correspond to the {@link module:highlight/highlight~HighlightConfig#options `highlight.options`} configuration option. - You can change highlight of the current selection by executing command with proper value: + 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 passed to `highlight` corresponds to the `model` property in configuration object. For default configuration: + 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' }, @@ -119,16 +119,16 @@ The {@link module:highlight/highlight~Highlight} plugin registers: { model: 'bluePen', class: 'pen-blue', title: 'Blue pen', color: '#0091ff', type: 'pen' } ] ``` - - the `highlight` command will accept strings below as value: - - `'marker'` - and will be available as `highligth:marker'` button. - - `'greenMarker'` - and will be available as `highligth:greenMarker'` button. - - `'pinkMarker'` - and will be available as `highligth:pinkMarker'` button. - - `'redPen'` - and will be available as `highligth:redPen'` button. - - `'bluePen'` - and will be available as `highligth:bluePen'` button. - - passing an empty value will remove any `highlight` set: - + + 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' ); ``` From b79eb20b06e6a92b4f0609fd331c32bedba1c0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 19 Feb 2018 14:11:18 +0100 Subject: [PATCH 73/80] Fix: Changing highlight should not change collapsed selection. --- src/highlightcommand.js | 3 --- tests/highlightcommand.js | 14 +------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 71bbcbe..90f660a 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -83,9 +83,6 @@ export default class HighlightCommand extends Command { } else { // ...update `highlight` value. writer.setAttribute( 'highlight', highlighter, highlightRange ); - - // And create new range wrapping changed highlighter. - writer.setSelection( [ highlightRange ] ); } } else if ( highlighter ) { writer.setSelectionAttribute( 'highlight', highlighter ); diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index e9acfc1..a1badc6 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -85,7 +85,7 @@ describe( 'HighlightCommand', () => { command.execute( { value: 'greenMarker' } ); - expect( getData( model ) ).to.equal( '

abc[<$text highlight="greenMarker">foobar]xyz

' ); + expect( getData( model ) ).to.equal( '

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

' ); expect( command.value ).to.equal( 'greenMarker' ); } ); @@ -172,18 +172,6 @@ describe( 'HighlightCommand', () => { expect( command.value ).to.be.undefined; expect( doc.selection.hasAttribute( 'highlight' ) ).to.be.false; } ); - - it( 'should change entire highlight when inside highlighted text', () => { - setData( model, '

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

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

abc[<$text highlight="greenMarker">foobar]xyz

' ); - - expect( command.value ).to.equal( 'greenMarker' ); - } ); } ); describe( 'on not collapsed range', () => { From 4ddeaacfb821062a555aca6f23c6b521071620d0 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 19 Feb 2018 15:43:35 +0100 Subject: [PATCH 74/80] Docs: Improved the highlight package docs. --- src/highlight.js | 44 ++++++++++++++++++++++++++++++----------- src/highlightcommand.js | 11 ++++++----- src/highlightediting.js | 4 ++-- src/highlightui.js | 9 +++++++-- 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/highlight.js b/src/highlight.js index 65e06a5..5c266e9 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -39,16 +39,25 @@ export default class Highlight extends Plugin { } /** - * Highlight option descriptor. + * 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 Attribute's unique value in the model. - * @property {String} color Color used for highlighter. Should be coherent with `class` CSS setting. - * @property {String} class CSS Class used on `mark` element in view. Should be coherent with `color` setting. + * @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" - will use #color as highlight background, - * - "pen" - will use #color as highlight font color. + * - `'marker'` – uses the `color` as a `background-color` style, + * - `'pen'` – uses the `color` as a font `color` style. */ /** @@ -75,14 +84,25 @@ export default class Highlight extends Plugin { */ /** - * Available highlighters options. + * 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`. * - * There are two types of highlighters: - * - 'marker' - rendered as `` element with defined background color. - * - 'pen' - rendered as `` element with defined foreground (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**: 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. + * **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, { diff --git a/src/highlightcommand.js b/src/highlightcommand.js index 90f660a..4a7aece 100644 --- a/src/highlightcommand.js +++ b/src/highlightcommand.js @@ -12,12 +12,13 @@ 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 selection is collapsed inside - * text with highlight attribute the whole range with that attribute will be removed from the model. + * **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 */ @@ -30,8 +31,8 @@ export default class HighlightCommand extends Command { const doc = model.document; /** - * A value indicating whether the command is active. If the selection has highlight attribute - * set the value will be set to highlight attribute value. + * 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 diff --git a/src/highlightediting.js b/src/highlightediting.js index 094ba41..8fbd0aa 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -13,9 +13,9 @@ import { attributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/tw import HighlightCommand from './highlightcommand'; /** - * The highlight editing feature. It introduces {@link module:highlight/highlightcommand~HighlightCommand command } and the `highlight` + * 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 a class attribute (`...`) depending + * as a `` element with the class attribute (`...`) depending * on the {@link module:highlight/highlight~HighlightConfig configuration}. * * @extends module:core/plugin~Plugin diff --git a/src/highlightui.js b/src/highlightui.js index de36924..de95d9f 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -23,15 +23,20 @@ import { createDropdown, addToolbarToDropdown } from '@ckeditor/ckeditor5-ui/src import './../theme/highlight.css'; /** - * The default Highlight UI plugin. It introduces the `'highlightDropdown'` drop-down, `'removeHighlight'` and `'highlight:*'` buttons. + * The default Highlight UI plugin. It introduces: + * * the `'highlightDropdown'` drop-down, + * * `'removeHighlight'` and `'highlight:*'` buttons. * - * The default configuration provides below 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 { From 8af9a6bae181209335e8e4cd78e5603318249902 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 19 Feb 2018 16:34:52 +0100 Subject: [PATCH 75/80] Adjusted and extended the default highlight color palette for better look and a11y. --- .../features/build-highlight-source.html | 16 +++++---- .../features/custom-highlight-options.html | 2 +- .../features/custom-highlight-options.js | 2 +- .../_snippets/features/highlight-buttons.html | 2 +- docs/_snippets/features/highlight-buttons.js | 4 +-- docs/_snippets/features/highlight.html | 2 +- docs/features/highlight.md | 36 ++++++++++--------- src/highlight.js | 17 ++++----- src/highlightediting.js | 11 +++--- src/highlightui.js | 13 ++++--- tests/manual/highlight-buttons.html | 25 ++++++++----- tests/manual/highlight-buttons.js | 3 +- tests/manual/highlight-buttons.md | 6 ++-- tests/manual/highlight.html | 25 ++++++++----- tests/manual/highlight.md | 4 +-- 15 files changed, 99 insertions(+), 69 deletions(-) diff --git a/docs/_snippets/features/build-highlight-source.html b/docs/_snippets/features/build-highlight-source.html index a12a59a..6ed56a3 100644 --- a/docs/_snippets/features/build-highlight-source.html +++ b/docs/_snippets/features/build-highlight-source.html @@ -1,23 +1,27 @@ diff --git a/docs/_snippets/features/custom-highlight-options.html b/docs/_snippets/features/custom-highlight-options.html index 50a4976..7198cf6 100644 --- a/docs/_snippets/features/custom-highlight-options.html +++ b/docs/_snippets/features/custom-highlight-options.html @@ -1,5 +1,5 @@

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

diff --git a/docs/_snippets/features/custom-highlight-options.js b/docs/_snippets/features/custom-highlight-options.js index f47964e..f32eb57 100644 --- a/docs/_snippets/features/custom-highlight-options.js +++ b/docs/_snippets/features/custom-highlight-options.js @@ -15,7 +15,7 @@ ClassicEditor 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' } + { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#e91313', type: 'pen' }, ] } } ) diff --git a/docs/_snippets/features/highlight-buttons.html b/docs/_snippets/features/highlight-buttons.html index eb8d483..8cd5801 100644 --- a/docs/_snippets/features/highlight-buttons.html +++ b/docs/_snippets/features/highlight-buttons.html @@ -7,6 +7,6 @@

Highlight feature sample.

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

diff --git a/docs/_snippets/features/highlight-buttons.js b/docs/_snippets/features/highlight-buttons.js index 0293d49..6351625 100644 --- a/docs/_snippets/features/highlight-buttons.js +++ b/docs/_snippets/features/highlight-buttons.js @@ -9,8 +9,8 @@ ClassicEditor .create( document.querySelector( '#snippet-highlight-buttons' ), { toolbar: { items: [ - 'headings', '|', 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:bluePen', - 'highlight:redPen', 'removeHighlight', '|', 'undo', 'redo' + 'headings', '|', 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', + 'highlight:greenPen', 'highlight:redPen', 'removeHighlight', '|', 'undo', 'redo' ], viewportTopOffset: 60 } diff --git a/docs/_snippets/features/highlight.html b/docs/_snippets/features/highlight.html index fe3d1e4..5a663a0 100644 --- a/docs/_snippets/features/highlight.html +++ b/docs/_snippets/features/highlight.html @@ -7,6 +7,6 @@

Highlight feature sample.

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

diff --git a/docs/features/highlight.md b/docs/features/highlight.md index b4cbb29..0bdb05c 100644 --- a/docs/features/highlight.md +++ b/docs/features/highlight.md @@ -16,7 +16,7 @@ The {@link module:highlight/highlight~Highlight} feature offers a text marking t 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): +For example, the following editor supports only two styles (a green marker and a red pen): ```js ClassicEditor @@ -27,14 +27,14 @@ ClassicEditor model: 'greenMarker', class: 'marker-green', title: 'Green marker', - color: '#66ff00', + color: '#63f963', type: 'marker' }, { - model: 'bluePen', - class: 'pen-blue', - title: 'Blue pen', - color: '#0091ff', + model: 'redPen', + class: 'pen-red', + title: 'Red pen', + color: '#e91313', type: 'pen' } ] @@ -57,7 +57,7 @@ ClassicEditor toolbar: { items: [ 'headings', '|', 'highlight:marker', 'highlight:greenMarker', - 'highlight:pinkMarker', 'highlight:bluePen', + 'highlight:pinkMarker', 'highlight:greenPen', 'highlight:redPen', 'removeHighlight', 'undo', 'redo' ] } @@ -112,20 +112,22 @@ The {@link module:highlight/highlight~Highlight} plugin registers: 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' } + { model: 'marker', class: 'marker', title: 'Marker', color: '#fdfd77', type: 'marker' }, + { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#63f963', type: 'marker' }, + { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#fc7999', type: 'marker' }, + { model: 'blueMarker', class: 'marker-blue', title: 'Blue marker', color: '#72cdfd', type: 'marker' }, + { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#e91313', type: 'pen' }, + { model: 'greenPen', class: 'pen-green', title: 'Green pen', color: '#118800', 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. + - `'marker'` – available as a `'highlight:marker'` button, + - `'greenMarker'` – available as a `'highlight:greenMarker'` button, + - `'pinkMarker'` – available as a `'highlight:pinkMarker'` button, + - `'blueMarker'` – available as a `'highlight:blueMarker'` button, + - `'redPen'` – available as a `'highlight:redPen'` button, + - `'greenPen'` – available as a `'highlight:greenPen'` button. passing an empty `value` will remove any `highlight` from the selection: diff --git a/src/highlight.js b/src/highlight.js index 5c266e9..89fc412 100644 --- a/src/highlight.js +++ b/src/highlight.js @@ -45,7 +45,7 @@ export default class Highlight extends Plugin { * model: 'pinkMarker', * class: 'marker-pink', * title: 'Pink Marker', - * color: '#ff6fff', + * color: '#fc7999', * type: 'marker' * } * @@ -87,11 +87,12 @@ export default class Highlight extends Plugin { * 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' } + * { model: 'marker', class: 'marker', title: 'Marker', color: '#fdfd77', type: 'marker' }, + * { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#63f963', type: 'marker' }, + * { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#fc7999', type: 'marker' }, + * { model: 'blueMarker', class: 'marker-blue', title: 'Blue marker', color: '#72cdfd', type: 'marker' }, + * { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#e91313', type: 'pen' }, + * { model: 'greenPen', class: 'pen-green', title: 'Green pen', color: '#118800', type: 'pen' } * ] * * There are two types of highlighters available: @@ -112,14 +113,14 @@ export default class Highlight extends Plugin { * model: 'pinkMarker', * class: 'marker-pink', * title: 'Pink Marker', - * color: '#ff6fff', + * color: '#fc7999', * type: 'marker' * }, * { * model: 'redPen', * class: 'pen-red', * title: 'Red Pen', - * color: '#ff2929', + * color: '#e91313', * type: 'pen' * }, * ] diff --git a/src/highlightediting.js b/src/highlightediting.js index 8fbd0aa..af3de80 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -29,11 +29,12 @@ export default class HighlightEditing extends Plugin { 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' } + { model: 'marker', class: 'marker', title: 'Marker', color: '#fdfd77', type: 'marker' }, + { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#63f963', type: 'marker' }, + { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#fc7999', type: 'marker' }, + { model: 'blueMarker', class: 'marker-blue', title: 'Blue marker', color: '#72cdfd', type: 'marker' }, + { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#e91313', type: 'pen' }, + { model: 'greenPen', class: 'pen-green', title: 'Green pen', color: '#118800', type: 'pen' } ] } ); } diff --git a/src/highlightui.js b/src/highlightui.js index de95d9f..ef82f22 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -29,10 +29,11 @@ import './../theme/highlight.css'; * * The default configuration includes the following buttons: * * `'highlight:marker'` - * * `'highlight:pinkMarker'` * * `'highlight:greenMarker'` + * * `'highlight:pinkMarker'` + * * `'highlight:blueMarker'` * * `'highlight:redPen'` - * * `'highlight:bluePen'` + * * `'highlight:greenPen'` * * See the {@link module:highlight/highlight~HighlightConfig#options configuration} to learn more * about the defaults. @@ -49,8 +50,9 @@ export default class HighlightUI extends Plugin { * * `'Marker'`, * * `'Green marker'`, * * `'Pink marker'`, - * * `'Blue pen'`. - * * `'Red pen'`. + * * `'Blue marker'`, + * * `'Red pen'`, + * * `'Green pen'`. * * @readonly * @type {Object.} @@ -62,8 +64,9 @@ export default class HighlightUI extends Plugin { 'Marker': t( 'Marker' ), 'Green marker': t( 'Green marker' ), 'Pink marker': t( 'Pink marker' ), + 'Blue marker': t( 'Blue marker' ), 'Red pen': t( 'Red pen' ), - 'Blue pen': t( 'Blue pen' ) + 'Green pen': t( 'Green pen' ) }; } diff --git a/tests/manual/highlight-buttons.html b/tests/manual/highlight-buttons.html index 8c99f1e..6f7ff5f 100644 --- a/tests/manual/highlight-buttons.html +++ b/tests/manual/highlight-buttons.html @@ -1,24 +1,28 @@ @@ -26,11 +30,16 @@

Highlight feature example.

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

+
    +
  • the yellow one,
  • +
  • the pink one,
  • +
  • the green one,
  • +
  • the blue one
  • +

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

CKEditor logo diff --git a/tests/manual/highlight-buttons.js b/tests/manual/highlight-buttons.js index 5aba8b8..c8d596f 100644 --- a/tests/manual/highlight-buttons.js +++ b/tests/manual/highlight-buttons.js @@ -13,7 +13,8 @@ ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ ArticlePluginSet, Highlight ], toolbar: [ - 'highlight:marker', 'highlight:pinkMarker', 'highlight:greenMarker', 'highlight:redPen', 'highlight:bluePen', 'removeHighlight', + 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:blueMarker', + 'highlight:redPen', 'highlight:greenPen', 'removeHighlight', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] } ) diff --git a/tests/manual/highlight-buttons.md b/tests/manual/highlight-buttons.md index a39d5bd..5d36455 100644 --- a/tests/manual/highlight-buttons.md +++ b/tests/manual/highlight-buttons.md @@ -1,7 +1,7 @@ ### 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. +2. The toolbar should have 5 highlight buttons and one remove highlight button. ### Testing @@ -13,7 +13,7 @@ You should be able to: 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' } ); +editor.execute( 'highlight', { class: 'pen-green' } ); ``` diff --git a/tests/manual/highlight.html b/tests/manual/highlight.html index 8c99f1e..6f7ff5f 100644 --- a/tests/manual/highlight.html +++ b/tests/manual/highlight.html @@ -1,24 +1,28 @@ @@ -26,11 +30,16 @@

Highlight feature example.

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

+
    +
  • the yellow one,
  • +
  • the pink one,
  • +
  • the green one,
  • +
  • the blue one
  • +

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

CKEditor logo diff --git a/tests/manual/highlight.md b/tests/manual/highlight.md index f169f1e..2843183 100644 --- a/tests/manual/highlight.md +++ b/tests/manual/highlight.md @@ -12,7 +12,7 @@ You should be able to: 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' } ); +editor.execute( 'highlight', { class: 'pen-green' } ); ``` From 870c78c6cee2febfe6fc8fd10dfc4fb6b99d461d Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 19 Feb 2018 16:42:17 +0100 Subject: [PATCH 76/80] Tests: Adjusted tests to the new color palette. --- tests/highlightediting.js | 11 ++++++----- tests/highlightui.js | 22 ++++++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/tests/highlightediting.js b/tests/highlightediting.js index 8de135e..d58c609 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -85,11 +85,12 @@ describe( 'HighlightEditing', () => { it( 'should be set', () => { 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' } + { model: 'marker', class: 'marker', title: 'Marker', color: '#fdfd77', type: 'marker' }, + { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#63f963', type: 'marker' }, + { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#fc7999', type: 'marker' }, + { model: 'blueMarker', class: 'marker-blue', title: 'Blue marker', color: '#72cdfd', type: 'marker' }, + { model: 'redPen', class: 'pen-red', title: 'Red pen', color: '#e91313', type: 'pen' }, + { model: 'greenPen', class: 'pen-green', title: 'Green pen', color: '#118800', type: 'pen' } ] } ); } ); diff --git a/tests/highlightui.js b/tests/highlightui.js index a44ad8d..4478fbd 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -37,8 +37,9 @@ describe( 'HighlightUI', () => { 'Marker': 'Marker', 'Green marker': 'Zielony marker', 'Pink marker': 'Różowy marker', + 'Blue marker': 'Niebieski marker', 'Red pen': 'Czerwony długopis', - 'Blue pen': 'Niebieski długopis', + 'Green pen': 'Zielony długopis', 'Remove highlighting': 'Usuń zaznaczenie' } ); } ); @@ -88,7 +89,7 @@ describe( 'HighlightUI', () => { 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 ] ); + .to.deep.equal( [ true, true, true, true, true, true, false, false ] ); } ); it( 'should have proper icons in dropdown', () => { @@ -98,7 +99,7 @@ describe( 'HighlightUI', () => { command.value = undefined; expect( toolbar.items.map( item => item.icon ) ) - .to.deep.equal( [ markerIcon, markerIcon, markerIcon, penIcon, penIcon, undefined, eraserIcon ] ); + .to.deep.equal( [ markerIcon, markerIcon, markerIcon, markerIcon, penIcon, penIcon, undefined, eraserIcon ] ); } ); it( 'should activate current option in dropdown', () => { @@ -108,13 +109,13 @@ describe( 'HighlightUI', () => { command.value = undefined; expect( toolbar.items.map( item => item.isOn ) ) - .to.deep.equal( [ false, false, false, false, false, undefined, false ] ); + .to.deep.equal( [ false, 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 ] ); + expect( toolbar.items.map( item => item.isOn ) ).to.deep.equal( [ false, true, false, false, false, false, undefined, false ] ); } ); describe( 'toolbar button behavior', () => { @@ -138,7 +139,7 @@ describe( 'HighlightUI', () => { it( 'should change button on selection', () => { command.value = 'redPen'; - validateButton( 3 ); + validateButton( 4 ); command.value = undefined; @@ -149,13 +150,13 @@ describe( 'HighlightUI', () => { command.value = 'marker'; validateButton( 0 ); - buttons[ 4 ].fire( 'execute' ); - command.value = 'bluePen'; + buttons[ 5 ].fire( 'execute' ); + command.value = 'greenPen'; // Simulate selection moved to not highlighted text. command.value = undefined; - validateButton( 4 ); + validateButton( 5 ); } ); it( 'should focus view after command execution', () => { @@ -197,8 +198,9 @@ describe( 'HighlightUI', () => { 'Marker', 'Zielony marker', 'Różowy marker', + 'Niebieski marker', 'Czerwony długopis', - 'Niebieski długopis', + 'Zielony długopis', 'Usuń zaznaczenie' ] ); } ); From 10227bd6fee479ed4ee79b74ca151c4093111b82 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 19 Feb 2018 16:44:51 +0100 Subject: [PATCH 77/80] Simplified the pen icon. --- theme/icons/pen.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme/icons/pen.svg b/theme/icons/pen.svg index e77bdc6..23499d5 100644 --- a/theme/icons/pen.svg +++ b/theme/icons/pen.svg @@ -1 +1 @@ - + From 3e2d2cd656f42f4b11308892bba1df2bda951acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 19 Feb 2018 17:08:01 +0100 Subject: [PATCH 78/80] Changed: Aligning with new conversion helpers API. --- src/highlightediting.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/highlightediting.js b/src/highlightediting.js index af3de80..7c26f7a 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -8,7 +8,6 @@ */ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; -import { attributeToElement } from '@ckeditor/ckeditor5-engine/src/conversion/two-way-converters'; import HighlightCommand from './highlightcommand'; @@ -50,23 +49,33 @@ export default class HighlightEditing extends Plugin { const options = editor.config.get( 'highlight.options' ); - attributeToElement( editor.conversion, 'highlight', options.map( _getConverterDefinition ) ); + // Set-up the two-way conversion. + editor.conversion.attributeToElement( _buildDefinition( options ) ); editor.commands.add( 'highlight', new HighlightCommand( editor ) ); } } -// Converts {@link module:highlight/highlight~HighlightOption} -// to {@link module:engine/conversion/definition-based-converters~ConverterDefinition} +// Converts options array to a converter definition. // -// @param {module:highlight/highlight~HighlightOption} option -// @returns {module:engine/conversion/definition-based-converters~ConverterDefinition} -function _getConverterDefinition( option ) { - return { - model: option.model, - view: { +// @param {Array.} options Array with configured options. +// @returns {module:engine/conversion/conversion~ConverterDefinition} +function _buildDefinition( options ) { + const definition = { + model: { + key: 'highlight', + values: [] + }, + view: {} + }; + + for ( const option of options ) { + definition.model.values.push( option.model ); + definition.view[ option.model ] = { name: 'mark', class: option.class - } - }; + }; + } + + return definition; } From 0112fd148a7fb8f2b284eefe5877b61114da16a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 19 Feb 2018 17:20:57 +0100 Subject: [PATCH 79/80] Changed: Renamed 'marker' option to 'yellowMarker'. --- docs/features/highlight.md | 4 +-- src/highlightediting.js | 2 +- src/highlightui.js | 4 +-- tests/highlightcommand.js | 54 +++++++++++++++---------------- tests/highlightediting.js | 16 ++++----- tests/highlightui.js | 8 ++--- tests/integration.js | 12 +++---- tests/manual/highlight-buttons.js | 2 +- tests/manual/highlight-buttons.md | 11 ++++--- tests/manual/highlight.md | 11 ++++--- 10 files changed, 63 insertions(+), 61 deletions(-) diff --git a/docs/features/highlight.md b/docs/features/highlight.md index 0bdb05c..33674bb 100644 --- a/docs/features/highlight.md +++ b/docs/features/highlight.md @@ -112,7 +112,7 @@ The {@link module:highlight/highlight~Highlight} plugin registers: The `value` corresponds to the `model` property in configuration object. For the default configuration: ```js highlight.options = [ - { model: 'marker', class: 'marker', title: 'Marker', color: '#fdfd77', type: 'marker' }, + { model: 'yellowMarker', class: 'marker-yellow', title: 'Yellow Marker', color: '#fdfd77', type: 'marker' }, { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#63f963', type: 'marker' }, { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#fc7999', type: 'marker' }, { model: 'blueMarker', class: 'marker-blue', title: 'Blue marker', color: '#72cdfd', type: 'marker' }, @@ -122,7 +122,7 @@ The {@link module:highlight/highlight~Highlight} plugin registers: ``` the `highlight` command will accept the corresponding strings as values: - - `'marker'` – available as a `'highlight:marker'` button, + - `'yellowMarker'` – available as a `'highlight:yellowMarker'` button, - `'greenMarker'` – available as a `'highlight:greenMarker'` button, - `'pinkMarker'` – available as a `'highlight:pinkMarker'` button, - `'blueMarker'` – available as a `'highlight:blueMarker'` button, diff --git a/src/highlightediting.js b/src/highlightediting.js index 7c26f7a..e349178 100644 --- a/src/highlightediting.js +++ b/src/highlightediting.js @@ -28,7 +28,7 @@ export default class HighlightEditing extends Plugin { editor.config.define( 'highlight', { options: [ - { model: 'marker', class: 'marker', title: 'Marker', color: '#fdfd77', type: 'marker' }, + { model: 'yellowMarker', class: 'marker-yellow', title: 'Yellow marker', color: '#fdfd77', type: 'marker' }, { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#63f963', type: 'marker' }, { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#fc7999', type: 'marker' }, { model: 'blueMarker', class: 'marker-blue', title: 'Blue marker', color: '#72cdfd', type: 'marker' }, diff --git a/src/highlightui.js b/src/highlightui.js index ef82f22..c2d1d2f 100644 --- a/src/highlightui.js +++ b/src/highlightui.js @@ -47,7 +47,7 @@ export default class HighlightUI extends Plugin { * The following localized titles corresponding with default * {@link module:highlight/highlight~HighlightConfig#options} are available: * - * * `'Marker'`, + * * `'Yellow marker'`, * * `'Green marker'`, * * `'Pink marker'`, * * `'Blue marker'`, @@ -61,7 +61,7 @@ export default class HighlightUI extends Plugin { const t = this.editor.t; return { - 'Marker': t( 'Marker' ), + 'Yellow marker': t( 'Yellow marker' ), 'Green marker': t( 'Green marker' ), 'Pink marker': t( 'Pink marker' ), 'Blue marker': t( 'Blue marker' ), diff --git a/tests/highlightcommand.js b/tests/highlightcommand.js index a1badc6..6122748 100644 --- a/tests/highlightcommand.js +++ b/tests/highlightcommand.js @@ -46,9 +46,9 @@ describe( 'HighlightCommand', () => { describe( 'value', () => { it( 'is set to highlight attribute value when selection is in text with highlight attribute', () => { - setData( model, '

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

' ); + setData( model, '

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

' ); - expect( command ).to.have.property( 'value', 'marker' ); + expect( command ).to.have.property( 'value', 'yellowMarker' ); } ); it( 'is undefined when selection is not in text with highlight attribute', () => { @@ -79,9 +79,9 @@ describe( 'HighlightCommand', () => { 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

' ); + setData( model, '

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

' ); - expect( command.value ).to.equal( 'marker' ); + expect( command.value ).to.equal( 'yellowMarker' ); command.execute( { value: 'greenMarker' } ); @@ -91,11 +91,11 @@ describe( 'HighlightCommand', () => { } ); it( 'should remove entire highlight when inside highlighted text of the same value', () => { - setData( model, '

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

' ); + setData( model, '

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

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

abcfoo[]barxyz

' ); @@ -103,7 +103,7 @@ describe( 'HighlightCommand', () => { } ); it( 'should change selection attribute in non-empty parent', () => { - setData( model, '

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

' ); + setData( model, '

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

' ); expect( command.value ).to.be.undefined; command.execute( { value: 'foo' } ); @@ -124,7 +124,7 @@ describe( 'HighlightCommand', () => { } ); it( 'should not store attribute change on selection if selection is collapsed in non-empty parent', () => { - setData( model, '

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

' ); + setData( model, '

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

' ); command.execute( { value: 'foo' } ); @@ -142,7 +142,7 @@ describe( 'HighlightCommand', () => { } ); it( 'should change selection attribute and store it if selection is collapsed in empty parent', () => { - setData( model, '

abc<$text highlight="marker">foobarxyz

[]

' ); + setData( model, '

abc<$text highlight="yellowMarker">foobarxyz

[]

' ); expect( command.value ).to.be.undefined; @@ -176,15 +176,15 @@ describe( 'HighlightCommand', () => { 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

' ); + setData( model, '

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

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

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

' ); + expect( getData( model ) ).to.equal( '

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

' ); } ); it( 'should set highlight attribute on selected node when passed as parameter (multiple nodes)', () => { @@ -195,26 +195,26 @@ describe( 'HighlightCommand', () => { '

barbar]bar

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

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

' + - '

<$text highlight="marker">foofoofoo

' + - '

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

' + '

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

' + + '

<$text highlight="yellowMarker">foofoofoo

' + + '

<$text highlight="yellowMarker">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

' ); + setData( model, '

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

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

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

' + '

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

' ); expect( command.value ).to.equal( 'foo' ); @@ -225,9 +225,9 @@ describe( 'HighlightCommand', () => { 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

' ); + setData( model, '

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

' ); - expect( command.value ).to.equal( 'marker' ); + expect( command.value ).to.equal( 'yellowMarker' ); command.execute(); @@ -239,13 +239,13 @@ describe( 'HighlightCommand', () => { 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

' ); + setData( model, '

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

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

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

' ); + expect( getData( model ) ).to.equal( '

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

' ); expect( command.value ).to.be.undefined; } ); diff --git a/tests/highlightediting.js b/tests/highlightediting.js index d58c609..a915b11 100644 --- a/tests/highlightediting.js +++ b/tests/highlightediting.js @@ -43,18 +43,18 @@ describe( 'HighlightEditing', () => { describe( 'data pipeline conversions', () => { it( 'should convert defined marker classes', () => { - const data = '

foo

'; + const data = '

foo

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

foo

' ); + editor.setData( '

foo

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

foo

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

foo

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

foo

' ); + expect( editor.getData() ).to.equal( '

foo

' ); } ); } ); @@ -85,7 +85,7 @@ describe( 'HighlightEditing', () => { it( 'should be set', () => { expect( editor.config.get( 'highlight' ) ).to.deep.equal( { options: [ - { model: 'marker', class: 'marker', title: 'Marker', color: '#fdfd77', type: 'marker' }, + { model: 'yellowMarker', class: 'marker-yellow', title: 'Yellow marker', color: '#fdfd77', type: 'marker' }, { model: 'greenMarker', class: 'marker-green', title: 'Green marker', color: '#63f963', type: 'marker' }, { model: 'pinkMarker', class: 'marker-pink', title: 'Pink marker', color: '#fc7999', type: 'marker' }, { model: 'blueMarker', class: 'marker-blue', title: 'Blue marker', color: '#72cdfd', type: 'marker' }, diff --git a/tests/highlightui.js b/tests/highlightui.js index 4478fbd..dc30325 100644 --- a/tests/highlightui.js +++ b/tests/highlightui.js @@ -24,7 +24,7 @@ describe( 'HighlightUI', () => { before( () => { addTranslations( 'en', { 'Highlight': 'Highlight', - 'Marker': 'Marker', + 'Yellow marker': 'Yellow marker', 'Green marker': 'Green marker', 'Pink marker': 'Pink marker', 'Red pen': 'Red pen', @@ -34,7 +34,7 @@ describe( 'HighlightUI', () => { addTranslations( 'pl', { 'Highlight': 'Zakreślacz', - 'Marker': 'Marker', + 'Yellow marker': 'Żółty marker', 'Green marker': 'Zielony marker', 'Pink marker': 'Różowy marker', 'Blue marker': 'Niebieski marker', @@ -147,7 +147,7 @@ describe( 'HighlightUI', () => { } ); it( 'should change button on execute option', () => { - command.value = 'marker'; + command.value = 'yellowMarker'; validateButton( 0 ); buttons[ 5 ].fire( 'execute' ); @@ -195,7 +195,7 @@ describe( 'HighlightUI', () => { const listView = dropdown.toolbarView; expect( listView.items.map( item => item.label ).filter( label => !!label ) ).to.deep.equal( [ - 'Marker', + 'Żółty marker', 'Zielony marker', 'Różowy marker', 'Niebieski marker', diff --git a/tests/integration.js b/tests/integration.js index 3a18367..ded2d7a 100644 --- a/tests/integration.js +++ b/tests/integration.js @@ -45,10 +45,10 @@ describe( 'Highlight', () => { it( 'does work inside image caption', () => { setModelData( model, 'foo[bar]baz' ); - editor.execute( 'highlight', { value: 'marker' } ); + editor.execute( 'highlight', { value: 'yellowMarker' } ); expect( getModelData( model ) ) - .to.equal( 'foo[<$text highlight="marker">bar]baz' ); + .to.equal( 'foo[<$text highlight="yellowMarker">bar]baz' ); } ); it( 'does work on selection with image', () => { @@ -57,12 +57,12 @@ describe( 'Highlight', () => { 'foo[fooabcbar]bar' ); - editor.execute( 'highlight', { value: 'marker' } ); + editor.execute( 'highlight', { value: 'yellowMarker' } ); expect( getModelData( model ) ).to.equal( - 'foo[<$text highlight="marker">foo' + - '<$text highlight="marker">abc' + - '<$text highlight="marker">bar]bar' + 'foo[<$text highlight="yellowMarker">foo' + + '<$text highlight="yellowMarker">abc' + + '<$text highlight="yellowMarker">bar]bar' ); } ); } ); diff --git a/tests/manual/highlight-buttons.js b/tests/manual/highlight-buttons.js index c8d596f..b3b9020 100644 --- a/tests/manual/highlight-buttons.js +++ b/tests/manual/highlight-buttons.js @@ -13,7 +13,7 @@ ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ ArticlePluginSet, Highlight ], toolbar: [ - 'highlight:marker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:blueMarker', + 'highlight:yellowMarker', 'highlight:greenMarker', 'highlight:pinkMarker', 'highlight:blueMarker', 'highlight:redPen', 'highlight:greenPen', 'removeHighlight', '|', 'bold', 'italic', 'link', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo' ] diff --git a/tests/manual/highlight-buttons.md b/tests/manual/highlight-buttons.md index 5d36455..c24af99 100644 --- a/tests/manual/highlight-buttons.md +++ b/tests/manual/highlight-buttons.md @@ -10,10 +10,11 @@ You should be able to: - 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', { value: 'yellowMarker' } ); +editor.execute( 'highlight', { value: 'greenMarker' } ); +editor.execute( 'highlight', { value: 'pinkMarker' } ); +editor.execute( 'highlight', { value: 'blueMarker' } ); -editor.execute( 'highlight', { class: 'pen-red' } ); -editor.execute( 'highlight', { class: 'pen-green' } ); +editor.execute( 'highlight', { value: 'redPen' } ); +editor.execute( 'highlight', { value: 'greenPen' } ); ``` diff --git a/tests/manual/highlight.md b/tests/manual/highlight.md index 2843183..c86c5c0 100644 --- a/tests/manual/highlight.md +++ b/tests/manual/highlight.md @@ -9,10 +9,11 @@ You should be able to: - 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', { value: 'yellowMarker' } ); +editor.execute( 'highlight', { value: 'greenMarker' } ); +editor.execute( 'highlight', { value: 'pinkMarker' } ); +editor.execute( 'highlight', { value: 'blueMarker' } ); -editor.execute( 'highlight', { class: 'pen-red' } ); -editor.execute( 'highlight', { class: 'pen-green' } ); +editor.execute( 'highlight', { value: 'redPen' } ); +editor.execute( 'highlight', { value: 'greenPen' } ); ``` From 07059455400354826763407c4f6c9a1418764caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Go=C5=82aszewski?= Date: Mon, 19 Feb 2018 17:25:09 +0100 Subject: [PATCH 80/80] Fix: Missing `marker-yellow` CSS class definition in tests and docs. --- docs/_snippets/features/build-highlight-source.html | 2 +- docs/_snippets/features/highlight-buttons.html | 10 +++++++--- docs/_snippets/features/highlight.html | 10 +++++++--- docs/features/highlight.md | 2 +- src/highlight.js | 2 +- tests/manual/highlight-buttons.html | 6 +++--- tests/manual/highlight.html | 6 +++--- 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/_snippets/features/build-highlight-source.html b/docs/_snippets/features/build-highlight-source.html index 6ed56a3..5626e61 100644 --- a/docs/_snippets/features/build-highlight-source.html +++ b/docs/_snippets/features/build-highlight-source.html @@ -1,5 +1,5 @@