8
0
Просмотр исходного кода

Merge pull request #105 from ckeditor/t/104

Feature: Introduced HeadingButtonsUI plugin. Closes #104.
Piotr Jasiun 7 лет назад
Родитель
Сommit
4d6f685419

+ 2 - 0
packages/ckeditor5-heading/src/heading.js

@@ -111,4 +111,6 @@ export default class Heading extends Plugin {
  * @property {module:engine/view/elementdefinition~ElementDefinition} view Definition of a view element to convert from/to.
  * @property {String} title The user-readable title of the option.
  * @property {String} class The class which will be added to the dropdown item representing this option.
+ * @property {String} [icon] Icon used by {@link module:heading/headingbuttonsui~HeadingButtonsUI}. It can be omitted when using
+ * the default configuration.
  */

+ 93 - 0
packages/ckeditor5-heading/src/headingbuttonsui.js

@@ -0,0 +1,93 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module heading/headingbuttonsui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import { getLocalizedOptions } from './utils';
+import iconHeading1 from '../theme/icons/heading1.svg';
+import iconHeading2 from '../theme/icons/heading2.svg';
+import iconHeading3 from '../theme/icons/heading3.svg';
+
+const defaultIcons = {
+	heading1: iconHeading1,
+	heading2: iconHeading2,
+	heading3: iconHeading3
+};
+
+/**
+ * HeadingButtonsUI class creates a set of UI buttons that can be used instead of drop down component.
+ * It is not enabled by default when using {@link module:heading/heading~Heading heading plugin}, and needs to be
+ * added manually to the editor configuration.
+ *
+ * Plugin introduces button UI elements, which names are same as `model` property from {@link module:heading/heading~HeadingOption}.
+ *
+ *		ClassicEditor
+ *			.create( {
+ *				plugins: [ ..., Heading, Paragraph, HeadingButtonsUI, ParagraphButtonUI ]
+ *				heading: {
+ *					options: [
+ *						{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
+ *						{ model: 'heading1', view: 'h2', title: 'Heading 1', class: 'ck-heading_heading1' },
+ *						{ model: 'heading2', view: 'h3', title: 'Heading 2', class: 'ck-heading_heading2' },
+ *						{ model: 'heading3', view: 'h4', title: 'Heading 3', class: 'ck-heading_heading3' }
+ *					]
+ * 				},
+ * 				toolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3' ]
+ *			} )
+ *			.then( ... )
+ *			.catch( ... );
+ *
+ * NOTE: Paragraph button is defined in {@link module:paragraph/paragraphbuttonui~ParagraphButtonUI} plugin that needs
+ * to be loaded manually as well.
+ *
+ * It is possible to use custom icons by providing `icon` config option provided in {@link module:heading/heading~HeadingOption}.
+ * For the default configuration standard icons are used.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class HeadingButtonsUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const options = getLocalizedOptions( this.editor );
+
+		options
+			.filter( item => item.model !== 'paragraph' )
+			.map( item => this._createButton( item ) );
+	}
+
+	/**
+	 * Creates single button view from provided configuration option.
+	 *
+	 * @private
+	 * @param {Object} option
+	 */
+	_createButton( option ) {
+		const editor = this.editor;
+
+		editor.ui.componentFactory.add( option.model, locale => {
+			const view = new ButtonView( locale );
+			const command = editor.commands.get( 'heading' );
+
+			view.label = option.title;
+			view.icon = option.icon || defaultIcons[ option.model ];
+			view.tooltip = true;
+			view.bind( 'isEnabled' ).to( command );
+			view.bind( 'isOn' ).to( command, 'value', value => value == option.model );
+
+			view.on( 'execute', () => {
+				editor.execute( 'heading', { value: option.model } );
+			} );
+
+			return view;
+		} );
+	}
+}

+ 2 - 34
packages/ckeditor5-heading/src/headingui.js

@@ -11,6 +11,7 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Model from '@ckeditor/ckeditor5-ui/src/model';
 
 import { createDropdown, addListToDropdown } from '@ckeditor/ckeditor5-ui/src/dropdown/utils';
+import { getLocalizedOptions } from './utils';
 
 import Collection from '@ckeditor/ckeditor5-utils/src/collection';
 
@@ -28,7 +29,7 @@ export default class HeadingUI extends Plugin {
 	init() {
 		const editor = this.editor;
 		const t = editor.t;
-		const options = this._getLocalizedOptions();
+		const options = getLocalizedOptions( editor );
 		const defaultTitle = t( 'Choose heading' );
 		const dropdownTooltip = t( 'Heading' );
 
@@ -102,37 +103,4 @@ export default class HeadingUI extends Plugin {
 			return dropdownView;
 		} );
 	}
-
-	/**
-	 * Returns heading options as defined in `config.heading.options` but processed to consider
-	 * editor localization, i.e. to display {@link module:heading/heading~HeadingOption}
-	 * in the correct language.
-	 *
-	 * Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t}
-	 * when the user config is defined because the editor does not exist yet.
-	 *
-	 * @private
-	 * @returns {Array.<module:heading/heading~HeadingOption>}.
-	 */
-	_getLocalizedOptions() {
-		const editor = this.editor;
-		const t = editor.t;
-		const localizedTitles = {
-			Paragraph: t( 'Paragraph' ),
-			'Heading 1': t( 'Heading 1' ),
-			'Heading 2': t( 'Heading 2' ),
-			'Heading 3': t( 'Heading 3' )
-		};
-
-		return editor.config.get( 'heading.options' ).map( option => {
-			const title = localizedTitles[ option.title ];
-
-			if ( title && title != option.title ) {
-				// Clone the option to avoid altering the original `config.heading.options`.
-				option = Object.assign( {}, option, { title } );
-			}
-
-			return option;
-		} );
-	}
 }

+ 31 - 0
packages/ckeditor5-heading/src/utils.js

@@ -0,0 +1,31 @@
+/**
+ * Returns heading options as defined in `config.heading.options` but processed to consider
+ * editor localization, i.e. to display {@link module:heading/heading~HeadingOption}
+ * in the correct language.
+ *
+ * Note: The reason behind this method is that there's no way to use {@link module:utils/locale~Locale#t}
+ * when the user config is defined because the editor does not exist yet.
+ *
+ * @param {module:core/editor/editor~Editor} editor
+ * @returns {Array.<module:heading/heading~HeadingOption>}.
+ */
+export function getLocalizedOptions( editor ) {
+	const t = editor.t;
+	const localizedTitles = {
+		Paragraph: t( 'Paragraph' ),
+		'Heading 1': t( 'Heading 1' ),
+		'Heading 2': t( 'Heading 2' ),
+		'Heading 3': t( 'Heading 3' )
+	};
+
+	return editor.config.get( 'heading.options' ).map( option => {
+		const title = localizedTitles[ option.title ];
+
+		if ( title && title != option.title ) {
+			// Clone the option to avoid altering the original `config.heading.options`.
+			option = Object.assign( {}, option, { title } );
+		}
+
+		return option;
+	} );
+}

+ 128 - 0
packages/ckeditor5-heading/tests/headingbuttonsui.js

@@ -0,0 +1,128 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import HeadingEditing from '../src/headingediting';
+import HeadingButtonsUI from '../src/headingbuttonsui';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import { getLocalizedOptions } from '../src/utils';
+import iconHeading2 from '../theme/icons/heading2.svg';
+
+describe( 'HeadingButtonUI', () => {
+	let editorElement, editor;
+
+	describe( 'default config', () => {
+		beforeEach( () => {
+			editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ HeadingButtonsUI, HeadingEditing ],
+					toolbar: [ 'heading1', 'heading2', 'heading3' ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					// Set data so the commands will be enabled.
+					setData( editor.model, '<heading1>f{}oo</heading1>' );
+				} );
+		} );
+
+		afterEach( () => {
+			editorElement.remove();
+
+			return editor.destroy();
+		} );
+
+		it( 'should define default buttons', () => {
+			const factory = editor.ui.componentFactory;
+
+			expect( factory.create( 'heading1' ) ).to.be.instanceOf( ButtonView );
+			expect( factory.create( 'heading2' ) ).to.be.instanceOf( ButtonView );
+			expect( factory.create( 'heading3' ) ).to.be.instanceOf( ButtonView );
+		} );
+
+		it( 'should intialize buttons with correct localized data', () => {
+			const localizedOptions = getLocalizedOptions( editor ).filter( option => option.model == 'heading2' )[ 0 ];
+			const heading2Button = editor.ui.componentFactory.create( 'heading2' );
+
+			expect( heading2Button.label ).to.equal( localizedOptions.title );
+			expect( heading2Button.icon ).to.equal( iconHeading2 );
+			expect( heading2Button.tooltip ).to.equal( true );
+		} );
+
+		it( 'should bind buttons to correct commands', () => {
+			const headingButton = editor.ui.componentFactory.create( 'heading1' );
+			const headingCommand = editor.commands.get( 'heading' );
+
+			expect( headingCommand.isEnabled ).to.be.true;
+			expect( headingButton.isEnabled ).to.be.true;
+
+			headingCommand.isEnabled = false;
+			expect( headingButton.isEnabled ).to.be.false;
+
+			expect( headingCommand.value ).to.equal( 'heading1' );
+			expect( headingButton.isOn ).to.be.true;
+
+			setData( editor.model, '<heading2>f{}oo</heading2>' );
+
+			expect( headingCommand.value ).to.equal( 'heading2' );
+			expect( headingButton.isOn ).to.be.false;
+		} );
+
+		it( 'should bind button execute to command execute', () => {
+			const headingButton = editor.ui.componentFactory.create( 'heading1' );
+			const executeCommandSpy = sinon.spy( editor, 'execute' );
+
+			headingButton.fire( 'execute' );
+
+			sinon.assert.calledOnce( executeCommandSpy );
+			sinon.assert.calledWithExactly( executeCommandSpy, 'heading', { value: 'heading1' } );
+		} );
+	} );
+
+	describe( 'custom config', () => {
+		const customIcon = '<svg></svg>';
+
+		beforeEach( () => {
+			editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					heading: {
+						options: [
+							{ model: 'paragraph' },
+							{ model: 'heading1', view: 'h2', icon: customIcon },
+						]
+					},
+					plugins: [ HeadingButtonsUI, HeadingEditing ],
+					toolbar: [ 'heading1', 'heading2', 'heading3' ]
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+
+					// Set data so the commands will be enabled.
+					setData( editor.model, '<heading1>f{}oo</heading1>' );
+				} );
+		} );
+
+		afterEach( () => {
+			editorElement.remove();
+
+			return editor.destroy();
+		} );
+
+		it( 'should allow to pass custom image to the configuration', () => {
+			const headingButton = editor.ui.componentFactory.create( 'heading1' );
+
+			expect( headingButton.icon ).to.equal( customIcon );
+		} );
+	} );
+} );

+ 6 - 0
packages/ckeditor5-heading/tests/manual/heading-buttons.html

@@ -0,0 +1,6 @@
+<div id="editor">
+	<h2>Heading 1</h2>
+	<h3>Heading 2</h3>
+	<h4>Heading 3</h4>
+	<p>Paragraph</p>
+</div>

+ 27 - 0
packages/ckeditor5-heading/tests/manual/heading-buttons.js

@@ -0,0 +1,27 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals console, document, window */
+
+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 Heading from '../../src/heading';
+import HeadingButtonsUI from '../../src/headingbuttonsui';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import ParagraphButtonUI from '@ckeditor/ckeditor5-paragraph/src/paragraphbuttonui';
+import Undo from '@ckeditor/ckeditor5-undo/src/undo';
+
+ClassicEditor
+	.create( document.querySelector( '#editor' ), {
+		plugins: [ Enter, Typing, Undo, Heading, Paragraph, HeadingButtonsUI, ParagraphButtonUI ],
+		toolbar: [ 'paragraph', 'heading1', 'heading2', 'heading3', '|', 'undo', 'redo' ]
+	} )
+	.then( editor => {
+		window.editor = editor;
+	} )
+	.catch( err => {
+		console.error( err.stack );
+	} );

+ 5 - 0
packages/ckeditor5-heading/tests/manual/heading-buttons.md

@@ -0,0 +1,5 @@
+## Headings button UI feature
+
+1. The data should be loaded with three headings and one paragraph.
+2. Put selection inside each block and check if correct button is selected.
+3. Switch headings using buttons.

+ 68 - 0
packages/ckeditor5-heading/tests/utils.js

@@ -0,0 +1,68 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* globals document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+import Heading from '../src/heading';
+import { getLocalizedOptions } from '../src/utils';
+
+testUtils.createSinonSandbox();
+
+describe( 'utils', () => {
+	describe( 'getLocalizedOptions()', () => {
+		let editor, editorElement;
+
+		before( () => {
+			addTranslations( 'pl', {
+				'Choose heading': 'Wybierz nagłówek',
+				'Paragraph': 'Akapit',
+				'Heading': 'Nagłówek',
+				'Heading 1': 'Nagłówek 1',
+				'Heading 2': 'Nagłówek 2',
+				'Heading 3': 'Nagłówek 3'
+			} );
+		} );
+
+		after( () => {
+			clearTranslations();
+		} );
+
+		beforeEach( () => {
+			editorElement = document.createElement( 'div' );
+			document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Heading ],
+					toolbar: [ 'heading' ],
+					language: 'pl'
+				} )
+				.then( newEditor => {
+					editor = newEditor;
+				} );
+		} );
+
+		afterEach( () => {
+			editorElement.remove();
+
+			return editor.destroy();
+		} );
+
+		it( 'should return localized options', () => {
+			const localized = getLocalizedOptions( editor );
+
+			expect( Array.isArray( localized ) ).to.be.true;
+			expect( localized.length ).to.equal( editor.config.get( 'heading.options' ).length );
+
+			expect( localized.find( item => item.model == 'paragraph' ).title ).to.equal( 'Akapit' );
+			expect( localized.find( item => item.model == 'heading1' ).title ).to.equal( 'Nagłówek 1' );
+			expect( localized.find( item => item.model == 'heading2' ).title ).to.equal( 'Nagłówek 2' );
+			expect( localized.find( item => item.model == 'heading3' ).title ).to.equal( 'Nagłówek 3' );
+		} );
+	} );
+} );

+ 1 - 0
packages/ckeditor5-heading/theme/icons/heading1.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><g fill="#000"><path d="M1.3 17c-1 0-1.3-.3-1.3-1V4c0-.7.4-1 1.3-1 .8 0 1.2.3 1.2 1v4.4c0 .1 0 .2.2.2h5V4s0-1 1.3-1 1.3 1 1.3 1v12c0 .6-.4 1-1.3 1-.8 0-1.2-.4-1.2-1v-5.2l-.2-.1h-5V16c0 .7-.5 1-1.4 1zm12.4-7c-.2.2-.3.2-.5.1a.4.4 0 0 1-.2-.4v-.6c0-.5.2-1 .7-1.4l1.6-1.2c.4-.3 1-.5 1.5-.5h.5c.2 0 .4 0 .6.3l.2.6v9.3c0 .5-.3.8-1 .8-1.1 0-1.1-.8-1.1-.8V8.3l-2.3 1.8z"/></g></g></svg>

+ 1 - 0
packages/ckeditor5-heading/theme/icons/heading2.svg

@@ -0,0 +1 @@
+<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><g fill="#000"><path d="M20 9.2c0 .9-.3 1.8-1 2.6a19.8 19.8 0 0 1-3.4 3.5 234.8 234.8 0 0 0 4.1.3c.2.1.3.3.3.6 0 .2 0 .4-.3.5-.1.2-.3.3-.5.3h-5.5c-.2 0-.4 0-.6-.3a.8.8 0 0 1-.2-.5c0-.6.2-1 .6-1.4 1.7-1.4 2.9-2.4 3.5-3.2.5-.8.8-1.5.8-2.2 0-1.1-.6-1.7-1.7-1.7-.6 0-1.4.2-2.2.5h-.6a.5.5 0 0 1-.2-.5v-.2c0-.3 0-.5.2-.7.1-.2.3-.4.6-.4.8-.3 1.6-.4 2.5-.4a4 4 0 0 1 2.6.8 3 3 0 0 1 1 2.4zM1 17a1 1 0 0 1-.7-.3 1 1 0 0 1-.3-.8V4.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.3h.3c.3 0 .6.1.8.3.2.2.3.5.3.8v4.3c0 .1 0 .2.2.2h5V4.1c0-.3.2-.6.4-.8.2-.2.5-.3.8-.3h.3c.3 0 .6.1.8.3.2.2.3.5.3.8v11.8c0 .3-.1.6-.3.8a1 1 0 0 1-.8.3h-.3a1 1 0 0 1-.8-.3 1 1 0 0 1-.3-.8v-5c0-.2 0-.2-.2-.2h-5v5.2c0 .3-.2.6-.4.8a1 1 0 0 1-.8.3h-.3z"/></g></g></svg>

+ 1 - 0
packages/ckeditor5-heading/theme/icons/heading3.svg

@@ -0,0 +1 @@
+<svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"><g fill="#000" fill-rule="evenodd"><path d="M1.074 17c-.294 0-.547-.109-.758-.326A1.066 1.066 0 0 1 0 15.907V4.093c0-.294.105-.55.316-.767C.527 3.109.78 3 1.074 3h.345c.294 0 .55.109.767.326.217.217.326.473.326.767v4.354c0 .102.05.153.153.153H7.63c.102 0 .153-.051.153-.153V4.093c0-.294.109-.55.326-.767A1.05 1.05 0 0 1 8.876 3h.328c.294 0 .546.109.757.326.21.217.316.473.316.767v11.814c0 .294-.105.55-.316.767-.21.217-.463.326-.757.326h-.328a1.05 1.05 0 0 1-.767-.326 1.05 1.05 0 0 1-.326-.767v-5.082c0-.115-.05-.173-.153-.173H2.665c-.102 0-.153.058-.153.173v5.082c0 .294-.109.55-.326.767a1.05 1.05 0 0 1-.767.326h-.345zM13.4 14.856c.162-.251.34-.328.531-.229.06.03.156.087.289.174a3.783 3.783 0 0 0 .99.447 3.73 3.73 0 0 0 1.76.037c.231-.06.44-.153.628-.281.424-.29.636-.697.636-1.22 0-.483-.22-.857-.658-1.123-.414-.247-.954-.37-1.619-.37-.3 0-.52.012-.658.037-.172.03-.286.044-.34.044-.054 0-.108-.034-.163-.103a.762.762 0 0 1-.125-.244 1.562 1.562 0 0 1-.082-.462c0-.136.021-.247.063-.333.042-.086.132-.144.27-.174 1.434-.32 2.339-.79 2.713-1.412a1.36 1.36 0 0 0 .214-.724c0-.251-.043-.455-.129-.61a1.15 1.15 0 0 0-.351-.388c-.296-.212-.658-.318-1.087-.318-.34 0-.626.064-.857.192-.429.237-.68.52-.754.85-.06.232-.211.348-.455.348-.244 0-.432-.021-.565-.063a.87.87 0 0 1-.31-.159c-.12-.108-.178-.277-.178-.506 0-.23.048-.451.144-.666.096-.214.226-.406.392-.576.165-.17.356-.32.573-.447.216-.129.446-.234.687-.318a4.113 4.113 0 0 1 1.4-.259c.457 0 .878.054 1.265.163.387.108.73.273 1.031.495.67.493 1.006 1.175 1.006 2.048 0 .724-.281 1.343-.843 1.855-.39.35-.877.624-1.464.82.784 0 1.43.237 1.937.71.488.459.732.98.732 1.564 0 .584-.105 1.077-.314 1.478a3.24 3.24 0 0 1-.817 1.017 3.682 3.682 0 0 1-2.395.85c-1.36 0-2.408-.303-3.142-.91-.237-.187-.355-.338-.355-.454 0-.116.027-.216.081-.3l.318-.48z"/></g></svg>