8
0
Просмотр исходного кода

Merge pull request #43 from ckeditor/t/ckeditor5-ui/442

Feature: Introduced toolbar for the Media Embed feature. Closes #41. Closes #28.
Piotr Jasiun 7 лет назад
Родитель
Сommit
9665aa9df0

+ 1 - 0
packages/ckeditor5-media-embed/package.json

@@ -20,6 +20,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@ckeditor/ckeditor5-basic-styles": "^10.0.2",
     "@ckeditor/ckeditor5-basic-styles": "^10.0.2",
+    "@ckeditor/ckeditor5-editor-balloon": "^11.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^11.0.0",
     "@ckeditor/ckeditor5-editor-classic": "^11.0.0",
     "@ckeditor/ckeditor5-link": "^10.0.3",
     "@ckeditor/ckeditor5-link": "^10.0.3",
     "@ckeditor/ckeditor5-list": "^11.0.1",
     "@ckeditor/ckeditor5-list": "^11.0.1",

+ 57 - 0
packages/ckeditor5-media-embed/src/mediaembedtoolbar.js

@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2016 - 2018, CKSource - Frederico Knabben. All rights reserved.
+ */
+
+/**
+ * @module media-embed/mediaembedtoolbar
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import WidgetToolbarRepository from '@ckeditor/ckeditor5-widget/src/widgettoolbarrepository';
+import { isMediaWidgetSelected } from './utils';
+
+/**
+ * The media embed toolbar plugin. It creates a toolbar for media embed that shows up when the media element is selected.
+ *
+ * Instances of toolbar components (e.g. buttons) are created based on the
+ * {@link module:media-embed/mediaembedtoolbar~MediaEmbedConfig#toolbar `media.toolbar` configuration option}.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class MediaEmbedToolbar extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get requires() {
+		return [ WidgetToolbarRepository ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'MediaEmbedToolbar';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	afterInit() {
+		const editor = this.editor;
+		const widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository );
+
+		widgetToolbarRepository.register( 'mediaEmbed', {
+			items: editor.config.get( 'mediaEmbed.toolbar' ) || [],
+			visibleWhen: isMediaWidgetSelected,
+		} );
+	}
+}
+
+/**
+ * Items to be placed in the media embed toolbar.
+ * This option requires adding {@link module:media-embed/mediaembedtoolbar~MediaEmbedToolbar} to the plugin list.
+ *
+ * Read more about configuring toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
+ *
+ * @member {Array.<String>} module:media-embed/mediaembedtoolbar~MediaEmbedConfig#toolbar
+ */

+ 11 - 1
packages/ckeditor5-media-embed/src/utils.js

@@ -8,7 +8,7 @@
  */
  */
 
 
 import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
 import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
-import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
+import { isWidget, toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
 
 
 const mediaSymbol = Symbol( 'isMedia' );
 const mediaSymbol = Symbol( 'isMedia' );
 
 
@@ -28,6 +28,16 @@ export function toMediaWidget( viewElement, writer, label ) {
 	return toWidget( viewElement, writer, { label } );
 	return toWidget( viewElement, writer, { label } );
 }
 }
 
 
+export function isMediaWidgetSelected( viewSelection ) {
+	const viewElement = viewSelection.getSelectedElement();
+
+	return !!( viewElement && isMediaWidget( viewElement ) );
+}
+
+export function isMediaWidget( viewElement ) {
+	return !!viewElement.getCustomProperty( mediaSymbol ) && isWidget( viewElement );
+}
+
 /**
 /**
  * Creates a view element representing the media. Either "semantic" one for the data pipeline:
  * Creates a view element representing the media. Either "semantic" one for the data pipeline:
  *
  *

+ 4 - 2
packages/ckeditor5-media-embed/tests/manual/mediaembed.js

@@ -8,15 +8,17 @@
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
 import MediaEmbed from '../../src/mediaembed';
 import MediaEmbed from '../../src/mediaembed';
+import MediaEmbedToolbar from '../../src/mediaembedtoolbar';
 
 
 ClassicEditor
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, MediaEmbed ],
+		plugins: [ ArticlePluginSet, MediaEmbed, MediaEmbedToolbar ],
 		toolbar: [
 		toolbar: [
 			'heading', '|', 'mediaEmbed', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'link', 'undo', 'redo'
 			'heading', '|', 'mediaEmbed', '|', 'bold', 'italic', 'bulletedList', 'numberedList', 'blockQuote', 'link', 'undo', 'redo'
 		],
 		],
 		mediaEmbed: {
 		mediaEmbed: {
-			mediaPreviewsInData: true
+			mediaPreviewsInData: true,
+			toolbar: [ 'blockQuote' ]
 		}
 		}
 	} )
 	} )
 	.then( editor => {
 	.then( editor => {

+ 7 - 0
packages/ckeditor5-media-embed/tests/manual/mediaembed.md

@@ -45,3 +45,10 @@
 1. Hover the URL in the content (the tooltip should show up),
 1. Hover the URL in the content (the tooltip should show up),
 1. Click the URL,
 1. Click the URL,
 1. A new browser tab should open with the media URL.
 1. A new browser tab should open with the media URL.
+
+### Media embed toolbar
+
+1. Click the media,
+1. The block quote button should be visible,
+1. Click the block quote button,
+1. The block quote should be applied to the media.

+ 250 - 0
packages/ckeditor5-media-embed/tests/mediaembedtoolbar.js

@@ -0,0 +1,250 @@
+/**
+ * Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved.
+ */
+
+/* global document */
+
+import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
+import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor';
+import MediaEmbed from '../src/mediaembed';
+import MediaEmbedToolbar from '../src/mediaembedtoolbar';
+import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph';
+import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold';
+
+import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
+
+describe( 'MediaEmbedToolbar', () => {
+	let editor, element, widgetToolbarRepository, balloon, toolbar, model;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+
+		return ClassicTestEditor.create( element, {
+			plugins: [ Paragraph, MediaEmbed, MediaEmbedToolbar, FakeButton ],
+			mediaEmbed: {
+				toolbar: [ 'fake_button' ]
+			}
+		} ).then( _editor => {
+			editor = _editor;
+			model = editor.model;
+			widgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' );
+			toolbar = widgetToolbarRepository._toolbars.get( 'mediaEmbed' ).view;
+			balloon = editor.plugins.get( 'ContextualBalloon' );
+		} );
+	} );
+
+	afterEach( () => {
+		return editor.destroy()
+			.then( () => element.remove() );
+	} );
+
+	it( 'should be loaded', () => {
+		expect( editor.plugins.get( MediaEmbedToolbar ) ).to.be.instanceOf( MediaEmbedToolbar );
+	} );
+
+	describe( 'toolbar', () => {
+		it( 'should use the config.table.tableWidget 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, '[<media url=""></media>]' );
+
+			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 media widget is selected', () => {
+			editor.ui.focusTracker.isFocused = true;
+
+			setData( editor.model, '[<media url=""></media>]' );
+
+			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 media widget is selected', () => {
+			editor.ui.focusTracker.isFocused = false;
+
+			setData( editor.model, '[<media url=""></media>]' );
+
+			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', () => {
+		beforeEach( () => {
+			editor.ui.focusTracker.isFocused = true;
+		} );
+
+		it( 'should show the toolbar on ui#update when the media widget is selected', () => {
+			setData( editor.model, '<paragraph>[foo]</paragraph><media url=""></media>' );
+
+			expect( balloon.visibleView ).to.be.null;
+
+			editor.ui.fire( 'update' );
+
+			expect( balloon.visibleView ).to.be.null;
+
+			editor.model.change( writer => {
+				// Select the [<media></media>]
+				writer.setSelection( editor.model.document.getRoot().getChild( 1 ), 'on' );
+			} );
+
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			// Make sure successive change does not throw, e.g. attempting
+			// to insert the toolbar twice.
+			editor.ui.fire( 'update' );
+			expect( balloon.visibleView ).to.equal( toolbar );
+		} );
+
+		it( 'should not engage when the toolbar is in the balloon yet invisible', () => {
+			setData( editor.model, '<media url=""></media>' );
+
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			const lastView = new View();
+			lastView.element = document.createElement( 'div' );
+
+			balloon.add( {
+				view: lastView,
+				position: {
+					target: document.body
+				}
+			} );
+
+			expect( balloon.visibleView ).to.equal( lastView );
+
+			editor.ui.fire( 'update' );
+
+			expect( balloon.visibleView ).to.equal( lastView );
+		} );
+
+		it( 'should hide the toolbar on ui#update if the media is de–selected', () => {
+			setData( model, '<paragraph>foo</paragraph>[<media url=""></media>]' );
+
+			expect( balloon.visibleView ).to.equal( toolbar );
+
+			model.change( writer => {
+				// Select the <paragraph>[...]</paragraph>
+				writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' );
+			} );
+
+			expect( balloon.visibleView ).to.be.null;
+
+			// Make sure successive change does not throw, e.g. attempting
+			// to remove the toolbar twice.
+			editor.ui.fire( 'update' );
+			expect( balloon.visibleView ).to.be.null;
+		} );
+	} );
+} );
+
+describe( 'MediaEmbedToolbar - integration with BalloonEditor', () => {
+	let clock, editor, balloonToolbar, element, widgetToolbarRepository, balloon, toolbar, model;
+
+	testUtils.createSinonSandbox();
+
+	beforeEach( () => {
+		element = document.createElement( 'div' );
+		document.body.appendChild( element );
+		clock = testUtils.sinon.useFakeTimers();
+
+		return BalloonEditor.create( element, {
+			plugins: [ Paragraph, MediaEmbed, MediaEmbedToolbar, FakeButton, Bold ],
+			balloonToolbar: [ 'bold' ],
+			media: {
+				toolbar: [ 'fake_button' ]
+			}
+		} ).then( _editor => {
+			editor = _editor;
+			model = editor.model;
+			widgetToolbarRepository = editor.plugins.get( 'WidgetToolbarRepository' );
+			toolbar = widgetToolbarRepository._toolbars.get( 'mediaEmbed' ).view;
+			balloon = editor.plugins.get( 'ContextualBalloon' );
+			balloonToolbar = editor.plugins.get( 'BalloonToolbar' );
+		} );
+	} );
+
+	beforeEach( () => {
+		editor.ui.focusTracker.isFocused = true;
+	} );
+
+	afterEach( () => {
+		return editor.destroy()
+			.then( () => element.remove() );
+	} );
+
+	it( 'balloon toolbar should be hidden when media widget is selected', () => {
+		setData( model, '<paragraph>[abc]</paragraph><media url=""></media>' );
+		editor.editing.view.document.isFocused = true;
+
+		expect( balloon.visibleView ).to.equal( null );
+
+		model.change( writer => {
+			// Select the [<media></media>]
+			writer.setSelection( model.document.getRoot().getChild( 1 ), 'on' );
+		} );
+
+		expect( balloon.visibleView ).to.equal( toolbar );
+
+		clock.tick( 200 );
+
+		expect( balloon.visibleView ).to.equal( toolbar );
+	} );
+
+	it( 'balloon toolbar should be visible when media widget is not selected', () => {
+		setData( model, '<paragraph>abc</paragraph>[<media url=""></media>]' );
+		editor.editing.view.document.isFocused = true;
+
+		expect( balloon.visibleView ).to.equal( toolbar );
+
+		model.change( writer => {
+			// Select the <paragraph>[abc]</paragraph>
+			writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' );
+		} );
+
+		clock.tick( 200 );
+
+		expect( balloon.visibleView ).to.equal( balloonToolbar.toolbarView );
+	} );
+} );
+
+// 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;
+		} );
+	}
+}