diff --git a/lang/contexts.json b/lang/contexts.json index 5c482be..9c5a1cc 100644 --- a/lang/contexts.json +++ b/lang/contexts.json @@ -1,4 +1,5 @@ { "Bold": "Toolbar button tooltip for the Bold feature.", - "Italic": "Toolbar button tooltip for the Italic feature." -} \ No newline at end of file + "Italic": "Toolbar button tooltip for the Italic feature.", + "Underline": "Toolbar button tooltip for the Underline feature." +} diff --git a/src/underline.js b/src/underline.js new file mode 100644 index 0000000..1f23070 --- /dev/null +++ b/src/underline.js @@ -0,0 +1,68 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module basic-styles/underline + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import UnderlineEngine from './underlineengine'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import underlineIcon from '../theme/icons/underline.svg'; + +/** + * The underline feature. It introduces the Underline button and the Ctrl+U keystroke. + * + * It uses the {@link module:basic-styles/underlineengine~UnderlineEngine underline engine feature}. + * + * @extends module:core/plugin~Plugin + */ +export default class Underline extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ UnderlineEngine ]; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'Underline'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const t = editor.t; + const command = editor.commands.get( 'underline' ); + const keystroke = 'CTRL+U'; + + // Add bold button to feature components. + editor.ui.componentFactory.add( 'underline', locale => { + const view = new ButtonView( locale ); + + view.set( { + label: t( 'Underline' ), + icon: underlineIcon, + keystroke, + tooltip: true + } ); + + view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); + + // Execute command. + this.listenTo( view, 'execute', () => editor.execute( 'underline' ) ); + + return view; + } ); + + // Set the Ctrl+U keystroke. + editor.keystrokes.set( keystroke, 'underline' ); + } +} diff --git a/src/underlineengine.js b/src/underlineengine.js new file mode 100644 index 0000000..22e61a5 --- /dev/null +++ b/src/underlineengine.js @@ -0,0 +1,53 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module basic-styles/underlineengine + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import buildModelConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildmodelconverter'; +import buildViewConverter from '@ckeditor/ckeditor5-engine/src/conversion/buildviewconverter'; +import AttributeCommand from './attributecommand'; + +const UNDERLINE = 'underline'; + +/** + * The underline engine feature. + * + * It registers the `underline` command and introduces the `underline` attribute in the model which renders to the view + * as an `` element. + * + * @extends module:core/plugin~Plugin + */ +export default class UnderlineEngine extends Plugin { + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const data = editor.data; + const editing = editor.editing; + + // Allow underline attribute on all inline nodes. + editor.document.schema.allow( { name: '$inline', attributes: UNDERLINE, inside: '$block' } ); + // Temporary workaround. See https://github.com/ckeditor/ckeditor5/issues/477. + editor.document.schema.allow( { name: '$inline', attributes: UNDERLINE, inside: '$clipboardHolder' } ); + + // Build converter from model to view for data and editing pipelines. + buildModelConverter().for( data.modelToView, editing.modelToView ) + .fromAttribute( UNDERLINE ) + .toElement( 'u' ); + + // Build converter from view to model for data pipeline. + buildViewConverter().for( data.viewToModel ) + .fromElement( 'u' ) + .fromAttribute( 'style', { 'text-decoration': 'underline' } ) + .toAttribute( UNDERLINE, true ); + + // Create underline command. + editor.commands.add( UNDERLINE, new AttributeCommand( editor, UNDERLINE ) ); + } +} diff --git a/tests/manual/basic-styles.html b/tests/manual/basic-styles.html index 8f93bbb..718749d 100644 --- a/tests/manual/basic-styles.html +++ b/tests/manual/basic-styles.html @@ -1,3 +1,3 @@
-

This is an editor instance.

+

This is an editor instance.

diff --git a/tests/manual/basic-styles.js b/tests/manual/basic-styles.js index 573e483..96b175c 100644 --- a/tests/manual/basic-styles.js +++ b/tests/manual/basic-styles.js @@ -12,11 +12,12 @@ import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Undo from '@ckeditor/ckeditor5-undo/src/undo'; import Bold from '../../src/bold'; import Italic from '../../src/italic'; +import Underline from '../../src/underline'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Enter, Typing, Paragraph, Undo, Bold, Italic ], - toolbar: [ 'bold', 'italic', 'undo', 'redo' ] + plugins: [ Enter, Typing, Paragraph, Undo, Bold, Italic, Underline ], + toolbar: [ 'bold', 'italic', 'underline', 'undo', 'redo' ] } ) .then( editor => { window.editor = editor; diff --git a/tests/manual/basic-styles.md b/tests/manual/basic-styles.md index dbbeeb4..1056bfa 100644 --- a/tests/manual/basic-styles.md +++ b/tests/manual/basic-styles.md @@ -2,5 +2,6 @@ 1. The data should be loaded with: * italic "This", - * bold "editor". -2. Test the bold and italic features live. + * bold "editor", + * underline "instance". +2. Test the bold, italic and underline features live. diff --git a/tests/underline.js b/tests/underline.js new file mode 100644 index 0000000..3243381 --- /dev/null +++ b/tests/underline.js @@ -0,0 +1,95 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/* globals document */ + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import Underline from '../src/underline'; +import UnderlineEngine from '../src/underlineengine'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; +import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard'; + +testUtils.createSinonSandbox(); + +describe( 'Underline', () => { + let editor, underlineView; + + beforeEach( () => { + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ Underline ] + } ) + .then( newEditor => { + editor = newEditor; + + underlineView = editor.ui.componentFactory.create( 'underline' ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should be loaded', () => { + expect( editor.plugins.get( Underline ) ).to.be.instanceOf( Underline ); + } ); + + it( 'should load UnderlineEngine', () => { + expect( editor.plugins.get( UnderlineEngine ) ).to.be.instanceOf( UnderlineEngine ); + } ); + + it( 'should register underline feature component', () => { + expect( underlineView ).to.be.instanceOf( ButtonView ); + expect( underlineView.isOn ).to.be.false; + expect( underlineView.label ).to.equal( 'Underline' ); + expect( underlineView.icon ).to.match( / { + const executeSpy = testUtils.sinon.spy( editor, 'execute' ); + + underlineView.fire( 'execute' ); + + sinon.assert.calledOnce( executeSpy ); + sinon.assert.calledWithExactly( executeSpy, 'underline' ); + } ); + + it( 'should bind model to underline command', () => { + const command = editor.commands.get( 'underline' ); + + expect( underlineView.isOn ).to.be.false; + + expect( underlineView.isEnabled ).to.be.false; + + command.value = true; + expect( underlineView.isOn ).to.be.true; + + command.isEnabled = true; + expect( underlineView.isEnabled ).to.be.true; + } ); + + it( 'should set keystroke in the model', () => { + expect( underlineView.keystroke ).to.equal( 'CTRL+U' ); + } ); + + it( 'should set editor keystroke', () => { + const spy = sinon.spy( editor, 'execute' ); + + const wasHandled = editor.keystrokes.press( { + keyCode: keyCodes.u, + ctrlKey: true, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + } ); + + expect( wasHandled ).to.be.true; + expect( spy.calledOnce ).to.be.true; + } ); +} ); diff --git a/tests/underlineengine.js b/tests/underlineengine.js new file mode 100644 index 0000000..0b6071a --- /dev/null +++ b/tests/underlineengine.js @@ -0,0 +1,91 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import UnderlineEngine from '../src/underlineengine'; + +import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import AttributeCommand from '../src/attributecommand'; + +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'; + +describe( 'UnderlineEngine', () => { + let editor, doc; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, UnderlineEngine ] + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.document; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should be loaded', () => { + expect( editor.plugins.get( UnderlineEngine ) ).to.be.instanceOf( UnderlineEngine ); + } ); + + it( 'should set proper schema rules', () => { + expect( doc.schema.check( { name: '$inline', attributes: 'underline', inside: '$root' } ) ).to.be.false; + expect( doc.schema.check( { name: '$inline', attributes: 'underline', inside: '$block' } ) ).to.be.true; + expect( doc.schema.check( { name: '$inline', attributes: 'underline', inside: '$clipboardHolder' } ) ).to.be.true; + } ); + + describe( 'command', () => { + it( 'should register underline command', () => { + const command = editor.commands.get( 'underline' ); + + expect( command ).to.be.instanceOf( AttributeCommand ); + expect( command ).to.have.property( 'attributeKey', 'underline' ); + } ); + } ); + + describe( 'data pipeline conversions', () => { + it( 'should convert to underline attribute', () => { + editor.setData( '

foobar

' ); + + expect( getModelData( doc, { withoutSelection: true } ) ) + .to.equal( '<$text underline="true">foobar' ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should convert text-decoration:underline to underline attribute', () => { + editor.setData( '

foobar

' ); + + expect( getModelData( doc, { withoutSelection: true } ) ) + .to.equal( '<$text underline="true">foobar' ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + + it( 'should be integrated with autoparagraphing', () => { + // Incorrect results because autoparagraphing works incorrectly (issue in paragraph). + // https://github.com/ckeditor/ckeditor5-paragraph/issues/10 + + editor.setData( 'foobar' ); + + expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( 'foobar' ); + + expect( editor.getData() ).to.equal( '

foobar

' ); + } ); + } ); + + describe( 'editing pipeline conversion', () => { + it( 'should convert attribute', () => { + setModelData( doc, '<$text underline="true">foobar' ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '

foobar

' ); + } ); + } ); +} ); diff --git a/theme/icons/underline.svg b/theme/icons/underline.svg new file mode 100644 index 0000000..ad290f2 --- /dev/null +++ b/theme/icons/underline.svg @@ -0,0 +1 @@ + \ No newline at end of file