Răsfoiți Sursa

Merge pull request #7452 from ckeditor/i/7331

Feature (link): Introduced the `LinkImageUI` plugin that brings a UI to wrap images in links. Closes #7331.

Internal (image): Reduced unnecessary margin under a linked image. See #7452 (comment).
Aleksander Nowodzinski 5 ani în urmă
părinte
comite
878257e43d

+ 1 - 1
packages/ckeditor5-image/theme/image.css

@@ -11,7 +11,7 @@
 	/* Make sure there is some space between the content and the image. Center image by default. */
 	margin: 1em auto;
 
-	& > img {
+	& img {
 		/* Prevent unnecessary margins caused by line-height (see #44). */
 		display: block;
 

+ 1 - 1
packages/ckeditor5-link/src/linkimage.js

@@ -15,7 +15,7 @@ import LinkImageUI from './linkimageui';
  * The `LinkImage` plugin.
  *
  * This is a "glue" plugin that loads the {@link module:link/linkimageediting~LinkImageEditing link image editing feature}
- * and {@link module:link/linkimageui~LinkImageUI linkimage UI feature}.
+ * and {@link module:link/linkimageui~LinkImageUI link image UI feature}.
  *
  * @extends module:core/plugin~Plugin
  */

+ 82 - 1
packages/ckeditor5-link/src/linkimageui.js

@@ -7,15 +7,21 @@
  * @module link/linkimageui
  */
 
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import Image from '@ckeditor/ckeditor5-image/src/image';
 import LinkUI from './linkui';
 import LinkEditing from './linkediting';
+import { isImageWidget } from '@ckeditor/ckeditor5-image/src/image/utils';
+import { LINK_KEYSTROKE } from './utils';
+
+import linkIcon from '../theme/icons/link.svg';
 
 /**
  * The link image UI plugin.
  *
- * TODO: Docs.
+ * This plugin brings a `'linkImage'` button that can be displayed in the {@link module:image/imagetoolbar~ImageToolbar}
+ * and used to wrap images in links.
  *
  * @extends module:core/plugin~Plugin
  */
@@ -34,6 +40,81 @@ export default class LinkImageUI extends Plugin {
 		return 'LinkImageUI';
 	}
 
+	/**
+	 * @inheritDoc
+	 */
 	init() {
+		const editor = this.editor;
+		const viewDocument = editor.editing.view.document;
+
+		this.listenTo( viewDocument, 'click', ( evt, data ) => {
+			const hasLink = isImageLinked( viewDocument.selection.getSelectedElement() );
+
+			if ( hasLink ) {
+				data.preventDefault();
+			}
+		} );
+
+		this._createToolbarLinkImageButton();
+	}
+
+	/**
+	 * Creates a `LinkImageUI` button view.
+	 *
+	 * Clicking this button shows a {@link module:link/linkui~LinkUI#_balloon} attached to the selection.
+	 * When an image is already linked, the view shows {@link module:link/linkui~LinkUI#actionsView} or
+	 * {@link module:link/linkui~LinkUI#formView} if it's not.
+	 *
+	 * @private
+	 */
+	_createToolbarLinkImageButton() {
+		const editor = this.editor;
+		const t = editor.t;
+
+		editor.ui.componentFactory.add( 'linkImage', locale => {
+			const button = new ButtonView( locale );
+			const plugin = editor.plugins.get( 'LinkUI' );
+			const linkCommand = editor.commands.get( 'link' );
+
+			button.set( {
+				isEnabled: true,
+				label: t( 'Link image' ),
+				icon: linkIcon,
+				keystroke: LINK_KEYSTROKE,
+				tooltip: true,
+				isToggleable: true
+			} );
+
+			// Bind button to the command.
+			button.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
+			button.bind( 'isOn' ).to( linkCommand, 'value', value => !!value );
+
+			// Show the actionsView or formView (both from LinkUI) on button click depending on whether the image is linked already.
+			this.listenTo( button, 'execute', () => {
+				const hasLink = isImageLinked( editor.editing.view.document.selection.getSelectedElement() );
+
+				if ( hasLink ) {
+					plugin._addActionsView();
+				} else {
+					plugin._showUI( true );
+				}
+			} );
+
+			return button;
+		} );
 	}
 }
+
+// A helper function that checks whether the element is a linked image.
+//
+// @param {module:engine/model/element~Element} element
+// @returns {Boolean}
+function isImageLinked( element ) {
+	const isImage = element && isImageWidget( element );
+
+	if ( !isImage ) {
+		return false;
+	}
+
+	return element.getChild( 0 ).is( 'a' );
+}

+ 5 - 5
packages/ckeditor5-link/src/linkui.js

@@ -9,7 +9,8 @@
 
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
-import { isLinkElement } from './utils';
+import { isLinkElement, LINK_KEYSTROKE } from './utils';
+
 import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
 
 import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
@@ -20,7 +21,6 @@ import LinkActionsView from './ui/linkactionsview';
 
 import linkIcon from '../theme/icons/link.svg';
 
-const linkKeystroke = 'Ctrl+K';
 const protocolRegExp = /^((\w+:(\/{2,})?)|(\W))/i;
 const emailRegExp = /[\w-]+@[\w-]+\.+[\w-]+/i;
 const VISUAL_SELECTION_MARKER_NAME = 'link-ui';
@@ -146,7 +146,7 @@ export default class LinkUI extends Plugin {
 		} );
 
 		// Open the form view on Ctrl+K when the **actions have focus**..
-		actionsView.keystrokes.set( linkKeystroke, ( data, cancel ) => {
+		actionsView.keystrokes.set( LINK_KEYSTROKE, ( data, cancel ) => {
 			this._addFormView();
 			cancel();
 		} );
@@ -215,7 +215,7 @@ export default class LinkUI extends Plugin {
 		const t = editor.t;
 
 		// Handle the `Ctrl+K` keystroke and show the panel.
-		editor.keystrokes.set( linkKeystroke, ( keyEvtData, cancel ) => {
+		editor.keystrokes.set( LINK_KEYSTROKE, ( keyEvtData, cancel ) => {
 			// Prevent focusing the search bar in FF, Chrome and Edge. See https://github.com/ckeditor/ckeditor5/issues/4811.
 			cancel();
 
@@ -228,7 +228,7 @@ export default class LinkUI extends Plugin {
 			button.isEnabled = true;
 			button.label = t( 'Link' );
 			button.icon = linkIcon;
-			button.keystroke = linkKeystroke;
+			button.keystroke = LINK_KEYSTROKE;
 			button.tooltip = true;
 			button.isToggleable = true;
 

+ 5 - 0
packages/ckeditor5-link/src/utils.js

@@ -12,6 +12,11 @@ import { upperFirst } from 'lodash-es';
 const ATTRIBUTE_WHITESPACES = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex
 const SAFE_URL = /^(?:(?:https?|ftps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))/i;
 
+/**
+ * A keystroke used by the {@link module:link/linkui~LinkUI link UI feature}.
+ */
+export const LINK_KEYSTROKE = 'Ctrl+K';
+
 /**
  * Returns `true` if a given view node is the link element.
  *

+ 116 - 4
packages/ckeditor5-link/tests/linkimageui.js

@@ -6,11 +6,18 @@
 /* globals document */
 
 import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo';
+import DomEventData from '@ckeditor/ckeditor5-engine/src/view/observer/domeventdata';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
 import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+import LinkImage from '../src/linkimage';
 import LinkImageUI from '../src/linkimageui';
 
 describe( 'LinkImageUI', () => {
-	let editor, editorElement;
+	let editor, viewDocument, editorElement;
+	let plugin, linkButton;
 
 	testUtils.createSinonSandbox();
 
@@ -20,10 +27,14 @@ describe( 'LinkImageUI', () => {
 
 		return ClassicTestEditor
 			.create( editorElement, {
-				plugins: [ LinkImageUI ]
+				plugins: [ LinkImageUI, LinkImage, Paragraph ]
 			} )
 			.then( newEditor => {
 				editor = newEditor;
+				viewDocument = editor.editing.view.document;
+				linkButton = editor.ui.componentFactory.create( 'linkImage' );
+
+				plugin = editor.plugins.get( 'LinkImageUI' );
 			} );
 	} );
 
@@ -33,9 +44,110 @@ describe( 'LinkImageUI', () => {
 		return editor.destroy();
 	} );
 
+	it( 'should be named"', () => {
+		expect( LinkImageUI.pluginName ).to.equal( 'LinkImageUI' );
+	} );
+
 	describe( 'init()', () => {
-		it( 'does nothing', () => {
-			expect( editor.plugins.get( LinkImageUI ).init() ).to.equal( undefined );
+		it( 'should listen to the click event on the images', () => {
+			const listenToSpy = sinon.stub( plugin, 'listenTo' );
+
+			listenToSpy( viewDocument, 'click' );
+
+			viewDocument.fire( 'click' );
+
+			sinon.assert.calledOnce( listenToSpy );
+		} );
+	} );
+
+	describe( 'link toolbar UI component', () => {
+		it( 'should be registered', () => {
+			expect( linkButton ).to.be.instanceOf( ButtonView );
+		} );
+
+		describe( 'link button', () => {
+			it( 'should have a toggleable button', () => {
+				expect( linkButton.isToggleable ).to.be.true;
+			} );
+
+			it( 'should be bound to the link command', () => {
+				const command = editor.commands.get( 'link' );
+
+				command.isEnabled = true;
+				command.value = 'http://ckeditor.com';
+
+				expect( linkButton.isOn ).to.be.true;
+				expect( linkButton.isEnabled ).to.be.true;
+
+				command.isEnabled = false;
+				command.value = undefined;
+
+				expect( linkButton.isOn ).to.be.false;
+				expect( linkButton.isEnabled ).to.be.false;
+			} );
+
+			it( 'should call #_showUI upon #execute', () => {
+				const spy = testUtils.sinon.stub( editor.plugins.get( 'LinkUI' ), '_showUI' );
+
+				linkButton.fire( 'execute' );
+				sinon.assert.calledWithExactly( spy, true );
+			} );
+		} );
+	} );
+
+	describe( 'click', () => {
+		it( 'should prevent default behavior if image is wrapped with a link', () => {
+			editor.setData( '<figure class="image"><a href="https://example.com"><img src="" /></a></figure>' );
+
+			editor.editing.view.change( writer => {
+				writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+			} );
+
+			const img = viewDocument.selection.getSelectedElement();
+			const data = fakeEventData();
+			const eventInfo = new EventInfo( img, 'click' );
+			const domEventDataMock = new DomEventData( viewDocument, eventInfo, data );
+
+			viewDocument.fire( 'click', domEventDataMock );
+
+			expect( img.getChild( 0 ).name ).to.equal( 'a' );
+			expect( data.preventDefault.called ).to.be.true;
+		} );
+	} );
+
+	describe( 'event handling', () => {
+		it( 'should show plugin#actionsView after "execute" if image is already linked', () => {
+			const linkUIPlugin = editor.plugins.get( 'LinkUI' );
+
+			editor.setData( '<figure class="image"><a href="https://example.com"><img src="" /></a></figure>' );
+
+			editor.editing.view.change( writer => {
+				writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+			} );
+
+			linkButton.fire( 'execute' );
+
+			expect( linkUIPlugin._balloon.visibleView ).to.equals( linkUIPlugin.actionsView );
+		} );
+
+		it( 'should show plugin#formView after "execute" if image is not linked', () => {
+			const linkUIPlugin = editor.plugins.get( 'LinkUI' );
+
+			editor.setData( '<figure class="image"><img src="" /></a>' );
+
+			editor.editing.view.change( writer => {
+				writer.setSelection( viewDocument.getRoot().getChild( 0 ), 'on' );
+			} );
+
+			linkButton.fire( 'execute' );
+
+			expect( linkUIPlugin._balloon.visibleView ).to.equals( linkUIPlugin.formView );
 		} );
 	} );
 } );
+
+function fakeEventData() {
+	return {
+		preventDefault: sinon.spy()
+	};
+}

+ 1 - 3
packages/ckeditor5-link/tests/manual/linkimage.html

@@ -5,7 +5,5 @@
 		</a>
 		<figcaption>CKEditor logo - caption</figcaption>
 	</figure>
-	<a href="https://cksource.com">
-		<img alt="bar" src="sample.jpg">
-	</a>
+	<img alt="bar" src="sample.jpg">
 </div>

+ 1 - 1
packages/ckeditor5-link/tests/manual/linkimage.js

@@ -31,7 +31,7 @@ ClassicEditor
 			'redo'
 		],
 		image: {
-			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative' ]
+			toolbar: [ 'imageStyle:full', 'imageStyle:side', '|', 'imageTextAlternative', '|', 'linkImage' ]
 		},
 		table: {
 			contentToolbar: [