Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #54 from ckeditor/t/51
Browse files Browse the repository at this point in the history
Feature: Introduced the `Underline` plugin. Closes #51.
  • Loading branch information
szymonkups authored Aug 28, 2017
2 parents 8f22f2e + 2a1e748 commit f724ae0
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 7 deletions.
5 changes: 3 additions & 2 deletions lang/contexts.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"Bold": "Toolbar button tooltip for the Bold feature.",
"Italic": "Toolbar button tooltip for the Italic feature."
}
"Italic": "Toolbar button tooltip for the Italic feature.",
"Underline": "Toolbar button tooltip for the Underline feature."
}
68 changes: 68 additions & 0 deletions src/underline.js
Original file line number Diff line number Diff line change
@@ -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 <kbd>Ctrl+U</kbd> 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' );
}
}
53 changes: 53 additions & 0 deletions src/underlineengine.js
Original file line number Diff line number Diff line change
@@ -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 `<u>` 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 ) );
}
}
2 changes: 1 addition & 1 deletion tests/manual/basic-styles.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div id="editor">
<p><i>This</i> is an <strong>editor</strong> instance.</p>
<p><i>This</i> is an <strong>editor</strong> <u>instance</u>.</p>
</div>
5 changes: 3 additions & 2 deletions tests/manual/basic-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions tests/manual/basic-styles.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
95 changes: 95 additions & 0 deletions tests/underline.js
Original file line number Diff line number Diff line change
@@ -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( /<svg / );
expect( underlineView.keystroke ).to.equal( 'CTRL+U' );
} );

it( 'should execute underline command on model execute event', () => {
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;
} );
} );
91 changes: 91 additions & 0 deletions tests/underlineengine.js
Original file line number Diff line number Diff line change
@@ -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 <u> to underline attribute', () => {
editor.setData( '<p><u>foo</u>bar</p>' );

expect( getModelData( doc, { withoutSelection: true } ) )
.to.equal( '<paragraph><$text underline="true">foo</$text>bar</paragraph>' );

expect( editor.getData() ).to.equal( '<p><u>foo</u>bar</p>' );
} );

it( 'should convert text-decoration:underline to underline attribute', () => {
editor.setData( '<p><span style="text-decoration: underline;">foo</span>bar</p>' );

expect( getModelData( doc, { withoutSelection: true } ) )
.to.equal( '<paragraph><$text underline="true">foo</$text>bar</paragraph>' );

expect( editor.getData() ).to.equal( '<p><u>foo</u>bar</p>' );
} );

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( '<u>foo</u>bar' );

expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<paragraph>foobar</paragraph>' );

expect( editor.getData() ).to.equal( '<p>foobar</p>' );
} );
} );

describe( 'editing pipeline conversion', () => {
it( 'should convert attribute', () => {
setModelData( doc, '<paragraph><$text underline="true">foo</$text>bar</paragraph>' );

expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '<p><u>foo</u>bar</p>' );
} );
} );
} );
1 change: 1 addition & 0 deletions theme/icons/underline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f724ae0

Please sign in to comment.