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

Commit c000c93

Browse files
authored
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.
2 parents c2d0631 + deb6b06 commit c000c93

File tree

5 files changed

+238
-10
lines changed

5 files changed

+238
-10
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"@ckeditor/ckeditor5-image": "^13.0.1",
2626
"@ckeditor/ckeditor5-link": "^11.0.1",
2727
"@ckeditor/ckeditor5-list": "^12.0.1",
28+
"@ckeditor/ckeditor5-mention": "^10.0.0",
2829
"@ckeditor/ckeditor5-paragraph": "^11.0.1",
2930
"@ckeditor/ckeditor5-typing": "^12.0.1",
3031
"eslint": "^5.5.0",

src/panel/balloon/contextualballoon.js

+38-7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const toPx = toUnit( 'px' );
4747
* If there are no views in the current stack, the balloon panel will try to switch to the next stack. If there are no
4848
* panels in any stack then balloon panel will be hidden.
4949
*
50+
* **Note**: To force balloon panel to show only one view - even if there are other stacks - use `singleViewMode=true` option
51+
* when {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon#add adding} view to a panel.
52+
*
5053
* From the implementation point of view, contextual ballon plugin is reusing a single
5154
* {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance to display multiple contextual balloon
5255
* 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 {
138141
*/
139142
this.set( '_numberOfStacks', 0 );
140143

144+
/**
145+
* Flag that controls the single view mode.
146+
*
147+
* @private
148+
* @readonly
149+
* @observable
150+
* @member {Boolean} #_singleViewMode
151+
*/
152+
this.set( '_singleViewMode', false );
153+
141154
/**
142155
* Rotator view embedded in the contextual balloon.
143156
* Displays currently visible view in the balloon and provides navigation for switching stacks.
@@ -176,6 +189,7 @@ export default class ContextualBalloon extends Plugin {
176189
* @param {module:utils/dom/position~Options} [data.position] Positioning options.
177190
* @param {String} [data.balloonClassName] Additional CSS class added to the {@link #view balloon} when visible.
178191
* @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
192+
* @param {Boolean} [data.singleViewMode=false] Whether the view should be only visible view - even if other stacks were added.
179193
*/
180194
add( data ) {
181195
if ( this.hasView( data.view ) ) {
@@ -195,7 +209,7 @@ export default class ContextualBalloon extends Plugin {
195209
this._viewToStack.set( data.view, this._idToStack.get( stackId ) );
196210
this._numberOfStacks = this._idToStack.size;
197211

198-
if ( !this._visibleStack ) {
212+
if ( !this._visibleStack || data.singleViewMode ) {
199213
this.showStack( stackId );
200214
}
201215

@@ -204,6 +218,10 @@ export default class ContextualBalloon extends Plugin {
204218

205219
const stack = this._idToStack.get( stackId );
206220

221+
if ( data.singleViewMode ) {
222+
this.showStack( stackId );
223+
}
224+
207225
// Add new view to the stack.
208226
stack.set( data.view, data );
209227
this._viewToStack.set( data.view, stack );
@@ -234,6 +252,10 @@ export default class ContextualBalloon extends Plugin {
234252

235253
const stack = this._viewToStack.get( view );
236254

255+
if ( this._singleViewMode && this.visibleView === view ) {
256+
this._singleViewMode = false;
257+
}
258+
237259
// When visible view will be removed we need to show a preceding view or next stack
238260
// if a view is the only view in the stack.
239261
if ( this.visibleView === view ) {
@@ -281,6 +303,7 @@ export default class ContextualBalloon extends Plugin {
281303
* @param {String} id
282304
*/
283305
showStack( id ) {
306+
this.visibleStack = id;
284307
const stack = this._idToStack.get( id );
285308

286309
if ( !stack ) {
@@ -368,13 +391,15 @@ export default class ContextualBalloon extends Plugin {
368391

369392
this.view.content.add( view );
370393

371-
// Hide navigation when there is only a one stack.
372-
view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', value => value > 1 );
394+
// Hide navigation when there is only a one stack & not in single view mode.
395+
view.bind( 'isNavigationVisible' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( value, isSingleViewMode ) => {
396+
return !isSingleViewMode && value > 1;
397+
} );
373398

374399
// Update balloon position after toggling navigation.
375400
view.on( 'change:isNavigationVisible', () => ( this.updatePosition() ), { priority: 'low' } );
376401

377-
// Show stacks counter.
402+
// Update stacks counter value.
378403
view.bind( 'counter' ).to( this, 'visibleView', this, '_numberOfStacks', ( visibleView, numberOfStacks ) => {
379404
if ( numberOfStacks < 2 ) {
380405
return '';
@@ -414,8 +439,10 @@ export default class ContextualBalloon extends Plugin {
414439
_createFakePanelsView() {
415440
const view = new FakePanelsView( this.editor.locale, this.view );
416441

417-
view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', number => {
418-
return number < 2 ? 0 : Math.min( number - 1, 2 );
442+
view.bind( 'numberOfPanels' ).to( this, '_numberOfStacks', this, '_singleViewMode', ( number, isSingleViewMode ) => {
443+
const showPanels = !isSingleViewMode && number >= 2;
444+
445+
return showPanels ? Math.min( number - 1, 2 ) : 0;
419446
} );
420447

421448
view.listenTo( this.view, 'change:top', () => view.updatePosition() );
@@ -436,14 +463,18 @@ export default class ContextualBalloon extends Plugin {
436463
* @param {String} [data.balloonClassName=''] Additional class name which will be added to the {@link #view balloon}.
437464
* @param {Boolean} [data.withArrow=true] Whether the {@link #view balloon} should be rendered with an arrow.
438465
*/
439-
_showView( { view, balloonClassName = '', withArrow = true } ) {
466+
_showView( { view, balloonClassName = '', withArrow = true, singleViewMode = false } ) {
440467
this.view.class = balloonClassName;
441468
this.view.withArrow = withArrow;
442469

443470
this._rotatorView.showView( view );
444471
this.visibleView = view;
445472
this.view.pin( this._getBalloonPosition() );
446473
this._fakePanelsView.updatePosition();
474+
475+
if ( singleViewMode ) {
476+
this._singleViewMode = true;
477+
}
447478
}
448479

449480
/**

tests/manual/contextualballoon/contextualballoon.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor';
99
import ArticlePluginSet from '@ckeditor/ckeditor5-core/tests/_utils/articlepluginset';
10+
import Mention from '@ckeditor/ckeditor5-mention/src/mention';
1011
import BalloonToolbar from '../../../src/toolbar/balloon/balloontoolbar';
1112
import ContextualBalloon from '../../../src/panel/balloon/contextualballoon';
1213
import View from '../../../src/view';
@@ -75,9 +76,17 @@ class CustomStackHighlight {
7576

7677
ClassicEditor
7778
.create( document.querySelector( '#editor' ), {
78-
plugins: [ ArticlePluginSet, BalloonToolbar, CustomStackHighlight ],
79+
plugins: [ ArticlePluginSet, BalloonToolbar, CustomStackHighlight, Mention ],
7980
toolbar: [ 'bold', 'link' ],
80-
balloonToolbar: [ 'bold', 'link' ]
81+
balloonToolbar: [ 'bold', 'link' ],
82+
mention: {
83+
feeds: [
84+
{
85+
marker: '@',
86+
feed: [ '@Barney', '@Lily', '@Marshall', '@Robin', '@Ted' ]
87+
}
88+
]
89+
}
8190
} )
8291
.then( editor => {
8392
window.editor = editor;

tests/manual/contextualballoon/contextualballoon.md

+6
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@
1717
## Fake panels - max
1818

1919
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.
20+
21+
## Force single view - Mention
22+
23+
1. Select text `[select]` (by non-collapsed selection).
24+
2. Type <kbd>space</kbd> + `@` to open mention panel. You should see mention panel with no layers under the balloon and without any counter.
25+
3. Move selection around `@` when leaving mention suggestions the balloon should be displayed as in above cases (layers, navigation buttons, etc).

tests/panel/balloon/contextualballoon.js

+182-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
1717
/* global document, Event */
1818

1919
describe( 'ContextualBalloon', () => {
20-
let editor, editorElement, balloon, viewA, viewB, viewC;
20+
let editor, editorElement, balloon, viewA, viewB, viewC, viewD;
21+
2122
testUtils.createSinonSandbox();
2223

2324
before( () => {
@@ -58,6 +59,7 @@ describe( 'ContextualBalloon', () => {
5859
viewA = new View();
5960
viewB = new View();
6061
viewC = new View();
62+
viewD = new View();
6163

6264
// Add viewA to the pane and init viewB.
6365
balloon.add( {
@@ -1031,5 +1033,184 @@ describe( 'ContextualBalloon', () => {
10311033
expect( rotatorView.buttonNextView.labelView.element.textContent ).to.equal( 'Następny' );
10321034
} );
10331035
} );
1036+
1037+
describe( 'singleViewMode', () => {
1038+
it( 'should not display navigation when there is more than one stack', () => {
1039+
const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
1040+
1041+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1042+
1043+
balloon.add( {
1044+
view: viewB,
1045+
stackId: 'second',
1046+
singleViewMode: true
1047+
} );
1048+
1049+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1050+
} );
1051+
1052+
it( 'should hide display navigation after adding view', () => {
1053+
const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
1054+
1055+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1056+
1057+
balloon.add( {
1058+
view: viewB,
1059+
stackId: 'second'
1060+
} );
1061+
1062+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
1063+
1064+
balloon.add( {
1065+
view: viewC,
1066+
stackId: 'third',
1067+
singleViewMode: true
1068+
} );
1069+
1070+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1071+
} );
1072+
1073+
it( 'should display navigation after removing a view', () => {
1074+
const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
1075+
1076+
balloon.add( {
1077+
view: viewB,
1078+
stackId: 'second'
1079+
} );
1080+
1081+
balloon.add( {
1082+
view: viewC,
1083+
stackId: 'third',
1084+
singleViewMode: true
1085+
} );
1086+
1087+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1088+
1089+
balloon.remove( viewC );
1090+
1091+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
1092+
} );
1093+
1094+
it( 'should not display navigation after removing a view if there is still some view with singleViewMode', () => {
1095+
const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
1096+
1097+
balloon.add( {
1098+
view: viewB,
1099+
stackId: 'second'
1100+
} );
1101+
1102+
balloon.add( {
1103+
view: viewC,
1104+
stackId: 'third',
1105+
singleViewMode: true
1106+
} );
1107+
1108+
balloon.add( {
1109+
view: viewD,
1110+
stackId: 'third',
1111+
singleViewMode: true
1112+
} );
1113+
1114+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1115+
1116+
balloon.remove( viewD );
1117+
1118+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1119+
1120+
balloon.remove( viewC );
1121+
1122+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.false;
1123+
} );
1124+
1125+
it( 'should not show fake panels when more than one stack is added to the balloon (max to 2 panels)', () => {
1126+
const fakePanelsView = editor.ui.view.body.last;
1127+
1128+
balloon.add( {
1129+
view: viewB,
1130+
stackId: 'second'
1131+
} );
1132+
1133+
expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
1134+
expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
1135+
1136+
balloon.add( {
1137+
view: viewC,
1138+
stackId: 'third',
1139+
singleViewMode: true
1140+
} );
1141+
1142+
expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.be.true;
1143+
expect( fakePanelsView.element.childElementCount ).to.equal( 0 );
1144+
1145+
balloon.remove( viewC );
1146+
1147+
expect( fakePanelsView.element.classList.contains( 'ck-hidden' ) ).to.equal( false );
1148+
expect( fakePanelsView.element.childElementCount ).to.equal( 1 );
1149+
1150+
balloon.remove( viewB );
1151+
} );
1152+
1153+
it( 'should switch visible view when adding a view to new stack', () => {
1154+
const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
1155+
1156+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1157+
1158+
balloon.add( {
1159+
view: viewB,
1160+
stackId: 'second'
1161+
} );
1162+
1163+
expect( balloon.visibleView ).to.equal( viewA );
1164+
1165+
balloon.add( {
1166+
view: viewC,
1167+
stackId: 'third',
1168+
singleViewMode: true
1169+
} );
1170+
1171+
expect( balloon.visibleView ).to.equal( viewC );
1172+
1173+
const viewD = new View();
1174+
1175+
balloon.add( {
1176+
view: viewD,
1177+
stackId: 'fifth',
1178+
singleViewMode: true
1179+
} );
1180+
1181+
expect( balloon.visibleView ).to.equal( viewD );
1182+
} );
1183+
1184+
it( 'should switch visible view when adding a view to the same stack', () => {
1185+
const navigationElement = rotatorView.element.querySelector( '.ck-balloon-rotator__navigation' );
1186+
1187+
expect( navigationElement.classList.contains( 'ck-hidden' ) ).to.be.true;
1188+
1189+
balloon.add( {
1190+
view: viewB,
1191+
stackId: 'second'
1192+
} );
1193+
1194+
expect( balloon.visibleView ).to.equal( viewA );
1195+
1196+
balloon.add( {
1197+
view: viewC,
1198+
stackId: 'third',
1199+
singleViewMode: true
1200+
} );
1201+
1202+
expect( balloon.visibleView ).to.equal( viewC );
1203+
1204+
const viewD = new View();
1205+
1206+
balloon.add( {
1207+
view: viewD,
1208+
stackId: 'third',
1209+
singleViewMode: true
1210+
} );
1211+
1212+
expect( balloon.visibleView ).to.equal( viewD );
1213+
} );
1214+
} );
10341215
} );
10351216
} );

0 commit comments

Comments
 (0)