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

Commit a39bac2

Browse files
author
Piotr Jasiun
authored
Merge pull request #105 from ckeditor/t/104
Feature: Introduced HeadingButtonsUI plugin. Closes #104.
2 parents e95bc8f + 1a4c954 commit a39bac2

File tree

12 files changed

+365
-34
lines changed

12 files changed

+365
-34
lines changed

src/heading.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,6 @@ export default class Heading extends Plugin {
111111
* @property {module:engine/view/elementdefinition~ElementDefinition} view Definition of a view element to convert from/to.
112112
* @property {String} title The user-readable title of the option.
113113
* @property {String} class The class which will be added to the dropdown item representing this option.
114+
* @property {String} [icon] Icon used by {@link module:heading/headingbuttonsui~HeadingButtonsUI}. It can be omitted when using
115+
* the default configuration.
114116
*/

src/headingbuttonsui.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/**
7+
* @module heading/headingbuttonsui
8+
*/
9+
10+
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
11+
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
12+
13+
import { getLocalizedOptions } from './utils';
14+
import iconHeading1 from '../theme/icons/heading1.svg';
15+
import iconHeading2 from '../theme/icons/heading2.svg';
16+
import iconHeading3 from '../theme/icons/heading3.svg';
17+
18+
const defaultIcons = {
19+
heading1: iconHeading1,
20+
heading2: iconHeading2,
21+
heading3: iconHeading3
22+
};
23+
24+
/**
25+
* HeadingButtonsUI class creates a set of UI buttons that can be used instead of drop down component.
26+
* It is not enabled by default when using {@link module:heading/heading~Heading heading plugin}, and needs to be
27+
* added manually to the editor configuration.
28+
*
29+
* Plugin introduces button UI elements, which names are same as `model` property from {@link module:heading/heading~HeadingOption}.
30+
*
31+
* ClassicEditor
32+
* .create( {
33+
* plugins: [ ..., Heading, Paragraph, HeadingButtonsUI, ParagraphButtonUI ]
34+
* heading: {
35+
* options: [
36+
* { model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
37+
* { model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
38+
* { model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
39+
* { model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
40+
* ]
41+
* },
42+
* toolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3' ]
43+
* } )
44+
* .then( ... )
45+
* .catch( ... );
46+
*
47+
* NOTE: Paragraph button is defined in {@link module:paragraph/paragraphbuttonui~ParagraphButtonUI} plugin that needs
48+
* to be loaded manually as well.
49+
*
50+
* It is possible to use custom icons by providing `icon` config option provided in {@link module:heading/heading~HeadingOption}.
51+
* For the default configuration standard icons are used.
52+
*
53+
* @extends module:core/plugin~Plugin
54+
*/
55+
export default class HeadingButtonsUI extends Plugin {
56+
/**
57+
* @inheritDoc
58+
*/
59+
init() {
60+
const options = getLocalizedOptions( this.editor );
61+
62+
options
63+
.filter( item => item.model !== 'paragraph' )
64+
.map( item => this._createButton( item ) );
65+
}
66+
67+
/**
68+
* Creates single button view from provided configuration option.
69+
*
70+
* @private
71+
* @param {Object} option
72+
*/
73+
_createButton( option ) {
74+
const editor = this.editor;
75+
76+
editor.ui.componentFactory.add( option.model, locale => {
77+
const view = new ButtonView( locale );
78+
const command = editor.commands.get( 'heading' );
79+
80+
view.label = option.title;
81+
view.icon = option.icon || defaultIcons[ option.model ];
82+
view.tooltip = true;
83+
view.bind( 'isEnabled' ).to( command );
84+
view.bind( 'isOn' ).to( command, 'value', value => value == option.model );
85+
86+
view.on( 'execute', () => {
87+
editor.execute( 'heading', { value: option.model } );
88+
} );
89+
90+
return view;
91+
} );
92+
}
93+
}

src/headingui.js

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
1111
import Model from '@ckeditor/ckeditor5-ui/src/model';
1212

1313
import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
14+
import { getLocalizedOptions } from './utils';
1415

1516
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
1617

@@ -28,7 +29,7 @@ export default class HeadingUI extends Plugin {
2829
init() {
2930
const editor = this.editor;
3031
const t = editor.t;
31-
const options = this._getLocalizedOptions();
32+
const options = getLocalizedOptions( editor );
3233
const defaultTitle = t( 'Choose heading' );
3334
const dropdownTooltip = t( 'Heading' );
3435

@@ -102,37 +103,4 @@ export default class HeadingUI extends Plugin {
102103
return dropdownView;
103104
} );
104105
}
105-
106-
/**
107-
* Returns heading options as defined in `config.heading.options` but processed to consider
108-
* editor localization, i.e. to display {@link module:heading/heading~HeadingOption}
109-
* in the correct language.
110-
*
111-
* Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t}
112-
* when the user config is defined because the editor does not exist yet.
113-
*
114-
* @private
115-
* @returns {Array.<module:heading/heading~HeadingOption>}.
116-
*/
117-
_getLocalizedOptions() {
118-
const editor = this.editor;
119-
const t = editor.t;
120-
const localizedTitles = {
121-
Paragraph: t( 'Paragraph' ),
122-
'Heading 1': t( 'Heading 1' ),
123-
'Heading 2': t( 'Heading 2' ),
124-
'Heading 3': t( 'Heading 3' )
125-
};
126-
127-
return editor.config.get( 'heading.options' ).map( option => {
128-
const title = localizedTitles[ option.title ];
129-
130-
if ( title && title != option.title ) {
131-
// Clone the option to avoid altering the original `config.heading.options`.
132-
option = Object.assign( {}, option, { title } );
133-
}
134-
135-
return option;
136-
} );
137-
}
138106
}

src/utils.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Returns heading options as defined in `config.heading.options` but processed to consider
3+
* editor localization, i.e. to display {@link module:heading/heading~HeadingOption}
4+
* in the correct language.
5+
*
6+
* Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t}
7+
* when the user config is defined because the editor does not exist yet.
8+
*
9+
* @param {module:core/editor/editor~Editor} editor
10+
* @returns {Array.<module:heading/heading~HeadingOption>}.
11+
*/
12+
export function getLocalizedOptions( editor ) {
13+
const t = editor.t;
14+
const localizedTitles = {
15+
Paragraph: t( 'Paragraph' ),
16+
'Heading 1': t( 'Heading 1' ),
17+
'Heading 2': t( 'Heading 2' ),
18+
'Heading 3': t( 'Heading 3' )
19+
};
20+
21+
return editor.config.get( 'heading.options' ).map( option => {
22+
const title = localizedTitles[ option.title ];
23+
24+
if ( title && title != option.title ) {
25+
// Clone the option to avoid altering the original `config.heading.options`.
26+
option = Object.assign( {}, option, { title } );
27+
}
28+
29+
return option;
30+
} );
31+
}

tests/headingbuttonsui.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* globals document */
7+
8+
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
9+
import HeadingEditing from '../src/headingediting';
10+
import HeadingButtonsUI from '../src/headingbuttonsui';
11+
import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
12+
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
13+
import { getLocalizedOptions } from '../src/utils';
14+
import iconHeading2 from '../theme/icons/heading2.svg';
15+
16+
describe( 'HeadingButtonUI', () => {
17+
let editorElement, editor;
18+
19+
describe( 'default config', () => {
20+
beforeEach( () => {
21+
editorElement = document.createElement( 'div' );
22+
document.body.appendChild( editorElement );
23+
24+
return ClassicTestEditor
25+
.create( editorElement, {
26+
plugins: [ HeadingButtonsUI, HeadingEditing ],
27+
toolbar: [ 'heading1', 'heading2', 'heading3' ]
28+
} )
29+
.then( newEditor => {
30+
editor = newEditor;
31+
32+
// Set data so the commands will be enabled.
33+
setData( editor.model, '<heading1>f{}oo</heading1>' );
34+
} );
35+
} );
36+
37+
afterEach( () => {
38+
editorElement.remove();
39+
40+
return editor.destroy();
41+
} );
42+
43+
it( 'should define default buttons', () => {
44+
const factory = editor.ui.componentFactory;
45+
46+
expect( factory.create( 'heading1' ) ).to.be.instanceOf( ButtonView );
47+
expect( factory.create( 'heading2' ) ).to.be.instanceOf( ButtonView );
48+
expect( factory.create( 'heading3' ) ).to.be.instanceOf( ButtonView );
49+
} );
50+
51+
it( 'should intialize buttons with correct localized data', () => {
52+
const localizedOptions = getLocalizedOptions( editor ).filter( option => option.model == 'heading2' )[ 0 ];
53+
const heading2Button = editor.ui.componentFactory.create( 'heading2' );
54+
55+
expect( heading2Button.label ).to.equal( localizedOptions.title );
56+
expect( heading2Button.icon ).to.equal( iconHeading2 );
57+
expect( heading2Button.tooltip ).to.equal( true );
58+
} );
59+
60+
it( 'should bind buttons to correct commands', () => {
61+
const headingButton = editor.ui.componentFactory.create( 'heading1' );
62+
const headingCommand = editor.commands.get( 'heading' );
63+
64+
expect( headingCommand.isEnabled ).to.be.true;
65+
expect( headingButton.isEnabled ).to.be.true;
66+
67+
headingCommand.isEnabled = false;
68+
expect( headingButton.isEnabled ).to.be.false;
69+
70+
expect( headingCommand.value ).to.equal( 'heading1' );
71+
expect( headingButton.isOn ).to.be.true;
72+
73+
setData( editor.model, '<heading2>f{}oo</heading2>' );
74+
75+
expect( headingCommand.value ).to.equal( 'heading2' );
76+
expect( headingButton.isOn ).to.be.false;
77+
} );
78+
79+
it( 'should bind button execute to command execute', () => {
80+
const headingButton = editor.ui.componentFactory.create( 'heading1' );
81+
const executeCommandSpy = sinon.spy( editor, 'execute' );
82+
83+
headingButton.fire( 'execute' );
84+
85+
sinon.assert.calledOnce( executeCommandSpy );
86+
sinon.assert.calledWithExactly( executeCommandSpy, 'heading', { value: 'heading1' } );
87+
} );
88+
} );
89+
90+
describe( 'custom config', () => {
91+
const customIcon = '<svg></svg>';
92+
93+
beforeEach( () => {
94+
editorElement = document.createElement( 'div' );
95+
document.body.appendChild( editorElement );
96+
97+
return ClassicTestEditor
98+
.create( editorElement, {
99+
heading: {
100+
options: [
101+
{ model: 'paragraph' },
102+
{ model: 'heading1', view: 'h2', icon: customIcon },
103+
]
104+
},
105+
plugins: [ HeadingButtonsUI, HeadingEditing ],
106+
toolbar: [ 'heading1', 'heading2', 'heading3' ]
107+
} )
108+
.then( newEditor => {
109+
editor = newEditor;
110+
111+
// Set data so the commands will be enabled.
112+
setData( editor.model, '<heading1>f{}oo</heading1>' );
113+
} );
114+
} );
115+
116+
afterEach( () => {
117+
editorElement.remove();
118+
119+
return editor.destroy();
120+
} );
121+
122+
it( 'should allow to pass custom image to the configuration', () => {
123+
const headingButton = editor.ui.componentFactory.create( 'heading1' );
124+
125+
expect( headingButton.icon ).to.equal( customIcon );
126+
} );
127+
} );
128+
} );

tests/manual/heading-buttons.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div id="editor">
2+
<h2>Heading 1</h2>
3+
<h3>Heading 2</h3>
4+
<h4>Heading 3</h4>
5+
<p>Paragraph</p>
6+
</div>

tests/manual/heading-buttons.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
3+
* For licensing, see LICENSE.md.
4+
*/
5+
6+
/* globals console, document, window */
7+
8+
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
9+
import Enter from '@ckeditor/ckeditor5-enter/src/enter';
10+
import Typing from '@ckeditor/ckeditor5-typing/src/typing';
11+
import Heading from '../../src/heading';
12+
import HeadingButtonsUI from '../../src/headingbuttonsui';
13+
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
14+
import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui';
15+
import Undo from '@ckeditor/ckeditor5-undo/src/undo';
16+
17+
ClassicEditor
18+
.create( document.querySelector( '#editor' ), {
19+
plugins: [ Enter, Typing, Undo, Heading, Paragraph, HeadingButtonsUI, ParagraphButtonUI ],
20+
toolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', '|', 'undo', 'redo' ]
21+
} )
22+
.then( editor => {
23+
window.editor = editor;
24+
} )
25+
.catch( err => {
26+
console.error( err.stack );
27+
} );

tests/manual/heading-buttons.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Headings button UI feature
2+
3+
1. The data should be loaded with three headings and one paragraph.
4+
2. Put selection inside each block and check if correct button is selected.
5+
3. Switch headings using buttons.

0 commit comments

Comments
 (0)