From 019a2f5e4b83210ea75fd910478c39fabd354600 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 11 Apr 2019 10:38:20 +0200 Subject: [PATCH 01/81] Some PoC of adding target autoamtically to links. --- src/linkediting.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/linkediting.js b/src/linkediting.js index 51d0287..47ff4c5 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -26,6 +26,17 @@ const HIGHLIGHT_CLASS = 'ck-link_selected'; * @extends module:core/plugin~Plugin */ export default class LinkEditing extends Plugin { + /** + * @inheritDoc + */ + constructor( editor ) { + super( editor ); + + editor.config.define( 'link', { + targetDecorator: true + } ); + } + /** * @inheritDoc */ @@ -57,6 +68,40 @@ export default class LinkEditing extends Plugin { } } ); + if ( editor.config.get( 'link.targetDecorator' ) ) { + const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//; + + editor.conversion.for( 'downcast' ).add( dispatcher => { + // Links are represented in the model as a "linkHref" attribute. + // Use the "low" listener priority to apply the changes after the Link feature. + dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => { + if ( conversionApi.consumable.consume( data.item, 'attribute:linkHref' ) ) { + return; + } + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; + + const viewElement = viewWriter.createAttributeElement( 'a', { + target: '_blank' + }, { + priority: 5 + } ); + + if ( !( EXTERNAL_LINKS_REGEXP.test( data.attributeNewValue ) ) ) { + viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement ); + } else { + if ( data.item.is( 'selection' ) ) { + viewWriter.wrap( viewSelection.getFirstRange(), viewElement ); + } else { + viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement ); + } + } + + evt.stop(); + } ); + }, { priority: 'low' } ); + } + // Create linking commands. editor.commands.add( 'link', new LinkCommand( editor ) ); editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); From 09a3c650f8619b9dd7e4b4c2643d6c7e4c975c87 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 11 Apr 2019 14:52:10 +0200 Subject: [PATCH 02/81] Add manual test. --- tests/manual/linkdecorator.html | 8 ++++++++ tests/manual/linkdecorator.js | 26 ++++++++++++++++++++++++++ tests/manual/linkdecorator.md | 1 + 3 files changed, 35 insertions(+) create mode 100644 tests/manual/linkdecorator.html create mode 100644 tests/manual/linkdecorator.js create mode 100644 tests/manual/linkdecorator.md diff --git a/tests/manual/linkdecorator.html b/tests/manual/linkdecorator.html new file mode 100644 index 0000000..b60c24a --- /dev/null +++ b/tests/manual/linkdecorator.html @@ -0,0 +1,8 @@ +
+

This is CKEditor5 from CKSource.

+

This is CKEditor5 as schemaless url.

+

This is anchor on this page.

+

This is some random ftp address.

+

This is some mail.

+

This is some phone number.

+
diff --git a/tests/manual/linkdecorator.js b/tests/manual/linkdecorator.js new file mode 100644 index 0000000..2e69698 --- /dev/null +++ b/tests/manual/linkdecorator.js @@ -0,0 +1,26 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals console:false, window, document */ + +import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; +import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Typing from '@ckeditor/ckeditor5-typing/src/typing'; +import Link from '../../src/link'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import Undo from '@ckeditor/ckeditor5-undo/src/undo'; +import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; + +ClassicEditor + .create( document.querySelector( '#editor' ), { + plugins: [ Link, Typing, Paragraph, Clipboard, Undo, Enter ], + toolbar: [ 'link', 'undo', 'redo' ] + } ) + .then( editor => { + window.editor = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); diff --git a/tests/manual/linkdecorator.md b/tests/manual/linkdecorator.md new file mode 100644 index 0000000..b98d3fb --- /dev/null +++ b/tests/manual/linkdecorator.md @@ -0,0 +1 @@ +## play with it From 00ecb34c61a1add956b22bd08521f44009bc6b59 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 11 Apr 2019 14:53:25 +0200 Subject: [PATCH 03/81] Separate part of the logic to own class. --- src/linkediting.js | 51 ++++++++++++------------------ src/utils/automaticdispatcher.js | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 src/utils/automaticdispatcher.js diff --git a/src/linkediting.js b/src/linkediting.js index 47ff4c5..6c2fb2a 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -11,11 +11,13 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import LinkCommand from './linkcommand'; import UnlinkCommand from './unlinkcommand'; import { createLinkElement, ensureSafeUrl } from './utils'; +import AutomaticDispatchers from './utils/automaticdispatcher'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; import findLinkRange from './findlinkrange'; import '../theme/link.css'; const HIGHLIGHT_CLASS = 'ck-link_selected'; +const AUTO = 'automatic'; /** * The link engine feature. @@ -68,40 +70,27 @@ export default class LinkEditing extends Plugin { } } ); - if ( editor.config.get( 'link.targetDecorator' ) ) { - const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//; - - editor.conversion.for( 'downcast' ).add( dispatcher => { - // Links are represented in the model as a "linkHref" attribute. - // Use the "low" listener priority to apply the changes after the Link feature. - dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => { - if ( conversionApi.consumable.consume( data.item, 'attribute:linkHref' ) ) { - return; - } - const viewWriter = conversionApi.writer; - const viewSelection = viewWriter.document.selection; - - const viewElement = viewWriter.createAttributeElement( 'a', { - target: '_blank' - }, { - priority: 5 - } ); - - if ( !( EXTERNAL_LINKS_REGEXP.test( data.attributeNewValue ) ) ) { - viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement ); - } else { - if ( data.item.is( 'selection' ) ) { - viewWriter.wrap( viewSelection.getFirstRange(), viewElement ); - } else { - viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement ); - } - } + const automaticDispatcher = new AutomaticDispatchers(); - evt.stop(); - } ); - }, { priority: 'low' } ); + if ( editor.config.get( 'link.targetDecorator' ) ) { + automaticDispatcher.add( { + mode: AUTO, + callback: url => { + const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//; + return EXTERNAL_LINKS_REGEXP.test( url ); + }, + attributes: { + target: '_blank', + rel: 'noopener noreferrer' + } + } ); } + const linkDecorators = editor.config.get( 'link.decorator' ) || []; + automaticDispatcher.add( linkDecorators.filter( item => item.mode === AUTO ) ); + + editor.conversion.for( 'downcast' ).add( automaticDispatcher.getCallback() ); + // Create linking commands. editor.commands.add( 'link', new LinkCommand( editor ) ); editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); diff --git a/src/utils/automaticdispatcher.js b/src/utils/automaticdispatcher.js new file mode 100644 index 0000000..7368ea9 --- /dev/null +++ b/src/utils/automaticdispatcher.js @@ -0,0 +1,54 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module link/utils/automaticdispatcher + */ + +export default class AutomaticDispatchers { + constructor() { + this._definitions = new Set(); + } + + add( item ) { + if ( Array.isArray( item ) ) { + item.forEach( item => this._definitions.add( item ) ); + } else { + this._definitions.add( item ); + } + } + + getCallback() { + return dispatcher => { + dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => { + // There is only test as this behavior decorates links and + // it is run before dispatcher which actually consumes this node. + // This allows on writing own dispatcher with highest priority, + // which blocks both native converter and this additional decoration. + if ( !conversionApi.consumable.test( data.item, 'attribute:linkHref' ) ) { + return; + } + const viewWriter = conversionApi.writer; + const viewSelection = viewWriter.document.selection; + + for ( const item of this._definitions ) { + const viewElement = viewWriter.createAttributeElement( 'a', item.attributes, { + priority: 5 + } ); + viewWriter.setCustomProperty( 'link', true, viewElement ); + if ( item.callback( data.attributeNewValue ) ) { + if ( data.item.is( 'selection' ) ) { + viewWriter.wrap( viewSelection.getFirstRange(), viewElement ); + } else { + viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), viewElement ); + } + } else { + viewWriter.unwrap( conversionApi.mapper.toViewRange( data.range ), viewElement ); + } + } + }, { priority: 'high' } ); + }; + } +} From 337e9a53e58229216f4884415ef3d7bb1bbc8b35 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 12 Apr 2019 10:08:33 +0200 Subject: [PATCH 04/81] Add unit test for link decorators. --- tests/linkediting.js | 176 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/tests/linkediting.js b/tests/linkediting.js index e44cfdd..ebbc200 100644 --- a/tests/linkediting.js +++ b/tests/linkediting.js @@ -351,4 +351,180 @@ describe( 'LinkEditing', () => { } ); } ); } ); + + describe( 'link attributes decorator', () => { + describe( 'default behavior', () => { + const testLinks = [ + { + external: true, + url: 'http://example.com' + }, { + external: true, + url: 'https://cksource.com' + }, { + external: false, + url: 'ftp://server.io' + }, { + external: true, + url: '//schemaless.org' + }, { + external: false, + url: 'www.ckeditor.com' + }, { + external: false, + url: '/relative/url.html' + }, { + external: false, + url: 'another/relative/url.html' + }, { + external: false, + url: '#anchor' + }, { + external: false, + url: 'mailto:some@user.org' + }, { + external: false, + url: 'tel:123456789' + } + ]; + it( 'link.targetDecorator is predefined as true value', () => { + expect( editor.config.get( 'link.targetDecorator' ) ).to.be.true; + } ); + + describe( 'for link.targetDecorator = false', () => { + beforeEach( () => { + editor.destroy(); + return VirtualTestEditor + .create( { + plugins: [ Paragraph, LinkEditing, Enter ], + link: { + targetDecorator: false + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + it( 'link.targetDecorator is predefined as false value', () => { + expect( editor.config.get( 'link.targetDecorator' ) ).to.be.false; + } ); + + testLinks.forEach( link => { + it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => { + editor.setData( `

foobar

` ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( `<$text linkHref="${ link.url }">foobar` ); + + expect( editor.getData() ).to.equal( `

foobar

` ); + } ); + } ); + } ); + + testLinks.forEach( link => { + it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => { + editor.setData( `

foobar

` ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( `<$text linkHref="${ link.url }">foobar` ); + + if ( link.external ) { + expect( editor.getData() ) + .to.equal( `

foobar

` ); + } else { + expect( editor.getData() ).to.equal( `

foobar

` ); + } + } ); + } ); + } ); + + describe( 'custom config', () => { + describe( 'mode: automatic', () => { + const testLinks = [ + { + url: 'relative/url.html', + attributes: {} + }, { + url: 'http://exmaple.com', + attributes: { + target: '_blank' + } + }, { + url: 'https://example.com/download/link.pdf', + attributes: { + target: '_blank', + download: 'download' + } + }, { + url: 'mailto:some@person.io', + attributes: { + class: 'mail-url' + } + } + ]; + + beforeEach( () => { + editor.destroy(); + return VirtualTestEditor + .create( { + plugins: [ Paragraph, LinkEditing, Enter ], + link: { + targetDecorator: false, + decorator: [ + { + mode: 'automatic', + callback: url => url.startsWith( 'http' ), + attributes: { + target: '_blank' + } + }, { + mode: 'automatic', + callback: url => url.includes( 'download' ), + attributes: { + download: 'download' + } + }, { + mode: 'automatic', + callback: url => url.startsWith( 'mailto:' ), + attributes: { + class: 'mail-url' + } + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + testLinks.forEach( link => { + it( `Link: ${ link.url } should get attributes: ${ JSON.stringify( link.attributes ) }`, () => { + const ORDER = [ 'target', 'download', 'class' ]; + const attr = Object.entries( link.attributes ).sort( ( a, b ) => { + const aIndex = ORDER.indexOf( a[ 0 ] ); + const bIndex = ORDER.indexOf( b[ 0 ] ); + return aIndex - bIndex; + } ); + const reducedAttr = attr.reduce( ( acc, cur ) => { + return acc + `${ cur[ 0 ] }="${ cur[ 1 ] }" `; + }, '' ); + + editor.setData( `

foobar

` ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( `<$text linkHref="${ link.url }">foobar` ); + + // Order of attributes is important, that's why this is assert is construct in such way. + expect( editor.getData() ).to.equal( `

foobar

` ); + } ); + } ); + } ); + } ); + } ); } ); From 53a9b15bc10bdf632f13c5971c0f9271bda761d9 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 12 Apr 2019 15:08:20 +0200 Subject: [PATCH 05/81] Change target decorator default option to be false, correct config values naming. --- src/linkediting.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/linkediting.js b/src/linkediting.js index 6c2fb2a..3e48107 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -35,7 +35,7 @@ export default class LinkEditing extends Plugin { super( editor ); editor.config.define( 'link', { - targetDecorator: true + targetDecorator: false } ); } @@ -86,7 +86,7 @@ export default class LinkEditing extends Plugin { } ); } - const linkDecorators = editor.config.get( 'link.decorator' ) || []; + const linkDecorators = editor.config.get( 'link.decorators' ) || []; automaticDispatcher.add( linkDecorators.filter( item => item.mode === AUTO ) ); editor.conversion.for( 'downcast' ).add( automaticDispatcher.getCallback() ); From 1fef5b6a042b8208af670eb5c481862219e22b38 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 12 Apr 2019 15:08:39 +0200 Subject: [PATCH 06/81] Add unit test to automatic mode. --- tests/linkediting.js | 84 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/tests/linkediting.js b/tests/linkediting.js index ebbc200..82b1752 100644 --- a/tests/linkediting.js +++ b/tests/linkediting.js @@ -10,6 +10,7 @@ import UnlinkCommand from '../src/unlinkcommand'; import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Enter from '@ckeditor/ckeditor5-enter/src/enter'; +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; import { isLinkElement } from '../src/utils'; @@ -387,8 +388,8 @@ describe( 'LinkEditing', () => { url: 'tel:123456789' } ]; - it( 'link.targetDecorator is predefined as true value', () => { - expect( editor.config.get( 'link.targetDecorator' ) ).to.be.true; + it( 'link.targetDecorator is predefined as false value', () => { + expect( editor.config.get( 'link.targetDecorator' ) ).to.be.false; } ); describe( 'for link.targetDecorator = false', () => { @@ -398,7 +399,7 @@ describe( 'LinkEditing', () => { .create( { plugins: [ Paragraph, LinkEditing, Enter ], link: { - targetDecorator: false + targetDecorator: true } } ) .then( newEditor => { @@ -407,36 +408,34 @@ describe( 'LinkEditing', () => { view = editor.editing.view; } ); } ); - - it( 'link.targetDecorator is predefined as false value', () => { - expect( editor.config.get( 'link.targetDecorator' ) ).to.be.false; + it( 'link.targetDecorator is set as true value', () => { + expect( editor.config.get( 'link.targetDecorator' ) ).to.be.true; } ); testLinks.forEach( link => { - it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => { + it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => { editor.setData( `

foobar

` ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `<$text linkHref="${ link.url }">foobar` ); - expect( editor.getData() ).to.equal( `

foobar

` ); + if ( link.external ) { + expect( editor.getData() ) + .to.equal( `

foobar

` ); + } else { + expect( editor.getData() ).to.equal( `

foobar

` ); + } } ); } ); } ); - testLinks.forEach( link => { - it( `link: ${ link.url } should be treat as ${ link.external ? 'external' : 'non-external' } link`, () => { + it( `link: ${ link.url } should not get 'target' and 'rel' attributes`, () => { editor.setData( `

foobar

` ); expect( getModelData( model, { withoutSelection: true } ) ) .to.equal( `<$text linkHref="${ link.url }">foobar` ); - if ( link.external ) { - expect( editor.getData() ) - .to.equal( `

foobar

` ); - } else { - expect( editor.getData() ).to.equal( `

foobar

` ); - } + expect( editor.getData() ).to.equal( `

foobar

` ); } ); } ); } ); @@ -473,7 +472,7 @@ describe( 'LinkEditing', () => { plugins: [ Paragraph, LinkEditing, Enter ], link: { targetDecorator: false, - decorator: [ + decorators: [ { mode: 'automatic', callback: url => url.startsWith( 'http' ), @@ -526,5 +525,56 @@ describe( 'LinkEditing', () => { } ); } ); } ); + + describe( 'custom linkHref converter', () => { + beforeEach( () => { + class CustomLinks extends Plugin { + init() { + const editor = this.editor; + + editor.conversion.for( 'downcast' ).add( dispatcher => { + dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => { + conversionApi.consumable.consume( data.item, 'attribute:linkHref' ); + + // Very simplified downcast just for test assertion. + const viewWriter = conversionApi.writer; + const linkElement = viewWriter.createAttributeElement( + 'a', + { + href: data.attributeNewValue + }, { + priority: 5 + } + ); + viewWriter.setCustomProperty( 'link', true, linkElement ); + viewWriter.wrap( conversionApi.mapper.toViewRange( data.range ), linkElement ); + }, { priority: 'highest' } ); + } ); + } + } + editor.destroy(); + return VirtualTestEditor + .create( { + plugins: [ Paragraph, LinkEditing, Enter, CustomLinks ], + link: { + targetDecorator: true, + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + view = editor.editing.view; + } ); + } ); + + it( 'has possibility to override default one', () => { + editor.setData( '

foobar

' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '<$text linkHref="http://example.com">foobar' ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + } ); } ); } ); From cf8ca0465bfa451d55943df9bdd74ddd0e924a4c Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 12 Apr 2019 15:24:02 +0200 Subject: [PATCH 07/81] Rename helper class. --- src/linkediting.js | 10 +++++----- .../{automaticdispatcher.js => automaticdecorators.js} | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/utils/{automaticdispatcher.js => automaticdecorators.js} (93%) diff --git a/src/linkediting.js b/src/linkediting.js index 3e48107..e3e5a0a 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -11,7 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import LinkCommand from './linkcommand'; import UnlinkCommand from './unlinkcommand'; import { createLinkElement, ensureSafeUrl } from './utils'; -import AutomaticDispatchers from './utils/automaticdispatcher'; +import AutomaticDecorators from './utils/automaticdecorators'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; import findLinkRange from './findlinkrange'; import '../theme/link.css'; @@ -70,10 +70,10 @@ export default class LinkEditing extends Plugin { } } ); - const automaticDispatcher = new AutomaticDispatchers(); + const automaticDecorators = new AutomaticDecorators(); if ( editor.config.get( 'link.targetDecorator' ) ) { - automaticDispatcher.add( { + automaticDecorators.add( { mode: AUTO, callback: url => { const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//; @@ -87,9 +87,9 @@ export default class LinkEditing extends Plugin { } const linkDecorators = editor.config.get( 'link.decorators' ) || []; - automaticDispatcher.add( linkDecorators.filter( item => item.mode === AUTO ) ); + automaticDecorators.add( linkDecorators.filter( item => item.mode === AUTO ) ); - editor.conversion.for( 'downcast' ).add( automaticDispatcher.getCallback() ); + editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() ); // Create linking commands. editor.commands.add( 'link', new LinkCommand( editor ) ); diff --git a/src/utils/automaticdispatcher.js b/src/utils/automaticdecorators.js similarity index 93% rename from src/utils/automaticdispatcher.js rename to src/utils/automaticdecorators.js index 7368ea9..3cfd6b5 100644 --- a/src/utils/automaticdispatcher.js +++ b/src/utils/automaticdecorators.js @@ -4,10 +4,10 @@ */ /** - * @module link/utils/automaticdispatcher + * @module link/utils/automaticdecorators */ -export default class AutomaticDispatchers { +export default class AutomaticDecorators { constructor() { this._definitions = new Set(); } @@ -20,7 +20,7 @@ export default class AutomaticDispatchers { } } - getCallback() { + getDispatcher() { return dispatcher => { dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => { // There is only test as this behavior decorates links and From 76da55aa95a4b48a1e953653d5ef9a3228dc3959 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 12 Apr 2019 16:14:21 +0200 Subject: [PATCH 08/81] Add documentation entries. --- src/link.js | 8 ++++++++ src/utils/automaticdecorators.js | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/link.js b/src/link.js index 775aa84..40b482d 100644 --- a/src/link.js +++ b/src/link.js @@ -34,3 +34,11 @@ export default class Link extends Plugin { return 'Link'; } } + +/** + * The configuration of the {@link module:link/link~Link} feature. + * + * Read more in {@link module:link/linkt~LinkConfig}. + * + * @member {module:link/link~LinkConfig} module:core/editor/editorconfig~EditorConfig#link + */ diff --git a/src/utils/automaticdecorators.js b/src/utils/automaticdecorators.js index 3cfd6b5..9832853 100644 --- a/src/utils/automaticdecorators.js +++ b/src/utils/automaticdecorators.js @@ -7,11 +7,27 @@ * @module link/utils/automaticdecorators */ +/** + * Helper class which stores information about automatic decorators for link plugin + * and provides dispatcher which applies all of them to the view. + */ export default class AutomaticDecorators { constructor() { + /** + * Stores definition of automatic decorators. Based on those values proper conversion has happens. + * + * @private + * @type {Set} + */ this._definitions = new Set(); } + /** + * Add item or array of items with autoamtic rules for applying decorators to link plugin. + * + * @param {Object|Array.} item configuration object of automatic rules for decorating links. + * It might be also array of such objects. + */ add( item ) { if ( Array.isArray( item ) ) { item.forEach( item => this._definitions.add( item ) ); @@ -20,6 +36,12 @@ export default class AutomaticDecorators { } } + /** + * Gets the conversion helper used in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method. + * + * @returns {Function} dispatcher function used as conversion helper + * in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} + */ getDispatcher() { return dispatcher => { dispatcher.on( 'attribute:linkHref', ( evt, data, conversionApi ) => { From 2e22a2b2ac01315d7688cd1c3579e7a1da833a15 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 15 Apr 2019 09:54:48 +0200 Subject: [PATCH 09/81] Add js code examples to documentation. --- src/link.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/link.js b/src/link.js index 40b482d..9553ce3 100644 --- a/src/link.js +++ b/src/link.js @@ -38,7 +38,96 @@ export default class Link extends Plugin { /** * The configuration of the {@link module:link/link~Link} feature. * - * Read more in {@link module:link/linkt~LinkConfig}. + * Read more in {@link module:link/link~LinkConfig}. * * @member {module:link/link~LinkConfig} module:core/editor/editorconfig~EditorConfig#link */ + +/** + * The configuration of the {@link module:link/link~Link link feature}. + * + * ClassicEditor + * .create( editorElement, { + * link: ... // Link feature configuration. + * } ) + * .then( ... ) + * .catch( ... ); + * + * See {@link module:core/editor/editorconfig~EditorConfig all editor options}. + * @interface LinkConfig + */ + +/** + * Configuration of the {@link module:link/link~Link} feature. If set to `true`, + * then default 'automatic' decorator is added to the link. + * + * @member {Boolean} module:link/link~LinkConfig#targetDecorator + */ + +/** + * Custom link decorators. + * + * **Warning** Currently there is no integration between 'automatic' and 'manual' decorators, + * which transforms the same attribute. For example, configuring `target` attribute through both + * 'automatic' and 'manual' decorator might result with quirk behavior. + * + * Decorators provides: + * * simple automatic rules based on url address to apply customized and predefined additional attributes. + * * manual rules, which adds UI checkbox, where user can simply trigger predefined attributes for given link. + * + * + * ```js + * const link.decorators = [ + * { + * mode: 'automatic', + * callback: url => url.startsWith( 'http://' ), + * attributes: { + * target: '_blank', + * rel: 'noopener noreferrer' + * } + * }, + * { + * mode: 'automatic', + * callback: url => url.includes( 'download' ) && url.endsWith( '.pdf' ), + * attributes: { + * download: 'download' + * } + * }, + * { + * mode: 'automatic', + * callback: url => url.includes( 'image' ) && url.endsWith( '.png' ), + * attributes: { + * class: 'light-gallery' + * } + * } + * ] + * ``` + * @member {Array.} module:link/link~LinkConfig#decorators + */ + +/** + * This object defining automatic decorator for the links. Based on this option data pipeline will extend links with proper attributes. + * For example, you can define rules, when attribute `target="_blank"` will be added to links. + * There is a default option which might be activated with {@link module:link/link~LinkConfig#targetDecorator}, + * which automatically adds attributes: + * * `target="_blank"` + * * `rel="noopener noreferrer"` + * for all links started with: `http://`, `https://` or `//`. + * + * ```js + * { + * mode: 'automatic', + * callback: url => /^(https?:)?\/\//.test( url ), + * attributes: { + * target: '_blank', + * rel: 'noopener noreferrer' + * } + * } + * ``` + * + * @typedef {Object} module:link/link~LinkDecoratorAutomaticOption + * @property {'automatic'} mode it should has always string value 'automatic' for automatic decorators + * @property {Function} callback takes `url` as parameter and should return `true` + * for urls that be decorate with this decorator. + * @property {Object} attributes key-value pairs used as attributes added to anchor during downcasting. + */ From 717f357dac7b417c7561dc76d10e7eabe7616514 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 16 Apr 2019 10:01:05 +0200 Subject: [PATCH 10/81] Add support for manual decorators. Update link command to support additioanl attributes. --- src/linkcommand.js | 37 ++++++++++++++++++++++++++-- src/linkediting.js | 61 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index c1dadf5..abe4407 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -25,6 +25,12 @@ export default class LinkCommand extends Command { * @member {Object|undefined} #value */ + constructor( editor ) { + super( editor ); + + this.customAttributes = new Map(); + } + /** * @inheritDoc */ @@ -52,10 +58,21 @@ export default class LinkCommand extends Command { * @fires execute * @param {String} href Link destination. */ - execute( href ) { + execute( href, customAttrs = {} ) { const model = this.editor.model; const selection = model.document.selection; + // Stores information about custom attributes to turn on/off. + const truthyCustomAttributes = []; + const falsyCustomAttributes = []; + Object.entries( customAttrs ).forEach( entriesPair => { + if ( entriesPair[ 1 ] ) { + truthyCustomAttributes.push( entriesPair[ 0 ] ); + } else { + falsyCustomAttributes.push( entriesPair[ 0 ] ); + } + } ); + model.change( writer => { // If selection is collapsed then update selected link or insert new one at the place of caret. if ( selection.isCollapsed ) { @@ -64,9 +81,15 @@ export default class LinkCommand extends Command { // When selection is inside text with `linkHref` attribute. if ( selection.hasAttribute( 'linkHref' ) ) { // Then update `linkHref` value. - const linkRange = findLinkRange( selection.getFirstPosition(), selection.getAttribute( 'linkHref' ), model ); + const linkRange = findLinkRange( position, selection.getAttribute( 'linkHref' ), model ); writer.setAttribute( 'linkHref', href, linkRange ); + truthyCustomAttributes.forEach( item => { + writer.setAttribute( item, true, linkRange ); + } ); + falsyCustomAttributes.forEach( item => { + writer.removeAttribute( item, linkRange ); + } ); // Create new range wrapping changed link. writer.setSelection( linkRange ); @@ -79,6 +102,10 @@ export default class LinkCommand extends Command { attributes.set( 'linkHref', href ); + truthyCustomAttributes.forEach( item => { + attributes.set( item, true ); + } ); + const node = writer.createText( href, attributes ); model.insertContent( node, position ); @@ -93,6 +120,12 @@ export default class LinkCommand extends Command { for ( const range of ranges ) { writer.setAttribute( 'linkHref', href, range ); + truthyCustomAttributes.forEach( item => { + writer.setAttribute( item, true, range ); + } ); + falsyCustomAttributes.forEach( item => { + writer.removeAttribute( item, range ); + } ); } } } ); diff --git a/src/linkediting.js b/src/linkediting.js index e3e5a0a..64e3944 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -18,6 +18,7 @@ import '../theme/link.css'; const HIGHLIGHT_CLASS = 'ck-link_selected'; const AUTO = 'automatic'; +const MANUAL = 'manual'; /** * The link engine feature. @@ -70,8 +71,26 @@ export default class LinkEditing extends Plugin { } } ); + // Create linking commands. + editor.commands.add( 'link', new LinkCommand( editor ) ); + editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); + + const linkDecorators = editor.config.get( 'link.decorators' ) || []; + this.enableAutomaticDecorators( linkDecorators.filter( item => item.mode === AUTO ) ); + this.enableManualDecorators( linkDecorators.filter( item => item.mode === MANUAL ) ); + + // Enable two-step caret movement for `linkHref` attribute. + bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' ); + + // Setup highlight over selected link. + this._setupLinkHighlight(); + } + + enableAutomaticDecorators( automaticDecoratorDefinitions ) { + const editor = this.editor; const automaticDecorators = new AutomaticDecorators(); + // Adds default decorator for external links. if ( editor.config.get( 'link.targetDecorator' ) ) { automaticDecorators.add( { mode: AUTO, @@ -86,20 +105,40 @@ export default class LinkEditing extends Plugin { } ); } - const linkDecorators = editor.config.get( 'link.decorators' ) || []; - automaticDecorators.add( linkDecorators.filter( item => item.mode === AUTO ) ); - + automaticDecorators.add( automaticDecoratorDefinitions ); editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() ); + } - // Create linking commands. - editor.commands.add( 'link', new LinkCommand( editor ) ); - editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); - - // Enable two-step caret movement for `linkHref` attribute. - bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' ); + enableManualDecorators( manualDecoratorDefinitions ) { + const editor = this.editor; + if ( !manualDecoratorDefinitions.length ) { + return; + } - // Setup highlight over selected link. - this._setupLinkHighlight(); + const command = editor.commands.get( 'link' ); + const attrMap = command.customAttributes; + + manualDecoratorDefinitions.forEach( ( decorator, index ) => { + const decoratorName = `linkManualDecorator${ index }`; + editor.model.schema.extend( '$text', { allowAttributes: decoratorName } ); + + attrMap.set( decoratorName, Object.assign( { value: undefined }, decorator ) ); + editor.conversion.for( 'downcast' ).attributeToElement( { + model: decoratorName, + view: ( manualDecoratorName, writer ) => { + if ( manualDecoratorName ) { + const element = writer.createAttributeElement( + 'a', + attrMap.get( decoratorName ).attributes, + { + priority: 5 + } + ); + writer.setCustomProperty( 'link', true, element ); + return element; + } + } } ); + } ); } /** From d6fe605932e68a65a666fded4cb4823c1cbea525 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 16 Apr 2019 10:30:45 +0200 Subject: [PATCH 11/81] Add removing custom attributes to unlink command. --- src/unlinkcommand.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/unlinkcommand.js b/src/unlinkcommand.js index 6e59dc7..6016b9a 100644 --- a/src/unlinkcommand.js +++ b/src/unlinkcommand.js @@ -32,8 +32,10 @@ export default class UnlinkCommand extends Command { * @fires execute */ execute() { + const editor = this.editor; const model = this.editor.model; const selection = model.document.selection; + const linkCommand = editor.commands.get( 'link' ); model.change( writer => { // Get ranges to unlink. @@ -43,6 +45,12 @@ export default class UnlinkCommand extends Command { // Remove `linkHref` attribute from specified ranges. for ( const range of rangesToUnlink ) { writer.removeAttribute( 'linkHref', range ); + // If there are registered custom attributes, then remove them during unlink. + if ( linkCommand ) { + linkCommand.customAttributes.forEach( ( val, key ) => { + writer.removeAttribute( key, range ); + } ); + } } } ); } From 9c9d0c263c89b852eb66f9bad8204333d0f0f36d Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 17 Apr 2019 17:31:45 +0200 Subject: [PATCH 12/81] Use switch button as toggle option in custom link atributes. --- src/linkcommand.js | 8 +++++++- src/linkediting.js | 22 +++++++++++++++++++--- src/linkui.js | 11 +++++++++-- src/ui/linkformview.js | 37 +++++++++++++++++++++++++++++++++---- src/unlinkcommand.js | 6 +++--- theme/linkform.css | 3 ++- 6 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index abe4407..8917e1e 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -10,6 +10,7 @@ import Command from '@ckeditor/ckeditor5-core/src/command'; import findLinkRange from './findlinkrange'; import toMap from '@ckeditor/ckeditor5-utils/src/tomap'; +import Collection from '@ckeditor/ckeditor5-utils/src/collection'; /** * The link command. It is used by the {@link module:link/link~Link link feature}. @@ -28,7 +29,7 @@ export default class LinkCommand extends Command { constructor( editor ) { super( editor ); - this.customAttributes = new Map(); + this.customAttributes = new Collection(); } /** @@ -39,6 +40,11 @@ export default class LinkCommand extends Command { const doc = model.document; this.value = doc.selection.getAttribute( 'linkHref' ); + + for ( const customAttr of this.customAttributes ) { + customAttr.value = doc.selection.getAttribute( customAttr.id ) || false; + } + this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'linkHref' ); } diff --git a/src/linkediting.js b/src/linkediting.js index 64e3944..63bbf65 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -15,6 +15,8 @@ import AutomaticDecorators from './utils/automaticdecorators'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; import findLinkRange from './findlinkrange'; import '../theme/link.css'; +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; const HIGHLIGHT_CLASS = 'ck-link_selected'; const AUTO = 'automatic'; @@ -116,20 +118,20 @@ export default class LinkEditing extends Plugin { } const command = editor.commands.get( 'link' ); - const attrMap = command.customAttributes; + const attrCollection = command.customAttributes; manualDecoratorDefinitions.forEach( ( decorator, index ) => { const decoratorName = `linkManualDecorator${ index }`; editor.model.schema.extend( '$text', { allowAttributes: decoratorName } ); - attrMap.set( decoratorName, Object.assign( { value: undefined }, decorator ) ); + attrCollection.add( new ManualDecorator( Object.assign( { id: decoratorName, value: undefined }, decorator ) ) ); editor.conversion.for( 'downcast' ).attributeToElement( { model: decoratorName, view: ( manualDecoratorName, writer ) => { if ( manualDecoratorName ) { const element = writer.createAttributeElement( 'a', - attrMap.get( decoratorName ).attributes, + attrCollection.get( decoratorName ).attributes, { priority: 5 } @@ -198,3 +200,17 @@ export default class LinkEditing extends Plugin { } ); } } + +class ManualDecorator { + constructor( { id, value, label, attributes } = {} ) { + this.id = id; + + this.set( 'value', value ); + + this.label = label; + + this.attributes = attributes; + } +} + +mix( ManualDecorator, ObservableMixin ); diff --git a/src/linkui.js b/src/linkui.js index 97feaff..2a4059d 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -142,9 +142,10 @@ export default class LinkUI extends Plugin { */ _createFormView() { const editor = this.editor; - const formView = new LinkFormView( editor.locale ); const linkCommand = editor.commands.get( 'link' ); + const formView = new LinkFormView( editor.locale, linkCommand.customAttributes ); + formView.urlInputView.bind( 'value' ).to( linkCommand, 'value' ); // Form elements should be read-only when corresponding commands are disabled. @@ -153,7 +154,13 @@ export default class LinkUI extends Plugin { // Execute link command after clicking the "Save" button. this.listenTo( formView, 'submit', () => { - editor.execute( 'link', formView.urlInputView.inputView.element.value ); + const customAttributes = {}; + + for ( const switchButton of formView.customAttributesView ) { + customAttributes[ switchButton.value ] = switchButton.isOn; + } + + editor.execute( 'link', formView.urlInputView.inputView.element.value, customAttributes ); this._closeFormView(); } ); diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index bc9b1d2..633c98b 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -11,6 +11,7 @@ import View from '@ckeditor/ckeditor5-ui/src/view'; import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import SwitchButtonView from '@ckeditor/ckeditor5-ui/src/button/switchbuttonview'; import LabeledInputView from '@ckeditor/ckeditor5-ui/src/labeledinput/labeledinputview'; import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview'; @@ -34,7 +35,7 @@ export default class LinkFormView extends View { /** * @inheritDoc */ - constructor( locale ) { + constructor( locale, customAttributes ) { super( locale ); const t = locale.t; @@ -77,6 +78,10 @@ export default class LinkFormView extends View { */ this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' ); + this.customAttributes = customAttributes; + + this.customAttributesView = this._createCustomAttributesView(); + /** * A collection of views which can be focused in the form. * @@ -122,7 +127,8 @@ export default class LinkFormView extends View { children: [ this.urlInputView, this.saveButtonView, - this.cancelButtonView + this.cancelButtonView, + ...this.customAttributesView, ] } ); } @@ -137,10 +143,12 @@ export default class LinkFormView extends View { view: this } ); + // Focus order should be different than position in DOM. Save/Cancel buttons should be focused at the end. const childViews = [ this.urlInputView, + ...this.customAttributesView, this.saveButtonView, - this.cancelButtonView + this.cancelButtonView, ]; childViews.forEach( v => { @@ -174,7 +182,6 @@ export default class LinkFormView extends View { const labeledInput = new LabeledInputView( this.locale, InputTextView ); labeledInput.label = t( 'Link URL' ); - labeledInput.inputView.placeholder = 'https://example.com'; return labeledInput; } @@ -210,6 +217,28 @@ export default class LinkFormView extends View { return button; } + + _createCustomAttributesView() { + const checkboxes = this.createCollection(); + + checkboxes.bindTo( this.customAttributes ).using( item => { + const checkbox = new SwitchButtonView( this.locale ); + checkbox.set( { + value: item.id, + label: item.label, + withText: true + } ); + + checkbox.bind( 'isOn' ).to( item, 'value' ); + + checkbox.on( 'execute', () => { + this.customAttributes.get( item.id ).set( 'value', !checkbox.isOn ); + } ); + + return checkbox; + } ); + return checkboxes; + } } /** diff --git a/src/unlinkcommand.js b/src/unlinkcommand.js index 6016b9a..372e5b1 100644 --- a/src/unlinkcommand.js +++ b/src/unlinkcommand.js @@ -47,9 +47,9 @@ export default class UnlinkCommand extends Command { writer.removeAttribute( 'linkHref', range ); // If there are registered custom attributes, then remove them during unlink. if ( linkCommand ) { - linkCommand.customAttributes.forEach( ( val, key ) => { - writer.removeAttribute( key, range ); - } ); + for ( const manualDecorator of linkCommand.customAttributes ) { + writer.removeAttribute( manualDecorator.id, range ); + } } } } ); diff --git a/theme/linkform.css b/theme/linkform.css index 2194683..eec08c7 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -8,7 +8,8 @@ .ck.ck-link-form { display: flex; flex-direction: row; - flex-wrap: nowrap; + flex-wrap: wrap; + max-width: 500px; & .ck-label { display: none; From f5160a74d81d3727783cedffaf28f1cb25f9768b Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 17 Apr 2019 17:35:03 +0200 Subject: [PATCH 13/81] Update manual tests to use manual link decorators. --- tests/manual/linkdecorator.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/manual/linkdecorator.js b/tests/manual/linkdecorator.js index 2e69698..a03d631 100644 --- a/tests/manual/linkdecorator.js +++ b/tests/manual/linkdecorator.js @@ -12,14 +12,43 @@ import Link from '../../src/link'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ Link, Typing, Paragraph, Clipboard, Undo, Enter ], - toolbar: [ 'link', 'undo', 'redo' ] + toolbar: [ 'link', 'undo', 'redo' ], + link: { + decorators: [ + { + mode: 'manual', + label: 'Open in new window', + attributes: { + target: '_blank', + rel: 'noopener noreferrer' + } + }, + { + mode: 'manual', + label: 'downloadable', + attributes: { + download: 'download' + } + }, + { + mode: 'manual', + label: 'gallery', + attributes: { + class: 'gallery' + } + } + ] + } } ) .then( editor => { + window.getModelData = getModelData; window.editor = editor; + window.model = editor.model; } ) .catch( err => { console.error( err.stack ); From 59acd705a020315319bbfd2ab08eac6550ca232b Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 18 Apr 2019 10:06:29 +0200 Subject: [PATCH 14/81] Add div wrappers to link form view. --- src/ui/linkformview.js | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 633c98b..635894e 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -82,6 +82,8 @@ export default class LinkFormView extends View { this.customAttributesView = this._createCustomAttributesView(); + this.children = this._createFormChildren(); + /** * A collection of views which can be focused in the form. * @@ -124,12 +126,7 @@ export default class LinkFormView extends View { tabindex: '-1' }, - children: [ - this.urlInputView, - this.saveButtonView, - this.cancelButtonView, - ...this.customAttributesView, - ] + children: this.children } ); } @@ -239,6 +236,38 @@ export default class LinkFormView extends View { } ); return checkboxes; } + + _createFormChildren() { + const children = this.createCollection(); + + const requiredButtonView = new View(); + requiredButtonView.setTemplate( { + tag: 'div', + children: [ + this.urlInputView, + this.saveButtonView, + this.cancelButtonView + ], + attributes: { + class: 'ck-link-form_required-buttons' + } + } ); + children.add( requiredButtonView ); + + if ( this.customAttributes.length ) { + const additionalButtonsView = new View(); + additionalButtonsView.setTemplate( { + tag: 'div', + children: [ ...this.customAttributesView ], + attributes: { + class: 'ck-link-form_additional-buttons' + } + } ); + children.add( additionalButtonsView ); + } + + return children; + } } /** From 75a3a2752c7cda88d7a501951b35b98de05e3947 Mon Sep 17 00:00:00 2001 From: dkonopka Date: Thu, 18 Apr 2019 10:27:46 +0200 Subject: [PATCH 15/81] Strech additional buttons to the full row width. --- theme/linkform.css | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/theme/linkform.css b/theme/linkform.css index eec08c7..2b806e7 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -5,16 +5,33 @@ @import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css"; +:root { + --ck-link-form-max-width: 330px; +} + .ck.ck-link-form { display: flex; - flex-direction: row; flex-wrap: wrap; - max-width: 500px; + max-width: var(--ck-link-form-max-width); & .ck-label { display: none; } + /* Buttons like `open in new window` with switch toggler. + Added div is a temporary solution to make stronger specificity than `.ck.ck-link-form > :not(:first-child)` in theme-lark. */ + & div.ck-link-form_additional-buttons { + display: flex; + flex-wrap: wrap; + margin-left: 0; + } + + & .ck-link-form_additional-buttons .ck-button { + width: 100%; + margin-left: 0; + margin-top: var(--ck-spacing-standard); + } + @mixin ck-media-phone { flex-wrap: wrap; From 633e90fd839c5ac168eb01cb1458ba256948f7cb Mon Sep 17 00:00:00 2001 From: dkonopka Date: Thu, 18 Apr 2019 10:30:14 +0200 Subject: [PATCH 16/81] Added missing style. --- theme/linkform.css | 1 + 1 file changed, 1 insertion(+) diff --git a/theme/linkform.css b/theme/linkform.css index 2b806e7..2b974bb 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -24,6 +24,7 @@ display: flex; flex-wrap: wrap; margin-left: 0; + width: 100%; } & .ck-link-form_additional-buttons .ck-button { From 5f59dcc058679bd2397d086c9495de0614859a58 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 18 Apr 2019 10:58:34 +0200 Subject: [PATCH 17/81] Remove wrapping div for required elements in link panel. --- src/ui/linkformview.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 635894e..d12ab5f 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -240,19 +240,9 @@ export default class LinkFormView extends View { _createFormChildren() { const children = this.createCollection(); - const requiredButtonView = new View(); - requiredButtonView.setTemplate( { - tag: 'div', - children: [ - this.urlInputView, - this.saveButtonView, - this.cancelButtonView - ], - attributes: { - class: 'ck-link-form_required-buttons' - } - } ); - children.add( requiredButtonView ); + children.add( this.urlInputView ); + children.add( this.saveButtonView ); + children.add( this.cancelButtonView ); if ( this.customAttributes.length ) { const additionalButtonsView = new View(); From b5abdc7cc73f49201598b5fa4f29c5c64145179a Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 18 Apr 2019 11:37:46 +0200 Subject: [PATCH 18/81] Update manual test description and samples. --- tests/manual/linkdecorator.html | 11 ++++++++ tests/manual/linkdecorator.js | 45 +++++++++++++++++++++++++++++---- tests/manual/linkdecorator.md | 20 ++++++++++++++- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/tests/manual/linkdecorator.html b/tests/manual/linkdecorator.html index b60c24a..c5a24fb 100644 --- a/tests/manual/linkdecorator.html +++ b/tests/manual/linkdecorator.html @@ -1,3 +1,4 @@ +

Manual decorators

This is CKEditor5 from CKSource.

This is CKEditor5 as schemaless url.

@@ -6,3 +7,13 @@

This is some mail.

This is some phone number.

+ +

Automatic decorators

+
+

This is CKEditor5 from CKSource.

+

This is CKEditor5 as schemaless url.

+

This is anchor on this page.

+

This is some random ftp address.

+

This is some mail.

+

This is some phone number.

+
diff --git a/tests/manual/linkdecorator.js b/tests/manual/linkdecorator.js index a03d631..e654c9f 100644 --- a/tests/manual/linkdecorator.js +++ b/tests/manual/linkdecorator.js @@ -12,7 +12,6 @@ import Link from '../../src/link'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; -import { getData as getModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; ClassicEditor .create( document.querySelector( '#editor' ), { @@ -30,14 +29,14 @@ ClassicEditor }, { mode: 'manual', - label: 'downloadable', + label: 'Downloadable', attributes: { download: 'download' } }, { mode: 'manual', - label: 'gallery', + label: 'Gallery link', attributes: { class: 'gallery' } @@ -46,9 +45,45 @@ ClassicEditor } } ) .then( editor => { - window.getModelData = getModelData; + if ( !window.editors ) { + window.editors = {}; + } window.editor = editor; - window.model = editor.model; + window.editors.manualDecorators = editor; + } ) + .catch( err => { + console.error( err.stack ); + } ); + +ClassicEditor + .create( document.querySelector( '#editor2' ), { + plugins: [ Link, Typing, Paragraph, Clipboard, Undo, Enter ], + toolbar: [ 'link', 'undo', 'redo' ], + link: { + decorators: [ + { + mode: 'automatic', + callback: url => url.startsWith( 'tel:' ), + attributes: { + class: 'phone' + } + }, + { + mode: 'automatic', + callback: url => url.startsWith( '#' ), + attributes: { + class: 'internal' + } + } + ], + targetDecorator: true + } + } ) + .then( editor => { + if ( !window.editors ) { + window.editors = {}; + } + window.editors.autoamticDecorators = editor; } ) .catch( err => { console.error( err.stack ); diff --git a/tests/manual/linkdecorator.md b/tests/manual/linkdecorator.md index b98d3fb..423d8f7 100644 --- a/tests/manual/linkdecorator.md +++ b/tests/manual/linkdecorator.md @@ -1 +1,19 @@ -## play with it +## Link decorators + +### Manual decorators (window.editors.manualDecorators): +1. Should be available for every link. +2. Should apply changes after accepting changes. +3. There should be available 3 manual decorators: + * Open in new window + * Downloadable + * Gallery link +4. State of buttons should reflect state of currently selected link. +5. Switch buttons should be focused (with tab key) after input. Save and cancel buttons should be focused at the end + +### Automatic decorators (window.editors.automaticDecorators). +1. There should be turned on default automatic decorator, which adds `target="_blank"` and `rel="noopener noreferrer"` attributes to all external links (links started with `http://`, `https://` or `//`); +2. There should not be any changes in model or view of the editors. +3. Additional data should be added during downcast ( you need to run `window.editors.automaticDecorators.getData()` to see how it works ). +4. There are 2 additional decorators: + * phone, which detects all links started with `tel:` and adds `phone` class to such link + * internal, which adds `internal` class to all links started with `#` From b932625a0ef0d1e0bc5cbebd76bb35cbdc401395 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 18 Apr 2019 12:19:50 +0200 Subject: [PATCH 19/81] Fix old unit tests. --- tests/linkui.js | 2 +- tests/ui/linkformview.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/linkui.js b/tests/linkui.js index 51a0904..ca1b511 100644 --- a/tests/linkui.js +++ b/tests/linkui.js @@ -885,7 +885,7 @@ describe( 'LinkUI', () => { formView.fire( 'submit' ); expect( executeSpy.calledOnce ).to.be.true; - expect( executeSpy.calledWithExactly( 'link', 'http://cksource.com' ) ).to.be.true; + expect( executeSpy.calledWithExactly( 'link', 'http://cksource.com', {} ) ).to.be.true; } ); it( 'should hide and reveal the #actionsView on formView#submit event', () => { diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 71b8506..b2a4b7f 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -20,7 +20,7 @@ describe( 'LinkFormView', () => { testUtils.createSinonSandbox(); beforeEach( () => { - view = new LinkFormView( { t: val => val } ); + view = new LinkFormView( { t: val => val }, [] ); view.render(); } ); From 39b4dc2a9a95a769beaa12b9c78a58466fd810fa Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 18 Apr 2019 13:05:42 +0200 Subject: [PATCH 20/81] Restore accidently removed placeholder. --- src/ui/linkformview.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index d12ab5f..6f13ce7 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -179,6 +179,7 @@ export default class LinkFormView extends View { const labeledInput = new LabeledInputView( this.locale, InputTextView ); labeledInput.label = t( 'Link URL' ); + labeledInput.inputView.placeholder = 'https://example.com'; return labeledInput; } From 124a383639256567d6b766d5dafc0f8d259db428 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 18 Apr 2019 13:31:58 +0200 Subject: [PATCH 21/81] Fix another part of nit tests. --- tests/ui/linkformview.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index b2a4b7f..329d8af 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -39,9 +39,9 @@ describe( 'LinkFormView', () => { expect( view.saveButtonView.element.classList.contains( 'ck-button-save' ) ).to.be.true; expect( view.cancelButtonView.element.classList.contains( 'ck-button-cancel' ) ).to.be.true; - expect( view._unboundChildren.get( 0 ) ).to.equal( view.urlInputView ); - expect( view._unboundChildren.get( 1 ) ).to.equal( view.saveButtonView ); - expect( view._unboundChildren.get( 2 ) ).to.equal( view.cancelButtonView ); + expect( view.children.get( 0 ) ).to.equal( view.urlInputView ); + expect( view.children.get( 1 ) ).to.equal( view.saveButtonView ); + expect( view.children.get( 2 ) ).to.equal( view.cancelButtonView ); } ); it( 'should create #focusTracker instance', () => { @@ -78,12 +78,12 @@ describe( 'LinkFormView', () => { describe( 'template', () => { it( 'has url input view', () => { - expect( view.template.children[ 0 ] ).to.equal( view.urlInputView ); + expect( view.template.children[ 0 ].get( 0 ) ).to.equal( view.urlInputView ); } ); it( 'has button views', () => { - expect( view.template.children[ 1 ] ).to.equal( view.saveButtonView ); - expect( view.template.children[ 2 ] ).to.equal( view.cancelButtonView ); + expect( view.template.children[ 0 ].get( 1 ) ).to.equal( view.saveButtonView ); + expect( view.template.children[ 0 ].get( 2 ) ).to.equal( view.cancelButtonView ); } ); } ); } ); @@ -100,7 +100,7 @@ describe( 'LinkFormView', () => { it( 'should register child views\' #element in #focusTracker', () => { const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' ); - view = new LinkFormView( { t: () => {} } ); + view = new LinkFormView( { t: () => {} }, [] ); view.render(); sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element ); @@ -109,7 +109,7 @@ describe( 'LinkFormView', () => { } ); it( 'starts listening for #keystrokes coming from #element', () => { - view = new LinkFormView( { t: () => {} } ); + view = new LinkFormView( { t: () => {} }, [] ); const spy = sinon.spy( view.keystrokes, 'listenTo' ); From 662ade1509bd119c5f16f35034acf501eea87938 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 18 Apr 2019 14:52:20 +0200 Subject: [PATCH 22/81] Add unit test for link commands with manual decorator. --- src/linkediting.js | 2 +- tests/linkcommand.js | 76 ++++++++++++++++++++++++++++++++++++++++++ tests/unlinkcommand.js | 53 +++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/src/linkediting.js b/src/linkediting.js index 63bbf65..7d6b527 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -201,7 +201,7 @@ export default class LinkEditing extends Plugin { } } -class ManualDecorator { +export class ManualDecorator { constructor( { id, value, label, attributes } = {} ) { this.id = id; diff --git a/tests/linkcommand.js b/tests/linkcommand.js index 32812d4..690fc93 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -5,6 +5,7 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import LinkCommand from '../src/linkcommand'; +import { ManualDecorator } from '../src/linkediting'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'LinkCommand', () => { @@ -259,4 +260,79 @@ describe( 'LinkCommand', () => { } ); } ); } ); + + describe( 'manual decorators', () => { + beforeEach( () => { + editor.destroy(); + return ModelTestEditor.create() + .then( newEditor => { + editor = newEditor; + model = editor.model; + command = new LinkCommand( editor ); + + command.customAttributes.add( new ManualDecorator( { + id: 'linkManualDecorator0', + value: undefined, + label: 'Foo', + attributes: { + class: 'Foo' + } + } ) ); + command.customAttributes.add( new ManualDecorator( { + id: 'linkManualDecorator1', + value: undefined, + label: 'Bar', + attributes: { + target: '_blank' + } + } ) ); + + model.schema.extend( '$text', { + allowIn: '$root', + allowAttributes: 'linkHref' + } ); + + model.schema.register( 'p', { inheritAllFrom: '$block' } ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should insert additional attributes to link when is created', () => { + setData( model, 'foo[]bar' ); + + command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + + expect( getData( model ) ).to + .equal( 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">url]bar' ); + } ); + + it( 'should remove additional attributes to link if those are falsy', () => { + setData( model, 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >url]bar' ); + + command.execute( 'url', { linkManualDecorator0: false, linkManualDecorator1: false } ); + + expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); + } ); + + it( 'should add additional attributes to link when link is modified', () => { + setData( model, 'f<$text linkHref="url">o[]obar' ); + + command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + + expect( getData( model ) ).to + .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + } ); + + it( 'should insert additional attributes for range selection', () => { + setData( model, 'f[ooba]r' ); + + command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + + expect( getData( model ) ).to + .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + } ); + } ); } ); diff --git a/tests/unlinkcommand.js b/tests/unlinkcommand.js index 04b93c8..414a461 100644 --- a/tests/unlinkcommand.js +++ b/tests/unlinkcommand.js @@ -5,6 +5,7 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import UnlinkCommand from '../src/unlinkcommand'; +import LinkEditing from '../src/linkediting'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; @@ -227,4 +228,56 @@ describe( 'UnlinkCommand', () => { } ); } ); } ); + + describe.skip( 'manual decorators', () => { + beforeEach( () => { + editor.destroy(); + return ModelTestEditor.create( { + extraPlugins: [ LinkEditing ], + link: { + decorators: [ + { + mode: 'manual', + label: 'Foo', + attributes: { + class: 'foo' + } + }, + { + mode: 'manual', + label: 'Bar', + attributes: { + target: '_blank' + } + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + document = model.document; + command = new UnlinkCommand( editor ); + + model.schema.extend( '$text', { + allowIn: '$root', + allowAttributes: 'linkHref' + } ); + + model.schema.register( 'p', { inheritAllFrom: '$block' } ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should remove manual decorators from links together with linkHref', () => { + setData( model, '<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >f[]oobar' ); + + command.execute(); + + expect( getData( model ) ).to.equal( 'f[]oobar' ); + } ); + } ); } ); From 22cdc1d71319bc26d02f19b809781f670910bbc1 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 24 Apr 2019 09:23:36 +0200 Subject: [PATCH 23/81] Remove remaining 'skip' method from unit tests. --- tests/unlinkcommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unlinkcommand.js b/tests/unlinkcommand.js index 414a461..d237b7f 100644 --- a/tests/unlinkcommand.js +++ b/tests/unlinkcommand.js @@ -229,7 +229,7 @@ describe( 'UnlinkCommand', () => { } ); } ); - describe.skip( 'manual decorators', () => { + describe( 'manual decorators', () => { beforeEach( () => { editor.destroy(); return ModelTestEditor.create( { From a63899c6310f85ea1294952a37031d8d9effd5a1 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Sun, 28 Apr 2019 17:29:31 +0200 Subject: [PATCH 24/81] Add unit test for automaticdispatcher, fix typos. --- src/linkediting.js | 7 ++- src/utils/automaticdecorators.js | 2 +- tests/utils/automaticdecorators.js | 94 ++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 tests/utils/automaticdecorators.js diff --git a/src/linkediting.js b/src/linkediting.js index 7d6b527..5eb0695 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -78,6 +78,7 @@ export default class LinkEditing extends Plugin { editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); const linkDecorators = editor.config.get( 'link.decorators' ) || []; + this.enableAutomaticDecorators( linkDecorators.filter( item => item.mode === AUTO ) ); this.enableManualDecorators( linkDecorators.filter( item => item.mode === MANUAL ) ); @@ -108,15 +109,17 @@ export default class LinkEditing extends Plugin { } automaticDecorators.add( automaticDecoratorDefinitions ); - editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() ); + if ( automaticDecorators.length ) { + editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() ); + } } enableManualDecorators( manualDecoratorDefinitions ) { - const editor = this.editor; if ( !manualDecoratorDefinitions.length ) { return; } + const editor = this.editor; const command = editor.commands.get( 'link' ); const attrCollection = command.customAttributes; diff --git a/src/utils/automaticdecorators.js b/src/utils/automaticdecorators.js index 9832853..2ada8fb 100644 --- a/src/utils/automaticdecorators.js +++ b/src/utils/automaticdecorators.js @@ -23,7 +23,7 @@ export default class AutomaticDecorators { } /** - * Add item or array of items with autoamtic rules for applying decorators to link plugin. + * Add item or array of items with automatic rules for applying decorators to link plugin. * * @param {Object|Array.} item configuration object of automatic rules for decorating links. * It might be also array of such objects. diff --git a/tests/utils/automaticdecorators.js b/tests/utils/automaticdecorators.js new file mode 100644 index 0000000..b82acef --- /dev/null +++ b/tests/utils/automaticdecorators.js @@ -0,0 +1,94 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import AutomaticDecorators from '../../src/utils/automaticdecorators'; + +describe( 'Automatic Decorators', () => { + let automaticDecorators; + beforeEach( () => { + automaticDecorators = new AutomaticDecorators(); + } ); + + describe( 'constructor()', () => { + it( 'initialise with empty Set', () => { + expect( automaticDecorators._definitions ).to.be.instanceOf( Set ); + } ); + } ); + + describe( 'add()', () => { + const tests = [ + { + mode: 'automatic', + callback: () => {}, + attributes: { + foo: 'bar' + } + }, + { + mode: 'automatic', + callback: () => {}, + attributes: { + bar: 'baz' + } + }, + { + mode: 'automatic', + callback: () => {}, + attributes: { + test1: 'one', + test2: 'two', + test3: 'three' + } + } + ]; + it( 'can accept single object', () => { + expect( automaticDecorators._definitions.size ).to.equal( 0 ); + + automaticDecorators.add( tests[ 0 ] ); + expect( automaticDecorators._definitions.size ).to.equal( 1 ); + + const firstValue = automaticDecorators._definitions.values().next().value; + + expect( firstValue ).to.deep.include( { + mode: 'automatic', + attributes: { + foo: 'bar' + } + } ); + expect( firstValue ).to.have.property( 'callback' ); + expect( firstValue.callback ).to.be.a( 'function' ); + } ); + + it( 'can accept array of objects', () => { + expect( automaticDecorators._definitions.size ).to.equal( 0 ); + + automaticDecorators.add( tests ); + + expect( automaticDecorators._definitions.size ).to.equal( 3 ); + + const setIterator = automaticDecorators._definitions.values(); + setIterator.next(); + setIterator.next(); + const thirdValue = setIterator.next().value; + + expect( thirdValue ).to.deep.include( { + mode: 'automatic', + attributes: { + test1: 'one', + test2: 'two', + test3: 'three' + } + } ); + expect( thirdValue ).to.have.property( 'callback' ); + expect( thirdValue.callback ).to.be.a( 'function' ); + } ); + } ); + + describe( 'getDispatcher()', () => { + it( 'should return a dispatcher function', () => { + expect( automaticDecorators.getDispatcher() ).to.be.a( 'function' ); + } ); + } ); +} ); From 81bc05e2018af69921b8b8ed89301447f10d3083 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 29 Apr 2019 09:13:41 +0200 Subject: [PATCH 25/81] Add documentation for manual decorator options. --- src/link.js | 59 +++++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/src/link.js b/src/link.js index 9553ce3..357131c 100644 --- a/src/link.js +++ b/src/link.js @@ -87,22 +87,16 @@ export default class Link extends Plugin { * } * }, * { - * mode: 'automatic', - * callback: url => url.includes( 'download' ) && url.endsWith( '.pdf' ), + * mode: 'manual', + * label: 'Downloadable', * attributes: { * download: 'download' * } - * }, - * { - * mode: 'automatic', - * callback: url => url.includes( 'image' ) && url.endsWith( '.png' ), - * attributes: { - * class: 'light-gallery' - * } * } * ] * ``` - * @member {Array.} module:link/link~LinkConfig#decorators + * @member {Array.} + * module:link/link~LinkConfig#decorators */ /** @@ -114,16 +108,16 @@ export default class Link extends Plugin { * * `rel="noopener noreferrer"` * for all links started with: `http://`, `https://` or `//`. * - * ```js - * { - * mode: 'automatic', - * callback: url => /^(https?:)?\/\//.test( url ), - * attributes: { - * target: '_blank', - * rel: 'noopener noreferrer' - * } - * } - * ``` + *```js + * { + * mode: 'automatic', + * callback: url => /^(https?:)?\/\//.test( url ), + * attributes: { + * target: '_blank', + * rel: 'noopener noreferrer' + * } + * } + *``` * * @typedef {Object} module:link/link~LinkDecoratorAutomaticOption * @property {'automatic'} mode it should has always string value 'automatic' for automatic decorators @@ -131,3 +125,28 @@ export default class Link extends Plugin { * for urls that be decorate with this decorator. * @property {Object} attributes key-value pairs used as attributes added to anchor during downcasting. */ + +/** + * This object defining manual decorator for the links. Based on this options, there will be added UI switches do link balloon. + * User will be able activate pre-configured attributes for the link by simple switch button change. + * For example, you can define rules, when attribute `target="_blank"` will be added to links, which adds attributes + * when user select proper options in UI: + * * `target="_blank"` + * * `rel="noopener noreferrer"` + * + *```js + * { + * mode: 'manual', + * label: 'Open link in new window', + * attributes: { + * target: '_blank', + * rel: 'noopener noreferrer' + * } + * } + *``` + * + * @typedef {Object} module:link/link~LinkDecoratorManualOption + * @property {'manual'} mode it should has always string value 'manual' for manual decorators + * @property {String} label the label for ui switch, which will be responsible for activation of given attributes set + * @property {Object} attributes key-value pairs used as attributes added to anchor during downcasting. + */ From 9f56f83f3d80e928860435abf0cef29ba52e0687 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 29 Apr 2019 10:41:34 +0200 Subject: [PATCH 26/81] Correct unit test, add 'length' property to automaticdecorators class. --- src/linkediting.js | 1 - src/utils/automaticdecorators.js | 11 +++++++++++ tests/linkediting.js | 11 ++++++++++- tests/utils/automaticdecorators.js | 12 ++++++++---- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/linkediting.js b/src/linkediting.js index 5eb0695..3d68f2d 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -92,7 +92,6 @@ export default class LinkEditing extends Plugin { enableAutomaticDecorators( automaticDecoratorDefinitions ) { const editor = this.editor; const automaticDecorators = new AutomaticDecorators(); - // Adds default decorator for external links. if ( editor.config.get( 'link.targetDecorator' ) ) { automaticDecorators.add( { diff --git a/src/utils/automaticdecorators.js b/src/utils/automaticdecorators.js index 2ada8fb..bfcff75 100644 --- a/src/utils/automaticdecorators.js +++ b/src/utils/automaticdecorators.js @@ -22,6 +22,17 @@ export default class AutomaticDecorators { this._definitions = new Set(); } + /** + * Gives information how many decorators is stored in {@link module:link/utils/automaticdecorators~AutomaticDecorators} instance. + * + * @readonly + * @protected + * @type {Number} + */ + get length() { + return this._definitions.size; + } + /** * Add item or array of items with automatic rules for applying decorators to link plugin. * diff --git a/tests/linkediting.js b/tests/linkediting.js index 82b1752..6561711 100644 --- a/tests/linkediting.js +++ b/tests/linkediting.js @@ -33,6 +33,10 @@ describe( 'LinkEditing', () => { } ); } ); + afterEach( () => { + editor.destroy(); + } ); + it( 'should be loaded', () => { expect( editor.plugins.get( LinkEditing ) ).to.be.instanceOf( LinkEditing ); } ); @@ -393,8 +397,8 @@ describe( 'LinkEditing', () => { } ); describe( 'for link.targetDecorator = false', () => { + let editor, model; beforeEach( () => { - editor.destroy(); return VirtualTestEditor .create( { plugins: [ Paragraph, LinkEditing, Enter ], @@ -408,6 +412,11 @@ describe( 'LinkEditing', () => { view = editor.editing.view; } ); } ); + + afterEach( () => { + editor.destroy(); + } ); + it( 'link.targetDecorator is set as true value', () => { expect( editor.config.get( 'link.targetDecorator' ) ).to.be.true; } ); diff --git a/tests/utils/automaticdecorators.js b/tests/utils/automaticdecorators.js index b82acef..64a12d4 100644 --- a/tests/utils/automaticdecorators.js +++ b/tests/utils/automaticdecorators.js @@ -17,6 +17,10 @@ describe( 'Automatic Decorators', () => { } ); } ); + it( 'has length equal 0 after initialization', () => { + expect( automaticDecorators.length ).to.equal( 0 ); + } ); + describe( 'add()', () => { const tests = [ { @@ -44,10 +48,10 @@ describe( 'Automatic Decorators', () => { } ]; it( 'can accept single object', () => { - expect( automaticDecorators._definitions.size ).to.equal( 0 ); + expect( automaticDecorators.length ).to.equal( 0 ); automaticDecorators.add( tests[ 0 ] ); - expect( automaticDecorators._definitions.size ).to.equal( 1 ); + expect( automaticDecorators.length ).to.equal( 1 ); const firstValue = automaticDecorators._definitions.values().next().value; @@ -62,11 +66,11 @@ describe( 'Automatic Decorators', () => { } ); it( 'can accept array of objects', () => { - expect( automaticDecorators._definitions.size ).to.equal( 0 ); + expect( automaticDecorators.length ).to.equal( 0 ); automaticDecorators.add( tests ); - expect( automaticDecorators._definitions.size ).to.equal( 3 ); + expect( automaticDecorators.length ).to.equal( 3 ); const setIterator = automaticDecorators._definitions.values(); setIterator.next(); From 7c68d985c699a41c0b363e54e521735b01bc17c2 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 29 Apr 2019 11:08:27 +0200 Subject: [PATCH 27/81] Add unit tests for link to increase coverage. --- src/linkediting.js | 2 +- tests/linkcommand.js | 63 +++++++++++++++++++++++++++++--------------- tests/linkui.js | 55 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 22 deletions(-) diff --git a/src/linkediting.js b/src/linkediting.js index 3d68f2d..5470b34 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -204,7 +204,7 @@ export default class LinkEditing extends Plugin { } export class ManualDecorator { - constructor( { id, value, label, attributes } = {} ) { + constructor( { id, value, label, attributes } ) { this.id = id; this.set( 'value', value ); diff --git a/tests/linkcommand.js b/tests/linkcommand.js index 690fc93..3787b55 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -300,39 +300,60 @@ describe( 'LinkCommand', () => { return editor.destroy(); } ); - it( 'should insert additional attributes to link when is created', () => { - setData( model, 'foo[]bar' ); + describe( 'collapsed selection', () => { + it( 'should insert additional attributes to link when it is created', () => { + setData( model, 'foo[]bar' ); - command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); - expect( getData( model ) ).to - .equal( 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">url]bar' ); - } ); + expect( getData( model ) ).to + .equal( 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">url]bar' ); + } ); - it( 'should remove additional attributes to link if those are falsy', () => { - setData( model, 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >url]bar' ); + it( 'should add additional attributes to link when link is modified', () => { + setData( model, 'f<$text linkHref="url">o[]obar' ); - command.execute( 'url', { linkManualDecorator0: false, linkManualDecorator1: false } ); + command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); - expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); - } ); + expect( getData( model ) ).to + .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + } ); - it( 'should add additional attributes to link when link is modified', () => { - setData( model, 'f<$text linkHref="url">o[]obar' ); + it( 'should remove additional attributes to link if those are falsy', () => { + setData( model, 'foo<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >u[]rlbar' ); - command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + command.execute( 'url', { linkManualDecorator0: false, linkManualDecorator1: false } ); - expect( getData( model ) ).to - .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); + } ); } ); - it( 'should insert additional attributes for range selection', () => { - setData( model, 'f[ooba]r' ); + describe( 'range selection', () => { + it( 'should insert additional attributes to link when it is created', () => { + setData( model, 'f[ooba]r' ); - command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); - expect( getData( model ) ).to - .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + expect( getData( model ) ).to + .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + } ); + + it( 'should add additional attributes to link when link is modified', () => { + setData( model, 'f[<$text linkHref="foo">ooba]r' ); + + command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + + expect( getData( model ) ).to + .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + } ); + + it( 'should remove additional attributes to link if those are falsy', () => { + setData( model, 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >url]bar' ); + + command.execute( 'url', { linkManualDecorator0: false, linkManualDecorator1: false } ); + + expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); + } ); } ); } ); } ); diff --git a/tests/linkui.js b/tests/linkui.js index ca1b511..ffecff3 100644 --- a/tests/linkui.js +++ b/tests/linkui.js @@ -958,6 +958,61 @@ describe( 'LinkUI', () => { expect( focusSpy.calledBefore( removeSpy ) ).to.equal( true ); } ); + + it( 'should gather information about manual decorators', () => { + let editor, model, formView; + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ LinkEditing, LinkUI, Paragraph ], + link: { + decorators: [ + { + mode: 'manual', + label: 'Foo', + attributes: { + foo: 'bar' + } + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + + model.schema.extend( '$text', { + allowIn: '$root', + allowAttributes: 'linkHref' + } ); + + const linkUIFeature = editor.plugins.get( LinkUI ); + const balloon = editor.plugins.get( ContextualBalloon ); + + formView = linkUIFeature.formView; + + // There is no point to execute BalloonPanelView attachTo and pin methods so lets override it. + testUtils.sinon.stub( balloon.view, 'attachTo' ).returns( {} ); + testUtils.sinon.stub( balloon.view, 'pin' ).returns( {} ); + + formView.render(); + } ) + .then( () => { + const executeSpy = testUtils.sinon.spy( editor, 'execute' ); + + setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); + expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' ); + expect( formView.customAttributesView.length ).to.equal( 1 ); + expect( formView.customAttributesView.first.isOn ).to.be.true; + + formView.fire( 'submit' ); + + expect( executeSpy.calledOnce ).to.be.true; + expect( executeSpy.calledWithExactly( 'link', 'url', { linkManualDecorator0: true } ) ).to.be.true; + } ); + } ); } ); } ); } ); From dca949c314664bcf4c1be550fde16e54feaa520f Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 29 Apr 2019 15:07:48 +0200 Subject: [PATCH 28/81] Extract manual decorator class to utils and cover it with tests. --- src/linkediting.js | 19 ++--------- src/utils/manualdecorator.js | 62 ++++++++++++++++++++++++++++++++++ tests/linkcommand.js | 4 +-- tests/utils/manualdecorator.js | 43 +++++++++++++++++++++++ 4 files changed, 108 insertions(+), 20 deletions(-) create mode 100644 src/utils/manualdecorator.js create mode 100644 tests/utils/manualdecorator.js diff --git a/src/linkediting.js b/src/linkediting.js index 5470b34..cc89e56 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -12,11 +12,10 @@ import LinkCommand from './linkcommand'; import UnlinkCommand from './unlinkcommand'; import { createLinkElement, ensureSafeUrl } from './utils'; import AutomaticDecorators from './utils/automaticdecorators'; +import ManualDecorator from './utils/manualdecorator'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; import findLinkRange from './findlinkrange'; import '../theme/link.css'; -import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; -import mix from '@ckeditor/ckeditor5-utils/src/mix'; const HIGHLIGHT_CLASS = 'ck-link_selected'; const AUTO = 'automatic'; @@ -126,7 +125,7 @@ export default class LinkEditing extends Plugin { const decoratorName = `linkManualDecorator${ index }`; editor.model.schema.extend( '$text', { allowAttributes: decoratorName } ); - attrCollection.add( new ManualDecorator( Object.assign( { id: decoratorName, value: undefined }, decorator ) ) ); + attrCollection.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); editor.conversion.for( 'downcast' ).attributeToElement( { model: decoratorName, view: ( manualDecoratorName, writer ) => { @@ -202,17 +201,3 @@ export default class LinkEditing extends Plugin { } ); } } - -export class ManualDecorator { - constructor( { id, value, label, attributes } ) { - this.id = id; - - this.set( 'value', value ); - - this.label = label; - - this.attributes = attributes; - } -} - -mix( ManualDecorator, ObservableMixin ); diff --git a/src/utils/manualdecorator.js b/src/utils/manualdecorator.js new file mode 100644 index 0000000..46852f4 --- /dev/null +++ b/src/utils/manualdecorator.js @@ -0,0 +1,62 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/** + * @module link/utils/manualdecorator + */ + +import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; +import mix from '@ckeditor/ckeditor5-utils/src/mix'; + +/** + * Class which stores manual decorators with observable {@link module:link/utils/manualdecorator~ManualDecorator#value} + * to handle integration with ui state. + * + * @mixes module:utils/observablemixin~ObservableMixin + */ +export default class ManualDecorator { + /** + * Creates new instance of {@link module:link/utils/manualdecorator~ManualDecorator}. + * + * @param {Object} config + * @param {String} config.id Manual decorator id, which is a name of attribute in model, for example 'linkManualDecorator0'. + * @param {String} config.label The label used in user interface to switch manual decorator. + * @param {Object} config.attributes Set of attributes added to downcasted data, when decorator is activated for specific link. + * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. + */ + constructor( { id, label, attributes } ) { + /** + * Manual decorator id, which is a name of attribute in model, for example 'linkManualDecorator0'. + * + * @type {String} + */ + this.id = id; + + /** + * Value of current manual decorator. It reflects its state from UI. + * + * @observable + * @member {Boolean} module:link/utils/manualdecorator~ManualDecorator#value + */ + this.set( 'value' ); + + /** + * The label used in user interface to switch manual decorator. + * + * @type {String} + */ + this.label = label; + + /** + * Set of attributes added to downcasted data, when decorator is activated for specific link. + * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. + * + * @type {Object} + */ + this.attributes = attributes; + } +} + +mix( ManualDecorator, ObservableMixin ); diff --git a/tests/linkcommand.js b/tests/linkcommand.js index 3787b55..a49f8c9 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -5,7 +5,7 @@ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor'; import LinkCommand from '../src/linkcommand'; -import { ManualDecorator } from '../src/linkediting'; +import ManualDecorator from '../src/utils/manualdecorator'; import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; describe( 'LinkCommand', () => { @@ -272,7 +272,6 @@ describe( 'LinkCommand', () => { command.customAttributes.add( new ManualDecorator( { id: 'linkManualDecorator0', - value: undefined, label: 'Foo', attributes: { class: 'Foo' @@ -280,7 +279,6 @@ describe( 'LinkCommand', () => { } ) ); command.customAttributes.add( new ManualDecorator( { id: 'linkManualDecorator1', - value: undefined, label: 'Bar', attributes: { target: '_blank' diff --git a/tests/utils/manualdecorator.js b/tests/utils/manualdecorator.js new file mode 100644 index 0000000..38c8133 --- /dev/null +++ b/tests/utils/manualdecorator.js @@ -0,0 +1,43 @@ +/** + * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +import ManualDecorator from '../../src/utils/manualdecorator'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +describe( 'Manual Decorator', () => { + let manualDecorator; + testUtils.createSinonSandbox(); + + beforeEach( () => { + manualDecorator = new ManualDecorator( { + id: 'foo', + label: 'bar', + attributes: { + one: 'two' + } + } ); + } ); + + it( 'constructor', () => { + expect( manualDecorator.id ).to.equal( 'foo' ); + expect( manualDecorator.label ).to.equal( 'bar' ); + expect( manualDecorator.attributes ).to.deep.equal( { one: 'two' } ); + } ); + + it( '#value is observable', () => { + const spy = testUtils.sinon.spy(); + expect( manualDecorator.value ).to.be.undefined; + + manualDecorator.on( 'change:value', spy ); + manualDecorator.value = true; + + expect( spy.calledOnce ).to.be.true; + testUtils.sinon.assert.calledWithExactly( spy.firstCall, testUtils.sinon.match.any, 'value', true, undefined ); + + manualDecorator.value = false; + expect( spy.calledTwice ).to.be.true; + testUtils.sinon.assert.calledWithExactly( spy.secondCall, testUtils.sinon.match.any, 'value', false, true ); + } ); +} ); From 5a2e283a3727279821a3bbf65b7365f359eaf9a8 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 29 Apr 2019 17:32:02 +0200 Subject: [PATCH 29/81] Add missing documentation. --- src/linkcommand.js | 7 +++++++ src/linkediting.js | 20 ++++++++++++++++++++ src/ui/linkformview.js | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index 8917e1e..15942d5 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -29,6 +29,13 @@ export default class LinkCommand extends Command { constructor( editor ) { super( editor ); + /** + * Keeps collection of {@link module:link/utils/manualdecorator~ManualDecorator} + * recognized in {@link module:link/link~LinkConfig#decorators}. + * + * @readonly + * @type {module:utils/collection~Collection} + */ this.customAttributes = new Collection(); } diff --git a/src/linkediting.js b/src/linkediting.js index cc89e56..f9452bb 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -88,6 +88,15 @@ export default class LinkEditing extends Plugin { this._setupLinkHighlight(); } + /** + * Method process {@link module:link/link~LinkDecoratorAutomaticOption} by creating instance of + * {@link module:link/utils/automaticdecorators~AutomaticDecorators}. If there are available automatic decorators, then + * there is registered {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} to handle + * those configurations. + * + * @private + * @param {Array.} automaticDecoratorDefinitions + */ enableAutomaticDecorators( automaticDecoratorDefinitions ) { const editor = this.editor; const automaticDecorators = new AutomaticDecorators(); @@ -112,6 +121,17 @@ export default class LinkEditing extends Plugin { } } + /** + * Method process {@link module:link/link~LinkDecoratorManualOption} by transformation those configuration options into + * {@link module:link/utils/manualdecorator~ManualDecorator}. Manual decorators are added to + * {@link module:link/linkcommand~LinkCommand#customAttributes} collections, which might be considered as a model + * for manual decorators state. It also provides proper + * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} converter for each + * manual decorator and extends model schema with adequate attributes. + * + * @private + * @param {Array.} manualDecoratorDefinitions + */ enableManualDecorators( manualDecoratorDefinitions ) { if ( !manualDecoratorDefinitions.length ) { return; diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 6f13ce7..d3f5588 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -33,7 +33,13 @@ import '../../theme/linkform.css'; */ export default class LinkFormView extends View { /** - * @inheritDoc + * Creates an instance of the {@link module:link/ui/linkformview~LinkFormView} class. + * + * Also see {@link #render}. + * + * @param {module:utils/locale~Locale} [locale] The localization services instance. + * @param {module:utils/collection~Collection} [customAttributes] Reference to custom attributes in + * {@link module:link/linkcommand~LinkCommand#customAttributes}. */ constructor( locale, customAttributes ) { super( locale ); @@ -78,10 +84,29 @@ export default class LinkFormView extends View { */ this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' ); + /** + * Keeps reference to {@link module:link/linkcommand~LinkCommand#customAttributes}. + * + * @readonly + * @type {model:utils/collection~Collection} + */ this.customAttributes = customAttributes; + /** + * Keeps reference to {@link module:ui/button/switchbuttonview~SwitchButtonView} made based on {@link #customAttributes}. + * It use {@link #_createCustomAttributesView} to generate proper collection. + * + * @readonly + * @type {module:ui/viewcollection~ViewCollection} + */ this.customAttributesView = this._createCustomAttributesView(); + /** + * Collection of views used as children elements in {@link module:link/ui/linkformview~LinkFormView}. + * + * @readonly + * @type {module:ui/viewcollection~ViewCollection} + */ this.children = this._createFormChildren(); /** @@ -216,6 +241,12 @@ export default class LinkFormView extends View { return button; } + /** + * Prepare {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView} + * made based on {@link #customAttributes} + * + * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. + */ _createCustomAttributesView() { const checkboxes = this.createCollection(); @@ -238,6 +269,12 @@ export default class LinkFormView extends View { return checkboxes; } + /** + * Creates {@link #children} for {@link module:link/ui/linkformview~LinkFormView}. If there exist {@link #customAttributes}, + * Then additional View wrapping all {@link #customAttributesView} will be added as a child of LinkFormView. + * + * @returns {module:ui/viewcollection~ViewCollection} children of LinkFormView. + */ _createFormChildren() { const children = this.createCollection(); From 7f0c8e7760a6aed2de4eb6e12d74c7e81714fa4d Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 30 Apr 2019 10:47:47 +0200 Subject: [PATCH 30/81] Rename variables and properties. --- src/linkui.js | 2 +- src/ui/linkformview.js | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/linkui.js b/src/linkui.js index 2a4059d..c71faa9 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -157,7 +157,7 @@ export default class LinkUI extends Plugin { const customAttributes = {}; for ( const switchButton of formView.customAttributesView ) { - customAttributes[ switchButton.value ] = switchButton.isOn; + customAttributes[ switchButton.name ] = switchButton.isOn; } editor.execute( 'link', formView.urlInputView.inputView.element.value, customAttributes ); diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index d3f5588..164387f 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -248,25 +248,26 @@ export default class LinkFormView extends View { * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. */ _createCustomAttributesView() { - const checkboxes = this.createCollection(); + const switches = this.createCollection(); - checkboxes.bindTo( this.customAttributes ).using( item => { - const checkbox = new SwitchButtonView( this.locale ); - checkbox.set( { - value: item.id, + switches.bindTo( this.customAttributes ).using( item => { + const switchButton = new SwitchButtonView( this.locale ); + + switchButton.set( { + name: item.id, label: item.label, withText: true } ); - checkbox.bind( 'isOn' ).to( item, 'value' ); + switchButton.bind( 'isOn' ).to( item, 'value' ); - checkbox.on( 'execute', () => { - this.customAttributes.get( item.id ).set( 'value', !checkbox.isOn ); + switchButton.on( 'execute', () => { + item.set( 'value', !switchButton.isOn ); } ); - return checkbox; + return switchButton; } ); - return checkboxes; + return switches; } /** From 3ca7d5153e8023ea456561072dcceeb0bf889568 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 30 Apr 2019 11:08:26 +0200 Subject: [PATCH 31/81] Add tests for link form view. --- tests/ui/linkformview.js | 78 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 329d8af..50a1ba7 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -13,6 +13,8 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler'; import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import ManualDecorator from '../../src/utils/manualdecorator'; +import Collection from '@ckeditor/ckeditor5-utils/src/collection'; describe( 'LinkFormView', () => { let view; @@ -24,6 +26,10 @@ describe( 'LinkFormView', () => { view.render(); } ); + afterEach( () => { + view.destroy(); + } ); + describe( 'constructor()', () => { it( 'should create element from template', () => { expect( view.element.classList.contains( 'ck' ) ).to.true; @@ -182,4 +188,76 @@ describe( 'LinkFormView', () => { sinon.assert.calledOnce( spy ); } ); } ); + + describe( 'customAttributes', () => { + let view, collection; + beforeEach( () => { + collection = new Collection(); + collection.add( new ManualDecorator( { + id: 'decorator1', + label: 'Foo', + attributes: { + foo: 'bar' + } + } ) ); + collection.add( new ManualDecorator( { + id: 'decorator2', + label: 'Download', + attributes: { + download: 'download' + } + } ) ); + collection.add( new ManualDecorator( { + id: 'decorator3', + label: 'Multi', + attributes: { + class: 'fancy-class', + target: '_blank', + rel: 'noopener noreferrer' + } + } ) ); + + view = new LinkFormView( { t: val => val }, collection ); + view.render(); + } ); + + afterEach( () => { + view.destroy(); + collection.clear(); + } ); + it( 'switch buttons reflects state of customAttributes', () => { + expect( view.customAttributesView.length ).to.equal( 3 ); + + expect( view.customAttributesView.get( 0 ) ).to.deep.include( { + name: 'decorator1', + label: 'Foo' + } ); + expect( view.customAttributesView.get( 1 ) ).to.deep.include( { + name: 'decorator2', + label: 'Download' + } ); + expect( view.customAttributesView.get( 2 ) ).to.deep.include( { + name: 'decorator3', + label: 'Multi' + } ); + } ); + + it( 'reacts on switch button changes', () => { + const modelItem = collection.first; + const viewItem = view.customAttributesView.first; + + expect( modelItem.value ).to.be.undefined; + expect( viewItem.isOn ).to.be.undefined; + + viewItem.element.dispatchEvent( new Event( 'click' ) ); + + expect( modelItem.value ).to.be.true; + expect( viewItem.isOn ).to.be.true; + + viewItem.element.dispatchEvent( new Event( 'click' ) ); + + expect( modelItem.value ).to.be.false; + expect( viewItem.isOn ).to.be.false; + } ); + } ); } ); From 7482f30b3da396310e5841173e62ba2f2c6426d0 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 30 Apr 2019 11:59:20 +0200 Subject: [PATCH 32/81] Update docs descriptions. --- src/link.js | 15 ++++++++------- src/linkcommand.js | 5 +++-- src/linkediting.js | 9 ++++++--- src/utils/automaticdecorators.js | 8 ++++---- src/utils/manualdecorator.js | 8 ++++---- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/link.js b/src/link.js index 357131c..91ce872 100644 --- a/src/link.js +++ b/src/link.js @@ -102,7 +102,7 @@ export default class Link extends Plugin { /** * This object defining automatic decorator for the links. Based on this option data pipeline will extend links with proper attributes. * For example, you can define rules, when attribute `target="_blank"` will be added to links. - * There is a default option which might be activated with {@link module:link/link~LinkConfig#targetDecorator}, + * Please notice that, there is a default option which can be activated with {@link module:link/link~LinkConfig#targetDecorator}, * which automatically adds attributes: * * `target="_blank"` * * `rel="noopener noreferrer"` @@ -124,15 +124,15 @@ export default class Link extends Plugin { * @property {Function} callback takes `url` as parameter and should return `true` * for urls that be decorate with this decorator. * @property {Object} attributes key-value pairs used as attributes added to anchor during downcasting. + * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. */ /** - * This object defining manual decorator for the links. Based on this options, there will be added UI switches do link balloon. - * User will be able activate pre-configured attributes for the link by simple switch button change. - * For example, you can define rules, when attribute `target="_blank"` will be added to links, which adds attributes - * when user select proper options in UI: - * * `target="_blank"` - * * `rel="noopener noreferrer"` + * This object defining manual decorator for the links. Based on this options, there will be added UI switches to link balloon. + * User will be able to activate pre-configured attributes for the link by simple switch button change. + * + * For example, you can define decorator, which show up switch responsible for adding attributes `target="_blank"` and + * `rel="noopener noreferrer"` when user select proper options in UI. * *```js * { @@ -149,4 +149,5 @@ export default class Link extends Plugin { * @property {'manual'} mode it should has always string value 'manual' for manual decorators * @property {String} label the label for ui switch, which will be responsible for activation of given attributes set * @property {Object} attributes key-value pairs used as attributes added to anchor during downcasting. + * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. */ diff --git a/src/linkcommand.js b/src/linkcommand.js index 15942d5..c89959f 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -30,8 +30,9 @@ export default class LinkCommand extends Command { super( editor ); /** - * Keeps collection of {@link module:link/utils/manualdecorator~ManualDecorator} - * recognized in {@link module:link/link~LinkConfig#decorators}. + * Keeps collection of {@link module:link/utils~ManualDecorator} + * recognized from {@link module:link/link~LinkConfig#decorators}. + * You can consider it as a model of states for custom attributes added to links. * * @readonly * @type {module:utils/collection~Collection} diff --git a/src/linkediting.js b/src/linkediting.js index f9452bb..874d462 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -90,10 +90,13 @@ export default class LinkEditing extends Plugin { /** * Method process {@link module:link/link~LinkDecoratorAutomaticOption} by creating instance of - * {@link module:link/utils/automaticdecorators~AutomaticDecorators}. If there are available automatic decorators, then + * {@link module:link/utils~AutomaticDecorators}. If there are available automatic decorators, then * there is registered {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} to handle * those configurations. * + * Please notice, that automatic decorator will be also added, when {@link module:link/link~LinkConfig#targetDecorator} + * will be set to `true`. + * * @private * @param {Array.} automaticDecoratorDefinitions */ @@ -123,11 +126,11 @@ export default class LinkEditing extends Plugin { /** * Method process {@link module:link/link~LinkDecoratorManualOption} by transformation those configuration options into - * {@link module:link/utils/manualdecorator~ManualDecorator}. Manual decorators are added to + * {@link module:link/utils~ManualDecorator}. Manual decorators are added to * {@link module:link/linkcommand~LinkCommand#customAttributes} collections, which might be considered as a model * for manual decorators state. It also provides proper * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} converter for each - * manual decorator and extends model schema with adequate attributes. + * manual decorator and extends {@link module:engine/model/schema~Schema model's schema} with adequate model attributes. * * @private * @param {Array.} manualDecoratorDefinitions diff --git a/src/utils/automaticdecorators.js b/src/utils/automaticdecorators.js index bfcff75..c34f027 100644 --- a/src/utils/automaticdecorators.js +++ b/src/utils/automaticdecorators.js @@ -4,7 +4,7 @@ */ /** - * @module link/utils/automaticdecorators + * @module link/utils */ /** @@ -23,7 +23,7 @@ export default class AutomaticDecorators { } /** - * Gives information how many decorators is stored in {@link module:link/utils/automaticdecorators~AutomaticDecorators} instance. + * Gives information how many decorators is stored in {@link module:link/utils~AutomaticDecorators} instance. * * @readonly * @protected @@ -36,8 +36,8 @@ export default class AutomaticDecorators { /** * Add item or array of items with automatic rules for applying decorators to link plugin. * - * @param {Object|Array.} item configuration object of automatic rules for decorating links. - * It might be also array of such objects. + * @param {module:link/link~LinkDecoratorAutomaticOption|Array.} item + * configuration object of automatic rules for decorating links. It might be also array of such objects. */ add( item ) { if ( Array.isArray( item ) ) { diff --git a/src/utils/manualdecorator.js b/src/utils/manualdecorator.js index 46852f4..f666ffc 100644 --- a/src/utils/manualdecorator.js +++ b/src/utils/manualdecorator.js @@ -4,21 +4,21 @@ */ /** - * @module link/utils/manualdecorator + * @module link/utils */ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; /** - * Class which stores manual decorators with observable {@link module:link/utils/manualdecorator~ManualDecorator#value} + * Class which stores manual decorators with observable {@link module:link/utils~ManualDecorator#value} * to handle integration with ui state. * * @mixes module:utils/observablemixin~ObservableMixin */ export default class ManualDecorator { /** - * Creates new instance of {@link module:link/utils/manualdecorator~ManualDecorator}. + * Creates new instance of {@link module:link/utils~ManualDecorator}. * * @param {Object} config * @param {String} config.id Manual decorator id, which is a name of attribute in model, for example 'linkManualDecorator0'. @@ -38,7 +38,7 @@ export default class ManualDecorator { * Value of current manual decorator. It reflects its state from UI. * * @observable - * @member {Boolean} module:link/utils/manualdecorator~ManualDecorator#value + * @member {Boolean} module:link/utils~ManualDecorator#value */ this.set( 'value' ); From aab5b6a44b5cfa2fcf6bb508adfa0ae60d2d8c2f Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 30 Apr 2019 12:50:00 +0200 Subject: [PATCH 33/81] Add localization options for manual decorator labels. --- src/ui/linkformview.js | 3 ++- tests/ui/linkformview.js | 55 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 164387f..2fb0a54 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -249,13 +249,14 @@ export default class LinkFormView extends View { */ _createCustomAttributesView() { const switches = this.createCollection(); + const t = this.locale.t; switches.bindTo( this.customAttributes ).using( item => { const switchButton = new SwitchButtonView( this.locale ); switchButton.set( { name: item.id, - label: item.label, + label: t( item.label ), withText: true } ); diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 50a1ba7..1ceeb28 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals Event */ +/* globals Event, document */ import LinkFormView from '../../src/ui/linkformview'; import View from '@ckeditor/ckeditor5-ui/src/view'; @@ -15,6 +15,9 @@ import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import ManualDecorator from '../../src/utils/manualdecorator'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; +import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Link from '../../src/link'; describe( 'LinkFormView', () => { let view; @@ -260,4 +263,54 @@ describe( 'LinkFormView', () => { expect( viewItem.isOn ).to.be.false; } ); } ); + + describe.only( 'localization of custom attributes', () => { + before( () => { + addTranslations( 'pl', { + 'Open in new window': 'Otwórz w nowym oknie' + } ); + } ); + after( () => { + clearTranslations(); + } ); + + let editor, editorElement, linkFormView; + + beforeEach( () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ Link ], + toolbar: [ 'link' ], + language: 'pl', + link: { + decorators: [ + { + mode: 'manual', + label: 'Open in new window', + attributes: { + target: '_blank' + } + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + linkFormView = new LinkFormView( editor.locale, editor.commands.get( 'link' ).customAttributes ); + } ); + } ); + + afterEach( () => { + editorElement.remove(); + + return editor.destroy(); + } ); + + it( 'translates labels of manual decorators UI', () => { + expect( linkFormView.customAttributesView.first.label ).to.equal( 'Otwórz w nowym oknie' ); + } ); + } ); } ); From 2f615f46f870957dc8db6c219a35fd54e8888dad Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 30 Apr 2019 12:58:37 +0200 Subject: [PATCH 34/81] Fix code typo. --- src/ui/linkformview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 2fb0a54..5323290 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -170,7 +170,7 @@ export default class LinkFormView extends View { this.urlInputView, ...this.customAttributesView, this.saveButtonView, - this.cancelButtonView, + this.cancelButtonView ]; childViews.forEach( v => { From 1cfb8173bb4d12de4233a2810065f8670a468bc7 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 14 May 2019 12:46:50 +0200 Subject: [PATCH 35/81] Add missing private keyword. --- src/ui/linkformview.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 5323290..4e07467 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -245,6 +245,7 @@ export default class LinkFormView extends View { * Prepare {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView} * made based on {@link #customAttributes} * + * @private * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. */ _createCustomAttributesView() { @@ -275,6 +276,7 @@ export default class LinkFormView extends View { * Creates {@link #children} for {@link module:link/ui/linkformview~LinkFormView}. If there exist {@link #customAttributes}, * Then additional View wrapping all {@link #customAttributesView} will be added as a child of LinkFormView. * + * @private * @returns {module:ui/viewcollection~ViewCollection} children of LinkFormView. */ _createFormChildren() { From 4e8a15ae5e73139da28106e0e6a5a3b23053eabd Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 14 May 2019 14:43:51 +0200 Subject: [PATCH 36/81] Fix unit tests for link. --- tests/ui/linkformview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 1ceeb28..ffd83c0 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -264,7 +264,7 @@ describe( 'LinkFormView', () => { } ); } ); - describe.only( 'localization of custom attributes', () => { + describe( 'localization of custom attributes', () => { before( () => { addTranslations( 'pl', { 'Open in new window': 'Otwórz w nowym oknie' From 514df54de8a63f6efba3339ef0a058a06972cfe4 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 14 May 2019 15:45:35 +0200 Subject: [PATCH 37/81] Replace div with unordered list, which better fit in this context. Correct css. --- src/ui/linkformview.js | 19 ++++++++++++++++--- tests/manual/linkdecorator.js | 3 +++ theme/linkform.css | 6 ++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 4e07467..c7623e7 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -289,10 +289,23 @@ export default class LinkFormView extends View { if ( this.customAttributes.length ) { const additionalButtonsView = new View(); additionalButtonsView.setTemplate( { - tag: 'div', - children: [ ...this.customAttributesView ], + tag: 'ul', + children: this.customAttributesView.map( switchButton => ( { + tag: 'li', + children: [ switchButton ], + attributes: { + class: [ + 'ck', + 'ck-list__item' + ] + } + } ) ), attributes: { - class: 'ck-link-form_additional-buttons' + class: [ + 'ck', + 'ck-reset', + 'ck-list' + ] } } ); children.add( additionalButtonsView ); diff --git a/tests/manual/linkdecorator.js b/tests/manual/linkdecorator.js index e654c9f..ab2a92a 100644 --- a/tests/manual/linkdecorator.js +++ b/tests/manual/linkdecorator.js @@ -13,6 +13,9 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import Clipboard from '@ckeditor/ckeditor5-clipboard/src/clipboard'; +// Just to have nicely styles switchbutton; +import '@ckeditor/ckeditor5-theme-lark/theme/ckeditor5-ui/components/list/list.css'; + ClassicEditor .create( document.querySelector( '#editor' ), { plugins: [ Link, Typing, Paragraph, Clipboard, Undo, Enter ], diff --git a/theme/linkform.css b/theme/linkform.css index 2b974bb..781f354 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -20,14 +20,12 @@ /* Buttons like `open in new window` with switch toggler. Added div is a temporary solution to make stronger specificity than `.ck.ck-link-form > :not(:first-child)` in theme-lark. */ - & div.ck-link-form_additional-buttons { - display: flex; - flex-wrap: wrap; + & ul.ck-list { margin-left: 0; width: 100%; } - & .ck-link-form_additional-buttons .ck-button { + & .ck-list .ck-switchbutton { width: 100%; margin-left: 0; margin-top: var(--ck-spacing-standard); From 0765e184f0820f5075225d57fd8d7a8b44e4ba15 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 29 May 2019 15:38:45 +0200 Subject: [PATCH 38/81] Fix unit test. --- tests/linkcommand.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/linkcommand.js b/tests/linkcommand.js index a49f8c9..1920994 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -287,7 +287,7 @@ describe( 'LinkCommand', () => { model.schema.extend( '$text', { allowIn: '$root', - allowAttributes: 'linkHref' + allowAttributes: [ 'linkHref', 'linkManualDecorator0', 'linkManualDecorator1' ] } ); model.schema.register( 'p', { inheritAllFrom: '$block' } ); From 83cf302239663a7384529f79d75f9fc76d5c2fd6 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 7 Jun 2019 16:39:26 +0200 Subject: [PATCH 39/81] Remove translation of a label. --- src/ui/linkformview.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index c7623e7..80779f6 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -250,14 +250,13 @@ export default class LinkFormView extends View { */ _createCustomAttributesView() { const switches = this.createCollection(); - const t = this.locale.t; switches.bindTo( this.customAttributes ).using( item => { const switchButton = new SwitchButtonView( this.locale ); switchButton.set( { name: item.id, - label: t( item.label ), + label: item.label, withText: true } ); From fd2682fd55a2e0cf9f2efb7db8af57b4af8f7510 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 7 Jun 2019 16:52:52 +0200 Subject: [PATCH 40/81] Update devDependencies --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index f6edfb7..7f3d071 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "@ckeditor/ckeditor5-ui": "^13.0.0" }, "devDependencies": { + "@ckeditor/ckeditor5-clipboard": "^11.0.2", "@ckeditor/ckeditor5-editor-classic": "^12.1.1", "@ckeditor/ckeditor5-enter": "^11.0.2", "@ckeditor/ckeditor5-paragraph": "^11.0.2", + "@ckeditor/ckeditor5-theme-lark": "^14.0.0", "@ckeditor/ckeditor5-typing": "^12.0.2", "@ckeditor/ckeditor5-undo": "^11.0.2", "@ckeditor/ckeditor5-utils": "^12.1.1", From 3c27957780f6eb7f76dbe387e5ec9bd6c3f84ea7 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 7 Jun 2019 17:02:44 +0200 Subject: [PATCH 41/81] Remove unit test, for wrongly translated label. --- tests/ui/linkformview.js | 55 +--------------------------------------- 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index ffd83c0..50a1ba7 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals Event, document */ +/* globals Event */ import LinkFormView from '../../src/ui/linkformview'; import View from '@ckeditor/ckeditor5-ui/src/view'; @@ -15,9 +15,6 @@ import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import ManualDecorator from '../../src/utils/manualdecorator'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; -import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; -import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; -import Link from '../../src/link'; describe( 'LinkFormView', () => { let view; @@ -263,54 +260,4 @@ describe( 'LinkFormView', () => { expect( viewItem.isOn ).to.be.false; } ); } ); - - describe( 'localization of custom attributes', () => { - before( () => { - addTranslations( 'pl', { - 'Open in new window': 'Otwórz w nowym oknie' - } ); - } ); - after( () => { - clearTranslations(); - } ); - - let editor, editorElement, linkFormView; - - beforeEach( () => { - editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - return ClassicTestEditor - .create( editorElement, { - plugins: [ Link ], - toolbar: [ 'link' ], - language: 'pl', - link: { - decorators: [ - { - mode: 'manual', - label: 'Open in new window', - attributes: { - target: '_blank' - } - } - ] - } - } ) - .then( newEditor => { - editor = newEditor; - linkFormView = new LinkFormView( editor.locale, editor.commands.get( 'link' ).customAttributes ); - } ); - } ); - - afterEach( () => { - editorElement.remove(); - - return editor.destroy(); - } ); - - it( 'translates labels of manual decorators UI', () => { - expect( linkFormView.customAttributesView.first.label ).to.equal( 'Otwórz w nowym oknie' ); - } ); - } ); } ); From 5e2b114991333718d3d98f67dea5a59543b70696 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 12 Jun 2019 16:46:18 +0200 Subject: [PATCH 42/81] Simple code and docs improvements related to requests from review. --- src/linkcommand.js | 13 +++++++------ src/linkediting.js | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index c89959f..dd662dc 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -31,7 +31,7 @@ export default class LinkCommand extends Command { /** * Keeps collection of {@link module:link/utils~ManualDecorator} - * recognized from {@link module:link/link~LinkConfig#decorators}. + * corresponding to {@link module:link/link~LinkConfig#decorators}. * You can consider it as a model of states for custom attributes added to links. * * @readonly @@ -79,13 +79,14 @@ export default class LinkCommand extends Command { // Stores information about custom attributes to turn on/off. const truthyCustomAttributes = []; const falsyCustomAttributes = []; - Object.entries( customAttrs ).forEach( entriesPair => { - if ( entriesPair[ 1 ] ) { - truthyCustomAttributes.push( entriesPair[ 0 ] ); + + for ( const name in customAttrs ) { + if ( customAttrs[ name ] ) { + truthyCustomAttributes.push( name ); } else { - falsyCustomAttributes.push( entriesPair[ 0 ] ); + falsyCustomAttributes.push( name ); } - } ); + } model.change( writer => { // If selection is collapsed then update selected link or insert new one at the place of caret. diff --git a/src/linkediting.js b/src/linkediting.js index 874d462..9761347 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -18,8 +18,9 @@ import findLinkRange from './findlinkrange'; import '../theme/link.css'; const HIGHLIGHT_CLASS = 'ck-link_selected'; -const AUTO = 'automatic'; -const MANUAL = 'manual'; +const DECORATOR_AUTOMATIC = 'automatic'; +const DECORATOR_MANUAL = 'manual'; +const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//; /** * The link engine feature. @@ -78,8 +79,8 @@ export default class LinkEditing extends Plugin { const linkDecorators = editor.config.get( 'link.decorators' ) || []; - this.enableAutomaticDecorators( linkDecorators.filter( item => item.mode === AUTO ) ); - this.enableManualDecorators( linkDecorators.filter( item => item.mode === MANUAL ) ); + this.enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) ); + this.enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) ); // Enable two-step caret movement for `linkHref` attribute. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' ); @@ -106,9 +107,8 @@ export default class LinkEditing extends Plugin { // Adds default decorator for external links. if ( editor.config.get( 'link.targetDecorator' ) ) { automaticDecorators.add( { - mode: AUTO, + mode: DECORATOR_AUTOMATIC, callback: url => { - const EXTERNAL_LINKS_REGEXP = /^(https?:)?\/\//; return EXTERNAL_LINKS_REGEXP.test( url ); }, attributes: { From 21a0619f7f9971fbc2646cd5a3f79eca08b317da Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 13 Jun 2019 11:30:16 +0200 Subject: [PATCH 43/81] Rename custom attribtues to manualdecorators, to keep consistency. Fix test related to that and docs links. --- src/linkcommand.js | 40 ++++++++++++++++++++++------------------ src/linkediting.js | 19 +++++++++++-------- src/linkui.js | 10 +++++----- src/ui/linkformview.js | 32 ++++++++++++++++---------------- src/unlinkcommand.js | 2 +- tests/linkcommand.js | 4 ++-- tests/linkui.js | 4 ++-- tests/ui/linkformview.js | 10 +++++----- 8 files changed, 64 insertions(+), 57 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index dd662dc..7ba0301 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -32,12 +32,12 @@ export default class LinkCommand extends Command { /** * Keeps collection of {@link module:link/utils~ManualDecorator} * corresponding to {@link module:link/link~LinkConfig#decorators}. - * You can consider it as a model of states for custom attributes added to links. + * You can consider it as a model with states of manual decorators added to currently selected link. * * @readonly * @type {module:utils/collection~Collection} */ - this.customAttributes = new Collection(); + this.manualDecorators = new Collection(); } /** @@ -49,8 +49,8 @@ export default class LinkCommand extends Command { this.value = doc.selection.getAttribute( 'linkHref' ); - for ( const customAttr of this.customAttributes ) { - customAttr.value = doc.selection.getAttribute( customAttr.id ) || false; + for ( const manualDecorator of this.manualDecorators ) { + manualDecorator.value = doc.selection.getAttribute( manualDecorator.id ) || false; } this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'linkHref' ); @@ -71,20 +71,20 @@ export default class LinkCommand extends Command { * * @fires execute * @param {String} href Link destination. + * @param {Object} [manualDecorators={}] Keeps information about turned on and off manual decorators applied with command. */ - execute( href, customAttrs = {} ) { + execute( href, manualDecorators = {} ) { const model = this.editor.model; const selection = model.document.selection; + // Stores information about manual decorators to turn them on/off when command is applied. + const truthyManualDecorators = []; + const falsyManualDecorators = []; - // Stores information about custom attributes to turn on/off. - const truthyCustomAttributes = []; - const falsyCustomAttributes = []; - - for ( const name in customAttrs ) { - if ( customAttrs[ name ] ) { - truthyCustomAttributes.push( name ); + for ( const name in manualDecorators ) { + if ( manualDecorators[ name ] ) { + truthyManualDecorators.push( name ); } else { - falsyCustomAttributes.push( name ); + falsyManualDecorators.push( name ); } } @@ -99,10 +99,12 @@ export default class LinkCommand extends Command { const linkRange = findLinkRange( position, selection.getAttribute( 'linkHref' ), model ); writer.setAttribute( 'linkHref', href, linkRange ); - truthyCustomAttributes.forEach( item => { + + truthyManualDecorators.forEach( item => { writer.setAttribute( item, true, linkRange ); } ); - falsyCustomAttributes.forEach( item => { + + falsyManualDecorators.forEach( item => { writer.removeAttribute( item, linkRange ); } ); @@ -117,7 +119,7 @@ export default class LinkCommand extends Command { attributes.set( 'linkHref', href ); - truthyCustomAttributes.forEach( item => { + truthyManualDecorators.forEach( item => { attributes.set( item, true ); } ); @@ -135,10 +137,12 @@ export default class LinkCommand extends Command { for ( const range of ranges ) { writer.setAttribute( 'linkHref', href, range ); - truthyCustomAttributes.forEach( item => { + + truthyManualDecorators.forEach( item => { writer.setAttribute( item, true, range ); } ); - falsyCustomAttributes.forEach( item => { + + falsyManualDecorators.forEach( item => { writer.removeAttribute( item, range ); } ); } diff --git a/src/linkediting.js b/src/linkediting.js index 9761347..6e3268c 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -79,8 +79,8 @@ export default class LinkEditing extends Plugin { const linkDecorators = editor.config.get( 'link.decorators' ) || []; - this.enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) ); - this.enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) ); + this._enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) ); + this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) ); // Enable two-step caret movement for `linkHref` attribute. bindTwoStepCaretToAttribute( editor.editing.view, editor.model, this, 'linkHref' ); @@ -101,7 +101,7 @@ export default class LinkEditing extends Plugin { * @private * @param {Array.} automaticDecoratorDefinitions */ - enableAutomaticDecorators( automaticDecoratorDefinitions ) { + _enableAutomaticDecorators( automaticDecoratorDefinitions ) { const editor = this.editor; const automaticDecorators = new AutomaticDecorators(); // Adds default decorator for external links. @@ -127,7 +127,7 @@ export default class LinkEditing extends Plugin { /** * Method process {@link module:link/link~LinkDecoratorManualOption} by transformation those configuration options into * {@link module:link/utils~ManualDecorator}. Manual decorators are added to - * {@link module:link/linkcommand~LinkCommand#customAttributes} collections, which might be considered as a model + * {@link module:link/linkcommand~LinkCommand#manualDecorators} collections, which might be considered as a model * for manual decorators state. It also provides proper * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} converter for each * manual decorator and extends {@link module:engine/model/schema~Schema model's schema} with adequate model attributes. @@ -135,27 +135,30 @@ export default class LinkEditing extends Plugin { * @private * @param {Array.} manualDecoratorDefinitions */ - enableManualDecorators( manualDecoratorDefinitions ) { + _enableManualDecorators( manualDecoratorDefinitions ) { if ( !manualDecoratorDefinitions.length ) { return; } const editor = this.editor; const command = editor.commands.get( 'link' ); - const attrCollection = command.customAttributes; + const manualDecorators = command.manualDecorators; manualDecoratorDefinitions.forEach( ( decorator, index ) => { const decoratorName = `linkManualDecorator${ index }`; + editor.model.schema.extend( '$text', { allowAttributes: decoratorName } ); - attrCollection.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); + // Keeps reference to manual decorator to decode its name to attributes during downcast. + manualDecorators.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); + editor.conversion.for( 'downcast' ).attributeToElement( { model: decoratorName, view: ( manualDecoratorName, writer ) => { if ( manualDecoratorName ) { const element = writer.createAttributeElement( 'a', - attrCollection.get( decoratorName ).attributes, + manualDecorators.get( decoratorName ).attributes, { priority: 5 } diff --git a/src/linkui.js b/src/linkui.js index c71faa9..7b2eb95 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -144,7 +144,7 @@ export default class LinkUI extends Plugin { const editor = this.editor; const linkCommand = editor.commands.get( 'link' ); - const formView = new LinkFormView( editor.locale, linkCommand.customAttributes ); + const formView = new LinkFormView( editor.locale, linkCommand.manualDecorators ); formView.urlInputView.bind( 'value' ).to( linkCommand, 'value' ); @@ -154,13 +154,13 @@ export default class LinkUI extends Plugin { // Execute link command after clicking the "Save" button. this.listenTo( formView, 'submit', () => { - const customAttributes = {}; + const manualDecorators = {}; - for ( const switchButton of formView.customAttributesView ) { - customAttributes[ switchButton.name ] = switchButton.isOn; + for ( const switchButton of formView.manualDecoratorsUIView ) { + manualDecorators[ switchButton.name ] = switchButton.isOn; } - editor.execute( 'link', formView.urlInputView.inputView.element.value, customAttributes ); + editor.execute( 'link', formView.urlInputView.inputView.element.value, manualDecorators ); this._closeFormView(); } ); diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 80779f6..f6308a4 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -38,10 +38,10 @@ export default class LinkFormView extends View { * Also see {@link #render}. * * @param {module:utils/locale~Locale} [locale] The localization services instance. - * @param {module:utils/collection~Collection} [customAttributes] Reference to custom attributes in - * {@link module:link/linkcommand~LinkCommand#customAttributes}. + * @param {module:utils/collection~Collection} [manualDecorators] Reference to custom attributes in + * {@link module:link/linkcommand~LinkCommand#manualDecorators}. */ - constructor( locale, customAttributes ) { + constructor( locale, manualDecorators ) { super( locale ); const t = locale.t; @@ -85,21 +85,21 @@ export default class LinkFormView extends View { this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' ); /** - * Keeps reference to {@link module:link/linkcommand~LinkCommand#customAttributes}. + * Keeps reference to {@link module:link/linkcommand~LinkCommand#manualDecorators}. * * @readonly * @type {model:utils/collection~Collection} */ - this.customAttributes = customAttributes; + this.manualDecorators = manualDecorators; /** - * Keeps reference to {@link module:ui/button/switchbuttonview~SwitchButtonView} made based on {@link #customAttributes}. - * It use {@link #_createCustomAttributesView} to generate proper collection. + * Keeps reference to {@link module:ui/button/switchbuttonview~SwitchButtonView} made based on {@link #manualDecorators}. + * It use {@link #_createManualDecoratorsUIView} to generate proper collection. * * @readonly * @type {module:ui/viewcollection~ViewCollection} */ - this.customAttributesView = this._createCustomAttributesView(); + this.manualDecoratorsUIView = this._createManualDecoratorsUIView(); /** * Collection of views used as children elements in {@link module:link/ui/linkformview~LinkFormView}. @@ -168,7 +168,7 @@ export default class LinkFormView extends View { // Focus order should be different than position in DOM. Save/Cancel buttons should be focused at the end. const childViews = [ this.urlInputView, - ...this.customAttributesView, + ...this.manualDecoratorsUIView, this.saveButtonView, this.cancelButtonView ]; @@ -243,15 +243,15 @@ export default class LinkFormView extends View { /** * Prepare {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView} - * made based on {@link #customAttributes} + * made based on {@link #manualDecorators} * * @private * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. */ - _createCustomAttributesView() { + _createManualDecoratorsUIView() { const switches = this.createCollection(); - switches.bindTo( this.customAttributes ).using( item => { + switches.bindTo( this.manualDecorators ).using( item => { const switchButton = new SwitchButtonView( this.locale ); switchButton.set( { @@ -272,8 +272,8 @@ export default class LinkFormView extends View { } /** - * Creates {@link #children} for {@link module:link/ui/linkformview~LinkFormView}. If there exist {@link #customAttributes}, - * Then additional View wrapping all {@link #customAttributesView} will be added as a child of LinkFormView. + * Creates {@link #children} for {@link module:link/ui/linkformview~LinkFormView}. If there exist {@link #manualDecorators}, + * Then additional View wrapping all {@link #manualDecoratorsUIView} will be added as a child of LinkFormView. * * @private * @returns {module:ui/viewcollection~ViewCollection} children of LinkFormView. @@ -285,11 +285,11 @@ export default class LinkFormView extends View { children.add( this.saveButtonView ); children.add( this.cancelButtonView ); - if ( this.customAttributes.length ) { + if ( this.manualDecorators.length ) { const additionalButtonsView = new View(); additionalButtonsView.setTemplate( { tag: 'ul', - children: this.customAttributesView.map( switchButton => ( { + children: this.manualDecoratorsUIView.map( switchButton => ( { tag: 'li', children: [ switchButton ], attributes: { diff --git a/src/unlinkcommand.js b/src/unlinkcommand.js index 372e5b1..5e0e8f6 100644 --- a/src/unlinkcommand.js +++ b/src/unlinkcommand.js @@ -47,7 +47,7 @@ export default class UnlinkCommand extends Command { writer.removeAttribute( 'linkHref', range ); // If there are registered custom attributes, then remove them during unlink. if ( linkCommand ) { - for ( const manualDecorator of linkCommand.customAttributes ) { + for ( const manualDecorator of linkCommand.manualDecorators ) { writer.removeAttribute( manualDecorator.id, range ); } } diff --git a/tests/linkcommand.js b/tests/linkcommand.js index 1920994..3def477 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -270,14 +270,14 @@ describe( 'LinkCommand', () => { model = editor.model; command = new LinkCommand( editor ); - command.customAttributes.add( new ManualDecorator( { + command.manualDecorators.add( new ManualDecorator( { id: 'linkManualDecorator0', label: 'Foo', attributes: { class: 'Foo' } } ) ); - command.customAttributes.add( new ManualDecorator( { + command.manualDecorators.add( new ManualDecorator( { id: 'linkManualDecorator1', label: 'Bar', attributes: { diff --git a/tests/linkui.js b/tests/linkui.js index ffecff3..66c7616 100644 --- a/tests/linkui.js +++ b/tests/linkui.js @@ -1004,8 +1004,8 @@ describe( 'LinkUI', () => { setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' ); - expect( formView.customAttributesView.length ).to.equal( 1 ); - expect( formView.customAttributesView.first.isOn ).to.be.true; + expect( formView.manualDecoratorsUIView.length ).to.equal( 1 ); + expect( formView.manualDecoratorsUIView.first.isOn ).to.be.true; formView.fire( 'submit' ); diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 50a1ba7..56b83a5 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -226,17 +226,17 @@ describe( 'LinkFormView', () => { collection.clear(); } ); it( 'switch buttons reflects state of customAttributes', () => { - expect( view.customAttributesView.length ).to.equal( 3 ); + expect( view.manualDecoratorsUIView.length ).to.equal( 3 ); - expect( view.customAttributesView.get( 0 ) ).to.deep.include( { + expect( view.manualDecoratorsUIView.get( 0 ) ).to.deep.include( { name: 'decorator1', label: 'Foo' } ); - expect( view.customAttributesView.get( 1 ) ).to.deep.include( { + expect( view.manualDecoratorsUIView.get( 1 ) ).to.deep.include( { name: 'decorator2', label: 'Download' } ); - expect( view.customAttributesView.get( 2 ) ).to.deep.include( { + expect( view.manualDecoratorsUIView.get( 2 ) ).to.deep.include( { name: 'decorator3', label: 'Multi' } ); @@ -244,7 +244,7 @@ describe( 'LinkFormView', () => { it( 'reacts on switch button changes', () => { const modelItem = collection.first; - const viewItem = view.customAttributesView.first; + const viewItem = view.manualDecoratorsUIView.first; expect( modelItem.value ).to.be.undefined; expect( viewItem.isOn ).to.be.undefined; From 4e35fdb80bf6c52abd63e881de29f39fccc3a74c Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 13 Jun 2019 12:53:27 +0200 Subject: [PATCH 44/81] Add additional class to form when manual decoratos are avialable in the editor. --- src/ui/linkformview.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index f6308a4..3e290e8 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -138,14 +138,17 @@ export default class LinkFormView extends View { } } ); + const classList = [ 'ck', 'ck-link-form' ]; + + if ( this.manualDecorators.length ) { + classList.push( 'ck-link-form_decorators' ); + } + this.setTemplate( { tag: 'form', attributes: { - class: [ - 'ck', - 'ck-link-form', - ], + class: classList, // https://github.com/ckeditor/ckeditor5-link/issues/90 tabindex: '-1' From 7ce2c170bd3f08fbfa7ebe8e2b70c4c02b6c8383 Mon Sep 17 00:00:00 2001 From: dkonopka Date: Thu, 13 Jun 2019 13:45:44 +0200 Subject: [PATCH 45/81] Redesigned link form view with manual decorators. --- theme/linkform.css | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/theme/linkform.css b/theme/linkform.css index 781f354..24c99a7 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -5,14 +5,8 @@ @import "@ckeditor/ckeditor5-ui/theme/mixins/_rwd.css"; -:root { - --ck-link-form-max-width: 330px; -} - .ck.ck-link-form { display: flex; - flex-wrap: wrap; - max-width: var(--ck-link-form-max-width); & .ck-label { display: none; @@ -43,3 +37,27 @@ } } } + +/* Style link form differently when manual decoratos are available. + * See: https://github.com/ckeditor/ckeditor5-link/issues/186. + */ +.ck.ck-link-form_decorators { + flex-wrap: wrap; + + & .ck-labeled-input { + flex-basis: 100%; + } + + + & .ck-button { + /* Let's move "Save" & "Cancel" buttons to the end of the link form view. */ + order: 10; + + flex-basis: 50%; + } + + & .ck-list .ck-switchbutton { + margin-top: 0; + } +} + From a8c90aeb52f63162e7d91a62b1b7521412ce14fa Mon Sep 17 00:00:00 2001 From: dkonopka Date: Thu, 13 Jun 2019 14:01:37 +0200 Subject: [PATCH 46/81] Removed obsolete styles. --- theme/linkform.css | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/theme/linkform.css b/theme/linkform.css index 24c99a7..a7fd590 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -12,19 +12,6 @@ display: none; } - /* Buttons like `open in new window` with switch toggler. - Added div is a temporary solution to make stronger specificity than `.ck.ck-link-form > :not(:first-child)` in theme-lark. */ - & ul.ck-list { - margin-left: 0; - width: 100%; - } - - & .ck-list .ck-switchbutton { - width: 100%; - margin-left: 0; - margin-top: var(--ck-spacing-standard); - } - @mixin ck-media-phone { flex-wrap: wrap; @@ -56,8 +43,8 @@ flex-basis: 50%; } - & .ck-list .ck-switchbutton { - margin-top: 0; + & .ck-list { + flex-basis: 100%; } } From 3f2900c9c560b020c37e787d895577767c144ad2 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Thu, 13 Jun 2019 16:56:34 +0200 Subject: [PATCH 47/81] Improve docs description of decorators. --- src/link.js | 141 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 55 deletions(-) diff --git a/src/link.js b/src/link.js index 91ce872..bf7f4d1 100644 --- a/src/link.js +++ b/src/link.js @@ -58,57 +58,83 @@ export default class Link extends Plugin { */ /** - * Configuration of the {@link module:link/link~Link} feature. If set to `true`, - * then default 'automatic' decorator is added to the link. + * Target decorator option solves one of the most popular cases, which is adding automatically `target` attribute to all external links. + * It activates predefined {@link module:link/link~LinkDecoratorAutomaticOption automatic decorator}, which decorates all + * external links with `target="blank"` and `rel="noopener noreferrer"` attributes, so there is no need to invent own decorator + * for such case. As external links are recognized those which starts with: `http`, `https` or `//`. + * There remains the possibility to add other {@link module:link/link~LinkDecoratorAutomaticOption automatic} + * or {@link module:link/link~LinkDecoratorManualOption manual} decorators. + * + * When there is need to apply target attribute manually, then {@link module:link/link~LinkDecoratorManualOption manual} decorator should + * be provided with the {@link module:link/link~LinkConfig link configuration} in {@link module:link/link~LinkConfig#decorators} array. + * In this scenario, `targetDecorator` option should remain `undefined` or `false` to not interfere with a created decorator. + * + * More information about decorators might be found in {@link module:link/link~LinkConfig#decorators}. * * @member {Boolean} module:link/link~LinkConfig#targetDecorator */ /** - * Custom link decorators. - * - * **Warning** Currently there is no integration between 'automatic' and 'manual' decorators, - * which transforms the same attribute. For example, configuring `target` attribute through both - * 'automatic' and 'manual' decorator might result with quirk behavior. - * - * Decorators provides: - * * simple automatic rules based on url address to apply customized and predefined additional attributes. - * * manual rules, which adds UI checkbox, where user can simply trigger predefined attributes for given link. - * - * - * ```js - * const link.decorators = [ - * { - * mode: 'automatic', - * callback: url => url.startsWith( 'http://' ), - * attributes: { - * target: '_blank', - * rel: 'noopener noreferrer' + * Decorators are {@link module:link/link~Link link's} plugin feature which can extend anchor with additional attributes. + * Decorators provide an easy way to configure and manage those attributes automatically or manually with the UI. + * + * For example, there is a quite common topic to add the `target="_blank"` attribute to only some of the links in the editor. + * Decorators help in mentioned case with either: added automatic rules based on link's href (URL), + * or added a toggleable UI switch for the user. + * + * **Warning:** Currently, there is no integration in-between decorators for any mix of decorators' types. + * For example, configuring `target` attribute through both 'automatic' and 'manual' decorators might result with quirk behavior + * as well as defining 2 manual or 2 automatic decorators for the same attribute. + * + * # Automatic decorators + * This type of decorators takes an object with key-value pairs of attributes and + * a {@link module:link/link~LinkDecoratorAutomaticOption callback} function. The function has to return boolean value based on link's href. + * If a given set of attributes should be applied to the link, then callback has to return the `true` value. + * For example, if there is a need to add the `target="_blank"` attribute to all links in the editor started with the `http://`, + * then configuration might look like this: + * + * const link.decorators = [ + * { + * mode: 'automatic', + * callback: url => url.startsWith( 'http://' ), + * attributes: { + * target: '_blank' + * } * } - * }, - * { - * mode: 'manual', - * label: 'Downloadable', - * attributes: { - * download: 'download' + * ] + * + * **Please notice:** As configuring target attribute for external links is a quite common situation, + * there is predefined automatic decorator, which might be turned on with even simpler option, + * just by setting {@link #targetDecorator} to `true`. More information might be found in the {@link #targetDecorator} description. + * + * # Manual decorators + * This type of decorators also takes an object with key-value pair of attributes, however, those are applied based on user choice. + * Manual decorator is defined with a {@link module:link/link~LinkDecoratorManualOption label}, + * which describes the given option for the user. Manual decorators are possible to toggle for the user in editing view of the link plugin. + * For example, if there is a need to give user full control over this which links should be opened in a new window, + * then configuration might looks as followed: + * + * const link.decorators = [ + * { + * mode: 'manual', + * label: 'Open in new window', + * attributes: { + * target: '_blank' + * } * } - * } - * ] - * ``` + * ] + * * @member {Array.} * module:link/link~LinkConfig#decorators */ /** - * This object defining automatic decorator for the links. Based on this option data pipeline will extend links with proper attributes. - * For example, you can define rules, when attribute `target="_blank"` will be added to links. - * Please notice that, there is a default option which can be activated with {@link module:link/link~LinkConfig#targetDecorator}, - * which automatically adds attributes: - * * `target="_blank"` - * * `rel="noopener noreferrer"` - * for all links started with: `http://`, `https://` or `//`. - * - *```js + * This object describes automatic {@link module:link/link~LinkConfig#decorators} for the links. Based on this option, + * output data will extend links with proper attributes. + * + * For example, if there is need to define a rule that automatically adds attribute `target="_blank"` and `rel="noopener noreferrer"` + * to the external links, then automatic decorator might looks as follows: + * * { * mode: 'automatic', * callback: url => /^(https?:)?\/\//.test( url ), @@ -117,24 +143,28 @@ export default class Link extends Plugin { * rel: 'noopener noreferrer' * } * } - *``` + * + * **Please notice**, there is a {@link module:link/link~LinkConfig#targetDecorator configuration option}, + * which automatically adds attributes: `target="_blank"` and `rel="noopener noreferrer"` for all links started with: + * `http://`, `https://` or `//`. * * @typedef {Object} module:link/link~LinkDecoratorAutomaticOption - * @property {'automatic'} mode it should has always string value 'automatic' for automatic decorators - * @property {Function} callback takes `url` as parameter and should return `true` - * for urls that be decorate with this decorator. - * @property {Object} attributes key-value pairs used as attributes added to anchor during downcasting. - * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. + * @property {'automatic'} mode should have string value equal 'automatic' for automatic decorators + * @property {Function} callback takes an `url` as a parameter and returns `true` + * for urls where given attributes should be applied. + * @property {Object} attributes key-value pairs used as attributes added to output data during + * {@glink framework/guides/architecture/editing-engine#conversion downcasting}. + * Attributes should have form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. */ /** - * This object defining manual decorator for the links. Based on this options, there will be added UI switches to link balloon. - * User will be able to activate pre-configured attributes for the link by simple switch button change. + * This object describes manual {@link module:link/link~LinkConfig#decorators} for the links. Based on this definition, + * there is added switch button in editing form for the link. This button poses label define here. After toggling and confirming it, + * preconfigured attributes are added to a selected link. * - * For example, you can define decorator, which show up switch responsible for adding attributes `target="_blank"` and - * `rel="noopener noreferrer"` when user select proper options in UI. + * For example, if there is need to define a rule, which adds a switch button to apply `target="_blank"` and + * `rel="noopener noreferrer"`, then manual decorator might be helpful and can look as follows: * - *```js * { * mode: 'manual', * label: 'Open link in new window', @@ -143,11 +173,12 @@ export default class Link extends Plugin { * rel: 'noopener noreferrer' * } * } - *``` * * @typedef {Object} module:link/link~LinkDecoratorManualOption - * @property {'manual'} mode it should has always string value 'manual' for manual decorators - * @property {String} label the label for ui switch, which will be responsible for activation of given attributes set - * @property {Object} attributes key-value pairs used as attributes added to anchor during downcasting. - * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. + * @property {'manual'} mode should have string value equal 'manual' for manual decorators + * @property {String} label the label for the UI switch button, which will be responsible for applying defined attributes + * to a currently edited link. + * @property {Object} attributes attributes key-value pairs used as attributes added to output data during + * {@glink framework/guides/architecture/editing-engine#conversion downcasting}. + * Attributes should have form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. */ From a2dfddf781eaf1b9b6a4fdd99a69b1202507ca90 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 14 Jun 2019 15:28:00 +0200 Subject: [PATCH 48/81] Another set of docs improvements with decorators. --- src/linkcommand.js | 16 ++++++++++++---- src/linkediting.js | 22 ++++++++++------------ src/ui/linkformview.js | 9 ++++++--- src/utils/automaticdecorators.js | 11 ++++++----- src/utils/manualdecorator.js | 18 ++++++++++-------- 5 files changed, 44 insertions(+), 32 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index 7ba0301..684dc05 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -69,19 +69,27 @@ export default class LinkCommand extends Command { * * When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated. * + * # Decorators + * Command has an optional second parameter, which can apply or remove {@link module:link/utils~ManualDecorator} together with href. + * Decorators are representing in a corresponding way as href, which are attributes for {@link module:engine/model/text~Text Text node}. + * Names of those attributes are stored as `id`s in {@link module:link/utils~ManualDecorator} and preserve convention of name: + * `linkManualDecoratorN`, where `N` is replaced with a number (for example: `'linkManualDecorator0'`). + * + * More about decorators might be found in {@link module:link/link~LinkConfig#decorators} + * * @fires execute * @param {String} href Link destination. - * @param {Object} [manualDecorators={}] Keeps information about turned on and off manual decorators applied with command. + * @param {Object} [manualDecoratorIds={}] Keeps information about turned on and off manual decorators applied with command. */ - execute( href, manualDecorators = {} ) { + execute( href, manualDecoratorIds = {} ) { const model = this.editor.model; const selection = model.document.selection; // Stores information about manual decorators to turn them on/off when command is applied. const truthyManualDecorators = []; const falsyManualDecorators = []; - for ( const name in manualDecorators ) { - if ( manualDecorators[ name ] ) { + for ( const name in manualDecoratorIds ) { + if ( manualDecoratorIds[ name ] ) { truthyManualDecorators.push( name ); } else { falsyManualDecorators.push( name ); diff --git a/src/linkediting.js b/src/linkediting.js index 6e3268c..6757cee 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -90,13 +90,11 @@ export default class LinkEditing extends Plugin { } /** - * Method process {@link module:link/link~LinkDecoratorAutomaticOption} by creating instance of - * {@link module:link/utils~AutomaticDecorators}. If there are available automatic decorators, then - * there is registered {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher} to handle - * those configurations. + * Method process array of {@link module:link/link~LinkDecoratorAutomaticOption} and register + * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher} for them. + * Downcast dispatcher is obtained with {@link module:link/utils~AutomaticDecorators#getDispatcher}. * - * Please notice, that automatic decorator will be also added, when {@link module:link/link~LinkConfig#targetDecorator} - * will be set to `true`. + * Method also kept configuration of decorator activated with {@link module:link/link~LinkConfig#targetDecorator}. * * @private * @param {Array.} automaticDecoratorDefinitions @@ -125,12 +123,12 @@ export default class LinkEditing extends Plugin { } /** - * Method process {@link module:link/link~LinkDecoratorManualOption} by transformation those configuration options into - * {@link module:link/utils~ManualDecorator}. Manual decorators are added to - * {@link module:link/linkcommand~LinkCommand#manualDecorators} collections, which might be considered as a model - * for manual decorators state. It also provides proper - * {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} converter for each - * manual decorator and extends {@link module:engine/model/schema~Schema model's schema} with adequate model attributes. + * Method process array of {@link module:link/link~LinkDecoratorManualOption} by transformation those configuration options into + * {@link module:link/utils~ManualDecorator} objects. Then those objects are added to + * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection, which is considered as a model for manual decorators state. + * It also provides a proper {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} + * converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema} + * with adequate model attributes. * * @private * @param {Array.} manualDecoratorDefinitions diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 3e290e8..8db4bef 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -85,7 +85,9 @@ export default class LinkFormView extends View { this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' ); /** - * Keeps reference to {@link module:link/linkcommand~LinkCommand#manualDecorators}. + * Keeps reference to {@link module:link/linkcommand~LinkCommand#manualDecorators} in link command. + * Collection is used to build proper UI element and synchronize its state between model located in + * {@link module:link/linkcommand~LinkCommand#manualDecorators link command} and the {@link #manualDecoratorsUIView UI}. * * @readonly * @type {model:utils/collection~Collection} @@ -93,8 +95,9 @@ export default class LinkFormView extends View { this.manualDecorators = manualDecorators; /** - * Keeps reference to {@link module:ui/button/switchbuttonview~SwitchButtonView} made based on {@link #manualDecorators}. - * It use {@link #_createManualDecoratorsUIView} to generate proper collection. + * Preserves collection of {@link module:ui/button/switchbuttonview~SwitchButtonView}, + * which are made based on {@link #manualDecorators}. It use {@link #_createManualDecoratorsUIView} method + * to generate proper collection. * * @readonly * @type {module:ui/viewcollection~ViewCollection} diff --git a/src/utils/automaticdecorators.js b/src/utils/automaticdecorators.js index c34f027..8b9c783 100644 --- a/src/utils/automaticdecorators.js +++ b/src/utils/automaticdecorators.js @@ -8,13 +8,14 @@ */ /** - * Helper class which stores information about automatic decorators for link plugin - * and provides dispatcher which applies all of them to the view. + * Helper class which tight together all {@link module:link/link~LinkDecoratorAutomaticOption} and provides + * a {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement downcast dispatcher} for them. */ export default class AutomaticDecorators { constructor() { /** - * Stores definition of automatic decorators. Based on those values proper conversion has happens. + * Stores definition of {@link module:link/link~LinkDecoratorAutomaticOption automatic decorators}. + * Those data are used as source for a downcast dispatcher to make proper conversion to output data. * * @private * @type {Set} @@ -34,7 +35,7 @@ export default class AutomaticDecorators { } /** - * Add item or array of items with automatic rules for applying decorators to link plugin. + * Add automatic decorator objects or array with them to be used during downcasting. * * @param {module:link/link~LinkDecoratorAutomaticOption|Array.} item * configuration object of automatic rules for decorating links. It might be also array of such objects. @@ -48,7 +49,7 @@ export default class AutomaticDecorators { } /** - * Gets the conversion helper used in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method. + * Provides the conversion helper used in an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} method. * * @returns {Function} dispatcher function used as conversion helper * in {@link module:engine/conversion/downcasthelpers~DowncastHelpers#add} diff --git a/src/utils/manualdecorator.js b/src/utils/manualdecorator.js index f666ffc..62e344b 100644 --- a/src/utils/manualdecorator.js +++ b/src/utils/manualdecorator.js @@ -11,8 +11,9 @@ import ObservableMixin from '@ckeditor/ckeditor5-utils/src/observablemixin'; import mix from '@ckeditor/ckeditor5-utils/src/mix'; /** - * Class which stores manual decorators with observable {@link module:link/utils~ManualDecorator#value} - * to handle integration with ui state. + * Helper class which stores manual decorators with observable {@link module:link/utils~ManualDecorator#value} + * to support integration with the UI state. An instance of this class is a model with state of single manual decorators. + * These decorators are kept as collections in {@link module:link/linkcommand~LinkCommand#manualDecorators}. * * @mixes module:utils/observablemixin~ObservableMixin */ @@ -21,14 +22,15 @@ export default class ManualDecorator { * Creates new instance of {@link module:link/utils~ManualDecorator}. * * @param {Object} config - * @param {String} config.id Manual decorator id, which is a name of attribute in model, for example 'linkManualDecorator0'. - * @param {String} config.label The label used in user interface to switch manual decorator. - * @param {Object} config.attributes Set of attributes added to downcasted data, when decorator is activated for specific link. - * Attributes should be added in a form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. + * @param {String} config.id name of attribute used in model, which represents given manual decorator. + * For example 'linkManualDecorator0'. + * @param {String} config.label The label used in user interface to toggle manual decorator. + * @param {Object} config.attributes Set of attributes added to output data, when decorator is active for specific link. + * Attributes should keep format of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. */ constructor( { id, label, attributes } ) { /** - * Manual decorator id, which is a name of attribute in model, for example 'linkManualDecorator0'. + * Id of manual decorator, which is a name of attribute in model, for example 'linkManualDecorator0'. * * @type {String} */ @@ -43,7 +45,7 @@ export default class ManualDecorator { this.set( 'value' ); /** - * The label used in user interface to switch manual decorator. + * The label used in user interface to toggle manual decorator. * * @type {String} */ From 53bcb89882de6e5cd57fba6c9b7109a32fe204de Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Fri, 14 Jun 2019 15:55:51 +0200 Subject: [PATCH 49/81] Rearrange order of elements in ui to be coherent with focus order. --- src/ui/linkformview.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 8db4bef..6e49ae6 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -171,7 +171,6 @@ export default class LinkFormView extends View { view: this } ); - // Focus order should be different than position in DOM. Save/Cancel buttons should be focused at the end. const childViews = [ this.urlInputView, ...this.manualDecoratorsUIView, @@ -288,8 +287,6 @@ export default class LinkFormView extends View { const children = this.createCollection(); children.add( this.urlInputView ); - children.add( this.saveButtonView ); - children.add( this.cancelButtonView ); if ( this.manualDecorators.length ) { const additionalButtonsView = new View(); @@ -316,6 +313,9 @@ export default class LinkFormView extends View { children.add( additionalButtonsView ); } + children.add( this.saveButtonView ); + children.add( this.cancelButtonView ); + return children; } } From 1dd2a54a8496b6f1be0472231f2bddc60d4e7cd3 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 17 Jun 2019 14:52:43 +0200 Subject: [PATCH 50/81] Docs: Improved the API documentation of the link decorators feature. --- src/link.js | 209 ++++++++++++++++++++++++++++------------------------ 1 file changed, 111 insertions(+), 98 deletions(-) diff --git a/src/link.js b/src/link.js index bf7f4d1..0e73caa 100644 --- a/src/link.js +++ b/src/link.js @@ -58,127 +58,140 @@ export default class Link extends Plugin { */ /** - * Target decorator option solves one of the most popular cases, which is adding automatically `target` attribute to all external links. - * It activates predefined {@link module:link/link~LinkDecoratorAutomaticOption automatic decorator}, which decorates all - * external links with `target="blank"` and `rel="noopener noreferrer"` attributes, so there is no need to invent own decorator - * for such case. As external links are recognized those which starts with: `http`, `https` or `//`. - * There remains the possibility to add other {@link module:link/link~LinkDecoratorAutomaticOption automatic} - * or {@link module:link/link~LinkDecoratorManualOption manual} decorators. + * When set `true`, the `target="blank"` and `rel="noopener noreferrer"` attributes are automatically added to all external links + * in the editor. By external are meant all links in the editor content starting with `http`, `https`, or `//`. * - * When there is need to apply target attribute manually, then {@link module:link/link~LinkDecoratorManualOption manual} decorator should - * be provided with the {@link module:link/link~LinkConfig link configuration} in {@link module:link/link~LinkConfig#decorators} array. - * In this scenario, `targetDecorator` option should remain `undefined` or `false` to not interfere with a created decorator. + * Internally, this option activates a predefined {@link module:link/link~LinkDecoratorAutomaticOption automatic link decorator}, + * which extends all external links with the `target` and `rel` attributes without additional configuration. * - * More information about decorators might be found in {@link module:link/link~LinkConfig#decorators}. + * **Note**: To control the `target` and `rel` attributes of specific links in the edited content, a dedicated + * {@link module:link/link~LinkDecoratorManualOption manual} decorator must be defined in the + * {@link module:link/link~LinkConfig#decorators `config.link.decodators`} array. In such scenario, + * the `config.link.targetDecorator` option should remain `undefined` or `false` to not interfere with the manual decorator. + * + * **Note**: It is possible to add other {@link module:link/link~LinkDecoratorAutomaticOption automatic} + * or {@link module:link/link~LinkDecoratorManualOption manual} link decorators when this option is active. + * + * More information about decorators can be found in the {@link module:link/link~LinkConfig#decorators decorators configuration} + * reference. * * @member {Boolean} module:link/link~LinkConfig#targetDecorator */ /** - * Decorators are {@link module:link/link~Link link's} plugin feature which can extend anchor with additional attributes. - * Decorators provide an easy way to configure and manage those attributes automatically or manually with the UI. - * - * For example, there is a quite common topic to add the `target="_blank"` attribute to only some of the links in the editor. - * Decorators help in mentioned case with either: added automatic rules based on link's href (URL), - * or added a toggleable UI switch for the user. - * - * **Warning:** Currently, there is no integration in-between decorators for any mix of decorators' types. - * For example, configuring `target` attribute through both 'automatic' and 'manual' decorators might result with quirk behavior - * as well as defining 2 manual or 2 automatic decorators for the same attribute. - * - * # Automatic decorators - * This type of decorators takes an object with key-value pairs of attributes and - * a {@link module:link/link~LinkDecoratorAutomaticOption callback} function. The function has to return boolean value based on link's href. - * If a given set of attributes should be applied to the link, then callback has to return the `true` value. - * For example, if there is a need to add the `target="_blank"` attribute to all links in the editor started with the `http://`, - * then configuration might look like this: - * - * const link.decorators = [ - * { - * mode: 'automatic', - * callback: url => url.startsWith( 'http://' ), - * attributes: { - * target: '_blank' - * } - * } - * ] - * - * **Please notice:** As configuring target attribute for external links is a quite common situation, - * there is predefined automatic decorator, which might be turned on with even simpler option, - * just by setting {@link #targetDecorator} to `true`. More information might be found in the {@link #targetDecorator} description. - * - * # Manual decorators - * This type of decorators also takes an object with key-value pair of attributes, however, those are applied based on user choice. - * Manual decorator is defined with a {@link module:link/link~LinkDecoratorManualOption label}, - * which describes the given option for the user. Manual decorators are possible to toggle for the user in editing view of the link plugin. - * For example, if there is a need to give user full control over this which links should be opened in a new window, - * then configuration might looks as followed: - * - * const link.decorators = [ - * { - * mode: 'manual', - * label: 'Open in new window', - * attributes: { - * target: '_blank' - * } - * } - * ] + * Decorators provide an easy way to configure and manage additional link attributes in the editor content. There are + * two types of link decorators: + * + * * **automatic** – they match links against pre–defined rules and manage the attributes based on the results, + * * **manual** – they allow users to control link attributes individually using the editor UI. + * + * The most common use case for decorators is applying the `target="_blank"` attribute to links in the content + * (`Link`). + * + * # {@link module:link/link~LinkDecoratorAutomaticOption Automatic decorators} + * + * This kind of a decorator matches all links in the editor content against a function which decides whether the link + * should gain a pre–defined set of attributes or not. + * + * It takes an object with key-value pairs of attributes and a {@link module:link/link~LinkDecoratorAutomaticOption `callback`} function + * which must return a boolean based on link's `href`. When the callback returns `true`, the `attributes` are applied + * to the link. + * + * For example, to add the `target="_blank"` attribute to all links starting with the `http://` in the content, + * the configuration could look as follows: + * + * const link.decorators = [ + * { + * mode: 'automatic', + * callback: url => url.startsWith( 'http://' ), + * attributes: { + * target: '_blank' + * } + * } + * ] + * + * **Note**: Since the `target` attribute management for external links is a common use case, there is a predefined automatic decorator + * dedicated for that purpose which can be enabled by turning a single option on. Check out the + * {@link module:link/link~LinkConfig#targetDecorator `config.link.targetDecorator`} configuration description to learn more. + * + * # {@link module:link/link~LinkDecoratorManualOption Manual decorators} + * + * This type of a decorator takes an object with key-value pair of attributes, but those are applied based on the user choice. + * + * Manual decorators are represented as switch buttons in the {@link module:link/linkui user interface} of the link feature. + * This is why each manual decorator requires a {@link module:link/link~LinkDecoratorManualOption label} which describes its purpose + * to the users. + * + * For example, if users are to be allowed to control which particular links should be opened in a new window, the configuration + * could look as follows: + * + * const link.decorators = [ + * { + * mode: 'manual', + * label: 'Open in new window', + * attributes: { + * target: '_blank' + * } + * } + * ] + * + * **Warning:** Currently, link decorators work independently and no conflict resolution mechanism exists. + * For example, configuring the `target` attribute using both an automatic and a manual decorator at a time could end up with a + * quirky behavior. The same applies if multiple manual or automatic decorators were defined for the same attribute. * * @member {Array.} * module:link/link~LinkConfig#decorators */ /** - * This object describes automatic {@link module:link/link~LinkConfig#decorators} for the links. Based on this option, - * output data will extend links with proper attributes. - * - * For example, if there is need to define a rule that automatically adds attribute `target="_blank"` and `rel="noopener noreferrer"` - * to the external links, then automatic decorator might looks as follows: - * - * { - * mode: 'automatic', - * callback: url => /^(https?:)?\/\//.test( url ), - * attributes: { - * target: '_blank', - * rel: 'noopener noreferrer' + * Describes an automatic link {@link module:link/link~LinkConfig#decorators decorator}. This kind of a decorator matches + * all links in the editor content against a function which decides whether the link should gain a pre–defined set of attributes + * or not. + * + * For example, to add the `target="_blank"` attribute to all links in the editor starting with the `http://`, + * then configuration could look like this: + * + * { + * mode: 'automatic', + * callback: url => url.startsWith( 'http://' ), + * attributes: { + * target: '_blank' + * } * } - * } * - * **Please notice**, there is a {@link module:link/link~LinkConfig#targetDecorator configuration option}, - * which automatically adds attributes: `target="_blank"` and `rel="noopener noreferrer"` for all links started with: - * `http://`, `https://` or `//`. + * **Note**: Since the `target` attribute management for external links is a common use case, there is a predefined automatic decorator + * dedicated for that purpose which can be enabled by turning a single option on. Check out the + * {@link module:link/link~LinkConfig#targetDecorator `config.link.targetDecorator`} configuration description to learn more. * * @typedef {Object} module:link/link~LinkDecoratorAutomaticOption - * @property {'automatic'} mode should have string value equal 'automatic' for automatic decorators - * @property {Function} callback takes an `url` as a parameter and returns `true` - * for urls where given attributes should be applied. - * @property {Object} attributes key-value pairs used as attributes added to output data during + * @property {'automatic'} mode The kind of the decorator. `'automatic'` for all automatic decorators. + * @property {Function} callback Takes an `url` as a parameter and returns `true` if the `attributes` should be applied to the link. + * @property {Object} attributes Key-value pairs used as link attributes added to the output during the * {@glink framework/guides/architecture/editing-engine#conversion downcasting}. - * Attributes should have form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. + * Attributes should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax. */ /** - * This object describes manual {@link module:link/link~LinkConfig#decorators} for the links. Based on this definition, - * there is added switch button in editing form for the link. This button poses label define here. After toggling and confirming it, - * preconfigured attributes are added to a selected link. - * - * For example, if there is need to define a rule, which adds a switch button to apply `target="_blank"` and - * `rel="noopener noreferrer"`, then manual decorator might be helpful and can look as follows: - * - * { - * mode: 'manual', - * label: 'Open link in new window', - * attributes: { - * target: '_blank', - * rel: 'noopener noreferrer' + * Describes a manual link {@link module:link/link~LinkConfig#decorators decorator}. This kind of a decorator is represented in + * the link feature's {@link module:link/linkui user interface} as a switch the user can use to control the presence + * of a pre–defined set of attributes. + * + * For instance, to allow users to manually control the presence of the `target="_blank"` and + * `rel="noopener noreferrer"` attributes on specific links, the decorator could look as follows: + * + * { + * mode: 'manual', + * label: 'Open link in new window', + * attributes: { + * target: '_blank', + * rel: 'noopener noreferrer' + * } * } - * } * * @typedef {Object} module:link/link~LinkDecoratorManualOption - * @property {'manual'} mode should have string value equal 'manual' for manual decorators - * @property {String} label the label for the UI switch button, which will be responsible for applying defined attributes - * to a currently edited link. - * @property {Object} attributes attributes key-value pairs used as attributes added to output data during + * @property {'automatic'} mode The kind of the decorator. `'manual'` for all manual decorators. + * @property {String} label The label of the UI button the user can use to control the presence of link attributes. + * @property {Object} attributes Key-value pairs used as link attributes added to the output during the * {@glink framework/guides/architecture/editing-engine#conversion downcasting}. - * Attributes should have form of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. + * Attributes should follow the {@link module:engine/view/elementdefinition~ElementDefinition} syntax. */ From 9684041898e416e7fcd26c8441b10244d8f2bde0 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 17 Jun 2019 15:46:34 +0200 Subject: [PATCH 51/81] Docs: Simplified link decorator configuration docs. --- src/link.js | 87 ++++++++++++++++++++++------------------------------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/src/link.js b/src/link.js index 0e73caa..6ef5913 100644 --- a/src/link.js +++ b/src/link.js @@ -61,7 +61,7 @@ export default class Link extends Plugin { * When set `true`, the `target="blank"` and `rel="noopener noreferrer"` attributes are automatically added to all external links * in the editor. By external are meant all links in the editor content starting with `http`, `https`, or `//`. * - * Internally, this option activates a predefined {@link module:link/link~LinkDecoratorAutomaticOption automatic link decorator}, + * Internally, this option activates a predefined {@link module:link/link~LinkConfig#decorators automatic link decorator}, * which extends all external links with the `target` and `rel` attributes without additional configuration. * * **Note**: To control the `target` and `rel` attributes of specific links in the edited content, a dedicated @@ -82,63 +82,45 @@ export default class Link extends Plugin { * Decorators provide an easy way to configure and manage additional link attributes in the editor content. There are * two types of link decorators: * - * * **automatic** – they match links against pre–defined rules and manage the attributes based on the results, - * * **manual** – they allow users to control link attributes individually using the editor UI. - * - * The most common use case for decorators is applying the `target="_blank"` attribute to links in the content - * (`Link`). - * - * # {@link module:link/link~LinkDecoratorAutomaticOption Automatic decorators} - * - * This kind of a decorator matches all links in the editor content against a function which decides whether the link - * should gain a pre–defined set of attributes or not. - * - * It takes an object with key-value pairs of attributes and a {@link module:link/link~LinkDecoratorAutomaticOption `callback`} function - * which must return a boolean based on link's `href`. When the callback returns `true`, the `attributes` are applied - * to the link. + * * {@link module:link/link~LinkDecoratorAutomaticOption automatic} – they match links against pre–defined rules and + * manage their attributes based on the results, + * * {@link module:link/link~LinkDecoratorManualOption manual} – they allow users to control link attributes individually + * using the editor UI. + * + * Link decorators are defined as an array of objects: + * + * const linkConfig = { + * decorators: [ + * { + * mode: 'automatic', + * callback: url => url.startsWith( 'http://' ), + * attributes: { + * target: '_blank', + * rel: 'noopener noreferrer' + * } + * }, + * { + * mode: 'manual', + * label: 'Downloadable', + * attributes: { + * download: 'file.png', + * } + * }, + * // ... + * ] + * } * - * For example, to add the `target="_blank"` attribute to all links starting with the `http://` in the content, - * the configuration could look as follows: + * To learn more about the configuration syntax, check out the {@link module:link/link~LinkDecoratorAutomaticOption automatic} + * and {@link module:link/link~LinkDecoratorManualOption manual} decorator option reference. * - * const link.decorators = [ - * { - * mode: 'automatic', - * callback: url => url.startsWith( 'http://' ), - * attributes: { - * target: '_blank' - * } - * } - * ] + * **Warning:** Currently, link decorators work independently and no conflict resolution mechanism exists. + * For example, configuring the `target` attribute using both an automatic and a manual decorator at a time could end up with a + * quirky results. The same applies if multiple manual or automatic decorators were defined for the same attribute. * * **Note**: Since the `target` attribute management for external links is a common use case, there is a predefined automatic decorator * dedicated for that purpose which can be enabled by turning a single option on. Check out the * {@link module:link/link~LinkConfig#targetDecorator `config.link.targetDecorator`} configuration description to learn more. * - * # {@link module:link/link~LinkDecoratorManualOption Manual decorators} - * - * This type of a decorator takes an object with key-value pair of attributes, but those are applied based on the user choice. - * - * Manual decorators are represented as switch buttons in the {@link module:link/linkui user interface} of the link feature. - * This is why each manual decorator requires a {@link module:link/link~LinkDecoratorManualOption label} which describes its purpose - * to the users. - * - * For example, if users are to be allowed to control which particular links should be opened in a new window, the configuration - * could look as follows: - * - * const link.decorators = [ - * { - * mode: 'manual', - * label: 'Open in new window', - * attributes: { - * target: '_blank' - * } - * } - * ] - * - * **Warning:** Currently, link decorators work independently and no conflict resolution mechanism exists. - * For example, configuring the `target` attribute using both an automatic and a manual decorator at a time could end up with a - * quirky behavior. The same applies if multiple manual or automatic decorators were defined for the same attribute. - * * @member {Array.} * module:link/link~LinkConfig#decorators */ @@ -148,6 +130,9 @@ export default class Link extends Plugin { * all links in the editor content against a function which decides whether the link should gain a pre–defined set of attributes * or not. * + * It takes an object with key-value pairs of attributes and a callback function which must return a boolean based on link's + * `href` (URL). When the callback returns `true`, attributes are applied to the link. + * * For example, to add the `target="_blank"` attribute to all links in the editor starting with the `http://`, * then configuration could look like this: * From 2a1590a42a301b4fe077c94100341ccccace70c1 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 17 Jun 2019 16:11:38 +0200 Subject: [PATCH 52/81] Docs: Refactoring of the decorators documentation in link feature classes. --- src/link.js | 1 + src/linkcommand.js | 25 ++++++++++++++----------- src/linkediting.js | 25 ++++++++++++++----------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/link.js b/src/link.js index 6ef5913..e3b4fa3 100644 --- a/src/link.js +++ b/src/link.js @@ -75,6 +75,7 @@ export default class Link extends Plugin { * More information about decorators can be found in the {@link module:link/link~LinkConfig#decorators decorators configuration} * reference. * + * @default false * @member {Boolean} module:link/link~LinkConfig#targetDecorator */ diff --git a/src/linkcommand.js b/src/linkcommand.js index 684dc05..6ebbc1f 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -30,12 +30,13 @@ export default class LinkCommand extends Command { super( editor ); /** - * Keeps collection of {@link module:link/utils~ManualDecorator} - * corresponding to {@link module:link/link~LinkConfig#decorators}. - * You can consider it as a model with states of manual decorators added to currently selected link. + * A collection of {@link module:link/utils~ManualDecorator manual decorators} + * corresponding to the {@link module:link/link~LinkConfig#decorators decorator configuration}. + * + * You can consider it a model with states of manual decorators added to currently selected link. * * @readonly - * @type {module:utils/collection~Collection} + * @type {module:utils/collection~Collection.} */ this.manualDecorators = new Collection(); } @@ -69,17 +70,19 @@ export default class LinkCommand extends Command { * * When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated. * - * # Decorators - * Command has an optional second parameter, which can apply or remove {@link module:link/utils~ManualDecorator} together with href. - * Decorators are representing in a corresponding way as href, which are attributes for {@link module:engine/model/text~Text Text node}. - * Names of those attributes are stored as `id`s in {@link module:link/utils~ManualDecorator} and preserve convention of name: - * `linkManualDecoratorN`, where `N` is replaced with a number (for example: `'linkManualDecorator0'`). + * # Decorators and attribute management + * + * This command has an optional argument, which applies or removes model attributes brought by + * {@link module:link/utils~ManualDecorator manual decorators}. Model attribute names correspond to + * decorator {@link module:link/utils~ManualDecorator#id ids} and follow the incremental pattern: + * `'linkManualDecorator0'`, `'linkManualDecorator1'`, `'linkManualDecorator2'`, etc.. * - * More about decorators might be found in {@link module:link/link~LinkConfig#decorators} + * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} + * documentation. * * @fires execute * @param {String} href Link destination. - * @param {Object} [manualDecoratorIds={}] Keeps information about turned on and off manual decorators applied with command. + * @param {Object} [manualDecoratorIds={}] The information about manual decorator attributes to be applied or removed upon execution. */ execute( href, manualDecoratorIds = {} ) { const model = this.editor.model; diff --git a/src/linkediting.js b/src/linkediting.js index 6757cee..8501967 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -90,11 +90,13 @@ export default class LinkEditing extends Plugin { } /** - * Method process array of {@link module:link/link~LinkDecoratorAutomaticOption} and register - * {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher} for them. - * Downcast dispatcher is obtained with {@link module:link/utils~AutomaticDecorators#getDispatcher}. + * Processes an array of configured {@link module:link/link~LinkDecoratorAutomaticOption automatic decorators} + * and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher} + * for each one of them. Downcast dispatchers are obtained using the + * {@link module:link/utils~AutomaticDecorators#getDispatcher} method. * - * Method also kept configuration of decorator activated with {@link module:link/link~LinkConfig#targetDecorator}. + * **Note**: This method also activates the automatic external link decorator if enabled via + * {@link module:link/link~LinkConfig#targetDecorator `config.link.targetDecorator`}. * * @private * @param {Array.} automaticDecoratorDefinitions @@ -102,13 +104,12 @@ export default class LinkEditing extends Plugin { _enableAutomaticDecorators( automaticDecoratorDefinitions ) { const editor = this.editor; const automaticDecorators = new AutomaticDecorators(); + // Adds default decorator for external links. if ( editor.config.get( 'link.targetDecorator' ) ) { automaticDecorators.add( { mode: DECORATOR_AUTOMATIC, - callback: url => { - return EXTERNAL_LINKS_REGEXP.test( url ); - }, + callback: url => EXTERNAL_LINKS_REGEXP.test( url ), attributes: { target: '_blank', rel: 'noopener noreferrer' @@ -117,16 +118,18 @@ export default class LinkEditing extends Plugin { } automaticDecorators.add( automaticDecoratorDefinitions ); + if ( automaticDecorators.length ) { editor.conversion.for( 'downcast' ).add( automaticDecorators.getDispatcher() ); } } /** - * Method process array of {@link module:link/link~LinkDecoratorManualOption} by transformation those configuration options into - * {@link module:link/utils~ManualDecorator} objects. Then those objects are added to - * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection, which is considered as a model for manual decorators state. - * It also provides a proper {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} + * Processes an array of configured {@link module:link/link~LinkDecoratorManualOption manual decorators} + * and transforms them into {@link module:link/utils~ManualDecorator} instances and stores them in the + * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state). + * + * Also registers an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} * converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema} * with adequate model attributes. * From f86b2f21ff1eb83146f781af1a74d06a1a1c47eb Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 17 Jun 2019 16:29:08 +0200 Subject: [PATCH 53/81] Docs: Refactoring of the decorators documentation in the LinkFormView class. --- src/ui/linkformview.js | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 6e49ae6..a7f4fe0 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -85,9 +85,12 @@ export default class LinkFormView extends View { this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' ); /** - * Keeps reference to {@link module:link/linkcommand~LinkCommand#manualDecorators} in link command. - * Collection is used to build proper UI element and synchronize its state between model located in - * {@link module:link/linkcommand~LinkCommand#manualDecorators link command} and the {@link #manualDecoratorsUIView UI}. + * A reference to {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators} + * collection of the {@link module:link/linkcommand~LinkCommand}. + * + * Helps bootstrap the {@link #manualDecoratorsUIView UI} so it corresponds with the global + * configuration of manual decorators and to synchronize its state later on when the user + * is editing the content. * * @readonly * @type {model:utils/collection~Collection} @@ -95,9 +98,9 @@ export default class LinkFormView extends View { this.manualDecorators = manualDecorators; /** - * Preserves collection of {@link module:ui/button/switchbuttonview~SwitchButtonView}, - * which are made based on {@link #manualDecorators}. It use {@link #_createManualDecoratorsUIView} method - * to generate proper collection. + * A collection of {@link module:ui/button/switchbuttonview~SwitchButtonView}, + * which corresponds to {@link #manualDecorators manual decorators} configured in the editor. + * Populated by {@link #_createManualDecoratorsUIView}. * * @readonly * @type {module:ui/viewcollection~ViewCollection} @@ -105,7 +108,7 @@ export default class LinkFormView extends View { this.manualDecoratorsUIView = this._createManualDecoratorsUIView(); /** - * Collection of views used as children elements in {@link module:link/ui/linkformview~LinkFormView}. + * Collection of child views in the form. * * @readonly * @type {module:ui/viewcollection~ViewCollection} @@ -247,7 +250,7 @@ export default class LinkFormView extends View { } /** - * Prepare {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView} + * Populates {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView} * made based on {@link #manualDecorators} * * @private @@ -273,12 +276,16 @@ export default class LinkFormView extends View { return switchButton; } ); + return switches; } /** - * Creates {@link #children} for {@link module:link/ui/linkformview~LinkFormView}. If there exist {@link #manualDecorators}, - * Then additional View wrapping all {@link #manualDecoratorsUIView} will be added as a child of LinkFormView. + * Populates the {@link #children} collection of the form. + * + * If {@link #manualDecorators manual decorators} are configured in the editor, creates an + * additional `View` wrapping all {@link #manualDecoratorsUIView} switch buttons corresponding + * to those decorators. * * @private * @returns {module:ui/viewcollection~ViewCollection} children of LinkFormView. @@ -290,6 +297,7 @@ export default class LinkFormView extends View { if ( this.manualDecorators.length ) { const additionalButtonsView = new View(); + additionalButtonsView.setTemplate( { tag: 'ul', children: this.manualDecoratorsUIView.map( switchButton => ( { From 63097bb4aae92d766f965818c429b739623296c7 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 18 Jun 2019 10:35:45 +0200 Subject: [PATCH 54/81] Rename targetDecorator to addTargetToExternalLinks. --- src/link.js | 10 ++++++---- src/linkediting.js | 6 +++--- tests/linkediting.js | 16 ++++++++-------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/link.js b/src/link.js index e3b4fa3..466285e 100644 --- a/src/link.js +++ b/src/link.js @@ -67,7 +67,7 @@ export default class Link extends Plugin { * **Note**: To control the `target` and `rel` attributes of specific links in the edited content, a dedicated * {@link module:link/link~LinkDecoratorManualOption manual} decorator must be defined in the * {@link module:link/link~LinkConfig#decorators `config.link.decodators`} array. In such scenario, - * the `config.link.targetDecorator` option should remain `undefined` or `false` to not interfere with the manual decorator. + * the `config.link.addTargetToExternalLinks` option should remain `undefined` or `false` to not interfere with the manual decorator. * * **Note**: It is possible to add other {@link module:link/link~LinkDecoratorAutomaticOption automatic} * or {@link module:link/link~LinkDecoratorManualOption manual} link decorators when this option is active. @@ -76,7 +76,7 @@ export default class Link extends Plugin { * reference. * * @default false - * @member {Boolean} module:link/link~LinkConfig#targetDecorator + * @member {Boolean} module:link/link~LinkConfig#addTargetToExternalLinks */ /** @@ -120,7 +120,8 @@ export default class Link extends Plugin { * * **Note**: Since the `target` attribute management for external links is a common use case, there is a predefined automatic decorator * dedicated for that purpose which can be enabled by turning a single option on. Check out the - * {@link module:link/link~LinkConfig#targetDecorator `config.link.targetDecorator`} configuration description to learn more. + * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`} + * configuration description to learn more. * * @member {Array.} * module:link/link~LinkConfig#decorators @@ -147,7 +148,8 @@ export default class Link extends Plugin { * * **Note**: Since the `target` attribute management for external links is a common use case, there is a predefined automatic decorator * dedicated for that purpose which can be enabled by turning a single option on. Check out the - * {@link module:link/link~LinkConfig#targetDecorator `config.link.targetDecorator`} configuration description to learn more. + * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`} + * configuration description to learn more. * * @typedef {Object} module:link/link~LinkDecoratorAutomaticOption * @property {'automatic'} mode The kind of the decorator. `'automatic'` for all automatic decorators. diff --git a/src/linkediting.js b/src/linkediting.js index 8501967..a9a047e 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -38,7 +38,7 @@ export default class LinkEditing extends Plugin { super( editor ); editor.config.define( 'link', { - targetDecorator: false + addTargetToExternalLinks: false } ); } @@ -96,7 +96,7 @@ export default class LinkEditing extends Plugin { * {@link module:link/utils~AutomaticDecorators#getDispatcher} method. * * **Note**: This method also activates the automatic external link decorator if enabled via - * {@link module:link/link~LinkConfig#targetDecorator `config.link.targetDecorator`}. + * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}. * * @private * @param {Array.} automaticDecoratorDefinitions @@ -106,7 +106,7 @@ export default class LinkEditing extends Plugin { const automaticDecorators = new AutomaticDecorators(); // Adds default decorator for external links. - if ( editor.config.get( 'link.targetDecorator' ) ) { + if ( editor.config.get( 'link.addTargetToExternalLinks' ) ) { automaticDecorators.add( { mode: DECORATOR_AUTOMATIC, callback: url => EXTERNAL_LINKS_REGEXP.test( url ), diff --git a/tests/linkediting.js b/tests/linkediting.js index 6561711..571e55e 100644 --- a/tests/linkediting.js +++ b/tests/linkediting.js @@ -392,18 +392,18 @@ describe( 'LinkEditing', () => { url: 'tel:123456789' } ]; - it( 'link.targetDecorator is predefined as false value', () => { - expect( editor.config.get( 'link.targetDecorator' ) ).to.be.false; + it( 'link.addTargetToExternalLinks is predefined as false value', () => { + expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.false; } ); - describe( 'for link.targetDecorator = false', () => { + describe( 'for link.addTargetToExternalLinks = false', () => { let editor, model; beforeEach( () => { return VirtualTestEditor .create( { plugins: [ Paragraph, LinkEditing, Enter ], link: { - targetDecorator: true + addTargetToExternalLinks: true } } ) .then( newEditor => { @@ -417,8 +417,8 @@ describe( 'LinkEditing', () => { editor.destroy(); } ); - it( 'link.targetDecorator is set as true value', () => { - expect( editor.config.get( 'link.targetDecorator' ) ).to.be.true; + it( 'link.addTargetToExternalLinks is set as true value', () => { + expect( editor.config.get( 'link.addTargetToExternalLinks' ) ).to.be.true; } ); testLinks.forEach( link => { @@ -480,7 +480,7 @@ describe( 'LinkEditing', () => { .create( { plugins: [ Paragraph, LinkEditing, Enter ], link: { - targetDecorator: false, + addTargetToExternalLinks: false, decorators: [ { mode: 'automatic', @@ -566,7 +566,7 @@ describe( 'LinkEditing', () => { .create( { plugins: [ Paragraph, LinkEditing, Enter, CustomLinks ], link: { - targetDecorator: true, + addTargetToExternalLinks: true, } } ) .then( newEditor => { From 33b8c419a8859f30179eb166aea34023d97c3f67 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 18 Jun 2019 10:42:11 +0200 Subject: [PATCH 55/81] Rename decorator's 'options' to 'definitiaon' to have more apropriate name. --- src/link.js | 20 ++++++++++---------- src/linkediting.js | 8 ++++---- src/utils/automaticdecorators.js | 6 +++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/link.js b/src/link.js index 466285e..6e560d7 100644 --- a/src/link.js +++ b/src/link.js @@ -65,12 +65,12 @@ export default class Link extends Plugin { * which extends all external links with the `target` and `rel` attributes without additional configuration. * * **Note**: To control the `target` and `rel` attributes of specific links in the edited content, a dedicated - * {@link module:link/link~LinkDecoratorManualOption manual} decorator must be defined in the + * {@link module:link/link~LinkDecoratorManualDefinition manual} decorator must be defined in the * {@link module:link/link~LinkConfig#decorators `config.link.decodators`} array. In such scenario, * the `config.link.addTargetToExternalLinks` option should remain `undefined` or `false` to not interfere with the manual decorator. * - * **Note**: It is possible to add other {@link module:link/link~LinkDecoratorAutomaticOption automatic} - * or {@link module:link/link~LinkDecoratorManualOption manual} link decorators when this option is active. + * **Note**: It is possible to add other {@link module:link/link~LinkDecoratorAutomaticDefinition automatic} + * or {@link module:link/link~LinkDecoratorManualDefinition manual} link decorators when this option is active. * * More information about decorators can be found in the {@link module:link/link~LinkConfig#decorators decorators configuration} * reference. @@ -83,9 +83,9 @@ export default class Link extends Plugin { * Decorators provide an easy way to configure and manage additional link attributes in the editor content. There are * two types of link decorators: * - * * {@link module:link/link~LinkDecoratorAutomaticOption automatic} – they match links against pre–defined rules and + * * {@link module:link/link~LinkDecoratorAutomaticDefinition automatic} – they match links against pre–defined rules and * manage their attributes based on the results, - * * {@link module:link/link~LinkDecoratorManualOption manual} – they allow users to control link attributes individually + * * {@link module:link/link~LinkDecoratorManualDefinition manual} – they allow users to control link attributes individually * using the editor UI. * * Link decorators are defined as an array of objects: @@ -111,8 +111,8 @@ export default class Link extends Plugin { * ] * } * - * To learn more about the configuration syntax, check out the {@link module:link/link~LinkDecoratorAutomaticOption automatic} - * and {@link module:link/link~LinkDecoratorManualOption manual} decorator option reference. + * To learn more about the configuration syntax, check out the {@link module:link/link~LinkDecoratorAutomaticDefinition automatic} + * and {@link module:link/link~LinkDecoratorManualDefinition manual} decorator option reference. * * **Warning:** Currently, link decorators work independently and no conflict resolution mechanism exists. * For example, configuring the `target` attribute using both an automatic and a manual decorator at a time could end up with a @@ -123,7 +123,7 @@ export default class Link extends Plugin { * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`} * configuration description to learn more. * - * @member {Array.} + * @member {Array.} * module:link/link~LinkConfig#decorators */ @@ -151,7 +151,7 @@ export default class Link extends Plugin { * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`} * configuration description to learn more. * - * @typedef {Object} module:link/link~LinkDecoratorAutomaticOption + * @typedef {Object} module:link/link~LinkDecoratorAutomaticDefinition * @property {'automatic'} mode The kind of the decorator. `'automatic'` for all automatic decorators. * @property {Function} callback Takes an `url` as a parameter and returns `true` if the `attributes` should be applied to the link. * @property {Object} attributes Key-value pairs used as link attributes added to the output during the @@ -176,7 +176,7 @@ export default class Link extends Plugin { * } * } * - * @typedef {Object} module:link/link~LinkDecoratorManualOption + * @typedef {Object} module:link/link~LinkDecoratorManualDefinition * @property {'automatic'} mode The kind of the decorator. `'manual'` for all manual decorators. * @property {String} label The label of the UI button the user can use to control the presence of link attributes. * @property {Object} attributes Key-value pairs used as link attributes added to the output during the diff --git a/src/linkediting.js b/src/linkediting.js index a9a047e..2c46dda 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -90,7 +90,7 @@ export default class LinkEditing extends Plugin { } /** - * Processes an array of configured {@link module:link/link~LinkDecoratorAutomaticOption automatic decorators} + * Processes an array of configured {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators} * and registers a {@link module:engine/conversion/downcastdispatcher~DowncastDispatcher downcast dispatcher} * for each one of them. Downcast dispatchers are obtained using the * {@link module:link/utils~AutomaticDecorators#getDispatcher} method. @@ -99,7 +99,7 @@ export default class LinkEditing extends Plugin { * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`}. * * @private - * @param {Array.} automaticDecoratorDefinitions + * @param {Array.} automaticDecoratorDefinitions */ _enableAutomaticDecorators( automaticDecoratorDefinitions ) { const editor = this.editor; @@ -125,7 +125,7 @@ export default class LinkEditing extends Plugin { } /** - * Processes an array of configured {@link module:link/link~LinkDecoratorManualOption manual decorators} + * Processes an array of configured {@link module:link/link~LinkDecoratorManualDefinition manual decorators} * and transforms them into {@link module:link/utils~ManualDecorator} instances and stores them in the * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state). * @@ -134,7 +134,7 @@ export default class LinkEditing extends Plugin { * with adequate model attributes. * * @private - * @param {Array.} manualDecoratorDefinitions + * @param {Array.} manualDecoratorDefinitions */ _enableManualDecorators( manualDecoratorDefinitions ) { if ( !manualDecoratorDefinitions.length ) { diff --git a/src/utils/automaticdecorators.js b/src/utils/automaticdecorators.js index 8b9c783..d0d4be4 100644 --- a/src/utils/automaticdecorators.js +++ b/src/utils/automaticdecorators.js @@ -8,13 +8,13 @@ */ /** - * Helper class which tight together all {@link module:link/link~LinkDecoratorAutomaticOption} and provides + * Helper class which tight together all {@link module:link/link~LinkDecoratorAutomaticDefinition} and provides * a {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement downcast dispatcher} for them. */ export default class AutomaticDecorators { constructor() { /** - * Stores definition of {@link module:link/link~LinkDecoratorAutomaticOption automatic decorators}. + * Stores definition of {@link module:link/link~LinkDecoratorAutomaticDefinition automatic decorators}. * Those data are used as source for a downcast dispatcher to make proper conversion to output data. * * @private @@ -37,7 +37,7 @@ export default class AutomaticDecorators { /** * Add automatic decorator objects or array with them to be used during downcasting. * - * @param {module:link/link~LinkDecoratorAutomaticOption|Array.} item + * @param {module:link/link~LinkDecoratorAutomaticDefinition|Array.} item * configuration object of automatic rules for decorating links. It might be also array of such objects. */ add( item ) { From a03abf5f1519f7de551fda5213ff306b41dc2b53 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 18 Jun 2019 10:51:26 +0200 Subject: [PATCH 56/81] Rename class of decorators container in UI. --- src/ui/linkformview.js | 2 +- theme/linkform.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index a7f4fe0..d319a48 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -147,7 +147,7 @@ export default class LinkFormView extends View { const classList = [ 'ck', 'ck-link-form' ]; if ( this.manualDecorators.length ) { - classList.push( 'ck-link-form_decorators' ); + classList.push( 'ck-link-form_layout-vertical' ); } this.setTemplate( { diff --git a/theme/linkform.css b/theme/linkform.css index a7fd590..52acb6b 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -25,10 +25,10 @@ } } -/* Style link form differently when manual decoratos are available. +/* Style link form differently when manual decorators are available. * See: https://github.com/ckeditor/ckeditor5-link/issues/186. */ -.ck.ck-link-form_decorators { +.ck.ck-link-form_layout-vertical { flex-wrap: wrap; & .ck-labeled-input { From b8c9c3c2be48eabf7bb437fdbe7d6c57fe8bffd3 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 18 Jun 2019 12:52:56 +0200 Subject: [PATCH 57/81] Rename var to be more accurate, expose another method to obtaine decorator status. Fix tests. --- src/linkui.js | 8 +------- src/ui/linkformview.js | 21 +++++++++++++++++---- tests/linkui.js | 3 +-- tests/ui/linkformview.js | 36 +++++++++++++++++++++++++++++------- 4 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/linkui.js b/src/linkui.js index 7b2eb95..32d816b 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -154,13 +154,7 @@ export default class LinkUI extends Plugin { // Execute link command after clicking the "Save" button. this.listenTo( formView, 'submit', () => { - const manualDecorators = {}; - - for ( const switchButton of formView.manualDecoratorsUIView ) { - manualDecorators[ switchButton.name ] = switchButton.isOn; - } - - editor.execute( 'link', formView.urlInputView.inputView.element.value, manualDecorators ); + editor.execute( 'link', formView.urlInputView.inputView.element.value, formView.getDecoratorSwitchesState() ); this._closeFormView(); } ); diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index d319a48..025046f 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -102,10 +102,11 @@ export default class LinkFormView extends View { * which corresponds to {@link #manualDecorators manual decorators} configured in the editor. * Populated by {@link #_createManualDecoratorsUIView}. * + * @private * @readonly * @type {module:ui/viewcollection~ViewCollection} */ - this.manualDecoratorsUIView = this._createManualDecoratorsUIView(); + this._manualDecoratorSwitches = this._createManualDecoratorSwitches(); /** * Collection of child views in the form. @@ -164,6 +165,18 @@ export default class LinkFormView extends View { } ); } + /** + * Obtain state of the switch buttons in a currently opened {@link module:link/ui/linkformview~LinkFormView}. + * + * @returns {Object} key-value pairs, where key is the name of the decorator and value is its state. + */ + getDecoratorSwitchesState() { + return Array.from( this._manualDecoratorSwitches ).reduce( ( accumulator, switchButton ) => { + accumulator[ switchButton.name ] = switchButton.isOn; + return accumulator; + }, {} ); + } + /** * @inheritDoc */ @@ -176,7 +189,7 @@ export default class LinkFormView extends View { const childViews = [ this.urlInputView, - ...this.manualDecoratorsUIView, + ...this._manualDecoratorSwitches, this.saveButtonView, this.cancelButtonView ]; @@ -256,7 +269,7 @@ export default class LinkFormView extends View { * @private * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. */ - _createManualDecoratorsUIView() { + _createManualDecoratorSwitches() { const switches = this.createCollection(); switches.bindTo( this.manualDecorators ).using( item => { @@ -300,7 +313,7 @@ export default class LinkFormView extends View { additionalButtonsView.setTemplate( { tag: 'ul', - children: this.manualDecoratorsUIView.map( switchButton => ( { + children: this._manualDecoratorSwitches.map( switchButton => ( { tag: 'li', children: [ switchButton ], attributes: { diff --git a/tests/linkui.js b/tests/linkui.js index 66c7616..e88c762 100644 --- a/tests/linkui.js +++ b/tests/linkui.js @@ -1004,8 +1004,7 @@ describe( 'LinkUI', () => { setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' ); - expect( formView.manualDecoratorsUIView.length ).to.equal( 1 ); - expect( formView.manualDecoratorsUIView.first.isOn ).to.be.true; + expect( formView.getDecoratorSwitchesState() ).to.deep.equal( { linkManualDecorator0: true } ); formView.fire( 'submit' ); diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 56b83a5..112db40 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -189,7 +189,7 @@ describe( 'LinkFormView', () => { } ); } ); - describe( 'customAttributes', () => { + describe( 'manual decorators', () => { let view, collection; beforeEach( () => { collection = new Collection(); @@ -225,18 +225,18 @@ describe( 'LinkFormView', () => { view.destroy(); collection.clear(); } ); - it( 'switch buttons reflects state of customAttributes', () => { - expect( view.manualDecoratorsUIView.length ).to.equal( 3 ); + it( 'switch buttons reflects state of manual decorators', () => { + expect( view._manualDecoratorSwitches.length ).to.equal( 3 ); - expect( view.manualDecoratorsUIView.get( 0 ) ).to.deep.include( { + expect( view._manualDecoratorSwitches.get( 0 ) ).to.deep.include( { name: 'decorator1', label: 'Foo' } ); - expect( view.manualDecoratorsUIView.get( 1 ) ).to.deep.include( { + expect( view._manualDecoratorSwitches.get( 1 ) ).to.deep.include( { name: 'decorator2', label: 'Download' } ); - expect( view.manualDecoratorsUIView.get( 2 ) ).to.deep.include( { + expect( view._manualDecoratorSwitches.get( 2 ) ).to.deep.include( { name: 'decorator3', label: 'Multi' } ); @@ -244,7 +244,7 @@ describe( 'LinkFormView', () => { it( 'reacts on switch button changes', () => { const modelItem = collection.first; - const viewItem = view.manualDecoratorsUIView.first; + const viewItem = view._manualDecoratorSwitches.first; expect( modelItem.value ).to.be.undefined; expect( viewItem.isOn ).to.be.undefined; @@ -259,5 +259,27 @@ describe( 'LinkFormView', () => { expect( modelItem.value ).to.be.false; expect( viewItem.isOn ).to.be.false; } ); + + describe( 'getDecoratorSwitchesState()', () => { + it( 'should provide object with decorators states', () => { + expect( view.getDecoratorSwitchesState() ).to.deep.equal( { + decorator1: undefined, + decorator2: undefined, + decorator3: undefined + } ); + + view._manualDecoratorSwitches.map( item => { + item.element.dispatchEvent( new Event( 'click' ) ); + } ); + + view._manualDecoratorSwitches.get( 2 ).element.dispatchEvent( new Event( 'click' ) ); + + expect( view.getDecoratorSwitchesState() ).to.deep.equal( { + decorator1: true, + decorator2: true, + decorator3: false + } ); + } ); + } ); } ); } ); From 3db53cfbb084ec42d512ae1103453b243aaef90e Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 18 Jun 2019 13:03:21 +0200 Subject: [PATCH 58/81] Simplify collection of switches with decorators. --- src/ui/linkformview.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 025046f..04749dc 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -272,23 +272,23 @@ export default class LinkFormView extends View { _createManualDecoratorSwitches() { const switches = this.createCollection(); - switches.bindTo( this.manualDecorators ).using( item => { + for ( const manualDecorator of this.manualDecorators ) { const switchButton = new SwitchButtonView( this.locale ); switchButton.set( { - name: item.id, - label: item.label, + name: manualDecorator.id, + label: manualDecorator.label, withText: true } ); - switchButton.bind( 'isOn' ).to( item, 'value' ); + switchButton.bind( 'isOn' ).to( manualDecorator, 'value' ); switchButton.on( 'execute', () => { - item.set( 'value', !switchButton.isOn ); + manualDecorator.set( 'value', !switchButton.isOn ); } ); - return switchButton; - } ); + switches.add( switchButton ); + } return switches; } From e5463c3b73ecbd3bcee14a8e0aad48983a2e4356 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 18 Jun 2019 14:37:49 +0200 Subject: [PATCH 59/81] Remove referrence to manualDecorators in form view. Rename manualDecorators to manualDecoratorCollection. Fix test and references to property. --- src/linkcommand.js | 4 ++-- src/linkediting.js | 6 +++--- src/linkui.js | 2 +- src/ui/linkformview.js | 35 +++++++++++++---------------------- src/unlinkcommand.js | 2 +- tests/linkcommand.js | 4 ++-- tests/ui/linkformview.js | 6 +++--- 7 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index 6ebbc1f..469afc5 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -38,7 +38,7 @@ export default class LinkCommand extends Command { * @readonly * @type {module:utils/collection~Collection.} */ - this.manualDecorators = new Collection(); + this.manualDecoratorCollection = new Collection(); } /** @@ -50,7 +50,7 @@ export default class LinkCommand extends Command { this.value = doc.selection.getAttribute( 'linkHref' ); - for ( const manualDecorator of this.manualDecorators ) { + for ( const manualDecorator of this.manualDecoratorCollection ) { manualDecorator.value = doc.selection.getAttribute( manualDecorator.id ) || false; } diff --git a/src/linkediting.js b/src/linkediting.js index 2c46dda..7d66d74 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -143,7 +143,7 @@ export default class LinkEditing extends Plugin { const editor = this.editor; const command = editor.commands.get( 'link' ); - const manualDecorators = command.manualDecorators; + const manualDecoratorCollection = command.manualDecoratorCollection; manualDecoratorDefinitions.forEach( ( decorator, index ) => { const decoratorName = `linkManualDecorator${ index }`; @@ -151,7 +151,7 @@ export default class LinkEditing extends Plugin { editor.model.schema.extend( '$text', { allowAttributes: decoratorName } ); // Keeps reference to manual decorator to decode its name to attributes during downcast. - manualDecorators.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); + manualDecoratorCollection.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); editor.conversion.for( 'downcast' ).attributeToElement( { model: decoratorName, @@ -159,7 +159,7 @@ export default class LinkEditing extends Plugin { if ( manualDecoratorName ) { const element = writer.createAttributeElement( 'a', - manualDecorators.get( decoratorName ).attributes, + manualDecoratorCollection.get( decoratorName ).attributes, { priority: 5 } diff --git a/src/linkui.js b/src/linkui.js index 32d816b..982d7e2 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -144,7 +144,7 @@ export default class LinkUI extends Plugin { const editor = this.editor; const linkCommand = editor.commands.get( 'link' ); - const formView = new LinkFormView( editor.locale, linkCommand.manualDecorators ); + const formView = new LinkFormView( editor.locale, linkCommand.manualDecoratorCollection ); formView.urlInputView.bind( 'value' ).to( linkCommand, 'value' ); diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 04749dc..242dc9f 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -38,10 +38,10 @@ export default class LinkFormView extends View { * Also see {@link #render}. * * @param {module:utils/locale~Locale} [locale] The localization services instance. - * @param {module:utils/collection~Collection} [manualDecorators] Reference to custom attributes in + * @param {module:utils/collection~Collection} [manualDecoratorCollection] Reference to manual decorators in * {@link module:link/linkcommand~LinkCommand#manualDecorators}. */ - constructor( locale, manualDecorators ) { + constructor( locale, manualDecoratorCollection = [] ) { super( locale ); const t = locale.t; @@ -84,19 +84,6 @@ export default class LinkFormView extends View { */ this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' ); - /** - * A reference to {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators} - * collection of the {@link module:link/linkcommand~LinkCommand}. - * - * Helps bootstrap the {@link #manualDecoratorsUIView UI} so it corresponds with the global - * configuration of manual decorators and to synchronize its state later on when the user - * is editing the content. - * - * @readonly - * @type {model:utils/collection~Collection} - */ - this.manualDecorators = manualDecorators; - /** * A collection of {@link module:ui/button/switchbuttonview~SwitchButtonView}, * which corresponds to {@link #manualDecorators manual decorators} configured in the editor. @@ -106,7 +93,7 @@ export default class LinkFormView extends View { * @readonly * @type {module:ui/viewcollection~ViewCollection} */ - this._manualDecoratorSwitches = this._createManualDecoratorSwitches(); + this._manualDecoratorSwitches = this._createManualDecoratorSwitches( manualDecoratorCollection ); /** * Collection of child views in the form. @@ -114,7 +101,7 @@ export default class LinkFormView extends View { * @readonly * @type {module:ui/viewcollection~ViewCollection} */ - this.children = this._createFormChildren(); + this.children = this._createFormChildren( manualDecoratorCollection ); /** * A collection of views which can be focused in the form. @@ -147,7 +134,7 @@ export default class LinkFormView extends View { const classList = [ 'ck', 'ck-link-form' ]; - if ( this.manualDecorators.length ) { + if ( manualDecoratorCollection.length ) { classList.push( 'ck-link-form_layout-vertical' ); } @@ -267,12 +254,14 @@ export default class LinkFormView extends View { * made based on {@link #manualDecorators} * * @private + * @param {module:link/linkcommand~LinkCommand#manualDecorators} manualDecoratorCollection reference to collection of manual decorators + * stored in link's command. * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. */ - _createManualDecoratorSwitches() { + _createManualDecoratorSwitches( manualDecoratorCollection ) { const switches = this.createCollection(); - for ( const manualDecorator of this.manualDecorators ) { + for ( const manualDecorator of manualDecoratorCollection ) { const switchButton = new SwitchButtonView( this.locale ); switchButton.set( { @@ -301,14 +290,16 @@ export default class LinkFormView extends View { * to those decorators. * * @private + * @param {module:link/linkcommand~LinkCommand#manualDecorators} manualDecoratorCollection reference to collection of manual decorators + * stored in link's command. * @returns {module:ui/viewcollection~ViewCollection} children of LinkFormView. */ - _createFormChildren() { + _createFormChildren( manualDecoratorCollection ) { const children = this.createCollection(); children.add( this.urlInputView ); - if ( this.manualDecorators.length ) { + if ( manualDecoratorCollection.length ) { const additionalButtonsView = new View(); additionalButtonsView.setTemplate( { diff --git a/src/unlinkcommand.js b/src/unlinkcommand.js index 5e0e8f6..47bdfc3 100644 --- a/src/unlinkcommand.js +++ b/src/unlinkcommand.js @@ -47,7 +47,7 @@ export default class UnlinkCommand extends Command { writer.removeAttribute( 'linkHref', range ); // If there are registered custom attributes, then remove them during unlink. if ( linkCommand ) { - for ( const manualDecorator of linkCommand.manualDecorators ) { + for ( const manualDecorator of linkCommand.manualDecoratorCollection ) { writer.removeAttribute( manualDecorator.id, range ); } } diff --git a/tests/linkcommand.js b/tests/linkcommand.js index 3def477..bc1db5b 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -270,14 +270,14 @@ describe( 'LinkCommand', () => { model = editor.model; command = new LinkCommand( editor ); - command.manualDecorators.add( new ManualDecorator( { + command.manualDecoratorCollection.add( new ManualDecorator( { id: 'linkManualDecorator0', label: 'Foo', attributes: { class: 'Foo' } } ) ); - command.manualDecorators.add( new ManualDecorator( { + command.manualDecoratorCollection.add( new ManualDecorator( { id: 'linkManualDecorator1', label: 'Bar', attributes: { diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 112db40..b0150b9 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -22,7 +22,7 @@ describe( 'LinkFormView', () => { testUtils.createSinonSandbox(); beforeEach( () => { - view = new LinkFormView( { t: val => val }, [] ); + view = new LinkFormView( { t: val => val } ); view.render(); } ); @@ -106,7 +106,7 @@ describe( 'LinkFormView', () => { it( 'should register child views\' #element in #focusTracker', () => { const spy = testUtils.sinon.spy( FocusTracker.prototype, 'add' ); - view = new LinkFormView( { t: () => {} }, [] ); + view = new LinkFormView( { t: () => {} } ); view.render(); sinon.assert.calledWithExactly( spy.getCall( 0 ), view.urlInputView.element ); @@ -115,7 +115,7 @@ describe( 'LinkFormView', () => { } ); it( 'starts listening for #keystrokes coming from #element', () => { - view = new LinkFormView( { t: () => {} }, [] ); + view = new LinkFormView( { t: () => {} } ); const spy = sinon.spy( view.keystrokes, 'listenTo' ); From dec7d4ad7b782ecc152a4347dc7ae99c1ede42eb Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Tue, 18 Jun 2019 14:53:15 +0200 Subject: [PATCH 60/81] Extend description and provide examples for link command. --- src/link.js | 2 +- src/linkcommand.js | 44 ++++++++++++++++++++++++++++++++++++ src/linkediting.js | 2 +- src/ui/linkformview.js | 26 +++++++++++---------- src/unlinkcommand.js | 10 ++++++++ src/utils/manualdecorator.js | 2 +- 6 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/link.js b/src/link.js index 6e560d7..8f4aeff 100644 --- a/src/link.js +++ b/src/link.js @@ -66,7 +66,7 @@ export default class Link extends Plugin { * * **Note**: To control the `target` and `rel` attributes of specific links in the edited content, a dedicated * {@link module:link/link~LinkDecoratorManualDefinition manual} decorator must be defined in the - * {@link module:link/link~LinkConfig#decorators `config.link.decodators`} array. In such scenario, + * {@link module:link/link~LinkConfig#decorators `config.link.decorators`} array. In such scenario, * the `config.link.addTargetToExternalLinks` option should remain `undefined` or `false` to not interfere with the manual decorator. * * **Note**: It is possible to add other {@link module:link/link~LinkDecoratorAutomaticDefinition automatic} diff --git a/src/linkcommand.js b/src/linkcommand.js index 469afc5..8d9ee19 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -80,6 +80,50 @@ export default class LinkCommand extends Command { * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} * documentation. * + * Usage of `manualDecoratorIds` in link command: + * + * const linkCommand = editor.commands.get( 'link' ); + * + * // 1. Add new decorator ( empty model ) + * linkCommand.execute( 'http://example.com', { + * linkDecorator0: true + * } ); + * // result: <$text href="http://example.com" linkDecorator0="true">http://example.com + * + * // 2. Remove decorator over selection: + * // [<$text href="http://example.com" linkDecorator0="true">http://example.com] + * linkCommand.execute( 'http://example.com', { + * linkDecorator0: false + * } ); + * // result: <$text href="http://example.com">http://example.com + * + * There is possibility to modify multiple decorators at the same time: + * + * const linkCommand = editor.commands.get( 'link' ); + * + * // 1. Add new decorator ( empty model ) + * linkCommand.execute( 'http://example.com', { + * linkDecorator0: true, + * linkDecorator2: true, + * } ); + * // result: + * // <$text href="http://example.com" linkDecorator0="true" linkDecorator2="true">http://example.com + * + * // 2. Remove and add new decorator over selection: + * // [<$text href="http://example.com" linkDecorator0="true" linkDecorator2="true">http://example.com] + * linkCommand.execute( 'http://example.com', { + * linkDecorator0: false, + * linkDecorator1: true, + * linkDecorator2: false, + * } ); + * // result: + * // <$text href="http://example.com" linkDecorator1="true">http://example.com + * + * **Note**: If decorator name is not passed to the command, then its state remains untouched. + * + * **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} also removes all decorators + * from the link. + * * @fires execute * @param {String} href Link destination. * @param {Object} [manualDecoratorIds={}] The information about manual decorator attributes to be applied or removed upon execution. diff --git a/src/linkediting.js b/src/linkediting.js index 7d66d74..20a3c1c 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -127,7 +127,7 @@ export default class LinkEditing extends Plugin { /** * Processes an array of configured {@link module:link/link~LinkDecoratorManualDefinition manual decorators} * and transforms them into {@link module:link/utils~ManualDecorator} instances and stores them in the - * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state). + * {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection} collection (a model for manual decorators state). * * Also registers an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} * converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema} diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index 242dc9f..ad30195 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -39,7 +39,7 @@ export default class LinkFormView extends View { * * @param {module:utils/locale~Locale} [locale] The localization services instance. * @param {module:utils/collection~Collection} [manualDecoratorCollection] Reference to manual decorators in - * {@link module:link/linkcommand~LinkCommand#manualDecorators}. + * {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection}. */ constructor( locale, manualDecoratorCollection = [] ) { super( locale ); @@ -86,8 +86,8 @@ export default class LinkFormView extends View { /** * A collection of {@link module:ui/button/switchbuttonview~SwitchButtonView}, - * which corresponds to {@link #manualDecorators manual decorators} configured in the editor. - * Populated by {@link #_createManualDecoratorsUIView}. + * which corresponds to {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection manual decorators} + * configured in the editor. * * @private * @readonly @@ -153,9 +153,11 @@ export default class LinkFormView extends View { } /** - * Obtain state of the switch buttons in a currently opened {@link module:link/ui/linkformview~LinkFormView}. + * Obtain state of the {@link module:ui/button/switchbuttonview~SwitchButtonView switch buttons} representing + * {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection manual decorators} + * in a currently opened {@link module:link/ui/linkformview~LinkFormView}. * - * @returns {Object} key-value pairs, where key is the name of the decorator and value is its state. + * @returns {Object} key-value pairs, where the key is the name of the decorator and the value is its state. */ getDecoratorSwitchesState() { return Array.from( this._manualDecoratorSwitches ).reduce( ( accumulator, switchButton ) => { @@ -251,11 +253,11 @@ export default class LinkFormView extends View { /** * Populates {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView} - * made based on {@link #manualDecorators} + * made based on {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection} * * @private - * @param {module:link/linkcommand~LinkCommand#manualDecorators} manualDecoratorCollection reference to collection of manual decorators - * stored in link's command. + * @param {module:link/linkcommand~LinkCommand#manualDecoratorCollection} manualDecoratorCollection reference to + * collection of manual decorators stored in link's command. * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. */ _createManualDecoratorSwitches( manualDecoratorCollection ) { @@ -285,13 +287,13 @@ export default class LinkFormView extends View { /** * Populates the {@link #children} collection of the form. * - * If {@link #manualDecorators manual decorators} are configured in the editor, creates an - * additional `View` wrapping all {@link #manualDecoratorsUIView} switch buttons corresponding + * If {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection manual decorators} are configured in the editor, creates an + * additional `View` wrapping all {@link #_manualDecoratorSwitches} switch buttons corresponding * to those decorators. * * @private - * @param {module:link/linkcommand~LinkCommand#manualDecorators} manualDecoratorCollection reference to collection of manual decorators - * stored in link's command. + * @param {module:link/linkcommand~LinkCommand#manualDecoratorCollection} manualDecoratorCollection reference to + * collection of manual decorators stored in link's command. * @returns {module:ui/viewcollection~ViewCollection} children of LinkFormView. */ _createFormChildren( manualDecoratorCollection ) { diff --git a/src/unlinkcommand.js b/src/unlinkcommand.js index 47bdfc3..3acf7fd 100644 --- a/src/unlinkcommand.js +++ b/src/unlinkcommand.js @@ -29,6 +29,16 @@ export default class UnlinkCommand extends Command { * When the selection is collapsed, removes the `linkHref` attribute from each node with the same `linkHref` attribute value. * When the selection is non-collapsed, removes the `linkHref` attribute from each node in selected ranges. * + * # Decorators + * If there are defined {@link module:link/link~LinkConfig#decorators decorators} in the editor's configuration, + * then all decorators are removed together with `linkHref` attribute. + * + * const unlinkCommand = editor.commands.get( 'unlink' )' + * + * // model: [<$text href="example.com" linkDecorator0="true">Foo bar] + * unlinkCommand.execute(); + * // model: [Foo bar] + * * @fires execute */ execute() { diff --git a/src/utils/manualdecorator.js b/src/utils/manualdecorator.js index 62e344b..0b2d441 100644 --- a/src/utils/manualdecorator.js +++ b/src/utils/manualdecorator.js @@ -13,7 +13,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; /** * Helper class which stores manual decorators with observable {@link module:link/utils~ManualDecorator#value} * to support integration with the UI state. An instance of this class is a model with state of single manual decorators. - * These decorators are kept as collections in {@link module:link/linkcommand~LinkCommand#manualDecorators}. + * These decorators are kept as collections in {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection}. * * @mixes module:utils/observablemixin~ObservableMixin */ From 90dfdcde4790e396596269bd406306ba2dd03406 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 19 Jun 2019 10:34:32 +0200 Subject: [PATCH 61/81] Add utils with localization of common labels. --- src/linkediting.js | 4 ++-- src/utils.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/linkediting.js b/src/linkediting.js index 20a3c1c..ec935db 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -10,7 +10,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import LinkCommand from './linkcommand'; import UnlinkCommand from './unlinkcommand'; -import { createLinkElement, ensureSafeUrl } from './utils'; +import { createLinkElement, ensureSafeUrl, getLocalizedDecorators } from './utils'; import AutomaticDecorators from './utils/automaticdecorators'; import ManualDecorator from './utils/manualdecorator'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; @@ -77,7 +77,7 @@ export default class LinkEditing extends Plugin { editor.commands.add( 'link', new LinkCommand( editor ) ); editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); - const linkDecorators = editor.config.get( 'link.decorators' ) || []; + const linkDecorators = getLocalizedDecorators( editor ); this._enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) ); this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) ); diff --git a/src/utils.js b/src/utils.js index 3c299b8..8ddcb23 100644 --- a/src/utils.js +++ b/src/utils.js @@ -59,3 +59,35 @@ function isSafeUrl( url ) { return normalizedUrl.match( SAFE_URL ); } + +/** + * Returns configuration options as defined in {@link module:link/link~LinkConfig#decorators `editor.config.decorators`} but processed + * to respect localization of the editor, i.e. to display {@link module:link/link~LinkDecoratorManualDefinition label} + * in the correct language. + * + * **Note:** Only few most commonly used labels has provided translations. In all other cases decorators configuration should be + * directly translated in configuration. + * + * @param {module:core/editor/editor~Editor} editor An editor instance + * @returns {Array.} + */ +export function getLocalizedDecorators( editor ) { + const t = editor.t; + const decorators = editor.config.get( 'link.decorators' ); + + if ( decorators ) { + const localizedDecoratorsLabels = { + 'Open in a new window': t( 'Open in a new window' ), + 'Downloadable': t( 'Downloadable' ) + }; + + return decorators.map( decorator => { + if ( decorator.label && localizedDecoratorsLabels[ decorator.label ] ) { + decorator.label = localizedDecoratorsLabels[ decorator.label ]; + } + return decorator; + } ); + } else { + return []; + } +} From ed2934f74fde026c729eb8aa008b162f627b2ae9 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 19 Jun 2019 10:36:38 +0200 Subject: [PATCH 62/81] Revert "Remove unit test, for wrongly translated label." Translation are now avaialble in different form. This reverts commit 3c27957780f6eb7f76dbe387e5ec9bd6c3f84ea7. --- tests/ui/linkformview.js | 55 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index b0150b9..cf90dea 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals Event */ +/* globals Event, document */ import LinkFormView from '../../src/ui/linkformview'; import View from '@ckeditor/ckeditor5-ui/src/view'; @@ -15,6 +15,9 @@ import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import ManualDecorator from '../../src/utils/manualdecorator'; import Collection from '@ckeditor/ckeditor5-utils/src/collection'; +import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service'; +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Link from '../../src/link'; describe( 'LinkFormView', () => { let view; @@ -282,4 +285,54 @@ describe( 'LinkFormView', () => { } ); } ); } ); + + describe( 'localization of custom attributes', () => { + before( () => { + addTranslations( 'pl', { + 'Open in new window': 'Otwórz w nowym oknie' + } ); + } ); + after( () => { + clearTranslations(); + } ); + + let editor, editorElement, linkFormView; + + beforeEach( () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ Link ], + toolbar: [ 'link' ], + language: 'pl', + link: { + decorators: [ + { + mode: 'manual', + label: 'Open in new window', + attributes: { + target: '_blank' + } + } + ] + } + } ) + .then( newEditor => { + editor = newEditor; + linkFormView = new LinkFormView( editor.locale, editor.commands.get( 'link' ).customAttributes ); + } ); + } ); + + afterEach( () => { + editorElement.remove(); + + return editor.destroy(); + } ); + + it( 'translates labels of manual decorators UI', () => { + expect( linkFormView.customAttributesView.first.label ).to.equal( 'Otwórz w nowym oknie' ); + } ); + } ); } ); From 727fe0f66185d4c1cd1b8ce316051a58efc1c9c3 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 19 Jun 2019 10:51:41 +0200 Subject: [PATCH 63/81] Fix references in localization tests. --- tests/ui/linkformview.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index cf90dea..0521ceb 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -289,7 +289,7 @@ describe( 'LinkFormView', () => { describe( 'localization of custom attributes', () => { before( () => { addTranslations( 'pl', { - 'Open in new window': 'Otwórz w nowym oknie' + 'Open in a new window': 'Otwórz w nowym oknie' } ); } ); after( () => { @@ -311,7 +311,7 @@ describe( 'LinkFormView', () => { decorators: [ { mode: 'manual', - label: 'Open in new window', + label: 'Open in a new window', attributes: { target: '_blank' } @@ -321,7 +321,7 @@ describe( 'LinkFormView', () => { } ) .then( newEditor => { editor = newEditor; - linkFormView = new LinkFormView( editor.locale, editor.commands.get( 'link' ).customAttributes ); + linkFormView = new LinkFormView( editor.locale, editor.commands.get( 'link' ).manualDecoratorCollection ); } ); } ); @@ -332,7 +332,7 @@ describe( 'LinkFormView', () => { } ); it( 'translates labels of manual decorators UI', () => { - expect( linkFormView.customAttributesView.first.label ).to.equal( 'Otwórz w nowym oknie' ); + expect( linkFormView._manualDecoratorSwitches.first.label ).to.equal( 'Otwórz w nowym oknie' ); } ); } ); } ); From 6299afed62a77fdff2ea29602970e42a5b6faf41 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 19 Jun 2019 14:58:51 +0200 Subject: [PATCH 64/81] Improvements to the decorator translations. --- src/link.js | 2 +- src/utils.js | 2 +- tests/ui/linkformview.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/link.js b/src/link.js index 8f4aeff..ae2851c 100644 --- a/src/link.js +++ b/src/link.js @@ -169,7 +169,7 @@ export default class Link extends Plugin { * * { * mode: 'manual', - * label: 'Open link in new window', + * label: 'Open in a new tab', * attributes: { * target: '_blank', * rel: 'noopener noreferrer' diff --git a/src/utils.js b/src/utils.js index 8ddcb23..812ec3a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -77,7 +77,7 @@ export function getLocalizedDecorators( editor ) { if ( decorators ) { const localizedDecoratorsLabels = { - 'Open in a new window': t( 'Open in a new window' ), + 'Open in a new tab': t( 'Open in a new tab' ), 'Downloadable': t( 'Downloadable' ) }; diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 0521ceb..fbe7835 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -289,7 +289,7 @@ describe( 'LinkFormView', () => { describe( 'localization of custom attributes', () => { before( () => { addTranslations( 'pl', { - 'Open in a new window': 'Otwórz w nowym oknie' + 'Open in a new tab': 'Otwórz w nowym oknie' } ); } ); after( () => { @@ -311,7 +311,7 @@ describe( 'LinkFormView', () => { decorators: [ { mode: 'manual', - label: 'Open in a new window', + label: 'Open in a new tab', attributes: { target: '_blank' } From 165244cc988bea2f80bb24feb635baa076ae6fb7 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 19 Jun 2019 15:02:07 +0200 Subject: [PATCH 65/81] Docs: Improved `LinkCommand#execute()` documentation section about decorators. --- src/linkcommand.js | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index 8d9ee19..339722e 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -80,49 +80,37 @@ export default class LinkCommand extends Command { * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} * documentation. * - * Usage of `manualDecoratorIds` in link command: + * Here is how to manage decorator attributes via the link command: * * const linkCommand = editor.commands.get( 'link' ); * - * // 1. Add new decorator ( empty model ) + * // Adding a new decorator attribute. * linkCommand.execute( 'http://example.com', { * linkDecorator0: true * } ); - * // result: <$text href="http://example.com" linkDecorator0="true">http://example.com * - * // 2. Remove decorator over selection: - * // [<$text href="http://example.com" linkDecorator0="true">http://example.com] + * // Removing a decorator attribute from a selection. * linkCommand.execute( 'http://example.com', { * linkDecorator0: false * } ); - * // result: <$text href="http://example.com">http://example.com * - * There is possibility to modify multiple decorators at the same time: - * - * const linkCommand = editor.commands.get( 'link' ); - * - * // 1. Add new decorator ( empty model ) + * // Adding multiple decorator attributes at a time. * linkCommand.execute( 'http://example.com', { * linkDecorator0: true, * linkDecorator2: true, * } ); - * // result: - * // <$text href="http://example.com" linkDecorator0="true" linkDecorator2="true">http://example.com * - * // 2. Remove and add new decorator over selection: - * // [<$text href="http://example.com" linkDecorator0="true" linkDecorator2="true">http://example.com] + * // Removing and adding decorator attributes at a time. * linkCommand.execute( 'http://example.com', { * linkDecorator0: false, * linkDecorator1: true, * linkDecorator2: false, * } ); - * // result: - * // <$text href="http://example.com" linkDecorator1="true">http://example.com * - * **Note**: If decorator name is not passed to the command, then its state remains untouched. + * **Note**: If decorator attribute name is not specified its state remains untouched. * - * **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} also removes all decorators - * from the link. + * **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all + * decorator attributes. * * @fires execute * @param {String} href Link destination. From c3edb6fa87934ba92645c5c08122af7af944ee3e Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 19 Jun 2019 15:14:51 +0200 Subject: [PATCH 66/81] Code refactoring. --- src/linkcommand.js | 4 ++-- src/linkediting.js | 8 ++++---- src/linkui.js | 2 +- src/ui/linkformview.js | 39 ++++++++++++++++++------------------ src/unlinkcommand.js | 11 +++------- src/utils.js | 8 ++++---- src/utils/manualdecorator.js | 2 +- tests/linkcommand.js | 4 ++-- tests/ui/linkformview.js | 2 +- 9 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index 339722e..8caca00 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -38,7 +38,7 @@ export default class LinkCommand extends Command { * @readonly * @type {module:utils/collection~Collection.} */ - this.manualDecoratorCollection = new Collection(); + this.manualDecorators = new Collection(); } /** @@ -50,7 +50,7 @@ export default class LinkCommand extends Command { this.value = doc.selection.getAttribute( 'linkHref' ); - for ( const manualDecorator of this.manualDecoratorCollection ) { + for ( const manualDecorator of this.manualDecorators ) { manualDecorator.value = doc.selection.getAttribute( manualDecorator.id ) || false; } diff --git a/src/linkediting.js b/src/linkediting.js index ec935db..13ecced 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -127,7 +127,7 @@ export default class LinkEditing extends Plugin { /** * Processes an array of configured {@link module:link/link~LinkDecoratorManualDefinition manual decorators} * and transforms them into {@link module:link/utils~ManualDecorator} instances and stores them in the - * {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection} collection (a model for manual decorators state). + * {@link module:link/linkcommand~LinkCommand#manualDecorators} collection (a model for manual decorators state). * * Also registers an {@link module:engine/conversion/downcasthelpers~DowncastHelpers#attributeToElement attributeToElement} * converter for each manual decorator and extends the {@link module:engine/model/schema~Schema model's schema} @@ -143,7 +143,7 @@ export default class LinkEditing extends Plugin { const editor = this.editor; const command = editor.commands.get( 'link' ); - const manualDecoratorCollection = command.manualDecoratorCollection; + const manualDecorators = command.manualDecorators; manualDecoratorDefinitions.forEach( ( decorator, index ) => { const decoratorName = `linkManualDecorator${ index }`; @@ -151,7 +151,7 @@ export default class LinkEditing extends Plugin { editor.model.schema.extend( '$text', { allowAttributes: decoratorName } ); // Keeps reference to manual decorator to decode its name to attributes during downcast. - manualDecoratorCollection.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); + manualDecorators.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); editor.conversion.for( 'downcast' ).attributeToElement( { model: decoratorName, @@ -159,7 +159,7 @@ export default class LinkEditing extends Plugin { if ( manualDecoratorName ) { const element = writer.createAttributeElement( 'a', - manualDecoratorCollection.get( decoratorName ).attributes, + manualDecorators.get( decoratorName ).attributes, { priority: 5 } diff --git a/src/linkui.js b/src/linkui.js index 982d7e2..32d816b 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -144,7 +144,7 @@ export default class LinkUI extends Plugin { const editor = this.editor; const linkCommand = editor.commands.get( 'link' ); - const formView = new LinkFormView( editor.locale, linkCommand.manualDecoratorCollection ); + const formView = new LinkFormView( editor.locale, linkCommand.manualDecorators ); formView.urlInputView.bind( 'value' ).to( linkCommand, 'value' ); diff --git a/src/ui/linkformview.js b/src/ui/linkformview.js index ad30195..6205d64 100644 --- a/src/ui/linkformview.js +++ b/src/ui/linkformview.js @@ -38,10 +38,10 @@ export default class LinkFormView extends View { * Also see {@link #render}. * * @param {module:utils/locale~Locale} [locale] The localization services instance. - * @param {module:utils/collection~Collection} [manualDecoratorCollection] Reference to manual decorators in - * {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection}. + * @param {module:utils/collection~Collection} [manualDecorators] Reference to manual decorators in + * {@link module:link/linkcommand~LinkCommand#manualDecorators}. */ - constructor( locale, manualDecoratorCollection = [] ) { + constructor( locale, manualDecorators = [] ) { super( locale ); const t = locale.t; @@ -86,14 +86,14 @@ export default class LinkFormView extends View { /** * A collection of {@link module:ui/button/switchbuttonview~SwitchButtonView}, - * which corresponds to {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection manual decorators} + * which corresponds to {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators} * configured in the editor. * * @private * @readonly * @type {module:ui/viewcollection~ViewCollection} */ - this._manualDecoratorSwitches = this._createManualDecoratorSwitches( manualDecoratorCollection ); + this._manualDecoratorSwitches = this._createManualDecoratorSwitches( manualDecorators ); /** * Collection of child views in the form. @@ -101,7 +101,7 @@ export default class LinkFormView extends View { * @readonly * @type {module:ui/viewcollection~ViewCollection} */ - this.children = this._createFormChildren( manualDecoratorCollection ); + this.children = this._createFormChildren( manualDecorators ); /** * A collection of views which can be focused in the form. @@ -134,7 +134,7 @@ export default class LinkFormView extends View { const classList = [ 'ck', 'ck-link-form' ]; - if ( manualDecoratorCollection.length ) { + if ( manualDecorators.length ) { classList.push( 'ck-link-form_layout-vertical' ); } @@ -153,11 +153,12 @@ export default class LinkFormView extends View { } /** - * Obtain state of the {@link module:ui/button/switchbuttonview~SwitchButtonView switch buttons} representing - * {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection manual decorators} - * in a currently opened {@link module:link/ui/linkformview~LinkFormView}. + * Obtains the state of the {@link module:ui/button/switchbuttonview~SwitchButtonView switch buttons} representing + * {@link module:link/linkcommand~LinkCommand#manualDecorators manual link decorators} + * in the {@link module:link/ui/linkformview~LinkFormView}. * - * @returns {Object} key-value pairs, where the key is the name of the decorator and the value is its state. + * @returns {Object.} key-value pairs, where the key is the name of the decorator and the value is + * its state. */ getDecoratorSwitchesState() { return Array.from( this._manualDecoratorSwitches ).reduce( ( accumulator, switchButton ) => { @@ -253,17 +254,17 @@ export default class LinkFormView extends View { /** * Populates {@link module:ui/viewcollection~ViewCollection} of {@link module:ui/button/switchbuttonview~SwitchButtonView} - * made based on {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection} + * made based on {@link module:link/linkcommand~LinkCommand#manualDecorators} * * @private - * @param {module:link/linkcommand~LinkCommand#manualDecoratorCollection} manualDecoratorCollection reference to + * @param {module:link/linkcommand~LinkCommand#manualDecorators} manualDecorators reference to * collection of manual decorators stored in link's command. * @returns {module:ui/viewcollection~ViewCollection} of Switch Buttons. */ - _createManualDecoratorSwitches( manualDecoratorCollection ) { + _createManualDecoratorSwitches( manualDecorators ) { const switches = this.createCollection(); - for ( const manualDecorator of manualDecoratorCollection ) { + for ( const manualDecorator of manualDecorators ) { const switchButton = new SwitchButtonView( this.locale ); switchButton.set( { @@ -287,21 +288,21 @@ export default class LinkFormView extends View { /** * Populates the {@link #children} collection of the form. * - * If {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection manual decorators} are configured in the editor, creates an + * If {@link module:link/linkcommand~LinkCommand#manualDecorators manual decorators} are configured in the editor, creates an * additional `View` wrapping all {@link #_manualDecoratorSwitches} switch buttons corresponding * to those decorators. * * @private - * @param {module:link/linkcommand~LinkCommand#manualDecoratorCollection} manualDecoratorCollection reference to + * @param {module:link/linkcommand~LinkCommand#manualDecorators} manualDecorators reference to * collection of manual decorators stored in link's command. * @returns {module:ui/viewcollection~ViewCollection} children of LinkFormView. */ - _createFormChildren( manualDecoratorCollection ) { + _createFormChildren( manualDecorators ) { const children = this.createCollection(); children.add( this.urlInputView ); - if ( manualDecoratorCollection.length ) { + if ( manualDecorators.length ) { const additionalButtonsView = new View(); additionalButtonsView.setTemplate( { diff --git a/src/unlinkcommand.js b/src/unlinkcommand.js index 3acf7fd..84f3846 100644 --- a/src/unlinkcommand.js +++ b/src/unlinkcommand.js @@ -30,14 +30,9 @@ export default class UnlinkCommand extends Command { * When the selection is non-collapsed, removes the `linkHref` attribute from each node in selected ranges. * * # Decorators - * If there are defined {@link module:link/link~LinkConfig#decorators decorators} in the editor's configuration, - * then all decorators are removed together with `linkHref` attribute. * - * const unlinkCommand = editor.commands.get( 'unlink' )' - * - * // model: [<$text href="example.com" linkDecorator0="true">Foo bar] - * unlinkCommand.execute(); - * // model: [Foo bar] + * If {@link module:link/link~LinkConfig#decorators `config.link.decorators`} is specified, + * all configured decorators are removed together with the `linkHref` attribute. * * @fires execute */ @@ -57,7 +52,7 @@ export default class UnlinkCommand extends Command { writer.removeAttribute( 'linkHref', range ); // If there are registered custom attributes, then remove them during unlink. if ( linkCommand ) { - for ( const manualDecorator of linkCommand.manualDecoratorCollection ) { + for ( const manualDecorator of linkCommand.manualDecorators ) { writer.removeAttribute( manualDecorator.id, range ); } } diff --git a/src/utils.js b/src/utils.js index 812ec3a..88216a4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -61,12 +61,12 @@ function isSafeUrl( url ) { } /** - * Returns configuration options as defined in {@link module:link/link~LinkConfig#decorators `editor.config.decorators`} but processed - * to respect localization of the editor, i.e. to display {@link module:link/link~LinkDecoratorManualDefinition label} + * Returns {@link module:link/link~LinkConfig#decorators `config.link.decorators`} configuration processed + * to respect the locale of the editor, i.e. to display {@link module:link/link~LinkDecoratorManualDefinition label} * in the correct language. * - * **Note:** Only few most commonly used labels has provided translations. In all other cases decorators configuration should be - * directly translated in configuration. + * **Note**: Only the few most commonly used labels are translated automatically. Other labels should be manually + * translated in the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} configuration. * * @param {module:core/editor/editor~Editor} editor An editor instance * @returns {Array.} diff --git a/src/utils/manualdecorator.js b/src/utils/manualdecorator.js index 0b2d441..62e344b 100644 --- a/src/utils/manualdecorator.js +++ b/src/utils/manualdecorator.js @@ -13,7 +13,7 @@ import mix from '@ckeditor/ckeditor5-utils/src/mix'; /** * Helper class which stores manual decorators with observable {@link module:link/utils~ManualDecorator#value} * to support integration with the UI state. An instance of this class is a model with state of single manual decorators. - * These decorators are kept as collections in {@link module:link/linkcommand~LinkCommand#manualDecoratorCollection}. + * These decorators are kept as collections in {@link module:link/linkcommand~LinkCommand#manualDecorators}. * * @mixes module:utils/observablemixin~ObservableMixin */ diff --git a/tests/linkcommand.js b/tests/linkcommand.js index bc1db5b..3def477 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -270,14 +270,14 @@ describe( 'LinkCommand', () => { model = editor.model; command = new LinkCommand( editor ); - command.manualDecoratorCollection.add( new ManualDecorator( { + command.manualDecorators.add( new ManualDecorator( { id: 'linkManualDecorator0', label: 'Foo', attributes: { class: 'Foo' } } ) ); - command.manualDecoratorCollection.add( new ManualDecorator( { + command.manualDecorators.add( new ManualDecorator( { id: 'linkManualDecorator1', label: 'Bar', attributes: { diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index fbe7835..14fadba 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -321,7 +321,7 @@ describe( 'LinkFormView', () => { } ) .then( newEditor => { editor = newEditor; - linkFormView = new LinkFormView( editor.locale, editor.commands.get( 'link' ).manualDecoratorCollection ); + linkFormView = new LinkFormView( editor.locale, editor.commands.get( 'link' ).manualDecorators ); } ); } ); From 0a2d62c771af5e15d5b21c19fdaef68caef5def9 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 19 Jun 2019 16:13:00 +0200 Subject: [PATCH 67/81] Fix bug when state of switch buttons was not reset when link form was cancelled. --- src/linkcommand.js | 23 ++++++++++++++++++++++- src/linkui.js | 4 ++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/linkcommand.js b/src/linkcommand.js index 8caca00..813edd7 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -41,6 +41,15 @@ export default class LinkCommand extends Command { this.manualDecorators = new Collection(); } + /** + * Synchronize state of the decorator with actually present elements in the model. + */ + restoreManualDecoratorStates() { + for ( const manualDecorator of this.manualDecorators ) { + manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id ); + } + } + /** * @inheritDoc */ @@ -51,7 +60,7 @@ export default class LinkCommand extends Command { this.value = doc.selection.getAttribute( 'linkHref' ); for ( const manualDecorator of this.manualDecorators ) { - manualDecorator.value = doc.selection.getAttribute( manualDecorator.id ) || false; + manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id ); } this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'linkHref' ); @@ -192,4 +201,16 @@ export default class LinkCommand extends Command { } } ); } + + /** + * Method provides information if given decorator is present in currently processed selection. + * + * @private + * @param {String} decoratorName name of a link decorator used in the model + * @returns {Boolean} Information if a given decorator is currently present in a selection + */ + _getDecoratorStateFromModel( decoratorName ) { + const doc = this.editor.model.document; + return doc.selection.getAttribute( decoratorName ) || false; + } } diff --git a/src/linkui.js b/src/linkui.js index 32d816b..5a04959 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -319,6 +319,10 @@ export default class LinkUI extends Plugin { _closeFormView() { const linkCommand = this.editor.commands.get( 'link' ); + // Reset manual decorator states to represent current model state. This case is important to reset switch buttons, + // when user cancel editing form. + linkCommand.restoreManualDecoratorStates(); + if ( linkCommand.value !== undefined ) { this._removeFormView(); } else { From 60829574fa505cf2206c13d0baac5e39f9b923b6 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 19 Jun 2019 16:26:09 +0200 Subject: [PATCH 68/81] Add description for string translations. --- lang/contexts.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lang/contexts.json b/lang/contexts.json index 328cb96..fb86ff8 100644 --- a/lang/contexts.json +++ b/lang/contexts.json @@ -4,5 +4,7 @@ "Link URL": "Label for the URL input in the Link URL editing balloon.", "Edit link": "Button opening the Link URL editing balloon.", "Open link in new tab": "Button opening the link in new browser tab.", - "This link has no URL": "Label explaining that a link has no URL set (the URL is empty)." + "This link has no URL": "Label explaining that a link has no URL set (the URL is empty).", + "Open link in a new tab": "One of the commonly used labels in manual decorators determines if given link can be opened in a new tab", + "Downloadable": "One of the commonly used labels in manual decorators determines if given link should be treat as link to download resources." } From a6951ec061ace70fffd976512acb9986e561f399 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Wed, 19 Jun 2019 16:49:31 +0200 Subject: [PATCH 69/81] Fix unit test name. --- tests/ui/linkformview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 14fadba..ed90af5 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -286,7 +286,7 @@ describe( 'LinkFormView', () => { } ); } ); - describe( 'localization of custom attributes', () => { + describe( 'localization of manual decorators', () => { before( () => { addTranslations( 'pl', { 'Open in a new tab': 'Otwórz w nowym oknie' From 3bfd2c564a019a68892641edc8e857698c70b76d Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 11:04:39 +0200 Subject: [PATCH 70/81] Provide unit test for new methods related to updateing switch button status. --- tests/linkcommand.js | 41 ++++++++++++++ tests/linkui.js | 113 ++++++++++++++++++++++++--------------- tests/ui/linkformview.js | 1 + 3 files changed, 112 insertions(+), 43 deletions(-) diff --git a/tests/linkcommand.js b/tests/linkcommand.js index 3def477..76ac11d 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -353,5 +353,46 @@ describe( 'LinkCommand', () => { expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); } ); } ); + + describe( 'restoreManualDecoratorStates()', () => { + it( 'synchronize values with current model state', () => { + setData( model, 'foo<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >u[]rlbar' ); + + expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { + linkManualDecorator0: true, + linkManualDecorator1: true + } ); + + command.manualDecorators.first.value = false; + + expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { + linkManualDecorator0: false, + linkManualDecorator1: true, + } ); + + command.restoreManualDecoratorStates(); + + expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { + linkManualDecorator0: true, + linkManualDecorator1: true, + } ); + } ); + } ); + + describe( '_getDecoratorStateFromModel', () => { + it( 'obtain current values from the model', () => { + setData( model, 'foo[<$text linkHref="url" linkManualDecorator1="true" >url]bar' ); + + expect( command._getDecoratorStateFromModel( 'linkManualDecorator0' ) ).to.be.false; + expect( command._getDecoratorStateFromModel( 'linkManualDecorator1' ) ).to.be.true; + } ); + } ); } ); } ); + +function decoratorStates( manualDecorators ) { + return Array.from( manualDecorators ).reduce( ( accumulator, currentValue ) => { + accumulator[ currentValue.id ] = currentValue.value; + return accumulator; + }, {} ); +} diff --git a/tests/linkui.js b/tests/linkui.js index e88c762..d3d2e48 100644 --- a/tests/linkui.js +++ b/tests/linkui.js @@ -959,58 +959,85 @@ describe( 'LinkUI', () => { expect( focusSpy.calledBefore( removeSpy ) ).to.equal( true ); } ); - it( 'should gather information about manual decorators', () => { - let editor, model, formView; - const editorElement = document.createElement( 'div' ); - document.body.appendChild( editorElement ); - - return ClassicTestEditor - .create( editorElement, { - plugins: [ LinkEditing, LinkUI, Paragraph ], - link: { - decorators: [ - { - mode: 'manual', - label: 'Foo', - attributes: { - foo: 'bar' + describe( 'support manual decorators', () => { + let editorElement, editor, model, formView, linkUIFeature; + + beforeEach( () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + return ClassicTestEditor + .create( editorElement, { + plugins: [ LinkEditing, LinkUI, Paragraph ], + link: { + decorators: [ + { + mode: 'manual', + label: 'Foo', + attributes: { + foo: 'bar' + } } - } - ] - } - } ) - .then( newEditor => { - editor = newEditor; - model = editor.model; - - model.schema.extend( '$text', { - allowIn: '$root', - allowAttributes: 'linkHref' + ] + } + } ) + .then( newEditor => { + editor = newEditor; + model = editor.model; + + model.schema.extend( '$text', { + allowIn: '$root', + allowAttributes: 'linkHref' + } ); + + linkUIFeature = editor.plugins.get( LinkUI ); + + const balloon = editor.plugins.get( ContextualBalloon ); + + formView = linkUIFeature.formView; + + // There is no point to execute BalloonPanelView attachTo and pin methods so lets override it. + testUtils.sinon.stub( balloon.view, 'attachTo' ).returns( {} ); + testUtils.sinon.stub( balloon.view, 'pin' ).returns( {} ); + + formView.render(); } ); + } ); - const linkUIFeature = editor.plugins.get( LinkUI ); - const balloon = editor.plugins.get( ContextualBalloon ); + afterEach( () => { + editorElement.remove(); + } ); + + it( 'should gather information about manual decorators', () => { + const executeSpy = testUtils.sinon.spy( editor, 'execute' ); + + setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); + expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' ); + expect( formView.getDecoratorSwitchesState() ).to.deep.equal( { linkManualDecorator0: true } ); - formView = linkUIFeature.formView; + formView.fire( 'submit' ); - // There is no point to execute BalloonPanelView attachTo and pin methods so lets override it. - testUtils.sinon.stub( balloon.view, 'attachTo' ).returns( {} ); - testUtils.sinon.stub( balloon.view, 'pin' ).returns( {} ); + expect( executeSpy.calledOnce ).to.be.true; + expect( executeSpy.calledWithExactly( 'link', 'url', { linkManualDecorator0: true } ) ).to.be.true; + } ); + + it( 'should reset switch state when form view is closed', () => { + setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); - formView.render(); - } ) - .then( () => { - const executeSpy = testUtils.sinon.spy( editor, 'execute' ); + const manualDecorators = editor.commands.get( 'link' ).manualDecorators; + const firstDecoratorModel = manualDecorators.first; + const firstDecoratorSwitch = formView._manualDecoratorSwitches.first; - setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); - expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' ); - expect( formView.getDecoratorSwitchesState() ).to.deep.equal( { linkManualDecorator0: true } ); + expect( firstDecoratorModel.value, 'Initial value should be read from the model (true)' ).to.be.true; + expect( firstDecoratorSwitch.isOn, 'Initial value should be read from the model (true)' ).to.be.true; - formView.fire( 'submit' ); + firstDecoratorSwitch.fire( 'execute' ); + expect( firstDecoratorModel.value, 'Pressing button toggles value' ).to.be.false; + expect( firstDecoratorSwitch.isOn, 'Pressing button toggles value' ).to.be.false; - expect( executeSpy.calledOnce ).to.be.true; - expect( executeSpy.calledWithExactly( 'link', 'url', { linkManualDecorator0: true } ) ).to.be.true; - } ); + linkUIFeature._closeFormView(); + expect( firstDecoratorModel.value, 'Close form view without submit resets value to initial state' ).to.be.true; + expect( firstDecoratorSwitch.isOn, 'Close form view without submit resets value to initial state' ).to.be.true; + } ); } ); } ); } ); diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index ed90af5..62b1a16 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -228,6 +228,7 @@ describe( 'LinkFormView', () => { view.destroy(); collection.clear(); } ); + it( 'switch buttons reflects state of manual decorators', () => { expect( view._manualDecoratorSwitches.length ).to.equal( 3 ); From aebe2892648f14534252fb66f0cee833e6d51609 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 12:35:55 +0200 Subject: [PATCH 71/81] Provide support for decorators' names. --- src/linkediting.js | 17 ++++++------ src/utils.js | 52 ++++++++++++++++++++++++----------- tests/manual/linkdecorator.js | 22 +++++++-------- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/src/linkediting.js b/src/linkediting.js index 13ecced..80f3372 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -10,7 +10,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import LinkCommand from './linkcommand'; import UnlinkCommand from './unlinkcommand'; -import { createLinkElement, ensureSafeUrl, getLocalizedDecorators } from './utils'; +import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, getNormalizedDecorators } from './utils'; import AutomaticDecorators from './utils/automaticdecorators'; import ManualDecorator from './utils/manualdecorator'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; @@ -77,7 +77,7 @@ export default class LinkEditing extends Plugin { editor.commands.add( 'link', new LinkCommand( editor ) ); editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); - const linkDecorators = getLocalizedDecorators( editor ); + const linkDecorators = getLocalizedDecorators( editor.t, getNormalizedDecorators( editor ) ); this._enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) ); this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) ); @@ -108,6 +108,7 @@ export default class LinkEditing extends Plugin { // Adds default decorator for external links. if ( editor.config.get( 'link.addTargetToExternalLinks' ) ) { automaticDecorators.add( { + id: 'linkDecoratorIsExternal', mode: DECORATOR_AUTOMATIC, callback: url => EXTERNAL_LINKS_REGEXP.test( url ), attributes: { @@ -145,21 +146,19 @@ export default class LinkEditing extends Plugin { const command = editor.commands.get( 'link' ); const manualDecorators = command.manualDecorators; - manualDecoratorDefinitions.forEach( ( decorator, index ) => { - const decoratorName = `linkManualDecorator${ index }`; - - editor.model.schema.extend( '$text', { allowAttributes: decoratorName } ); + manualDecoratorDefinitions.forEach( decorator => { + editor.model.schema.extend( '$text', { allowAttributes: decorator.id } ); // Keeps reference to manual decorator to decode its name to attributes during downcast. - manualDecorators.add( new ManualDecorator( Object.assign( { id: decoratorName }, decorator ) ) ); + manualDecorators.add( new ManualDecorator( decorator ) ); editor.conversion.for( 'downcast' ).attributeToElement( { - model: decoratorName, + model: decorator.id, view: ( manualDecoratorName, writer ) => { if ( manualDecoratorName ) { const element = writer.createAttributeElement( 'a', - manualDecorators.get( decoratorName ).attributes, + manualDecorators.get( decorator.id ).attributes, { priority: 5 } diff --git a/src/utils.js b/src/utils.js index 88216a4..f86dae3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -68,26 +68,46 @@ function isSafeUrl( url ) { * **Note**: Only the few most commonly used labels are translated automatically. Other labels should be manually * translated in the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} configuration. * - * @param {module:core/editor/editor~Editor} editor An editor instance + * @param {module:utils/locale~Locale#t} t shorthand for {@link module:utils/locale~Locale#t Locale#t} + * @param {Array.} decorators reference + * where labels' values should be localized. * @returns {Array.} */ -export function getLocalizedDecorators( editor ) { - const t = editor.t; +export function getLocalizedDecorators( t, decorators ) { + const localizedDecoratorsLabels = { + 'Open in a new tab': t( 'Open in a new tab' ), + 'Downloadable': t( 'Downloadable' ) + }; + + decorators.forEach( decorator => { + if ( decorator.label && localizedDecoratorsLabels[ decorator.label ] ) { + decorator.label = localizedDecoratorsLabels[ decorator.label ]; + } + return decorator; + } ); + + return decorators; +} + +/** + * Converts Obj of decorators to Array of decorators with nice identifiers. + * + * @param {module:core/editor/editor~Editor} editor + */ +export function getNormalizedDecorators( editor ) { const decorators = editor.config.get( 'link.decorators' ); + const retArray = []; if ( decorators ) { - const localizedDecoratorsLabels = { - 'Open in a new tab': t( 'Open in a new tab' ), - 'Downloadable': t( 'Downloadable' ) - }; - - return decorators.map( decorator => { - if ( decorator.label && localizedDecoratorsLabels[ decorator.label ] ) { - decorator.label = localizedDecoratorsLabels[ decorator.label ]; - } - return decorator; - } ); - } else { - return []; + for ( const [ key, value ] of Object.entries( decorators ) ) { + const decorator = Object.assign( + {}, + value, + { id: `linkDecorator${ key }` } + ); + retArray.push( decorator ); + } } + + return retArray; } diff --git a/tests/manual/linkdecorator.js b/tests/manual/linkdecorator.js index ab2a92a..c3cf024 100644 --- a/tests/manual/linkdecorator.js +++ b/tests/manual/linkdecorator.js @@ -21,8 +21,8 @@ ClassicEditor plugins: [ Link, Typing, Paragraph, Clipboard, Undo, Enter ], toolbar: [ 'link', 'undo', 'redo' ], link: { - decorators: [ - { + decorators: { + IxExternal: { mode: 'manual', label: 'Open in new window', attributes: { @@ -30,21 +30,21 @@ ClassicEditor rel: 'noopener noreferrer' } }, - { + IsDownloadable: { mode: 'manual', label: 'Downloadable', attributes: { download: 'download' } }, - { + IsGallery: { mode: 'manual', label: 'Gallery link', attributes: { class: 'gallery' } } - ] + } } } ) .then( editor => { @@ -63,30 +63,30 @@ ClassicEditor plugins: [ Link, Typing, Paragraph, Clipboard, Undo, Enter ], toolbar: [ 'link', 'undo', 'redo' ], link: { - decorators: [ - { + decorators: { + IsTelephone: { mode: 'automatic', callback: url => url.startsWith( 'tel:' ), attributes: { class: 'phone' } }, - { + IsInternal: { mode: 'automatic', callback: url => url.startsWith( '#' ), attributes: { class: 'internal' } } - ], - targetDecorator: true + }, + addTargetToExternalLinks: true } } ) .then( editor => { if ( !window.editors ) { window.editors = {}; } - window.editors.autoamticDecorators = editor; + window.editors.automaticDecorators = editor; } ) .catch( err => { console.error( err.stack ); From d48d33f8b137296c4dae61b3d51d2a454566da1f Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 13:04:01 +0200 Subject: [PATCH 72/81] Update unit test to follow new configuration of link decorators. --- tests/linkcommand.js | 50 ++++++++++++++++++++-------------------- tests/linkediting.js | 12 ++++++---- tests/linkui.js | 14 +++++------ tests/ui/linkformview.js | 6 ++--- tests/unlinkcommand.js | 12 +++++----- 5 files changed, 48 insertions(+), 46 deletions(-) diff --git a/tests/linkcommand.js b/tests/linkcommand.js index 76ac11d..ef9b134 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -271,14 +271,14 @@ describe( 'LinkCommand', () => { command = new LinkCommand( editor ); command.manualDecorators.add( new ManualDecorator( { - id: 'linkManualDecorator0', + id: 'linkDecoratorIsFoo', label: 'Foo', attributes: { class: 'Foo' } } ) ); command.manualDecorators.add( new ManualDecorator( { - id: 'linkManualDecorator1', + id: 'linkDecoratorIsBar', label: 'Bar', attributes: { target: '_blank' @@ -287,7 +287,7 @@ describe( 'LinkCommand', () => { model.schema.extend( '$text', { allowIn: '$root', - allowAttributes: [ 'linkHref', 'linkManualDecorator0', 'linkManualDecorator1' ] + allowAttributes: [ 'linkHref', 'linkDecoratorIsFoo', 'linkDecoratorIsBar' ] } ); model.schema.register( 'p', { inheritAllFrom: '$block' } ); @@ -302,25 +302,25 @@ describe( 'LinkCommand', () => { it( 'should insert additional attributes to link when it is created', () => { setData( model, 'foo[]bar' ); - command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); expect( getData( model ) ).to - .equal( 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">url]bar' ); + .equal( 'foo[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">url]bar' ); } ); it( 'should add additional attributes to link when link is modified', () => { setData( model, 'f<$text linkHref="url">o[]obar' ); - command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); expect( getData( model ) ).to - .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + .equal( 'f[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">ooba]r' ); } ); it( 'should remove additional attributes to link if those are falsy', () => { - setData( model, 'foo<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >u[]rlbar' ); + setData( model, 'foo<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">u[]rlbar' ); - command.execute( 'url', { linkManualDecorator0: false, linkManualDecorator1: false } ); + command.execute( 'url', { linkDecoratorIsFoo: false, linkDecoratorIsBar: false } ); expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); } ); @@ -330,25 +330,25 @@ describe( 'LinkCommand', () => { it( 'should insert additional attributes to link when it is created', () => { setData( model, 'f[ooba]r' ); - command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); expect( getData( model ) ).to - .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + .equal( 'f[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">ooba]r' ); } ); it( 'should add additional attributes to link when link is modified', () => { setData( model, 'f[<$text linkHref="foo">ooba]r' ); - command.execute( 'url', { linkManualDecorator0: true, linkManualDecorator1: true } ); + command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); expect( getData( model ) ).to - .equal( 'f[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true">ooba]r' ); + .equal( 'f[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">ooba]r' ); } ); it( 'should remove additional attributes to link if those are falsy', () => { - setData( model, 'foo[<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >url]bar' ); + setData( model, 'foo[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">url]bar' ); - command.execute( 'url', { linkManualDecorator0: false, linkManualDecorator1: false } ); + command.execute( 'url', { linkDecoratorIsFoo: false, linkDecoratorIsBar: false } ); expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); } ); @@ -356,35 +356,35 @@ describe( 'LinkCommand', () => { describe( 'restoreManualDecoratorStates()', () => { it( 'synchronize values with current model state', () => { - setData( model, 'foo<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >u[]rlbar' ); + setData( model, 'foo<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">u[]rlbar' ); expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { - linkManualDecorator0: true, - linkManualDecorator1: true + linkDecoratorIsFoo: true, + linkDecoratorIsBar: true } ); command.manualDecorators.first.value = false; expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { - linkManualDecorator0: false, - linkManualDecorator1: true, + linkDecoratorIsFoo: false, + linkDecoratorIsBar: true, } ); command.restoreManualDecoratorStates(); expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { - linkManualDecorator0: true, - linkManualDecorator1: true, + linkDecoratorIsFoo: true, + linkDecoratorIsBar: true, } ); } ); } ); describe( '_getDecoratorStateFromModel', () => { it( 'obtain current values from the model', () => { - setData( model, 'foo[<$text linkHref="url" linkManualDecorator1="true" >url]bar' ); + setData( model, 'foo[<$text linkDecoratorIsBar="true" linkHref="url">url]bar' ); - expect( command._getDecoratorStateFromModel( 'linkManualDecorator0' ) ).to.be.false; - expect( command._getDecoratorStateFromModel( 'linkManualDecorator1' ) ).to.be.true; + expect( command._getDecoratorStateFromModel( 'linkDecoratorIsFoo' ) ).to.be.false; + expect( command._getDecoratorStateFromModel( 'linkDecoratorIsBar' ) ).to.be.true; } ); } ); } ); diff --git a/tests/linkediting.js b/tests/linkediting.js index 571e55e..6b0ca22 100644 --- a/tests/linkediting.js +++ b/tests/linkediting.js @@ -481,27 +481,29 @@ describe( 'LinkEditing', () => { plugins: [ Paragraph, LinkEditing, Enter ], link: { addTargetToExternalLinks: false, - decorators: [ - { + decorators: { + IsExternal: { mode: 'automatic', callback: url => url.startsWith( 'http' ), attributes: { target: '_blank' } - }, { + }, + IsDownloadable: { mode: 'automatic', callback: url => url.includes( 'download' ), attributes: { download: 'download' } - }, { + }, + IsMail: { mode: 'automatic', callback: url => url.startsWith( 'mailto:' ), attributes: { class: 'mail-url' } } - ] + } } } ) .then( newEditor => { diff --git a/tests/linkui.js b/tests/linkui.js index d3d2e48..3763a38 100644 --- a/tests/linkui.js +++ b/tests/linkui.js @@ -969,15 +969,15 @@ describe( 'LinkUI', () => { .create( editorElement, { plugins: [ LinkEditing, LinkUI, Paragraph ], link: { - decorators: [ - { + decorators: { + IsFoo: { mode: 'manual', label: 'Foo', attributes: { foo: 'bar' } } - ] + } } } ) .then( newEditor => { @@ -1010,18 +1010,18 @@ describe( 'LinkUI', () => { it( 'should gather information about manual decorators', () => { const executeSpy = testUtils.sinon.spy( editor, 'execute' ); - setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); + setModelData( model, 'f[<$text linkHref="url" linkDecoratorIsFoo="true">ooba]r' ); expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' ); - expect( formView.getDecoratorSwitchesState() ).to.deep.equal( { linkManualDecorator0: true } ); + expect( formView.getDecoratorSwitchesState() ).to.deep.equal( { linkDecoratorIsFoo: true } ); formView.fire( 'submit' ); expect( executeSpy.calledOnce ).to.be.true; - expect( executeSpy.calledWithExactly( 'link', 'url', { linkManualDecorator0: true } ) ).to.be.true; + expect( executeSpy.calledWithExactly( 'link', 'url', { linkDecoratorIsFoo: true } ) ).to.be.true; } ); it( 'should reset switch state when form view is closed', () => { - setModelData( model, 'f[<$text linkHref="url" linkManualDecorator0="true">ooba]r' ); + setModelData( model, 'f[<$text linkHref="url" linkDecoratorIsFoo="true">ooba]r' ); const manualDecorators = editor.commands.get( 'link' ).manualDecorators; const firstDecoratorModel = manualDecorators.first; diff --git a/tests/ui/linkformview.js b/tests/ui/linkformview.js index 62b1a16..7dffd84 100644 --- a/tests/ui/linkformview.js +++ b/tests/ui/linkformview.js @@ -309,15 +309,15 @@ describe( 'LinkFormView', () => { toolbar: [ 'link' ], language: 'pl', link: { - decorators: [ - { + decorators: { + IsExternal: { mode: 'manual', label: 'Open in a new tab', attributes: { target: '_blank' } } - ] + } } } ) .then( newEditor => { diff --git a/tests/unlinkcommand.js b/tests/unlinkcommand.js index d237b7f..e9601ce 100644 --- a/tests/unlinkcommand.js +++ b/tests/unlinkcommand.js @@ -235,22 +235,22 @@ describe( 'UnlinkCommand', () => { return ModelTestEditor.create( { extraPlugins: [ LinkEditing ], link: { - decorators: [ - { + decorators: { + IsFoo: { mode: 'manual', label: 'Foo', attributes: { class: 'foo' } }, - { + IsBar: { mode: 'manual', label: 'Bar', attributes: { target: '_blank' } } - ] + } } } ) .then( newEditor => { @@ -261,7 +261,7 @@ describe( 'UnlinkCommand', () => { model.schema.extend( '$text', { allowIn: '$root', - allowAttributes: 'linkHref' + allowAttributes: [ 'linkHref', 'linkDecoratorIsFoo', 'linkDecoratorIsBar' ] } ); model.schema.register( 'p', { inheritAllFrom: '$block' } ); @@ -273,7 +273,7 @@ describe( 'UnlinkCommand', () => { } ); it( 'should remove manual decorators from links together with linkHref', () => { - setData( model, '<$text linkHref="url" linkManualDecorator0="true" linkManualDecorator1="true" >f[]oobar' ); + setData( model, '<$text linkHref="url" linkDecoratorIsFoo="true" linkDecoratorIsBar="true" >f[]oobar' ); command.execute(); From bc04315b9e54410754dd746a1970da9302ad4092 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 13:57:51 +0200 Subject: [PATCH 73/81] Change prefix from linkDecorator to link, make first letter of decorator uppercase for better readability. Fix all occurance related to this change. --- package.json | 3 ++- src/linkediting.js | 2 +- src/utils.js | 4 ++- tests/linkcommand.js | 50 +++++++++++++++++------------------ tests/linkediting.js | 6 ++--- tests/linkui.js | 10 +++---- tests/manual/linkdecorator.js | 10 +++---- tests/unlinkcommand.js | 8 +++--- 8 files changed, 48 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 7f3d071..e37344b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "dependencies": { "@ckeditor/ckeditor5-core": "^12.1.1", "@ckeditor/ckeditor5-engine": "^13.1.1", - "@ckeditor/ckeditor5-ui": "^13.0.0" + "@ckeditor/ckeditor5-ui": "^13.0.0", + "lodash-es": "^4.17.10" }, "devDependencies": { "@ckeditor/ckeditor5-clipboard": "^11.0.2", diff --git a/src/linkediting.js b/src/linkediting.js index 80f3372..cb7bd37 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -108,7 +108,7 @@ export default class LinkEditing extends Plugin { // Adds default decorator for external links. if ( editor.config.get( 'link.addTargetToExternalLinks' ) ) { automaticDecorators.add( { - id: 'linkDecoratorIsExternal', + id: 'linkIsExternal', mode: DECORATOR_AUTOMATIC, callback: url => EXTERNAL_LINKS_REGEXP.test( url ), attributes: { diff --git a/src/utils.js b/src/utils.js index f86dae3..921845d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -7,6 +7,8 @@ * @module link/utils */ +import { upperFirst } from 'lodash-es'; + const ATTRIBUTE_WHITESPACES = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex const SAFE_URL = /^(?:(?:https?|ftps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))/i; @@ -103,7 +105,7 @@ export function getNormalizedDecorators( editor ) { const decorator = Object.assign( {}, value, - { id: `linkDecorator${ key }` } + { id: `link${ upperFirst( key ) }` } ); retArray.push( decorator ); } diff --git a/tests/linkcommand.js b/tests/linkcommand.js index ef9b134..7027482 100644 --- a/tests/linkcommand.js +++ b/tests/linkcommand.js @@ -271,14 +271,14 @@ describe( 'LinkCommand', () => { command = new LinkCommand( editor ); command.manualDecorators.add( new ManualDecorator( { - id: 'linkDecoratorIsFoo', + id: 'linkIsFoo', label: 'Foo', attributes: { class: 'Foo' } } ) ); command.manualDecorators.add( new ManualDecorator( { - id: 'linkDecoratorIsBar', + id: 'linkIsBar', label: 'Bar', attributes: { target: '_blank' @@ -287,7 +287,7 @@ describe( 'LinkCommand', () => { model.schema.extend( '$text', { allowIn: '$root', - allowAttributes: [ 'linkHref', 'linkDecoratorIsFoo', 'linkDecoratorIsBar' ] + allowAttributes: [ 'linkHref', 'linkIsFoo', 'linkIsBar' ] } ); model.schema.register( 'p', { inheritAllFrom: '$block' } ); @@ -302,25 +302,25 @@ describe( 'LinkCommand', () => { it( 'should insert additional attributes to link when it is created', () => { setData( model, 'foo[]bar' ); - command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); + command.execute( 'url', { linkIsFoo: true, linkIsBar: true } ); expect( getData( model ) ).to - .equal( 'foo[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">url]bar' ); + .equal( 'foo[<$text linkHref="url" linkIsBar="true" linkIsFoo="true">url]bar' ); } ); it( 'should add additional attributes to link when link is modified', () => { setData( model, 'f<$text linkHref="url">o[]obar' ); - command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); + command.execute( 'url', { linkIsFoo: true, linkIsBar: true } ); expect( getData( model ) ).to - .equal( 'f[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">ooba]r' ); + .equal( 'f[<$text linkHref="url" linkIsBar="true" linkIsFoo="true">ooba]r' ); } ); it( 'should remove additional attributes to link if those are falsy', () => { - setData( model, 'foo<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">u[]rlbar' ); + setData( model, 'foo<$text linkHref="url" linkIsBar="true" linkIsFoo="true">u[]rlbar' ); - command.execute( 'url', { linkDecoratorIsFoo: false, linkDecoratorIsBar: false } ); + command.execute( 'url', { linkIsFoo: false, linkIsBar: false } ); expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); } ); @@ -330,25 +330,25 @@ describe( 'LinkCommand', () => { it( 'should insert additional attributes to link when it is created', () => { setData( model, 'f[ooba]r' ); - command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); + command.execute( 'url', { linkIsFoo: true, linkIsBar: true } ); expect( getData( model ) ).to - .equal( 'f[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">ooba]r' ); + .equal( 'f[<$text linkHref="url" linkIsBar="true" linkIsFoo="true">ooba]r' ); } ); it( 'should add additional attributes to link when link is modified', () => { setData( model, 'f[<$text linkHref="foo">ooba]r' ); - command.execute( 'url', { linkDecoratorIsFoo: true, linkDecoratorIsBar: true } ); + command.execute( 'url', { linkIsFoo: true, linkIsBar: true } ); expect( getData( model ) ).to - .equal( 'f[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">ooba]r' ); + .equal( 'f[<$text linkHref="url" linkIsBar="true" linkIsFoo="true">ooba]r' ); } ); it( 'should remove additional attributes to link if those are falsy', () => { - setData( model, 'foo[<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">url]bar' ); + setData( model, 'foo[<$text linkHref="url" linkIsBar="true" linkIsFoo="true">url]bar' ); - command.execute( 'url', { linkDecoratorIsFoo: false, linkDecoratorIsBar: false } ); + command.execute( 'url', { linkIsFoo: false, linkIsBar: false } ); expect( getData( model ) ).to.equal( 'foo[<$text linkHref="url">url]bar' ); } ); @@ -356,35 +356,35 @@ describe( 'LinkCommand', () => { describe( 'restoreManualDecoratorStates()', () => { it( 'synchronize values with current model state', () => { - setData( model, 'foo<$text linkDecoratorIsBar="true" linkDecoratorIsFoo="true" linkHref="url">u[]rlbar' ); + setData( model, 'foo<$text linkHref="url" linkIsBar="true" linkIsFoo="true">u[]rlbar' ); expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { - linkDecoratorIsFoo: true, - linkDecoratorIsBar: true + linkIsFoo: true, + linkIsBar: true } ); command.manualDecorators.first.value = false; expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { - linkDecoratorIsFoo: false, - linkDecoratorIsBar: true, + linkIsFoo: false, + linkIsBar: true, } ); command.restoreManualDecoratorStates(); expect( decoratorStates( command.manualDecorators ) ).to.deep.equal( { - linkDecoratorIsFoo: true, - linkDecoratorIsBar: true, + linkIsFoo: true, + linkIsBar: true, } ); } ); } ); describe( '_getDecoratorStateFromModel', () => { it( 'obtain current values from the model', () => { - setData( model, 'foo[<$text linkDecoratorIsBar="true" linkHref="url">url]bar' ); + setData( model, 'foo[<$text linkHref="url" linkIsBar="true">url]bar' ); - expect( command._getDecoratorStateFromModel( 'linkDecoratorIsFoo' ) ).to.be.false; - expect( command._getDecoratorStateFromModel( 'linkDecoratorIsBar' ) ).to.be.true; + expect( command._getDecoratorStateFromModel( 'linkIsFoo' ) ).to.be.false; + expect( command._getDecoratorStateFromModel( 'linkIsBar' ) ).to.be.true; } ); } ); } ); diff --git a/tests/linkediting.js b/tests/linkediting.js index 6b0ca22..712a9fc 100644 --- a/tests/linkediting.js +++ b/tests/linkediting.js @@ -482,21 +482,21 @@ describe( 'LinkEditing', () => { link: { addTargetToExternalLinks: false, decorators: { - IsExternal: { + isExternal: { mode: 'automatic', callback: url => url.startsWith( 'http' ), attributes: { target: '_blank' } }, - IsDownloadable: { + isDownloadable: { mode: 'automatic', callback: url => url.includes( 'download' ), attributes: { download: 'download' } }, - IsMail: { + isMail: { mode: 'automatic', callback: url => url.startsWith( 'mailto:' ), attributes: { diff --git a/tests/linkui.js b/tests/linkui.js index 3763a38..452f2c6 100644 --- a/tests/linkui.js +++ b/tests/linkui.js @@ -970,7 +970,7 @@ describe( 'LinkUI', () => { plugins: [ LinkEditing, LinkUI, Paragraph ], link: { decorators: { - IsFoo: { + isFoo: { mode: 'manual', label: 'Foo', attributes: { @@ -1010,18 +1010,18 @@ describe( 'LinkUI', () => { it( 'should gather information about manual decorators', () => { const executeSpy = testUtils.sinon.spy( editor, 'execute' ); - setModelData( model, 'f[<$text linkHref="url" linkDecoratorIsFoo="true">ooba]r' ); + setModelData( model, 'f[<$text linkHref="url" linkIsFoo="true">ooba]r' ); expect( formView.urlInputView.inputView.element.value ).to.equal( 'url' ); - expect( formView.getDecoratorSwitchesState() ).to.deep.equal( { linkDecoratorIsFoo: true } ); + expect( formView.getDecoratorSwitchesState() ).to.deep.equal( { linkIsFoo: true } ); formView.fire( 'submit' ); expect( executeSpy.calledOnce ).to.be.true; - expect( executeSpy.calledWithExactly( 'link', 'url', { linkDecoratorIsFoo: true } ) ).to.be.true; + expect( executeSpy.calledWithExactly( 'link', 'url', { linkIsFoo: true } ) ).to.be.true; } ); it( 'should reset switch state when form view is closed', () => { - setModelData( model, 'f[<$text linkHref="url" linkDecoratorIsFoo="true">ooba]r' ); + setModelData( model, 'f[<$text linkHref="url" linkIsFoo="true">ooba]r' ); const manualDecorators = editor.commands.get( 'link' ).manualDecorators; const firstDecoratorModel = manualDecorators.first; diff --git a/tests/manual/linkdecorator.js b/tests/manual/linkdecorator.js index c3cf024..3a11abe 100644 --- a/tests/manual/linkdecorator.js +++ b/tests/manual/linkdecorator.js @@ -22,7 +22,7 @@ ClassicEditor toolbar: [ 'link', 'undo', 'redo' ], link: { decorators: { - IxExternal: { + isExternal: { mode: 'manual', label: 'Open in new window', attributes: { @@ -30,14 +30,14 @@ ClassicEditor rel: 'noopener noreferrer' } }, - IsDownloadable: { + isDownloadable: { mode: 'manual', label: 'Downloadable', attributes: { download: 'download' } }, - IsGallery: { + isGallery: { mode: 'manual', label: 'Gallery link', attributes: { @@ -64,14 +64,14 @@ ClassicEditor toolbar: [ 'link', 'undo', 'redo' ], link: { decorators: { - IsTelephone: { + isTelephone: { mode: 'automatic', callback: url => url.startsWith( 'tel:' ), attributes: { class: 'phone' } }, - IsInternal: { + isInternal: { mode: 'automatic', callback: url => url.startsWith( '#' ), attributes: { diff --git a/tests/unlinkcommand.js b/tests/unlinkcommand.js index e9601ce..488120c 100644 --- a/tests/unlinkcommand.js +++ b/tests/unlinkcommand.js @@ -236,14 +236,14 @@ describe( 'UnlinkCommand', () => { extraPlugins: [ LinkEditing ], link: { decorators: { - IsFoo: { + isFoo: { mode: 'manual', label: 'Foo', attributes: { class: 'foo' } }, - IsBar: { + isBar: { mode: 'manual', label: 'Bar', attributes: { @@ -261,7 +261,7 @@ describe( 'UnlinkCommand', () => { model.schema.extend( '$text', { allowIn: '$root', - allowAttributes: [ 'linkHref', 'linkDecoratorIsFoo', 'linkDecoratorIsBar' ] + allowAttributes: [ 'linkHref', 'linkIsFoo', 'linkIsBar' ] } ); model.schema.register( 'p', { inheritAllFrom: '$block' } ); @@ -273,7 +273,7 @@ describe( 'UnlinkCommand', () => { } ); it( 'should remove manual decorators from links together with linkHref', () => { - setData( model, '<$text linkHref="url" linkDecoratorIsFoo="true" linkDecoratorIsBar="true" >f[]oobar' ); + setData( model, '<$text linkIsFoo="true" linkIsBar="true" linkHref="url">f[]oobar' ); command.execute(); From 3cfd86e8c177472f8e89629ecf28cd353ad7b71e Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 14:19:32 +0200 Subject: [PATCH 74/81] Rename utils method add unit test for it. --- src/linkediting.js | 4 ++-- src/utils.js | 5 ++-- tests/utils.js | 60 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/linkediting.js b/src/linkediting.js index cb7bd37..6a561ae 100644 --- a/src/linkediting.js +++ b/src/linkediting.js @@ -10,7 +10,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import LinkCommand from './linkcommand'; import UnlinkCommand from './unlinkcommand'; -import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, getNormalizedDecorators } from './utils'; +import { createLinkElement, ensureSafeUrl, getLocalizedDecorators, normalizeDecorators } from './utils'; import AutomaticDecorators from './utils/automaticdecorators'; import ManualDecorator from './utils/manualdecorator'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; @@ -77,7 +77,7 @@ export default class LinkEditing extends Plugin { editor.commands.add( 'link', new LinkCommand( editor ) ); editor.commands.add( 'unlink', new UnlinkCommand( editor ) ); - const linkDecorators = getLocalizedDecorators( editor.t, getNormalizedDecorators( editor ) ); + const linkDecorators = getLocalizedDecorators( editor.t, normalizeDecorators( editor.config.get( 'link.decorators' ) ) ); this._enableAutomaticDecorators( linkDecorators.filter( item => item.mode === DECORATOR_AUTOMATIC ) ); this._enableManualDecorators( linkDecorators.filter( item => item.mode === DECORATOR_MANUAL ) ); diff --git a/src/utils.js b/src/utils.js index 921845d..513e45a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -94,10 +94,9 @@ export function getLocalizedDecorators( t, decorators ) { /** * Converts Obj of decorators to Array of decorators with nice identifiers. * - * @param {module:core/editor/editor~Editor} editor + * @param {Object} decorators */ -export function getNormalizedDecorators( editor ) { - const decorators = editor.config.get( 'link.decorators' ); +export function normalizeDecorators( decorators ) { const retArray = []; if ( decorators ) { diff --git a/tests/utils.js b/tests/utils.js index 82507d1..1e1d162 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -9,7 +9,7 @@ import AttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeeleme import ContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; import Text from '@ckeditor/ckeditor5-engine/src/view/text'; -import { createLinkElement, isLinkElement, ensureSafeUrl } from '../src/utils'; +import { createLinkElement, isLinkElement, ensureSafeUrl, normalizeDecorators } from '../src/utils'; describe( 'utils', () => { describe( 'isLinkElement()', () => { @@ -157,4 +157,62 @@ describe( 'utils', () => { expect( ensureSafeUrl( url ) ).to.equal( '#' ); } ); } ); + + describe( 'normalizeDecorators()', () => { + it( 'should transform an entry object to a normalized array', () => { + const callback = () => {}; + const entryObject = { + foo: { + mode: 'manual', + label: 'Foo', + attributes: { + foo: 'foo' + } + }, + bar: { + mode: 'automatic', + callback, + attributes: { + bar: 'bar' + } + }, + baz: { + mode: 'manual', + label: 'Baz label', + attributes: { + target: '_blank', + rel: 'noopener noreferrer' + } + } + }; + + expect( normalizeDecorators( entryObject ) ).to.deep.equal( [ + { + id: 'linkFoo', + mode: 'manual', + label: 'Foo', + attributes: { + foo: 'foo' + } + }, + { + id: 'linkBar', + mode: 'automatic', + callback, + attributes: { + bar: 'bar' + } + }, + { + id: 'linkBaz', + mode: 'manual', + label: 'Baz label', + attributes: { + target: '_blank', + rel: 'noopener noreferrer' + } + } + ] ); + } ); + } ); } ); From 0ce359e07b9249fd4e1b1678407e633cf1a33aa7 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 17:06:19 +0200 Subject: [PATCH 75/81] Fix docuemntation after introduced changes in decoratorrs. --- src/link.js | 29 +++++++++++++++++++++-------- src/linkcommand.js | 29 +++++++++++++++-------------- src/linkui.js | 3 +++ src/utils.js | 6 ++++-- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/link.js b/src/link.js index ae2851c..93a71f1 100644 --- a/src/link.js +++ b/src/link.js @@ -88,11 +88,12 @@ export default class Link extends Plugin { * * {@link module:link/link~LinkDecoratorManualDefinition manual} – they allow users to control link attributes individually * using the editor UI. * - * Link decorators are defined as an array of objects: + * Link decorators are defined as an object with key-value pair, where key is a name provided for given decorator and value is decorator + * definition. * * const linkConfig = { - * decorators: [ - * { + * decorators: { + * isExternal: { * mode: 'automatic', * callback: url => url.startsWith( 'http://' ), * attributes: { @@ -100,7 +101,7 @@ export default class Link extends Plugin { * rel: 'noopener noreferrer' * } * }, - * { + * isDownloadable: { * mode: 'manual', * label: 'Downloadable', * attributes: { @@ -108,7 +109,7 @@ export default class Link extends Plugin { * } * }, * // ... - * ] + * } * } * * To learn more about the configuration syntax, check out the {@link module:link/link~LinkDecoratorAutomaticDefinition automatic} @@ -123,8 +124,20 @@ export default class Link extends Plugin { * {@link module:link/link~LinkConfig#addTargetToExternalLinks `config.link.addTargetToExternalLinks`} * configuration description to learn more. * - * @member {Array.} - * module:link/link~LinkConfig#decorators + * @member {Object.} module:link/link~LinkConfig#decorators + */ + +/** + * Represents a link decorator definition {@link module:link/link~LinkDecoratorManualDefinition `'manual'`} or + * {@link module:link/link~LinkDecoratorAutomaticDefinition `'automatic'`}. + * + * @interface LinkDecoratorDefinition + */ + +/** + * The kind of the decorator. `'manual'` for all manual decorators and `'automatic'` for all automatic decorators. + * + * @member {'manual'|'automatic'} module:link/link~LinkDecoratorDefinition#mode */ /** @@ -177,7 +190,7 @@ export default class Link extends Plugin { * } * * @typedef {Object} module:link/link~LinkDecoratorManualDefinition - * @property {'automatic'} mode The kind of the decorator. `'manual'` for all manual decorators. + * @property {'manual'} mode The kind of the decorator. `'manual'` for all manual decorators. * @property {String} label The label of the UI button the user can use to control the presence of link attributes. * @property {Object} attributes Key-value pairs used as link attributes added to the output during the * {@glink framework/guides/architecture/editing-engine#conversion downcasting}. diff --git a/src/linkcommand.js b/src/linkcommand.js index 813edd7..481f741 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -36,13 +36,13 @@ export default class LinkCommand extends Command { * You can consider it a model with states of manual decorators added to currently selected link. * * @readonly - * @type {module:utils/collection~Collection.} + * @type {module:utils/collection~Collection} */ this.manualDecorators = new Collection(); } /** - * Synchronize state of the decorator with actually present elements in the model. + * Synchronize state of {@link #manualDecorators} with actually present elements in the model. */ restoreManualDecoratorStates() { for ( const manualDecorator of this.manualDecorators ) { @@ -83,8 +83,9 @@ export default class LinkCommand extends Command { * * This command has an optional argument, which applies or removes model attributes brought by * {@link module:link/utils~ManualDecorator manual decorators}. Model attribute names correspond to - * decorator {@link module:link/utils~ManualDecorator#id ids} and follow the incremental pattern: - * `'linkManualDecorator0'`, `'linkManualDecorator1'`, `'linkManualDecorator2'`, etc.. + * decorator {@link module:link/utils~ManualDecorator#id ids} and and they are created based on decorator's entry in configuration. + * Attribute name is combination of `link` prefix with object's key used to define given decorator. For example, + * `'linkIsExternal'`, `'linkIsDownloadable'`, `'linkFoo'`, etc.. * * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} * documentation. @@ -95,25 +96,25 @@ export default class LinkCommand extends Command { * * // Adding a new decorator attribute. * linkCommand.execute( 'http://example.com', { - * linkDecorator0: true + * linkIsExternal: true * } ); * * // Removing a decorator attribute from a selection. * linkCommand.execute( 'http://example.com', { - * linkDecorator0: false + * linkIsExternal: false * } ); * * // Adding multiple decorator attributes at a time. * linkCommand.execute( 'http://example.com', { - * linkDecorator0: true, - * linkDecorator2: true, + * linkIsExternal: true, + * linkIsDownloadable: true, * } ); * * // Removing and adding decorator attributes at a time. * linkCommand.execute( 'http://example.com', { - * linkDecorator0: false, - * linkDecorator1: true, - * linkDecorator2: false, + * linkIsExternal: false, + * linkFoo: true, + * linkIsDownloadable: false, * } ); * * **Note**: If decorator attribute name is not specified its state remains untouched. @@ -203,11 +204,11 @@ export default class LinkCommand extends Command { } /** - * Method provides information if given decorator is present in currently processed selection. + * Method provides the information if a decorator with given name is present in currently processed selection. * * @private - * @param {String} decoratorName name of a link decorator used in the model - * @returns {Boolean} Information if a given decorator is currently present in a selection + * @param {String} decoratorName name of a manual decorator used in the model + * @returns {Boolean} The information if a given decorator is currently present in a selection */ _getDecoratorStateFromModel( decoratorName ) { const doc = this.editor.model.document; diff --git a/src/linkui.js b/src/linkui.js index 5a04959..ecee3a7 100644 --- a/src/linkui.js +++ b/src/linkui.js @@ -314,6 +314,9 @@ export default class LinkUI extends Plugin { * Closes form view. Decides whether the balloon should be hidden completely or if action view should be shown. This is decided upon * link command value (which has value if the document selection is in link). * + * If there are defined {@link module:link/link~LinkConfig#decorators} in editor's config, then there are additionally + * rest switch buttons state responsible for manual decorators handling. + * * @private */ _closeFormView() { diff --git a/src/utils.js b/src/utils.js index 513e45a..1b3669f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -92,9 +92,11 @@ export function getLocalizedDecorators( t, decorators ) { } /** - * Converts Obj of decorators to Array of decorators with nice identifiers. + * Converts an object with defined decorators to a normalized array of decorators. There is also added `id` key for each decorator, + * which is used as attribute's name in the model. * - * @param {Object} decorators + * @param {Object.} decorators + * @returns {Array.} */ export function normalizeDecorators( decorators ) { const retArray = []; From 5a6f3e7836c659336be191996ad320484f98638e Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 17:19:53 +0200 Subject: [PATCH 76/81] Replace 'or' operator with newly created interface of link decorator definition. --- src/utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.js b/src/utils.js index 1b3669f..bed11e9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -71,9 +71,9 @@ function isSafeUrl( url ) { * translated in the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} configuration. * * @param {module:utils/locale~Locale#t} t shorthand for {@link module:utils/locale~Locale#t Locale#t} - * @param {Array.} decorators reference + * @param {Array.} decorators reference * where labels' values should be localized. - * @returns {Array.} + * @returns {Array.} */ export function getLocalizedDecorators( t, decorators ) { const localizedDecoratorsLabels = { @@ -96,7 +96,7 @@ export function getLocalizedDecorators( t, decorators ) { * which is used as attribute's name in the model. * * @param {Object.} decorators - * @returns {Array.} + * @returns {Array.} */ export function normalizeDecorators( decorators ) { const retArray = []; From d816e7ef9a56d57bce991ecc28da786f17526cf4 Mon Sep 17 00:00:00 2001 From: Mateusz Samsel Date: Mon, 24 Jun 2019 17:25:05 +0200 Subject: [PATCH 77/81] Small leftover cleanup in docs. --- src/utils/manualdecorator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/manualdecorator.js b/src/utils/manualdecorator.js index 62e344b..703eee6 100644 --- a/src/utils/manualdecorator.js +++ b/src/utils/manualdecorator.js @@ -23,7 +23,7 @@ export default class ManualDecorator { * * @param {Object} config * @param {String} config.id name of attribute used in model, which represents given manual decorator. - * For example 'linkManualDecorator0'. + * For example 'linkIsExternal. * @param {String} config.label The label used in user interface to toggle manual decorator. * @param {Object} config.attributes Set of attributes added to output data, when decorator is active for specific link. * Attributes should keep format of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. From 3d8c8a574479ef786114faf9c192ab5f0dda1098 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Mon, 24 Jun 2019 17:25:09 +0200 Subject: [PATCH 78/81] Code style. --- theme/linkform.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/theme/linkform.css b/theme/linkform.css index 52acb6b..a3c61da 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -25,7 +25,8 @@ } } -/* Style link form differently when manual decorators are available. +/* + * Style link form differently when manual decorators are available. * See: https://github.com/ckeditor/ckeditor5-link/issues/186. */ .ck.ck-link-form_layout-vertical { @@ -35,7 +36,6 @@ flex-basis: 100%; } - & .ck-button { /* Let's move "Save" & "Cancel" buttons to the end of the link form view. */ order: 10; From 7b987bd135fd137d5eed311bb879860e34f91820 Mon Sep 17 00:00:00 2001 From: dkonopka Date: Tue, 25 Jun 2019 15:06:06 +0200 Subject: [PATCH 79/81] Removed obsolete values related to `display: flex`. --- theme/linkform.css | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/theme/linkform.css b/theme/linkform.css index a3c61da..057a6c9 100644 --- a/theme/linkform.css +++ b/theme/linkform.css @@ -30,21 +30,6 @@ * See: https://github.com/ckeditor/ckeditor5-link/issues/186. */ .ck.ck-link-form_layout-vertical { - flex-wrap: wrap; - - & .ck-labeled-input { - flex-basis: 100%; - } - - & .ck-button { - /* Let's move "Save" & "Cancel" buttons to the end of the link form view. */ - order: 10; - - flex-basis: 50%; - } - - & .ck-list { - flex-basis: 100%; - } + display: block; } From 27a71796aaa458cb68feed17d42ada1f594aaaf5 Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 25 Jun 2019 17:08:31 +0200 Subject: [PATCH 80/81] Docs: Improved link decorator feature documentation and translations. --- lang/contexts.json | 4 ++-- src/link.js | 7 +++++-- src/linkcommand.js | 14 ++++++++------ src/utils/manualdecorator.js | 2 +- tests/manual/linkdecorator.js | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/lang/contexts.json b/lang/contexts.json index fb86ff8..9f4b9be 100644 --- a/lang/contexts.json +++ b/lang/contexts.json @@ -5,6 +5,6 @@ "Edit link": "Button opening the Link URL editing balloon.", "Open link in new tab": "Button opening the link in new browser tab.", "This link has no URL": "Label explaining that a link has no URL set (the URL is empty).", - "Open link in a new tab": "One of the commonly used labels in manual decorators determines if given link can be opened in a new tab", - "Downloadable": "One of the commonly used labels in manual decorators determines if given link should be treat as link to download resources." + "Open link in a new tab": "The label of the switch button that controls whether the edited link will open in a new tab.", + "Downloadable": "The label of the switch button that controls whether the edited link refers to downloadable resource." } diff --git a/src/link.js b/src/link.js index 93a71f1..ee6dfa7 100644 --- a/src/link.js +++ b/src/link.js @@ -88,8 +88,11 @@ export default class Link extends Plugin { * * {@link module:link/link~LinkDecoratorManualDefinition manual} – they allow users to control link attributes individually * using the editor UI. * - * Link decorators are defined as an object with key-value pair, where key is a name provided for given decorator and value is decorator - * definition. + * Link decorators are defined as an object with key-value pairs, where the key is a name provided for a given decorator and the + * value is the decorator definition. + * + * The name of the decorator also corresponds to the {@glink framework/guides/architecture/editing-engine#text-attributes text attribute} + * in the model. For instance, the `isExternal` decorator below is represented as a `linkIsExternal` attribute in the model. * * const linkConfig = { * decorators: { diff --git a/src/linkcommand.js b/src/linkcommand.js index 481f741..6334d55 100644 --- a/src/linkcommand.js +++ b/src/linkcommand.js @@ -79,13 +79,15 @@ export default class LinkCommand extends Command { * * When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated. * - * # Decorators and attribute management + * # Decorators and model attribute management * - * This command has an optional argument, which applies or removes model attributes brought by - * {@link module:link/utils~ManualDecorator manual decorators}. Model attribute names correspond to - * decorator {@link module:link/utils~ManualDecorator#id ids} and and they are created based on decorator's entry in configuration. - * Attribute name is combination of `link` prefix with object's key used to define given decorator. For example, - * `'linkIsExternal'`, `'linkIsDownloadable'`, `'linkFoo'`, etc.. + * There is an optional argument to this command, which applies or removes model + * {@glink framework/guides/architecture/editing-engine#text-attributes text attributes} brought by + * {@link module:link/utils~ManualDecorator manual link decorators}. + * + * Text attribute names in the model correspond to the entries in the {@link module:link/link~LinkConfig#decorators configuration}. + * For every decorator configured, a model text attribute exists with the "link" prefix. For example, a `'linkMyDecorator'` attribute + * corresponds to the `'myDecorator'` in the configuration. * * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`} * documentation. diff --git a/src/utils/manualdecorator.js b/src/utils/manualdecorator.js index 703eee6..9d4ef59 100644 --- a/src/utils/manualdecorator.js +++ b/src/utils/manualdecorator.js @@ -23,7 +23,7 @@ export default class ManualDecorator { * * @param {Object} config * @param {String} config.id name of attribute used in model, which represents given manual decorator. - * For example 'linkIsExternal. + * For example `'linkIsExternal'`. * @param {String} config.label The label used in user interface to toggle manual decorator. * @param {Object} config.attributes Set of attributes added to output data, when decorator is active for specific link. * Attributes should keep format of attributes defined in {@link module:engine/view/elementdefinition~ElementDefinition}. diff --git a/tests/manual/linkdecorator.js b/tests/manual/linkdecorator.js index 3a11abe..31f9b16 100644 --- a/tests/manual/linkdecorator.js +++ b/tests/manual/linkdecorator.js @@ -24,7 +24,7 @@ ClassicEditor decorators: { isExternal: { mode: 'manual', - label: 'Open in new window', + label: 'Open in a new tab', attributes: { target: '_blank', rel: 'noopener noreferrer' From 27298c2e3862243865a5da9f1201fdca71845efb Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Tue, 25 Jun 2019 17:15:01 +0200 Subject: [PATCH 81/81] Tests: Improved the link decorator manual test description. [skip ci] --- tests/manual/linkdecorator.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/manual/linkdecorator.md b/tests/manual/linkdecorator.md index 423d8f7..310854a 100644 --- a/tests/manual/linkdecorator.md +++ b/tests/manual/linkdecorator.md @@ -1,19 +1,21 @@ ## Link decorators ### Manual decorators (window.editors.manualDecorators): + 1. Should be available for every link. -2. Should apply changes after accepting changes. -3. There should be available 3 manual decorators: - * Open in new window +2. Should be applied to the content only when "Save" was clicked. +3. There should be 3 manual decorators available: + * Open in new a new tab * Downloadable * Gallery link -4. State of buttons should reflect state of currently selected link. -5. Switch buttons should be focused (with tab key) after input. Save and cancel buttons should be focused at the end +4. State of the decorator switches should reflect the model of the currently selected link. +5. Switch buttons should be focusable (with the tab key), after the URL input. The "save" and "cancel" buttons should be focused last. ### Automatic decorators (window.editors.automaticDecorators). -1. There should be turned on default automatic decorator, which adds `target="_blank"` and `rel="noopener noreferrer"` attributes to all external links (links started with `http://`, `https://` or `//`); -2. There should not be any changes in model or view of the editors. -3. Additional data should be added during downcast ( you need to run `window.editors.automaticDecorators.getData()` to see how it works ). -4. There are 2 additional decorators: - * phone, which detects all links started with `tel:` and adds `phone` class to such link - * internal, which adds `internal` class to all links started with `#` + +1. There should be a default automatic decorator turned on, which adds `target="_blank"` and `rel="noopener noreferrer"` attributes to all external links (links starting with `http://`, `https://` or `//`). +2. There should be no changes to the model or the view of the editors. +3. Decorator data should be added during downcast (run `window.editors.automaticDecorators.getData()` to see how it works). +4. There are 2 additional decorators in this editor: + * phone, which detects all links starting with `tel:` and adds the `phone` CSS class to such link, + * internal, which adds the `internal` CSS class to all links starting with `#`.