diff --git a/docs/api/basic-styles.md b/docs/api/basic-styles.md index 2cf4f1f..e0760ea 100644 --- a/docs/api/basic-styles.md +++ b/docs/api/basic-styles.md @@ -6,7 +6,7 @@ category: api-reference [![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-basic-styles.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-basic-styles) -This package contains features allowing to apply basic text formatting such as bold, italic, underline and code in CKEditor 5. +This package contains features allowing to apply basic text formatting such as bold, italic, underline, strikethrough and code in CKEditor 5. ## Documentation @@ -14,6 +14,7 @@ Check out the following plugins: * {@link module:basic-styles/bold~Bold} * {@link module:basic-styles/italic~Italic} +* {@link module:basic-styles/strikethrough~Strikethrough} * {@link module:basic-styles/underline~Underline} * {@link module:basic-styles/code~Code} diff --git a/lang/contexts.json b/lang/contexts.json index d475f68..f55952b 100644 --- a/lang/contexts.json +++ b/lang/contexts.json @@ -2,5 +2,6 @@ "Bold": "Toolbar button tooltip for the Bold feature.", "Italic": "Toolbar button tooltip for the Italic feature.", "Underline": "Toolbar button tooltip for the Underline feature.", - "Code": "Toolbar button tooltip for the Code feature." + "Code": "Toolbar button tooltip for the Code feature.", + "Strikethrough": "Toolbar button tooltip for the Strikethrough feature." } diff --git a/src/strikethrough.js b/src/strikethrough.js new file mode 100644 index 0000000..125f38a --- /dev/null +++ b/src/strikethrough.js @@ -0,0 +1,68 @@ +/** + * @license Copyright (c) 2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module basic-styles/strikethrough + */ + +import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; +import StrikethroughEngine from './strikethroughengine'; +import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; +import strikethroughIcon from '../theme/icons/strikethrough.svg'; + +/** + * The strikethrough feature. It introduces the Strikethrough button and the Ctrl+Shift+X keystroke. + * + * It uses the {@link module:basic-styles/strikethroughengine~StrikethroughEngine strikethrough engine feature}. + * + * @extends module:core/plugin~Plugin + */ +export default class Strikethrough extends Plugin { + /** + * @inheritDoc + */ + static get requires() { + return [ StrikethroughEngine ]; + } + + /** + * @inheritDoc + */ + static get pluginName() { + return 'Strikethrough'; + } + + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const t = editor.t; + const command = editor.commands.get( 'strikethrough' ); + const keystroke = 'CTRL+SHIFT+X'; + + // Add strikethrough button to feature components. + editor.ui.componentFactory.add( 'strikethrough', locale => { + const view = new ButtonView( locale ); + + view.set( { + label: t( 'Strikethrough' ), + icon: strikethroughIcon, + keystroke, + tooltip: true + } ); + + view.bind( 'isOn', 'isEnabled' ).to( command, 'value', 'isEnabled' ); + + // Execute command. + this.listenTo( view, 'execute', () => editor.execute( 'strikethrough' ) ); + + return view; + } ); + + // Set the Ctrl+Shift+X keystroke. + editor.keystrokes.set( keystroke, 'strikethrough' ); + } +} diff --git a/src/strikethroughengine.js b/src/strikethroughengine.js new file mode 100644 index 0000000..632c190 --- /dev/null +++ b/src/strikethroughengine.js @@ -0,0 +1,56 @@ +/** + * @license Copyright (c) 2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +/** + * @module basic-styles/strikeengine + */ + +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 STRIKETHROUGH = 'strikethrough'; + +/** + * The strikethrough engine feature. + * + * It registers the `strikethrough` command and introduces the + * `strikethroughsthrough` attribute in the model which renders to the view + * as a `` element. + * + * @extends module:core/plugin~Plugin + */ +export default class StrikethroughEngine extends Plugin { + /** + * @inheritDoc + */ + init() { + const editor = this.editor; + const data = editor.data; + const editing = editor.editing; + + // Allow strikethrough attribute on all inline nodes. + editor.document.schema.allow( { name: '$inline', attributes: STRIKETHROUGH, inside: '$block' } ); + // Temporary workaround. See https://github.com/ckeditor/ckeditor5/issues/477. + editor.document.schema.allow( { name: '$inline', attributes: STRIKETHROUGH, inside: '$clipboardHolder' } ); + + // Build converter from model to view for data and editing pipelines. + buildModelConverter().for( data.modelToView, editing.modelToView ) + .fromAttribute( STRIKETHROUGH ) + .toElement( 's' ); + + // Build converter from view to model for data pipeline. + buildViewConverter().for( data.viewToModel ) + .fromElement( 's' ) + .fromElement( 'del' ) + .fromElement( 'strike' ) + .fromAttribute( 'style', { 'text-decoration': 'line-through' } ) + .toAttribute( STRIKETHROUGH, true ); + + // Create strikethrough command. + editor.commands.add( STRIKETHROUGH, new AttributeCommand( editor, STRIKETHROUGH ) ); + } +} diff --git a/tests/manual/basic-styles.html b/tests/manual/basic-styles.html index 77dd397..5ac17dc 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 e849929..1db7a2b 100644 --- a/tests/manual/basic-styles.js +++ b/tests/manual/basic-styles.js @@ -10,13 +10,14 @@ import Essentials from '@ckeditor/ckeditor5-essentials/src/essentials'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Bold from '../../src/bold'; import Italic from '../../src/italic'; +import Strikethrough from '../../src/strikethrough'; import Underline from '../../src/underline'; import Code from '../../src/code'; ClassicEditor .create( document.querySelector( '#editor' ), { - plugins: [ Essentials, Paragraph, Bold, Italic, Underline, Code ], - toolbar: [ 'bold', 'italic', 'underline', 'code', 'undo', 'redo' ] + plugins: [ Essentials, Paragraph, Bold, Italic, Strikethrough, Underline, Code ], + toolbar: [ 'bold', 'italic', 'strikethrough', 'underline', 'code', 'undo', 'redo' ] } ) .then( editor => { window.editor = editor; diff --git a/tests/manual/basic-styles.md b/tests/manual/basic-styles.md index 65f6869..994bbb8 100644 --- a/tests/manual/basic-styles.md +++ b/tests/manual/basic-styles.md @@ -1,8 +1,9 @@ ## Basic styles 1. The data should be loaded with: - * italic "This", - * bold "editor", + * italic _"This"_, + * bold **"editor"**, * underline "instance", - * code "is an". -2. Test the bold, italic, underline and code features live. + * strikethrough ~~"is"~~, + * code `"an"`. +2. Test the bold, italic, strikethrough, underline and code features live. diff --git a/tests/strikethrough.js b/tests/strikethrough.js new file mode 100644 index 0000000..69cdcf6 --- /dev/null +++ b/tests/strikethrough.js @@ -0,0 +1,98 @@ +/** + * @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 Strikethrough from '../src/strikethrough'; +import StrikethroughEngine from '../src/strikethroughengine'; +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( 'Strikethrough', () => { + let editor, strikeView; + + beforeEach( () => { + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor + .create( editorElement, { + plugins: [ Strikethrough ] + } ) + .then( newEditor => { + editor = newEditor; + + strikeView = editor.ui.componentFactory.create( 'strikethrough' ); + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should be loaded', () => { + expect( editor.plugins.get( Strikethrough ) ).to.be.instanceOf( Strikethrough ); + } ); + + it( 'should load StrikethroughEngine', () => { + expect( editor.plugins.get( StrikethroughEngine ) ).to.be.instanceOf( StrikethroughEngine ); + } ); + + it( 'should register strikethrough feature component', () => { + expect( strikeView ).to.be.instanceOf( ButtonView ); + expect( strikeView.isOn ).to.be.false; + expect( strikeView.label ).to.equal( 'Strikethrough' ); + expect( strikeView.icon ).to.match( / { + const executeSpy = testUtils.sinon.spy( editor, 'execute' ); + + strikeView.fire( 'execute' ); + + sinon.assert.calledOnce( executeSpy ); + sinon.assert.calledWithExactly( executeSpy, 'strikethrough' ); + } ); + + it( 'should bind model to strikethrough command', () => { + const command = editor.commands.get( 'strikethrough' ); + + expect( strikeView.isOn ).to.be.false; + + expect( strikeView.isEnabled ).to.be.false; + + command.value = true; + expect( strikeView.isOn ).to.be.true; + + command.isEnabled = true; + expect( strikeView.isEnabled ).to.be.true; + } ); + + it( 'should set keystroke in the model', () => { + expect( strikeView.keystroke ).to.equal( 'CTRL+SHIFT+X' ); + } ); + + it( 'should set editor keystroke', () => { + const spy = sinon.spy( editor, 'execute' ); + const keyEventData = { + keyCode: keyCodes.x, + shiftKey: true, + ctrlKey: true, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + const wasHandled = editor.keystrokes.press( keyEventData ); + + expect( wasHandled ).to.be.true; + expect( spy.calledOnce ).to.be.true; + expect( keyEventData.preventDefault.calledOnce ).to.be.true; + } ); +} ); diff --git a/tests/strikethroughengine.js b/tests/strikethroughengine.js new file mode 100644 index 0000000..9ece23a --- /dev/null +++ b/tests/strikethroughengine.js @@ -0,0 +1,108 @@ +/** + * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import StrikethroughEngine from '../src/strikethroughengine'; + +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( 'StrikethroughEngine', () => { + let editor, doc; + + beforeEach( () => { + return VirtualTestEditor + .create( { + plugins: [ Paragraph, StrikethroughEngine ] + } ) + .then( newEditor => { + editor = newEditor; + + doc = editor.document; + } ); + } ); + + afterEach( () => { + return editor.destroy(); + } ); + + it( 'should be loaded', () => { + expect( editor.plugins.get( StrikethroughEngine ) ).to.be.instanceOf( StrikethroughEngine ); + } ); + + it( 'should set proper schema rules', () => { + expect( doc.schema.check( { name: '$inline', attributes: 'strikethrough', inside: '$root' } ) ).to.be.false; + expect( doc.schema.check( { name: '$inline', attributes: 'strikethrough', inside: '$block' } ) ).to.be.true; + expect( doc.schema.check( { name: '$inline', attributes: 'strikethrough', inside: '$clipboardHolder' } ) ).to.be.true; + } ); + + describe( 'command', () => { + it( 'should register strikethrough command', () => { + const command = editor.commands.get( 'strikethrough' ); + + expect( command ).to.be.instanceOf( AttributeCommand ); + expect( command ).to.have.property( 'attributeKey', 'strikethrough' ); + } ); + } ); + + describe( 'data pipeline conversions', () => { + it( 'should convert to strikethrough attribute', () => { + editor.setData( '

foobar

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

foobar

' ); + } ); + it( 'should convert to strikethrough attribute', () => { + editor.setData( '

foobar

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

foobar

' ); + } ); + + it( 'should convert to strikethrough attribute', () => { + editor.setData( '

foobar

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

foobar

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

foobar

' ); + + expect( getModelData( doc, { withoutSelection: true } ) ) + .to.equal( '<$text strikethrough="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 strikethrough="true">foobar' ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '

foobar

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