ソースを参照

Implemented LinkActionsView as an intermediate step before editing a link. Refactored the LinkFormView.

Aleksander Nowodzinski 8 年 前
コミット
806ddaf0ac

+ 169 - 49
packages/ckeditor5-link/src/link.js

@@ -18,6 +18,7 @@ import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsid
 
 import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import LinkFormView from './ui/linkformview';
+import LinkActionsView from './ui/linkactionsview';
 
 import linkIcon from '../theme/icons/link.svg';
 
@@ -54,12 +55,19 @@ export default class Link extends Plugin {
 
 		editor.editing.view.addObserver( ClickObserver );
 
+		/**
+		 * The actions view displayed inside of the balloon.
+		 *
+		 * @member {module:link/ui/linkactionsview~LinkActionsView}
+		 */
+		this.actionsView = this._createActionsView();
+
 		/**
 		 * The form view displayed inside the balloon.
 		 *
 		 * @member {module:link/ui/linkformview~LinkFormView}
 		 */
-		this.formView = this._createForm();
+		this.formView = this._createFormView();
 
 		/**
 		 * The contextual balloon plugin instance.
@@ -73,7 +81,40 @@ export default class Link extends Plugin {
 		this._createToolbarLinkButton();
 
 		// Attach lifecycle actions to the the balloon.
-		this._attachActions();
+		this._enableUserBalloonInteractions();
+	}
+
+	/**
+	 * Creates the {@link module:link/ui/linkactionsview~LinkActionsView} instance.
+	 *
+	 * @private
+	 * @returns {module:link/ui/linkactionsview~LinkActionsView} The link actions view instance.
+	 */
+	_createActionsView() {
+		const editor = this.editor;
+		const actionsView = new LinkActionsView( editor.locale );
+		const linkCommand = editor.commands.get( 'link' );
+
+		actionsView.preView.bind( 'href' ).to( linkCommand, 'value' );
+
+		// Execute unlink command after clicking on the "Unlink" button.
+		this.listenTo( actionsView, 'edit', () => {
+			this._showForm( true );
+		} );
+
+		// Execute unlink command after clicking on the "Unlink" button.
+		this.listenTo( actionsView, 'unlink', () => {
+			editor.execute( 'unlink' );
+			this._hidePanel( true );
+		} );
+
+		// Close the panel on esc key press when the form has focus.
+		actionsView.keystrokes.set( 'Esc', ( data, cancel ) => {
+			this._hidePanel( true );
+			cancel();
+		} );
+
+		return actionsView;
 	}
 
 	/**
@@ -82,18 +123,16 @@ export default class Link extends Plugin {
 	 * @private
 	 * @returns {module:link/ui/linkformview~LinkFormView} The link form instance.
 	 */
-	_createForm() {
+	_createFormView() {
 		const editor = this.editor;
 		const formView = new LinkFormView( editor.locale );
 		const linkCommand = editor.commands.get( 'link' );
-		// const unlinkCommand = editor.commands.get( 'unlink' );
 
 		formView.urlInputView.bind( 'value' ).to( linkCommand, 'value' );
 
 		// Form elements should be read-only when corresponding commands are disabled.
 		formView.urlInputView.bind( 'isReadOnly' ).to( linkCommand, 'isEnabled', value => !value );
 		formView.saveButtonView.bind( 'isEnabled' ).to( linkCommand );
-		// formView.unlinkButtonView.bind( 'isEnabled' ).to( unlinkCommand );
 
 		// Execute link command after clicking on formView `Save` button.
 		this.listenTo( formView, 'submit', () => {
@@ -101,14 +140,10 @@ export default class Link extends Plugin {
 			this._hidePanel( true );
 		} );
 
-		// Execute unlink command after clicking on formView `Unlink` button.
-		// this.listenTo( formView, 'unlink', () => {
-		// 	editor.execute( 'unlink' );
-		// 	this._hidePanel( true );
-		// } );
-
 		// Hide the panel after clicking on formView `Cancel` button.
-		this.listenTo( formView, 'cancel', () => this._hidePanel( true ) );
+		this.listenTo( formView, 'cancel', () => {
+			this._hidePanel( true );
+		} );
 
 		// Close the panel on esc key press when the form has focus.
 		formView.keystrokes.set( 'Esc', ( data, cancel ) => {
@@ -136,7 +171,7 @@ export default class Link extends Plugin {
 			cancel();
 
 			if ( linkCommand.isEnabled ) {
-				this._showPanel( true );
+				this._showForm( true );
 			}
 		} );
 
@@ -153,7 +188,9 @@ export default class Link extends Plugin {
 			button.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
 
 			// Show the panel on button click.
-			this.listenTo( button, 'execute', () => this._showPanel( true ) );
+			this.listenTo( button, 'execute', () => {
+				this._showForm( true );
+			} );
 
 			return button;
 		} );
@@ -165,7 +202,7 @@ export default class Link extends Plugin {
 	 *
 	 * @private
 	 */
-	_attachActions() {
+	_enableUserBalloonInteractions() {
 		const viewDocument = this.editor.editing.view;
 
 		// Handle click on view document and show panel when selection is placed inside the link element.
@@ -175,14 +212,14 @@ export default class Link extends Plugin {
 
 			if ( parentLink ) {
 				// Then show panel but keep focus inside editor editable.
-				this._showPanel();
+				this._showActions();
 			}
 		} );
 
 		// Focus the form if the balloon is visible and the Tab key has been pressed.
 		this.editor.keystrokes.set( 'Tab', ( data, cancel ) => {
-			if ( this._balloon.visibleView === this.formView && !this.formView.focusTracker.isFocused ) {
-				this.formView.focus();
+			if ( this._areActionsVisible && !this.actionsView.focusTracker.isFocused ) {
+				this.actionsView.focus();
 				cancel();
 			}
 		}, {
@@ -194,7 +231,7 @@ export default class Link extends Plugin {
 
 		// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
 		this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
-			if ( this._balloon.visibleView === this.formView ) {
+			if ( this._isAnyUIVisible ) {
 				this._hidePanel();
 				cancel();
 			}
@@ -203,22 +240,77 @@ export default class Link extends Plugin {
 		// Close on click outside of balloon panel element.
 		clickOutsideHandler( {
 			emitter: this.formView,
-			activator: () => this._balloon.hasView( this.formView ),
+			activator: () => this._isAnyUIVisible,
 			contextElements: [ this._balloon.view.element ],
 			callback: () => this._hidePanel()
 		} );
 	}
 
+	/**
+	 * Adds the {@link #actionsView} to the {@link #_balloon}.
+	 *
+	 * @protected
+	 */
+	_showActions() {
+		this._startRepositioningUponRender();
+
+		if ( !this._areActionsInPanel ) {
+			this._balloon.add( {
+				view: this.actionsView,
+				position: this._getBalloonPositionData()
+			} );
+		}
+	}
+
 	/**
 	 * Adds the {@link #formView} to the {@link #_balloon}.
 	 *
 	 * @protected
 	 * @param {Boolean} [focusInput=false] When `true`, the link form will be focused on panel show.
 	 */
-	_showPanel( focusInput ) {
+	_showForm( focusInput ) {
 		const editor = this.editor;
 		const linkCommand = editor.commands.get( 'link' );
-		// const unlinkCommand = editor.commands.get( 'unlink' );
+
+		if ( !this._areActionsInPanel ) {
+			this._startRepositioningUponRender();
+		}
+
+		if ( this._isFormInPanel ) {
+			// Check if formView should be focused and focus it if is visible.
+			if ( focusInput && this._balloon.visibleView === this.formView ) {
+				this.formView.urlInputView.select();
+			}
+		} else {
+			this._balloon.add( {
+				view: this.formView,
+				position: this._getBalloonPositionData()
+			} );
+
+			if ( focusInput ) {
+				this.formView.urlInputView.select();
+			}
+		}
+
+		// Make sure that each time the panel shows up, the URL field remains in sync with the value of
+		// the command. If the user typed in the input, then canceled the balloon (`urlInputView#value` stays
+		// unaltered) and re-opened it without changing the value of the link command (e.g. because they
+		// clicked the same link), they would see the old value instead of the actual value of the command.
+		// https://github.com/ckeditor/ckeditor5-link/issues/78
+		// https://github.com/ckeditor/ckeditor5-link/issues/123
+		this.formView.urlInputView.inputView.element.value = linkCommand.value || '';
+	}
+
+	/**
+	 * Makes the UI react to the {@link module:engine/view/document~Document#event:render} in the view
+	 * document to reposition itself as the document changes.
+	 *
+	 * See: {@link _hidePanel} to learn when the UI stops reacting to the `render` event.
+	 *
+	 * @protected
+	 */
+	_startRepositioningUponRender() {
+		const editor = this.editor;
 		const editing = editor.editing;
 		const showViewDocument = editing.view;
 		const showIsCollapsed = showViewDocument.selection.isCollapsed;
@@ -247,39 +339,60 @@ export default class Link extends Plugin {
 				this._balloon.updatePosition( this._getBalloonPositionData() );
 			}
 		} );
+	}
 
-		if ( this._balloon.hasView( this.formView ) ) {
-			// Check if formView should be focused and focus it if is visible.
-			if ( focusInput && this._balloon.visibleView === this.formView ) {
-				this.formView.urlInputView.select();
-			}
-		} else {
-			this._balloon.add( {
-				view: this.formView,
-				position: this._getBalloonPositionData()
-			} );
+	/**
+	 * Returns true when {@link #formView} is in the {@link #_balloon}.
+	 *
+	 * @readonly
+	 * @protected
+	 * @type {Boolean}
+	 */
+	get _isFormInPanel() {
+		return this._balloon.hasView( this.formView );
+	}
 
-			if ( focusInput ) {
-				this.formView.urlInputView.select();
-			}
-		}
+	/**
+	 * Returns true when {@link #actionsView} is in the {@link #_balloon}.
+	 *
+	 * @readonly
+	 * @protected
+	 * @type {Boolean}
+	 */
+	get _areActionsInPanel() {
+		return this._balloon.hasView( this.actionsView );
+	}
 
-		// https://github.com/ckeditor/ckeditor5-link/issues/53
-		// this.formView.unlinkButtonView.isVisible = unlinkCommand.isEnabled;
+	/**
+	 * Returns true when {@link #actionsView} is in the {@link #_balloon} and it is
+	 * currently visible.
+	 *
+	 * @readonly
+	 * @protected
+	 * @type {Boolean}
+	 */
+	get _areActionsVisible() {
+		return this._balloon.visibleView === this.actionsView;
+	}
 
-		// Make sure that each time the panel shows up, the URL field remains in sync with the value of
-		// the command. If the user typed in the input, then canceled the balloon (`urlInputView#value` stays
-		// unaltered) and re-opened it without changing the value of the link command (e.g. because they
-		// clicked the same link), they would see the old value instead of the actual value of the command.
-		// https://github.com/ckeditor/ckeditor5-link/issues/78
-		// https://github.com/ckeditor/ckeditor5-link/issues/123
-		this.formView.urlInputView.inputView.element.value = linkCommand.value || '';
+	/**
+	 * Returns true when {@link #actionsView} or {@link #formView} is in the {@link #_balloon} and it is
+	 * currently visible.
+	 *
+	 * @readonly
+	 * @protected
+	 * @type {Boolean}
+	 */
+	get _isAnyUIVisible() {
+		const visibleView = this._balloon.visibleView;
+
+		return visibleView == this.formView || this._areActionsVisible;
 	}
 
 	/**
 	 * Removes the {@link #formView} from the {@link #_balloon}.
 	 *
-	 * See {@link #_showPanel}.
+	 * See {@link #_showForm}, {@link #_showActions}.
 	 *
 	 * @protected
 	 * @param {Boolean} [focusEditable=false] When `true`, editable focus will be restored on panel hide.
@@ -287,7 +400,7 @@ export default class Link extends Plugin {
 	_hidePanel( focusEditable ) {
 		this.stopListening( this.editor.editing.view, 'render' );
 
-		if ( !this._balloon.hasView( this.formView ) ) {
+		if ( !this._isFormInPanel && !this._areActionsInPanel ) {
 			return;
 		}
 
@@ -295,8 +408,15 @@ export default class Link extends Plugin {
 			this.editor.editing.view.focus();
 		}
 
-		this.stopListening( this.editor.editing.view, 'render' );
-		this._balloon.remove( this.formView );
+		const balloon = this._balloon;
+
+		if ( this._isFormInPanel ) {
+			balloon.remove( this.formView );
+		}
+
+		if ( this._areActionsInPanel ) {
+			balloon.remove( this.actionsView );
+		}
 	}
 
 	/**

+ 232 - 0
packages/ckeditor5-link/src/ui/linkactionsview.js

@@ -0,0 +1,232 @@
+/**
+ * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module link/ui/linkactionsview
+ */
+
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
+
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+
+import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
+import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
+import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
+import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
+
+import unlinkIcon from '../../theme/icons/unlink.svg';
+import linkIcon from '../../theme/icons/link.svg';
+import '../../theme/linkactions.css';
+
+/**
+ * The link actions view class. This view displays link preview, allows
+ * unlinking or editing the link.
+ *
+ * @extends module:ui/view~View
+ */
+export default class LinkActionsView extends View {
+	/**
+	 * @inheritDoc
+	 */
+	constructor( locale ) {
+		super( locale );
+
+		const t = locale.t;
+
+		/**
+		 * Tracks information about DOM focus in the actions.
+		 *
+		 * @readonly
+		 * @member {module:utils/focustracker~FocusTracker}
+		 */
+		this.focusTracker = new FocusTracker();
+
+		/**
+		 * An instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}.
+		 *
+		 * @readonly
+		 * @member {module:utils/keystrokehandler~KeystrokeHandler}
+		 */
+		this.keystrokes = new KeystrokeHandler();
+
+		/**
+		 * The href preview view.
+		 *
+		 * @member {module:ui/view~View}
+		 */
+		this.preView = this._createPreView();
+
+		/**
+		 * The unlink button view.
+		 *
+		 * @member {module:ui/button/buttonview~ButtonView}
+		 */
+		this.unlinkButtonView = this._createButton( t( 'Unlink' ), unlinkIcon, 'unlink' );
+
+		/**
+		 * The edit link button view.
+		 *
+		 * @member {module:ui/button/buttonview~ButtonView}
+		 */
+		this.editButtonView = this._createButton( t( 'Edit' ), linkIcon, 'edit' );
+
+		/**
+		 * A collection of views which can be focused in the view.
+		 *
+		 * @readonly
+		 * @protected
+		 * @member {module:ui/viewcollection~ViewCollection}
+		 */
+		this._focusables = new ViewCollection();
+
+		/**
+		 * Helps cycling over {@link #_focusables} in the view.
+		 *
+		 * @readonly
+		 * @protected
+		 * @member {module:ui/focuscycler~FocusCycler}
+		 */
+		this._focusCycler = new FocusCycler( {
+			focusables: this._focusables,
+			focusTracker: this.focusTracker,
+			keystrokeHandler: this.keystrokes,
+			actions: {
+				// Navigate fields backwards using the Shift + Tab keystroke.
+				focusPrevious: 'shift + tab',
+
+				// Navigate fields forwards using the Tab key.
+				focusNext: 'tab'
+			}
+		} );
+
+		this.setTemplate( {
+			tag: 'div',
+
+			attributes: {
+				class: [
+					'ck-link-actions',
+				],
+
+				// https://github.com/ckeditor/ckeditor5-link/issues/90
+				tabindex: '-1'
+			},
+
+			children: [
+				this.preView,
+				this.editButtonView,
+				this.unlinkButtonView
+			]
+		} );
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	render() {
+		super.render();
+
+		submitHandler( {
+			view: this
+		} );
+
+		const childViews = [
+			this.preView,
+			this.editButtonView,
+			this.unlinkButtonView
+		];
+
+		childViews.forEach( v => {
+			// Register the view as focusable.
+			this._focusables.add( v );
+
+			// Register the view in the focus tracker.
+			this.focusTracker.add( v.element );
+		} );
+
+		// Start listening for the keystrokes coming from #element.
+		this.keystrokes.listenTo( this.element );
+	}
+
+	/**
+	 * Focuses the fist {@link #_focusables} in the actions.
+	 */
+	focus() {
+		this._focusCycler.focusFirst();
+	}
+
+	/**
+	 * Creates a button view.
+	 *
+	 * @private
+	 * @param {String} label The button label.
+	 * @param {String} icon The button's icon.
+	 * @param {String} [eventName] An event name that the `ButtonView#execute` event will be delegated to.
+	 * @returns {module:ui/button/buttonview~ButtonView} The button view instance.
+	 */
+	_createButton( label, icon, eventName ) {
+		const button = new ButtonView( this.locale );
+
+		button.set( {
+			label,
+			icon,
+			tooltip: true
+		} );
+
+		if ( eventName ) {
+			button.delegate( 'execute' ).to( this, eventName );
+		}
+
+		return button;
+	}
+
+	/**
+	 * Creates a link href preview view.
+	 *
+	 * @private
+	 * @returns {module:ui/view~View} The href preview view instance.
+	 */
+	_createPreView() {
+		const preView = new View( this.locale );
+		const bind = preView.bindTemplate;
+
+		preView.set( 'href' );
+
+		preView.setTemplate( {
+			tag: 'a',
+			attributes: {
+				class: [
+					'ck-link-actions__preview'
+				],
+				target: '_blank',
+				href: bind.to( 'href' ),
+				tabindex: -1
+			},
+			children: [
+				{
+					text: bind.to( 'href' )
+				}
+			]
+		} );
+
+		preView.focus = function() {
+			this.element.focus();
+		};
+
+		return preView;
+	}
+}
+
+/**
+ * Fired when the {@link #editButtonView} is clicked.
+ *
+ * @event edit
+ */
+
+/**
+ * Fired when the {@link #unlinkButtonView} is clicked.
+ *
+ * @event unlink
+ */

+ 5 - 3
packages/ckeditor5-link/src/ui/linkformview.js

@@ -198,9 +198,11 @@ export default class LinkFormView extends View {
 	_createButton( label, icon, eventName ) {
 		const button = new ButtonView( this.locale );
 
-		button.label = label;
-		button.icon = icon;
-		button.tooltip = true;
+		button.set( {
+			label,
+			icon,
+			tooltip: true
+		} );
 
 		if ( eventName ) {
 			button.delegate( 'execute' ).to( this, eventName );

+ 11 - 0
packages/ckeditor5-link/theme/linkactions.css

@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+.ck-link-actions {
+	& .ck-link-actions__preview {
+		display: inline-block;
+		overflow: hidden;
+	}
+}

+ 9 - 5
packages/ckeditor5-link/theme/linkform.css

@@ -3,8 +3,12 @@
  * For licensing, see LICENSE.md.
  */
 
-/*
- * Note: This file should contain the wireframe styles only. But since there are no such styles,
- * it acts as a message to the builder telling that it should look for the corresponding styles
- * **in the theme** when compiling the editor.
- */
+.ck-link-form {
+	& .ck-labeled-input {
+		display: inline-block;
+	}
+
+	& .ck-label {
+		display: none;
+	}
+}