Răsfoiți Sursa

Image alternate text input WIP.

Szymon Kupś 9 ani în urmă
părinte
comite
02f1ffeb4e

+ 161 - 0
packages/ckeditor5-image/src/image.js

@@ -10,9 +10,43 @@
 import Plugin from 'ckeditor5-core/src/plugin';
 import ImageEngine from './imageengine';
 import Widget from './widget/widget';
+import ButtonView from 'ckeditor5-ui/src/button/buttonview';
+import BalloonPanelView from 'ckeditor5-ui/src/balloonpanel/balloonpanelview';
+import global from 'ckeditor5-utils/src/dom/global';
+import { isImageWidget } from './utils';
+import throttle from 'ckeditor5-utils/src/lib/lodash/throttle';
+import AlternateTextFormView from 'ckeditor5-image/src/ui/alternatetextformview';
+import clickOutsideHandler from 'ckeditor5-ui/src/bindings/clickoutsidehandler';
+import escPressHandler from 'ckeditor5-ui/src/bindings/escpresshandler';
+import alternateTextIcon from 'ckeditor5-core/theme/icons/source.svg';
 
 import '../theme/theme.scss';
 
+const arrowVOffset = BalloonPanelView.arrowVerticalOffset;
+const positions = {
+	//	   [text range]
+	//	        ^
+	//	+-----------------+
+	//	|     Balloon     |
+	//	+-----------------+
+	south: ( targetRect, balloonRect ) => ( {
+		top: targetRect.bottom + arrowVOffset,
+		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
+		name: 's'
+	} ),
+
+	//	+-----------------+
+	//	|     Balloon     |
+	//	+-----------------+
+	//	        V
+	//	   [text range]
+	north: ( targetRect, balloonRect ) => ( {
+		top: targetRect.top - balloonRect.height - arrowVOffset,
+		left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
+		name: 'n'
+	} )
+};
+
 /**
  * The image plugin.
  *
@@ -27,4 +61,131 @@ export default class Image extends Plugin {
 	static get requires() {
 		return [ ImageEngine, Widget ];
 	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		this._createAlternateTextChangeButton();
+
+		return this._createAlternateTextBalloonPanel();
+	}
+
+	/**
+	 * Creates button showing alternate text change balloon panel and registers it in
+	 * editor's {@link module:ui/componentfactory~ComponentFactory ComponentFactory}.
+	 *
+	 * @private
+	 */
+	_createAlternateTextChangeButton() {
+		const editor = this.editor;
+		const command = editor.commands.get( 'imageAlternateText' );
+		const t = editor.t;
+
+		return editor.ui.componentFactory.add( 'imageAlternateText', ( locale ) => {
+			const view = new ButtonView( locale );
+
+			view.set( {
+				label: t( 'Change alternate text' ),
+				icon: alternateTextIcon
+			} );
+
+			view.bind( 'isEnabled' ).to( command, 'isEnabled' );
+
+			this.listenTo( view, 'execute', () => this._showAlternateTextChangePanel() );
+
+			return view;
+		} );
+	}
+
+	_createAlternateTextBalloonPanel() {
+		const editor = this.editor;
+		const panel = this._alternateTextBalloonPanel = new BalloonPanelView( this.editor.locale );
+		panel.maxWidth = 300;
+
+		const editingView = editor.editing.view;
+		this._attachBalloonPanel = throttle( attachBalloonPanel, 100 );
+
+		return editor.ui.view.body.add( panel ).then( () => {
+			// Let the focusTracker know about new focusable UI element.
+			editor.ui.focusTracker.add( panel.element );
+
+			// Hide the panel when editor loses focus but no the other way around.
+			panel.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is, was ) => {
+				if ( was && !is ) {
+					panel.hide();
+				}
+			} );
+
+			// Hide toolbar when is no longer needed.
+			editor.listenTo( editingView, 'render', () => {
+				const selectedElement = editingView.selection.getSelectedElement();
+
+				if ( !selectedElement || !isImageWidget( selectedElement ) ) {
+					this._hideAlternateTextChangePanel();
+				}
+			}, { priority: 'low' } );
+
+			const form = this._alternateTextForm = new AlternateTextFormView( editor.locale );
+
+			this.listenTo( form, 'submit', () => {
+				editor.execute( 'imageAlternateText', form.alternateTextInput.value );
+				this._hideAlternateTextChangePanel();
+			} );
+
+			this.listenTo( form, 'cancel', () => this._hideAlternateTextChangePanel() );
+
+			// Close on `ESC` press.
+			escPressHandler( {
+				emitter: panel,
+				activator: () => panel.isVisible,
+				callback: () => this._hideAlternateTextChangePanel()
+			} );
+
+			// Close on click outside of balloon panel element.
+			clickOutsideHandler( {
+				emitter: panel,
+				activator: () => panel.isVisible,
+				contextElement: panel.element,
+				callback: () => this._hideAlternateTextChangePanel()
+			} );
+
+			return panel.content.add( this._alternateTextForm );
+		} );
+
+		function attachBalloonPanel() {
+			panel.attachTo( {
+				target: editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ),
+				positions: [ positions.north, positions.south ]
+			} );
+		}
+	}
+
+	_showAlternateTextChangePanel() {
+		const editor = this.editor;
+		const editingView = editor.editing.view;
+		const selectedElement = editingView.selection.getSelectedElement();
+
+		if ( selectedElement && isImageWidget( selectedElement ) ) {
+			const command = editor.commands.get( 'imageAlternateText' );
+
+			this._alternateTextForm.alternateTextInput.value = command.value || '';
+
+			this._attachBalloonPanel();
+			this._alternateTextForm.alternateTextInput.select();
+
+			editor.ui.view.listenTo( global.window, 'scroll', this._attachBalloonPanel );
+			editor.ui.view.listenTo( global.window, 'resize', this._attachBalloonPanel );
+		}
+	}
+
+	_hideAlternateTextChangePanel() {
+		const editor = this.editor;
+
+		this._alternateTextBalloonPanel.hide();
+		editor.editing.view.focus();
+
+		editor.ui.view.stopListening( global.window, 'scroll', this._attachBalloonPanel );
+		editor.ui.view.stopListening( global.window, 'resize', this._attachBalloonPanel );
+	}
 }

+ 48 - 0
packages/ckeditor5-image/src/imagealternatetextcommand.js

@@ -0,0 +1,48 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/alternatetextcommand
+ */
+
+import Command from 'ckeditor5-core/src/command/command';
+
+// TODO: isImage to image utils.
+import { isImage } from './imagestyle/utils';
+
+export default class ImageAlternateTextCommand extends Command {
+	constructor( editor ) {
+		super( editor );
+
+		this.set( 'value', false );
+
+		// Update current value and refresh state each time something change in model document.
+		this.listenTo( editor.document, 'changesDone', () => {
+			this._updateValue();
+			this.refreshState();
+		} );
+	}
+
+	_updateValue() {
+		const doc = this.editor.document;
+		const element = doc.selection.getSelectedElement();
+
+		if ( isImage( element ) && element.hasAttribute( 'alt' ) ) {
+			this.value = element.getAttribute( 'alt' );
+		} else {
+			this.value = false;
+		}
+	}
+
+	_checkEnabled() {
+		const element = this.editor.document.selection.getSelectedElement();
+
+		return isImage( element );
+	}
+
+	_doExecute() {
+		console.log( 'attribute change command execute' );
+	}
+}

+ 5 - 1
packages/ckeditor5-image/src/imageengine.js

@@ -10,6 +10,7 @@
 import Plugin from 'ckeditor5-core/src/plugin';
 import buildModelConverter from 'ckeditor5-engine/src/conversion/buildmodelconverter';
 import WidgetEngine from './widget/widgetengine';
+import ImageAlternateTextCommand from './imagealternatetextcommand.js';
 import { modelToViewImage, viewToModelImage, modelToViewSelection } from './converters';
 import { toImageWidget } from './utils';
 
@@ -18,7 +19,7 @@ import { toImageWidget } from './utils';
  * Registers `image` as a block element in document's schema and allows it to have two attributes: `src` and `alt`.
  * Registers converters for editing and data pipelines.
  *
- * @extends module:core/plugin~Plugin.
+ * @extends module:core/plugin~Plugin
  */
 export default class ImageEngine extends Plugin {
 	/**
@@ -59,5 +60,8 @@ export default class ImageEngine extends Plugin {
 
 		// Creates fake selection label if selection is placed around image widget.
 		editing.modelToView.on( 'selection', modelToViewSelection( editor.t ), { priority: 'lowest' } );
+
+		// Register ImageAlternateTextCommand.
+		editor.commands.set( 'imageAlternateText', new ImageAlternateTextCommand( editor ) );
 	}
 }

+ 103 - 0
packages/ckeditor5-image/src/ui/alternatetextformview.js

@@ -0,0 +1,103 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module image/ui/imagealternativetextformview
+ */
+
+import View from 'ckeditor5-ui/src/view';
+import ButtonView from 'ckeditor5-ui/src/button/buttonview';
+import Template from 'ckeditor5-ui/src/template';
+import LabeledInputView from 'ckeditor5-ui/src/labeledinput/labeledinputview';
+import InputTextView from 'ckeditor5-ui/src/inputtext/inputtextview';
+import submitHandler from 'ckeditor5-ui/src/bindings/submithandler';
+
+export default class AlternateTextFormView extends View{
+	constructor( locale ) {
+		super( locale );
+
+		this.alternateTextInput = this._createAlternateTextInput();
+
+		this.saveButtonView = this._createButton( 'Ok' );
+		this.saveButtonView.type = 'submit';
+
+		this.cancelButtonView = this._createButton( 'Cancel', 'cancel' );
+
+		// Register child views.
+		this.addChildren( [ this.saveButtonView ] );
+
+		Template.extend( this.saveButtonView.template, {
+			attributes: {
+				class: [
+					'ck-button-action'
+				]
+			}
+		} );
+
+		this.template = new Template( {
+			tag: 'form',
+
+			attributes: {
+				class: [
+					'ck-alternate-text-form',
+				]
+			},
+
+			children: [
+				this.alternateTextInput,
+				{
+					tag: 'div',
+
+					attributes: {
+						class: [
+							'ck-alternate-text-form__actions'
+						]
+					},
+
+					children: [
+						this.saveButtonView,
+						this.cancelButtonView
+					]
+				}
+			]
+		} );
+
+		submitHandler( {
+			view: this
+		} );
+	}
+
+	/**
+	 * Creates button View.
+	 *
+	 * @private
+	 * @param {String} label Button label
+	 * @param {String} [eventName] Event name which ButtonView#execute event will be delegated to.
+	 * @returns {module:ui/button/buttonview~ButtonView} Button view instance.
+	 */
+	_createButton( label, eventName ) {
+		const t = this.locale.t;
+		const button = new ButtonView( this.locale );
+
+		button.label = t( label );
+		button.withText = true;
+
+		if ( eventName ) {
+			button.delegate( 'execute' ).to( this, eventName );
+		}
+
+		return button;
+	}
+
+	_createAlternateTextInput() {
+		const t = this.locale.t;
+
+		const labeledInput = new LabeledInputView( this.locale, InputTextView );
+
+		labeledInput.label = t( 'Alternate image text' );
+
+		return labeledInput;
+	}
+}

+ 13 - 0
packages/ckeditor5-image/tests/manual/alternatetext.html

@@ -0,0 +1,13 @@
+<div id="editor">
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
+	<figure class="image">
+		<img src="logo.png" alt="CKEditor logo" />
+	</figure>
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
+	<figure class="image">
+		<img src="logo.png" />
+	</figure>
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
+	<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus consequat placerat. Vestibulum id tellus et mauris sagittis tincidunt quis id mauris. Curabitur consectetur lectus sit amet tellus mattis, non lobortis leo interdum. </p>
+</div>

+ 30 - 0
packages/ckeditor5-image/tests/manual/alternatetext.js

@@ -0,0 +1,30 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/* global document, console, window */
+
+import ClassicEditor from 'ckeditor5-editor-classic/src/classic';
+import EnterPlugin from 'ckeditor5-enter/src/enter';
+import TypingPlugin from 'ckeditor5-typing/src/typing';
+import ParagraphPlugin from 'ckeditor5-paragraph/src/paragraph';
+import HeadingPlugin from 'ckeditor5-heading/src/heading';
+import ImagePlugin from 'ckeditor5-image/src/image';
+import UndoPlugin from 'ckeditor5-undo/src/undo';
+import ClipboardPlugin from 'ckeditor5-clipboard/src/clipboard';
+import ImageToolbar from 'ckeditor5-image/src/imagetoolbar';
+
+ClassicEditor.create( document.querySelector( '#editor' ), {
+	plugins: [ EnterPlugin, TypingPlugin, ParagraphPlugin, HeadingPlugin, ImagePlugin, UndoPlugin, ClipboardPlugin, ImageToolbar ],
+	toolbar: [ 'headings', 'undo', 'redo' ],
+	image: {
+		toolbar: [ 'imageAlternateText' ]
+	}
+} )
+.then( editor => {
+	window.editor = editor;
+} )
+.catch( err => {
+	console.error( err.stack );
+} );

+ 1 - 0
packages/ckeditor5-image/tests/manual/alternatetext.md

@@ -0,0 +1 @@
+## Image alternate text

+ 30 - 0
packages/ckeditor5-image/theme/theme.scss

@@ -1,6 +1,8 @@
 // Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
 // For licensing, see LICENSE.md or http://ckeditor.com/license
 
+@import '~ckeditor5-theme-lark/theme/helpers/_spacing';
+
 // Common styles applied to all widgets.
 .ck-widget {
 	margin: 10px 0;
@@ -31,3 +33,31 @@
 	}
 }
 
+// Alternate text form.
+.ck-alternate-text {
+	&-form {
+		padding: ck-spacing( 'large' );
+		overflow: hidden;
+
+		.ck-label {
+			margin-bottom: ck-spacing( 'small' );
+		}
+
+		&__actions {
+			clear: both;
+			padding-top: ck-spacing( 'large' );
+
+			.ck-button {
+				float: right;
+
+				& + .ck-button {
+					margin-right: ck-spacing( 'medium' );
+
+					& + .ck-button {
+						float: left;
+					}
+				}
+			}
+		}
+	}
+}