Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #507 from ckeditor/t/ckeditor5-mention/74
Browse files Browse the repository at this point in the history
Feature: Implemented the single view mode for the `ContextualBalloon` plugin. See https://github.com/ckeditor/ckeditor5-mention/issues/74.
  • Loading branch information
Reinmar authored May 28, 2019
2 parents c2d0631 + deb6b06 commit c000c93
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 10 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
45 changes: 38 additions & 7 deletions src/panel/balloon/contextualballoon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 ) ) {
Expand All @@ -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 );
}

Expand All @@ -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 );
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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 '';
Expand Down Expand Up @@ -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() );
Expand All @@ -436,14 +463,18 @@ 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;

this._rotatorView.showView( view );
this.visibleView = view;
this.view.pin( this._getBalloonPosition() );
this._fakePanelsView.updatePosition();

if ( singleViewMode ) {
this._singleViewMode = true;
}
}

/**
Expand Down
13 changes: 11 additions & 2 deletions tests/manual/contextualballoon/contextualballoon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions tests/manual/contextualballoon/contextualballoon.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
183 changes: 182 additions & 1 deletion tests/panel/balloon/contextualballoon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( () => {
Expand Down Expand Up @@ -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( {
Expand Down Expand Up @@ -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 );
} );
} );
} );
} );

0 comments on commit c000c93

Please sign in to comment.