8
0
Prechádzať zdrojové kódy

Merge pull request #507 from ckeditor/t/ckeditor5-mention/74

Feature: Implemented the single view mode for the `ContextualBalloon` plugin. See https://github.com/ckeditor/ckeditor5-mention/issues/74.
Piotrek Koszuliński 6 rokov pred
rodič
commit
f7dc1df66a

+ 1 - 0
packages/ckeditor5-ui/package.json

@@ -25,6 +25,7 @@
     "@ckeditor/ckeditor5-image": "^13.0.1",
     "@ckeditor/ckeditor5-link": "^11.0.1",
     "@ckeditor/ckeditor5-list": "^12.0.1",
+    "@ckeditor/ckeditor5-mention": "^10.0.0",
     "@ckeditor/ckeditor5-paragraph": "^11.0.1",
     "@ckeditor/ckeditor5-typing": "^12.0.1",
     "eslint": "^5.5.0",

+ 38 - 7
packages/ckeditor5-ui/src/panel/balloon/contextualballoon.js

@@ -47,6 +47,9 @@ const toPx = toUnit( 'px' );
  * If there are no views in the current stack, the balloon panel will try to switch to the next stack. If there are no
  * panels in any stack then balloon panel will be hidden.
  *
+ * **Note**: To force balloon panel to show only one view - even if there are other stacks - use `singleViewMode=true` option
+ * when {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#add adding} view to a panel.
+ *
  * From the implementation point of view, contextual ballon plugin is reusing a single
  * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance to display multiple contextual balloon
  * panels in the editor. It also creates a special {@link module:ui/panel/balloon/contextualballoon~RotatorView rotator view},
@@ -138,6 +141,16 @@ export default class ContextualBalloon extends Plugin {
 		 */
 		this.set( '_numberOfStacks', 0 );
 
+		/**
+		 * Flag that controls the single view mode.
+		 *
+		 * @private
+		 * @readonly
+		 * @observable
+		 * @member {Boolean} #_singleViewMode
+		 */
+		this.set( '_singleViewMode', false );
+
 		/**
 		 * Rotator view embedded in the contextual balloon.
 		 * Displays currently visible view in the balloon and provides navigation for switching stacks.
@@ -176,6 +189,7 @@ export default class ContextualBalloon extends Plugin {
 	 * @param {module:utils/dom/position~Options} [data.position] Positioning options.
 	 * @param {String} [data.balloonClassName] Additional CSS class added to the {@link #view balloon} when visible.
 	 * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
+	 * @param {Boolean} [data.singleViewMode=false] Whether the view should be only visible view - even if other stacks were added.
 	 */
 	add( data ) {
 		if ( this.hasView( data.view ) ) {
@@ -195,7 +209,7 @@ export default class ContextualBalloon extends Plugin {
 			this._viewToStack.set( data.view, this._idToStack.get( stackId ) );
 			this._numberOfStacks = this._idToStack.size;
 
-			if ( !this._visibleStack ) {
+			if ( !this._visibleStack || data.singleViewMode ) {
 				this.showStack( stackId );
 			}
 
@@ -204,6 +218,10 @@ export default class ContextualBalloon extends Plugin {
 
 		const stack = this._idToStack.get( stackId );
 
+		if ( data.singleViewMode ) {
+			this.showStack( stackId );
+		}
+
 		// Add new view to the stack.
 		stack.set( data.view, data );
 		this._viewToStack.set( data.view, stack );
@@ -234,6 +252,10 @@ export default class ContextualBalloon extends Plugin {
 
 		const stack = this._viewToStack.get( view );
 
+		if ( this._singleViewMode && this.visibleView === view ) {
+			this._singleViewMode = false;
+		}
+
 		// When visible view will be removed we need to show a preceding view or next stack
 		// if a view is the only view in the stack.
 		if ( this.visibleView === view ) {
@@ -281,6 +303,7 @@ export default class ContextualBalloon extends Plugin {
 	 * @param {String} id
 	 */
 	showStack( id ) {
+		this.visibleStack = id;
 		const stack = this._idToStack.get( id );
 
 		if ( !stack ) {
@@ -368,13 +391,15 @@ export default class ContextualBalloon extends Plugin {
 
 		this.view.content.add( view );
 
-		// Hide navigation when there is only a one stack.
-		view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', value => value > 1 );
+		// Hide navigation when there is only a one stack & not in single view mode.
+		view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( value, isSingleViewMode ) => {
+			return !isSingleViewMode && value > 1;
+		} );
 
 		// Update balloon position after toggling navigation.
 		view.on( 'change:isNavigationVisible', () => ( this.updatePosition() ), { priority: 'low' } );
 
-		// Show stacks counter.
+		// Update stacks counter value.
 		view.bind( 'counter' ).to( this, 'visibleView', this, '_numberOfStacks', ( visibleView, numberOfStacks ) => {
 			if ( numberOfStacks < 2 ) {
 				return '';
@@ -414,8 +439,10 @@ export default class ContextualBalloon extends Plugin {
 	_createFakePanelsView() {
 		const view = new FakePanelsView( this.editor.locale, this.view );
 
-		view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', number => {
-			return number < 2 ? 0 : Math.min( number - 1, 2 );
+		view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( number, isSingleViewMode ) => {
+			const showPanels = !isSingleViewMode && number >= 2;
+
+			return showPanels ? Math.min( number - 1, 2 ) : 0;
 		} );
 
 		view.listenTo( this.view, 'change:top', () => view.updatePosition() );
@@ -436,7 +463,7 @@ export default class ContextualBalloon extends Plugin {
 	 * @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
 	 * @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
 	 */
-	_showView( { view, balloonClassName = '', withArrow = true } ) {
+	_showView( { view, balloonClassName = '', withArrow = true, singleViewMode = false } ) {
 		this.view.class = balloonClassName;
 		this.view.withArrow = withArrow;
 
@@ -444,6 +471,10 @@ export default class ContextualBalloon extends Plugin {
 		this.visibleView = view;
 		this.view.pin( this._getBalloonPosition() );
 		this._fakePanelsView.updatePosition();
+
+		if ( singleViewMode ) {
+			this._singleViewMode = true;
+		}
 	}
 
 	/**

+ 11 - 2
packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.js

@@ -7,6 +7,7 @@
 
 import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
 import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
+import Mention from '@ckeditor/ckeditor5-mention/src/mention';
 import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
 import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
 import View from '../../../src/view';
@@ -75,9 +76,17 @@ class CustomStackHighlight {
 
 ClassicEditor
 	.create( document.querySelector( '#editor' ), {
-		plugins: [ ArticlePluginSet, BalloonToolbar, CustomStackHighlight ],
+		plugins: [ ArticlePluginSet, BalloonToolbar, CustomStackHighlight, Mention ],
 		toolbar: [ 'bold', 'link' ],
-		balloonToolbar: [ 'bold', 'link' ]
+		balloonToolbar: [ 'bold', 'link' ],
+		mention: {
+			feeds: [
+				{
+					marker: '@',
+					feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ]
+				}
+			]
+		}
 	} )
 	.then( editor => {
 		window.editor = editor;

+ 6 - 0
packages/ckeditor5-ui/tests/manual/contextualballoon/contextualballoon.md

@@ -17,3 +17,9 @@
 ## Fake panels - max
 
 1. Select text `[select]` (by non-collapsed selection) from the lower highlight. You should see `1 of 4` status of pagination but only 2 additional layers under the balloon should be visible.
+
+## Force single view - Mention
+
+1. Select text `[select]` (by non-collapsed selection).
+2. Type <kbd>space</kbd> + `@` to open mention panel. You should see mention panel with no layers under the balloon and without any counter.
+3. Move selection around `@` when leaving mention suggestions the balloon should be displayed as in above cases (layers, navigation buttons, etc).

+ 182 - 1
packages/ckeditor5-ui/tests/panel/balloon/contextualballoon.js

@@ -17,7 +17,8 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 /* global document, Event */
 
 describe( 'ContextualBalloon', () => {
-	let editor, editorElement, balloon, viewA, viewB, viewC;
+	let editor, editorElement, balloon, viewA, viewB, viewC, viewD;
+
 	testUtils.createSinonSandbox();
 
 	before( () => {
@@ -58,6 +59,7 @@ describe( 'ContextualBalloon', () => {
 				viewA = new View();
 				viewB = new View();
 				viewC = new View();
+				viewD = new View();
 
 				// Add viewA to the pane and init viewB.
 				balloon.add( {
@@ -1031,5 +1033,184 @@ describe( 'ContextualBalloon', () => {
 					expect( rotatorView.buttonNextView.labelView.element.textContent ).to.equal( 'Następny' );
 				} );
 		} );
+
+		describe( 'singleViewMode', () => {
+			it( 'should not display navigation when there is more than one stack', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+			} );
+
+			it( 'should hide display navigation after adding view', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+			} );
+
+			it( 'should display navigation after removing a view', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.remove( viewC );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+			} );
+
+			it( 'should not display navigation after removing a view if there is still some view with singleViewMode', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				balloon.add( {
+					view: viewD,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.remove( viewD );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.remove( viewC );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
+			} );
+
+			it( 'should not show fake panels when more than one stack is added to the balloon (max to 2 panels)', () => {
+				const fakePanelsView = editor.ui.view.body.last;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+				expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.be.true;
+				expect( fakePanelsView.element.childElementCount ).to.equal( 0 );
+
+				balloon.remove( viewC );
+
+				expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
+				expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
+
+				balloon.remove( viewB );
+			} );
+
+			it( 'should switch visible view when adding a view to new stack', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewA );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewC );
+
+				const viewD = new View();
+
+				balloon.add( {
+					view: viewD,
+					stackId: 'fifth',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewD );
+			} );
+
+			it( 'should switch visible view when adding a view to the same stack', () => {
+				const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
+
+				expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
+
+				balloon.add( {
+					view: viewB,
+					stackId: 'second'
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewA );
+
+				balloon.add( {
+					view: viewC,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewC );
+
+				const viewD = new View();
+
+				balloon.add( {
+					view: viewD,
+					stackId: 'third',
+					singleViewMode: true
+				} );
+
+				expect( balloon.visibleView ).to.equal( viewD );
+			} );
+		} );
 	} );
 } );