Преглед изворни кода

Feature: Initial implementation of ButtonGroupView and ButtonDropdownView.

Maciej Gołaszewski пре 8 година
родитељ
комит
7f5b394321

+ 128 - 0
packages/ckeditor5-ui/src/buttongroup/buttongroupview.js

@@ -0,0 +1,128 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/buttongroup/buttongroupview
+ */
+
+import View from '../view';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import FocusCycler from '../focuscycler';
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+
+/**
+ * The button group view class.
+ *
+ * @extends module:ui/view~View
+ */
+export default class ButtonGroupView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( buttonsViews = [], options = {} ) {
+		super();
+
+		/**
+		 * Collection of the child button views.
+		 *
+		 * @readonly
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this.items = this.createCollection();
+
+		buttonsViews.map( button => this.items.add( button ) );
+
+		/**
+		 * Tracks information about DOM focus in the group.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * Helps cycling over focusable {@link #items} in the group.
+		 *
+		 * @readonly
+		 * @protected
+		 * @member {module:ui/focuscycler~FocusCycler}
+		 */
+		this._focusCycler = new FocusCycler( {
+			focusables: this.items,
+			focusTracker: this.focusTracker,
+			keystrokeHandler: this.keystrokes,
+			actions: {
+				// Navigate list items backwards using the arrowup key.
+				focusPrevious: [ 'arrowup', 'arrowleft' ],
+
+				// Navigate toolbar items forwards using the arrowdown key.
+				focusNext: [ 'arrowdown', 'arrowright' ]
+			}
+		} );
+
+		this.set( 'isVertical', !!options.isVertical );
+
+		const bind = this.bindTemplate;
+
+		this.setTemplate( {
+			tag: 'div',
+
+			attributes: {
+				class: [
+					'ck-reset',
+					'ck-button-group',
+					bind.if( 'isVertical', 'ck-button-group__vertical' )
+				]
+			},
+
+			children: this.items
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		// Items added before rendering should be known to the #focusTracker.
+		for ( const item of this.items ) {
+			this.focusTracker.add( item.element );
+		}
+
+		this.items.on( 'add', ( evt, item ) => {
+			this.focusTracker.add( item.element );
+		} );
+
+		this.items.on( 'remove', ( evt, item ) => {
+			this.focusTracker.remove( item.element );
+		} );
+
+		// Start listening for the keystrokes coming from #element.
+		this.keystrokes.listenTo( this.element );
+	}
+
+	/**
+	 * Focuses the first focusable in {@link #items}.
+	 */
+	focus() {
+		this._focusCycler.focusFirst();
+	}
+
+	/**
+	 * Focuses the last focusable in {@link #items}.
+	 */
+	focusLast() {
+		this._focusCycler.focusLast();
+	}
+}

+ 15 - 0
packages/ckeditor5-ui/src/dropdown/button/buttondropdownmodel.jsdoc

@@ -0,0 +1,15 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/button/buttondropdownmodel
+ */
+
+/**
+ * The button dropdown model interface.
+ *
+ * @implements module:ui/dropdown/dropdownmodel~DropdownModel
+ * @interface module:ui/dropdown/button/buttondropdownmodel~ButtonDropdownModel
+ */

+ 26 - 0
packages/ckeditor5-ui/src/dropdown/button/buttondropdownview.jsdoc

@@ -0,0 +1,26 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/button/createbuttonview
+ */
+
+/**
+ * The button dropdown view.
+ *
+ * See {@link module:ui/dropdown/button/createbuttonview~createButtonDropdown}.
+ *
+ * @abstract
+ * @class module:ui/dropdown/button/buttondropdownview~ButtonDropdownView
+ * @extends module:ui/dropdown/dropdownview~DropdownView
+ */
+
+/**
+ * A child {@link module:ui/buttongroup/buttongroupview~ButtonGroupView button group view} of the dropdown located
+ * in its {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
+ *
+ * @readonly
+ * @member {module:ui/buttongroup/buttongroup~ButtonGroupView} #buttonGroupView
+ */

+ 91 - 0
packages/ckeditor5-ui/src/dropdown/button/createbuttondropdown.js

@@ -0,0 +1,91 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/button/createbuttondropdown
+ */
+
+import createDropdown from '../createdropdown';
+
+import ButtonGroupView from '../../buttongroup/buttongroupview';
+import { closeDropdownOnBlur, closeDropdownOnExecute, openDropdownOnArrows } from '../utils';
+
+/**
+ * Creates an instance of {@link module:ui/dropdown/button/buttondropdownview~ButtonDropdownView} class using
+ * a provided {@link module:ui/dropdown/button/buttondropdownmodel~ButtonDropdownModel}.
+ *
+ *		const buttons = [];
+ *
+ * 		buttons.push( new ButtonView() );
+ *		buttons.push( editor.ui.componentFactory.get( 'someButton' ) );
+ *
+ *		const model = new Model( {
+ *			label: 'A button dropdown',
+ *			isVertical: true
+ *		} );
+ *
+ *		const dropdown = createButtonDropdown( model, buttons, locale );
+ *
+ *		// Will render a vertucak button dropdown labeled "A button dropdown"
+ *		// with a button group in the panel containing two buttons.
+ *		dropdown.render()
+ *		document.body.appendChild( dropdown.element );
+ *
+ * The model instance remains in control of the dropdown after it has been created. E.g. changes to the
+ * {@link module:ui/dropdown/dropdownmodel~DropdownModel#label `model.label`} will be reflected in the
+ * dropdown button's {@link module:ui/button/buttonview~ButtonView#label} attribute and in DOM.
+ *
+ * See {@link module:ui/dropdown/createdropdown~createDropdown} and {@link module:ui/buttongroup/buttongroup~ButtonGroup}.
+ *
+ * @param {module:ui/dropdown/button/buttondropdownmodel~ButtonDropdownModel} model Model of the list dropdown.
+ * @param {Array.<module:ui/button/buttonview~ButtonView>} buttonViews List of buttons to be included in dropdown.
+ * @param {module:utils/locale~Locale} locale The locale instance.
+ * @returns {module:ui/dropdown/button/buttondropdownview~ButtonDropdownView} The button dropdown view instance.
+ * @returns {module:ui/dropdown/dropdownview~DropdownView}
+ */
+export default function createButtonDropdown( model, buttonViews, locale ) {
+	// Make disabled when all buttons are disabled
+	model.bind( 'isEnabled' ).to(
+		// Bind to #isEnabled of each command...
+		...getBindingTargets( buttonViews, 'isEnabled' ),
+		// ...and set it true if any command #isEnabled is true.
+		( ...areEnabled ) => areEnabled.some( isEnabled => isEnabled )
+	);
+
+	// Make dropdown icon as any active button.
+	model.bind( 'icon' ).to(
+		// Bind to #isOn of each button...
+		...getBindingTargets( buttonViews, 'isOn' ),
+		// ...and chose the title of the first one which #isOn is true.
+		( ...areActive ) => {
+			const index = areActive.findIndex( value => value );
+
+			// If none of the commands is active, display first icon.
+			// TODO: make this configurable (defaultIcon)
+			return buttonViews[ index < 0 ? 0 : index ].icon;
+		}
+	);
+
+	const dropdownView = createDropdown( model, locale );
+
+	const buttonGroupView = dropdownView.buttonGroupView = new ButtonGroupView(
+		buttonViews,
+		{
+			isVertical: model.isVertical
+		}
+	);
+
+	dropdownView.panelView.children.add( buttonGroupView );
+
+	closeDropdownOnBlur( dropdownView );
+	closeDropdownOnExecute( dropdownView, buttonGroupView.items );
+	openDropdownOnArrows( dropdownView, buttonGroupView );
+
+	return dropdownView;
+}
+
+function getBindingTargets( buttons, attribute ) {
+	return Array.prototype.concat( ...buttons.map( button => [ button, attribute ] ) );
+}

+ 2 - 1
packages/ckeditor5-ui/src/dropdown/createdropdown.js

@@ -41,7 +41,8 @@ import DropdownPanelView from './dropdownpanelview';
  */
 export default function createDropdown( model, locale ) {
 	const buttonView = new ButtonView( locale );
-	buttonView.bind( 'label', 'isOn', 'isEnabled', 'withText', 'keystroke', 'tooltip' ).to( model );
+
+	buttonView.bind( 'label', 'isOn', 'isEnabled', 'withText', 'keystroke', 'tooltip', 'icon' ).to( model );
 
 	const panelView = new DropdownPanelView( locale );
 

+ 9 - 0
packages/ckeditor5-ui/src/dropdown/dropdownmodel.jsdoc

@@ -49,3 +49,12 @@
  * @observable
  * @member {Boolean} #withText
  */
+
+/**
+ * (Optional) Controls the icon of the dropdown.
+ *
+ * Also see {@link module:ui/button/buttonview~ButtonView#withText}.
+ *
+ * @observable
+ * @member {Boolean} #icon
+ */

+ 4 - 51
packages/ckeditor5-ui/src/dropdown/list/createlistdropdown.js

@@ -7,11 +7,10 @@
  * @module ui/dropdown/list/createlistdropdown
  */
 
-/* global document */
-
 import ListView from '../../list/listview';
 import ListItemView from '../../list/listitemview';
 import createDropdown from '../createdropdown';
+import { closeDropdownOnBlur, closeDropdownOnExecute, openDropdownOnArrows } from '../utils';
 
 /**
  * Creates an instance of {@link module:ui/dropdown/list/listdropdownview~ListDropdownView} class using
@@ -64,57 +63,11 @@ export default function createListDropdown( model, locale ) {
 		return item;
 	} );
 
-	// TODO: Delegate all events instead of just execute.
-	listView.items.delegate( 'execute' ).to( dropdownView );
-
 	dropdownView.panelView.children.add( listView );
 
-	dropdownView.on( 'change:isOpen', ( evt, name, value ) => {
-		if ( value ) {
-			attachDocumentClickListener( dropdownView );
-		} else {
-			dropdownView.stopListening( document );
-		}
-	} );
-
-	// Close the dropdown when one of the list items has been executed.
-	dropdownView.on( 'execute', () => {
-		dropdownView.isOpen = false;
-	} );
-
-	// If the dropdown panel is already open, the arrow down key should
-	// focus the first element in list.
-	dropdownView.keystrokes.set( 'arrowdown', ( data, cancel ) => {
-		if ( dropdownView.isOpen ) {
-			listView.focus();
-			cancel();
-		}
-	} );
-
-	// If the dropdown panel is already open, the arrow up key should
-	// focus the last element in the list.
-	dropdownView.keystrokes.set( 'arrowup', ( data, cancel ) => {
-		if ( dropdownView.isOpen ) {
-			listView.focusLast();
-			cancel();
-		}
-	} );
+	closeDropdownOnBlur( dropdownView );
+	closeDropdownOnExecute( dropdownView, listView.items );
+	openDropdownOnArrows( dropdownView, listView );
 
 	return dropdownView;
 }
-
-// Attaches a "click" listener in DOM to check if any element outside
-// the dropdown has been clicked.
-//
-// @private
-// @param {module:ui/dropdown/listdropdownview~ListDropdownView} dropdownView
-function attachDocumentClickListener( dropdownView ) {
-	// TODO: It will probably be focus/blur-based rather than click. It should be bound
-	// to focusmanager of some sort.
-	dropdownView.listenTo( document, 'click', ( evtInfo, { target: domEvtTarget } ) => {
-		// Collapse the dropdown when the webpage outside of the component is clicked.
-		if ( dropdownView.element != domEvtTarget && !dropdownView.element.contains( domEvtTarget ) ) {
-			dropdownView.isOpen = false;
-		}
-	} );
-}

+ 66 - 0
packages/ckeditor5-ui/src/dropdown/utils.js

@@ -0,0 +1,66 @@
+/**
+ * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module ui/dropdown/utils
+ */
+
+/* global document */
+
+export function openDropdownOnArrows( dropdownView, buttonGroupView ) {
+	// If the dropdown panel is already open, the arrow down key should
+	// focus the first element in list.
+	dropdownView.keystrokes.set( 'arrowdown', ( data, cancel ) => {
+		if ( dropdownView.isOpen ) {
+			buttonGroupView.focus();
+			cancel();
+		}
+	} );
+
+	// If the dropdown panel is already open, the arrow up key should
+	// focus the last element in the list.
+	dropdownView.keystrokes.set( 'arrowup', ( data, cancel ) => {
+		if ( dropdownView.isOpen ) {
+			buttonGroupView.focusLast();
+			cancel();
+		}
+	} );
+}
+
+export function closeDropdownOnExecute( dropdownView, items ) {
+	// TODO: Delegate all events instead of just execute.
+	items.delegate( 'execute' ).to( dropdownView );
+
+	// Close the dropdown when one of the list items has been executed.
+	dropdownView.on( 'execute', () => {
+		dropdownView.isOpen = false;
+	} );
+}
+
+export function closeDropdownOnBlur( dropdownView ) {
+	dropdownView.on( 'change:isOpen', ( evt, name, value ) => {
+		if ( value ) {
+			attachDocumentClickListener( document, dropdownView );
+		} else {
+			dropdownView.stopListening( document );
+		}
+	} );
+}
+
+// Attaches a "click" listener in DOM to check if any element outside
+// the dropdown has been clicked.
+//
+// @private
+// @param {module:ui/dropdown/listdropdownview~ListDropdownView} dropdownView
+function attachDocumentClickListener( document, dropdownView ) {
+	// TODO: It will probably be focus/blur-based rather than click. It should be bound
+	// to focusmanager of some sort.
+	dropdownView.listenTo( document, 'click', ( evtInfo, { target: domEvtTarget } ) => {
+		// Collapse the dropdown when the webpage outside of the component is clicked.
+		if ( dropdownView.element != domEvtTarget && !dropdownView.element.contains( domEvtTarget ) ) {
+			dropdownView.isOpen = false;
+		}
+	} );
+}

+ 11 - 0
packages/ckeditor5-ui/theme/components/buttongroup.scss

@@ -0,0 +1,11 @@
+// Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
+// For licensing, see LICENSE.md or http://ckeditor.com/license
+
+.ck-button-group {
+	@include ck-unselectable();
+}
+
+.ck-button-group__vertical {
+	display: flex;
+	flex-direction: column;
+}

+ 1 - 0
packages/ckeditor5-ui/theme/theme.scss

@@ -9,6 +9,7 @@
 @import 'components/icon';
 @import 'components/tooltip';
 @import 'components/button';
+@import 'components/buttongroup';
 @import 'components/toolbar/toolbar';
 @import 'components/dropdown';
 @import 'components/list';