Ver código fonte

Added: Initial TableToolbar implementation.

Maciej Gołaszewski 7 anos atrás
pai
commit
2a3cf2ca52

+ 3 - 2
packages/ckeditor5-table/src/converters/downcast.js

@@ -10,7 +10,8 @@
 import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
 import TableWalker from './../tablewalker';
-import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
+import { toWidgetEditable } from '@ckeditor/ckeditor5-widget/src/utils';
+import { toTableWidget } from '../utils';
 
 /**
  * Model table element to view table element conversion helper.
@@ -40,7 +41,7 @@ export function downcastInsertTable( options = {} ) {
 		let tableWidget;
 
 		if ( asWidget ) {
-			tableWidget = toWidget( tableElement, conversionApi.writer );
+			tableWidget = toTableWidget( tableElement, conversionApi.writer );
 		}
 
 		const tableWalker = new TableWalker( table );

+ 194 - 0
packages/ckeditor5-table/src/tabletoolbar.js

@@ -0,0 +1,194 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/tabletoolbar
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
+import { isTableWidgetSelected } from './utils';
+import { repositionContextualBalloon, getBalloonPositionData } from './ui/utils';
+
+const balloonClassName = 'ck-toolbar-container';
+
+/**
+ * The table toolbar class. Creates an table toolbar that shows up when the table widget is selected.
+ *
+ * Toolbar components are created using the editor {@link module:ui/componentfactory~ComponentFactory ComponentFactory}
+ * based on the {@link module:core/editor/editor~Editor#config configuration} stored under `table.toolbar`.
+ *
+ * The toolbar uses the {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon}.
+ *
+ * For a detailed overview, check the {@glink features/table#table-contextual-toolbar table contextual toolbar} documentation.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class TableToolbar extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ ContextualBalloon ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'TableToolbar';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+		const balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
+
+		// If the `BalloonToolbar` plugin is loaded, it should be disabled for tables
+		// which have their own toolbar to avoid duplication.
+		// https://github.com/ckeditor/ckeditor5-image/issues/110
+		if ( balloonToolbar ) {
+			this.listenTo( balloonToolbar, 'show', evt => {
+				if ( isTableWidgetSelected( editor.editing.view.document.selection ) ) {
+					evt.stop();
+				}
+			}, { priority: 'high' } );
+		}
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	afterInit() {
+		const editor = this.editor;
+		const toolbarConfig = editor.config.get( 'table.toolbar' );
+
+		// Don't add the toolbar if there is no configuration.
+		if ( !toolbarConfig || !toolbarConfig.length ) {
+			return;
+		}
+
+		/**
+		 * A contextual balloon plugin instance.
+		 *
+		 * @private
+		 * @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
+		 */
+		this._balloon = this.editor.plugins.get( 'ContextualBalloon' );
+
+		/**
+		 * A `ToolbarView` instance used to display the buttons specific for table editing.
+		 *
+		 * @protected
+		 * @type {module:ui/toolbar/toolbarview~ToolbarView}
+		 */
+		this._toolbar = new ToolbarView();
+
+		// Add buttons to the toolbar.
+		this._toolbar.fillFromConfig( toolbarConfig, editor.ui.componentFactory );
+
+		// Show balloon panel each time table widget is selected.
+		this.listenTo( editor.editing.view, 'render', () => {
+			this._checkIsVisible();
+		} );
+
+		// There is no render method after focus is back in editor, we need to check if balloon panel should be visible.
+		this.listenTo( editor.ui.focusTracker, 'change:isFocused', () => {
+			this._checkIsVisible();
+		}, { priority: 'low' } );
+	}
+
+	/**
+	 * Checks whether the toolbar should show up or hide depending on the current selection.
+	 *
+	 * @private
+	 */
+	_checkIsVisible() {
+		const editor = this.editor;
+
+		if ( !editor.ui.focusTracker.isFocused ) {
+			this._hideToolbar();
+		} else {
+			if ( isTableWidgetSelected( editor.editing.view.document.selection ) ) {
+				this._showToolbar();
+			} else {
+				this._hideToolbar();
+			}
+		}
+	}
+
+	/**
+	 * Shows the {@link #_toolbar} in the {@link #_balloon}.
+	 *
+	 * @private
+	 */
+	_showToolbar() {
+		const editor = this.editor;
+
+		if ( this._isVisible ) {
+			repositionContextualBalloon( editor );
+		} else {
+			if ( !this._balloon.hasView( this._toolbar ) ) {
+				this._balloon.add( {
+					view: this._toolbar,
+					position: getBalloonPositionData( editor ),
+					balloonClassName
+				} );
+			}
+		}
+	}
+
+	/**
+	 * Removes the {@link #_toolbar} from the {@link #_balloon}.
+	 *
+	 * @private
+	 */
+	_hideToolbar() {
+		if ( !this._isVisible ) {
+			return;
+		}
+
+		this._balloon.remove( this._toolbar );
+	}
+
+	/**
+	 * Returns `true` when the {@link #_toolbar} is the visible view in the {@link #_balloon}.
+	 *
+	 * @private
+	 * @type {Boolean}
+	 */
+	get _isVisible() {
+		return this._balloon.visibleView == this._toolbar;
+	}
+}
+
+/**
+ * Items to be placed in the table toolbar.
+ * This option is used by the {@link module:table/tabletoolbar~TableToolbar} feature.
+ *
+ * Assuming that you use the {@link module:table/tableui~TableUI} feature below toolbar items will be available
+ * in {@link module:ui/componentfactory~ComponentFactory}:
+ *
+ * * `'tableRow'`,
+ * * `'tableColumn'`,
+ * * `'mergeCell'`,
+ * * `'splitCell'`,
+ *
+ * so you can configure the toolbar like this:
+ *
+ *		const tableConfig = {
+ *			toolbar: [ 'tableRow', 'tableColumn', 'mergeCell', 'splitCell' ]
+ *		};
+ *
+ * Of course, the same buttons can also be used in the
+ * {@link module:core/editor/editorconfig~EditorConfig#toolbar main editor toolbar}.
+ *
+ * Read more about configuring toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
+ *
+ * @member {Array.<String>} module:table~TableConfig#toolbar
+ */

+ 51 - 0
packages/ckeditor5-table/src/ui/utils.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/image/ui/utils
+ */
+
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+import { getParentTable } from '../commands/utils';
+
+/**
+ * A helper utility that positions the
+ * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} instance
+ * with respect to the image in the editor content, if one is selected.
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ */
+export function repositionContextualBalloon( editor ) {
+	const balloon = editor.plugins.get( 'ContextualBalloon' );
+
+	balloon.updatePosition( getBalloonPositionData( editor ) );
+}
+
+/**
+ * Returns the positioning options that control the geometry of the
+ * {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon} with respect
+ * to the selected element in the editor content.
+ *
+ * @param {module:core/editor/editor~Editor} editor The editor instance.
+ * @returns {module:utils/dom/position~Options}
+ */
+export function getBalloonPositionData( editor ) {
+	const editingView = editor.editing.view;
+	const defaultPositions = BalloonPanelView.defaultPositions;
+
+	const parentTable = getParentTable( editingView.document.selection.getFirstPosition() );
+
+	return {
+		target: editingView.domConverter.viewToDom( parentTable ),
+		positions: [
+			defaultPositions.northArrowSouth,
+			defaultPositions.northArrowSouthWest,
+			defaultPositions.northArrowSouthEast,
+			defaultPositions.southArrowNorth,
+			defaultPositions.southArrowNorthWest,
+			defaultPositions.southArrowNorthEast
+		]
+	};
+}

+ 51 - 0
packages/ckeditor5-table/src/utils.js

@@ -0,0 +1,51 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module table/utils
+ */
+
+import { toWidget, isWidget } from '@ckeditor/ckeditor5-widget/src/utils';
+import { getParentTable } from './commands/utils';
+
+const tableSymbol = Symbol( 'isImage' );
+
+/**
+ * Converts a given {@link module:engine/view/element~Element} to an table widget:
+ * * Adds a {@link module:engine/view/element~Element#_setCustomProperty custom property} allowing to recognize the table widget element.
+ * * Calls the {@link module:widget/utils~toWidget} function with the proper element's label creator.
+ *
+ * @param {module:engine/view/element~Element} viewElement
+ * @param {module:engine/view/writer~Writer} writer An instance of the view writer.
+ * @param {String} label The element's label. It will be concatenated with the table `alt` attribute if one is present.
+ * @returns {module:engine/view/element~Element}
+ */
+export function toTableWidget( viewElement, writer ) {
+	writer.setCustomProperty( tableSymbol, true, viewElement );
+
+	return toWidget( viewElement, writer );
+}
+
+/**
+ * Checks if a given view element is an table widget.
+ *
+ * @param {module:engine/view/element~Element} viewElement
+ * @returns {Boolean}
+ */
+export function isTableWidget( viewElement ) {
+	return !!viewElement.getCustomProperty( tableSymbol ) && isWidget( viewElement );
+}
+
+/**
+ * Checks if an table widget is the only selected element.
+ *
+ * @param {module:engine/view/selection~Selection|module:engine/view/documentselection~DocumentSelection} selection
+ * @returns {Boolean}
+ */
+export function isTableWidgetSelected( selection ) {
+	const parentTable = getParentTable( selection.getFirstPosition() );
+
+	return !!( parentTable && isTableWidget( parentTable ) );
+}

+ 86 - 0
packages/ckeditor5-table/tests/integration.js

@@ -0,0 +1,86 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import BalloonToolbar from '@ckeditor/ckeditor5-ui/src/toolbar/balloon/balloontoolbar';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Table from '../src/table';
+import TableToolbar from '../src/tabletoolbar';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+
+describe( 'TableToolbar integration', () => {
+	describe( 'with the BalloonToolbar', () => {
+		let balloon, balloonToolbar, newEditor, editorElement;
+
+		beforeEach( () => {
+			editorElement = global.document.createElement( 'div' );
+			global.document.body.appendChild( editorElement );
+
+			return ClassicTestEditor
+				.create( editorElement, {
+					plugins: [ Table, TableToolbar, BalloonToolbar, Paragraph ]
+				} )
+				.then( editor => {
+					newEditor = editor;
+					balloon = newEditor.plugins.get( 'ContextualBalloon' );
+					balloonToolbar = newEditor.plugins.get( 'BalloonToolbar' );
+					const button = new View();
+
+					button.element = global.document.createElement( 'div' );
+
+					// There must be at least one toolbar items which is not disabled to show it.
+					// https://github.com/ckeditor/ckeditor5-ui/issues/269
+					balloonToolbar.toolbarView.items.add( button );
+
+					newEditor.editing.view.isFocused = true;
+					newEditor.editing.view.getDomRoot().focus();
+				} );
+		} );
+
+		afterEach( () => {
+			editorElement.remove();
+			return newEditor.destroy();
+		} );
+
+		it( 'should prevent the BalloonToolbar from being displayed when an table is selected', () => {
+			// When table is selected along with text.
+			setModelData( newEditor.model, '<paragraph>fo[o</paragraph><table><tableRow><tableCell></tableCell></tableRow></table>]' );
+
+			balloonToolbar.show();
+
+			// BalloonToolbar should be visible.
+			expect( balloon.visibleView ).to.equal( balloonToolbar.toolbarView );
+
+			// When only table is selected.
+			setModelData( newEditor.model, '<paragraph>foo</paragraph><table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			balloonToolbar.show();
+
+			// BalloonToolbar should not be visible.
+			expect( balloon.visibleView ).to.be.null;
+		} );
+
+		it( 'should listen to BalloonToolbar#show event with the high priority', () => {
+			const highestPrioritySpy = sinon.spy();
+			const highPrioritySpy = sinon.spy();
+			const normalPrioritySpy = sinon.spy();
+
+			// Select an table
+			setModelData( newEditor.model, '<paragraph>foo</paragraph><table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			newEditor.listenTo( balloonToolbar, 'show', highestPrioritySpy, { priority: 'highest' } );
+			newEditor.listenTo( balloonToolbar, 'show', highPrioritySpy, { priority: 'high' } );
+			newEditor.listenTo( balloonToolbar, 'show', normalPrioritySpy, { priority: 'normal' } );
+
+			balloonToolbar.show();
+
+			sinon.assert.calledOnce( highestPrioritySpy );
+			sinon.assert.notCalled( highPrioritySpy );
+			sinon.assert.notCalled( normalPrioritySpy );
+		} );
+	} );
+} );

+ 7 - 4
packages/ckeditor5-table/tests/manual/table.js

@@ -8,14 +8,17 @@
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import Table from '../../src/table';
+import TableToolbar from '../../src/tabletoolbar';
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, Table ],
+		plugins: [ ArticlePluginSet, Table, TableToolbar ],
 		toolbar: [
-			'heading', '|', 'insertTable', '|', 'tableColumn', 'tableRow', 'mergeCell', 'splitCell',
-			'|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
-		]
+			'heading', '|', 'insertTable', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'undo', 'redo'
+		],
+		table: {
+			toolbar: [ 'tableColumn', 'tableRow', 'mergeCell', 'splitCell' ]
+		}
 	} )
 	.then( editor => {
 		window.editor = editor;

+ 192 - 0
packages/ckeditor5-table/tests/tabletoolbar.js

@@ -0,0 +1,192 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document */
+
+import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
+import TableToolbar from '../src/tabletoolbar';
+import Table from '../src/table';
+import global from '@ckeditor/ckeditor5-utils/src/dom/global';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import Range from '@ckeditor/ckeditor5-engine/src/model/range';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+
+describe( 'TableToolbar', () => {
+	let editor, model, doc, editingView, plugin, toolbar, balloon, editorElement;
+
+	beforeEach( () => {
+		editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicEditor
+			.create( editorElement, {
+				plugins: [ Paragraph, Table, TableToolbar, FakeButton ],
+				table: {
+					toolbar: [ 'fake_button' ]
+				}
+			} )
+			.then( newEditor => {
+				editor = newEditor;
+				model = newEditor.model;
+				doc = model.document;
+				plugin = editor.plugins.get( TableToolbar );
+				toolbar = plugin._toolbar;
+				editingView = editor.editing.view;
+				balloon = editor.plugins.get( 'ContextualBalloon' );
+			} );
+	} );
+
+	afterEach( () => {
+		editorElement.remove();
+
+		return editor.destroy();
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( TableToolbar ) ).to.be.instanceOf( TableToolbar );
+	} );
+
+	it( 'should not initialize if there is no configuration', () => {
+		const editorElement = global.document.createElement( 'div' );
+		global.document.body.appendChild( editorElement );
+
+		return ClassicEditor.create( editorElement, {
+			plugins: [ TableToolbar ]
+		} )
+			.then( editor => {
+				expect( editor.plugins.get( TableToolbar )._toolbar ).to.be.undefined;
+
+				editorElement.remove();
+				return editor.destroy();
+			} );
+	} );
+
+	describe( 'toolbar', () => {
+		it( 'should use the config.table.toolbar to create items', () => {
+			expect( toolbar.items ).to.have.length( 1 );
+			expect( toolbar.items.get( 0 ).label ).to.equal( 'fake button' );
+		} );
+
+		it( 'should set proper CSS classes', () => {
+			const spy = sinon.spy( balloon, 'add' );
+
+			editor.ui.focusTracker.isFocused = true;
+
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			sinon.assert.calledWithMatch( spy, {
+				view: toolbar,
+				balloonClassName: 'ck-toolbar-container'
+			} );
+		} );
+	} );
+
+	describe( 'integration with the editor focus', () => {
+		it( 'should show the toolbar when the editor gains focus and the table is selected', () => {
+			editor.ui.focusTracker.isFocused = true;
+
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			editor.ui.focusTracker.isFocused = false;
+			expect( balloon.visibleView ).to.be.null;
+
+			editor.ui.focusTracker.isFocused = true;
+			expect( balloon.visibleView ).to.equal( toolbar );
+		} );
+
+		it( 'should hide the toolbar when the editor loses focus and the table is selected', () => {
+			editor.ui.focusTracker.isFocused = false;
+
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			editor.ui.focusTracker.isFocused = true;
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			editor.ui.focusTracker.isFocused = false;
+			expect( balloon.visibleView ).to.be.null;
+		} );
+	} );
+
+	describe( 'integration with the editor selection (#change event)', () => {
+		beforeEach( () => {
+			editor.ui.focusTracker.isFocused = true;
+		} );
+
+		it( 'should show the toolbar on render when the table is selected', () => {
+			setData( model, '<paragraph>[foo]</paragraph><table><tableRow><tableCell></tableCell></tableRow></table>' );
+
+			expect( balloon.visibleView ).to.be.null;
+
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.be.null;
+
+			model.change( writer => {
+				// Select the [<table></table>]
+				writer.setSelection(
+					Range.createOn( doc.getRoot().getChild( 1 ).getChild( 0 ).getChild( 0 ) )
+				);
+			} );
+
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			// Make sure successive change does not throw, e.g. attempting
+			// to insert the toolbar twice.
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.equal( toolbar );
+		} );
+
+		it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
+			setData( model, '<table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			const lastView = new View();
+			lastView.element = document.createElement( 'div' );
+
+			balloon.add( { view: lastView } );
+			expect( balloon.visibleView ).to.equal( lastView );
+
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.equal( lastView );
+		} );
+
+		it( 'should hide the toolbar on render if the table is de–selected', () => {
+			setData( model, '<paragraph>foo</paragraph><table><tableRow><tableCell>[]</tableCell></tableRow></table>' );
+
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			model.change( writer => {
+				// Select the <paragraph>[...]</paragraph>
+				writer.setSelection(
+					Range.createIn( doc.getRoot().getChild( 0 ) )
+				);
+			} );
+
+			expect( balloon.visibleView ).to.be.null;
+
+			// Make sure successive change does not throw, e.g. attempting
+			// to remove the toolbar twice.
+			editingView.change( () => {} );
+			expect( balloon.visibleView ).to.be.null;
+		} );
+	} );
+
+	// Plugin that adds fake_button to editor's component factory.
+	class FakeButton extends Plugin {
+		init() {
+			this.editor.ui.componentFactory.add( 'fake_button', locale => {
+				const view = new ButtonView( locale );
+
+				view.set( {
+					label: 'fake button'
+				} );
+
+				return view;
+			} );
+		}
+	}
+} );