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 #498 from ckeditor/cf/2808
Browse files Browse the repository at this point in the history
Other: Improved UX of ContextualBalloon with multiple stacks by adding fake panels. Closes #501.
  • Loading branch information
Piotr Jasiun committed May 22, 2019
2 parents 600b415 + 46e5c99 commit abd05b6
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 59 deletions.
8 changes: 7 additions & 1 deletion src/panel/balloon/balloonpanelview.js
Expand Up @@ -236,7 +236,13 @@ export default class BalloonPanelView extends View {
fitInViewport: true
}, options );

const { top, left, name: position } = BalloonPanelView._getOptimalPosition( positionOptions );
const optimalPosition = BalloonPanelView._getOptimalPosition( positionOptions );

// Usually browsers make some problems with super accurate values like 104.345px
// so it is better to use int values.
const left = parseInt( optimalPosition.left );
const top = parseInt( optimalPosition.top );
const position = optimalPosition.name;

Object.assign( this, { top, left, position } );
}
Expand Down
169 changes: 151 additions & 18 deletions src/panel/balloon/contextualballoon.js
Expand Up @@ -13,11 +13,16 @@ import View from '../../view';
import ButtonView from '../../button/buttonview';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';

import prevIcon from '../../../theme/icons/previous-arrow.svg';
import nextIcon from '../../../theme/icons/next-arrow.svg';

import '../../../theme/components/panel/balloonrotator.css';
import '../../../theme/components/panel/fakepanel.css';

const toPx = toUnit( 'px' );

/**
* Provides the common contextual balloon for the editor.
Expand Down Expand Up @@ -141,6 +146,14 @@ export default class ContextualBalloon extends Plugin {
* @type {module:ui/panel/balloon/contextualballoon~RotatorView}
*/
this._rotatorView = this._createRotatorView();

/**
* Displays fake panels under the balloon panel view when multiple stacks are added to the balloon.
*
* @private
* @type {module:ui/view~View}
*/
this._fakePanelsView = this._createFakePanelsView();
}

/**
Expand Down Expand Up @@ -259,7 +272,7 @@ export default class ContextualBalloon extends Plugin {
}

this.view.pin( this._getBalloonPosition() );
this._rotatorView.updateIsNarrow();
this._fakePanelsView.updatePosition();
}

/**
Expand Down Expand Up @@ -395,6 +408,24 @@ export default class ContextualBalloon extends Plugin {
return view;
}

/**
* @returns {module:ui/view~View}
*/
_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.listenTo( this.view, 'change:top', () => view.updatePosition() );
view.listenTo( this.view, 'change:left', () => view.updatePosition() );

this.editor.ui.view.body.add( view );

return view;
}

/**
* Sets the view as a content of the balloon and attaches balloon using position
* options of the first view.
Expand All @@ -412,6 +443,7 @@ export default class ContextualBalloon extends Plugin {
this._rotatorView.showView( view );
this.visibleView = view;
this.view.pin( this._getBalloonPosition() );
this._fakePanelsView.updatePosition();
}

/**
Expand Down Expand Up @@ -460,13 +492,6 @@ class RotatorView extends View {
*/
this.set( 'isNavigationVisible', true );

/**
* Defines whether balloon should be marked as narrow or not.
*
* @member {Boolean} #isNarrow
*/
this.set( 'isNarrow', false );

/**
* Used for checking if view is focused or not.
*
Expand Down Expand Up @@ -521,8 +546,7 @@ class RotatorView extends View {

attributes: {
class: [
'ck-balloon-rotator__counter',
bind.to( 'isNarrow', value => value ? 'ck-hidden' : '' )
'ck-balloon-rotator__counter'
]
},

Expand Down Expand Up @@ -555,13 +579,6 @@ class RotatorView extends View {
this.focusTracker.add( this.element );
}

/**
* Checks if view width is narrow and updated {@link ~RotatorView#isNarrow} state.
*/
updateIsNarrow() {
this.isNarrow = this.element.clientWidth <= 200;
}

/**
* Shows given view.
*
Expand All @@ -570,7 +587,6 @@ class RotatorView extends View {
showView( view ) {
this.hideView();
this.content.add( view );
this.updateIsNarrow();
}

/**
Expand Down Expand Up @@ -600,3 +616,120 @@ class RotatorView extends View {
return view;
}
}

// Displays additional layers under the balloon when multiple stacks are added to the balloon.
//
// @private
// @extends module:ui/view~View
class FakePanelsView extends View {
// @inheritDoc
constructor( locale, balloonPanelView ) {
super( locale );

const bind = this.bindTemplate;

// Fake panels top offset.
//
// @observable
// @member {Number} #top
this.set( 'top', 0 );

// Fake panels left offset.
//
// @observable
// @member {Number} #left
this.set( 'left', 0 );

// Fake panels height.
//
// @observable
// @member {Number} #height
this.set( 'height', 0 );

// Fake panels width.
//
// @observable
// @member {Number} #width
this.set( 'width', 0 );

// Number of rendered fake panels.
//
// @observable
// @member {Number} #numberOfPanels
this.set( 'numberOfPanels', 0 );

// Collection of the child views which creates fake panel content.
//
// @readonly
// @type {module:ui/viewcollection~ViewCollection}
this.content = this.createCollection();

// Context.
//
// @private
// @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
this._balloonPanelView = balloonPanelView;

this.setTemplate( {
tag: 'div',
attributes: {
class: [
'ck-fake-panel',
bind.to( 'numberOfPanels', number => number ? '' : 'ck-hidden' )
],
style: {
top: bind.to( 'top', toPx ),
left: bind.to( 'left', toPx ),
width: bind.to( 'width', toPx ),
height: bind.to( 'height', toPx )
}
},
children: this.content
} );

this.on( 'change:numberOfPanels', ( evt, name, next, prev ) => {
if ( next > prev ) {
this._addPanels( next - prev );
} else {
this._removePanels( prev - next );
}

this.updatePosition();
} );
}

// @private
// @param {Number} number
_addPanels( number ) {
while ( number-- ) {
const view = new View();

view.setTemplate( { tag: 'div' } );

this.content.add( view );
this.registerChild( view );
}
}

// @private
// @param {Number} number
_removePanels( number ) {
while ( number-- ) {
const view = this.content.last;

this.content.remove( view );
this.deregisterChild( view );
view.destroy();
}
}

// Updates coordinates of fake panels.
updatePosition() {
if ( this.numberOfPanels ) {
const { top, left } = this._balloonPanelView;
const { width, height } = new Rect( this._balloonPanelView.element );

Object.assign( this, { top, left, width, height } );
}
}
}
20 changes: 17 additions & 3 deletions tests/manual/contextualballoon/contextualballoon.html
Expand Up @@ -4,7 +4,7 @@
<p>Line of text, line of text, line of text, line of text.</p>
<p>Line of text, line of text, line of text, line of text.</p>
<p>Line of text, line of text, line of text, line of text.</p>
<p>Line of text, line of text, line of text, line of text.</p>
<p>Line of text, line of [select] text, line of text, line of text.</p>
<p>Line of text, line of text, line of text, line of text.</p>
<p>Line of text, line of text, line of text, line of text.</p>
<p>Line of text, line of text, line of text, line of text.</p>
Expand All @@ -21,11 +21,25 @@
max-width: 70%;
}

.highlight {
background: yellow;
.highlight.yellow {
background: hsla(55, 100%, 50%, 0.75);
}

.highlight.blue {
background: hsla(249, 100%, 50%, 0.75);
}

.highlight.pink {
background: hsla(313, 100%, 50%, 0.75);
}

.highlight.green {
background: hsla(114, 100%, 50%, 0.75);
}

.ck p.custom-view {
width: 300px;
text-align: center;
font-size: 14px;
padding: 10px 10px 9px;
}
Expand Down
22 changes: 13 additions & 9 deletions tests/manual/contextualballoon/contextualballoon.js
Expand Up @@ -25,8 +25,10 @@ class CustomStackHighlight {
init() {
this.editor.conversion.for( 'editingDowncast' ).markerToHighlight( {
model: 'highlight',
view: () => {
return { classes: 'highlight' };
view: data => {
const color = data.markerName.split( ':' )[ 2 ];

return { classes: 'highlight ' + color };
}
} );

Expand All @@ -51,7 +53,7 @@ class CustomStackHighlight {

this.editor.plugins.get( ContextualBalloon ).add( {
view,
stackId: 'custom',
stackId: 'custom-' + marker.name.split( ':' )[ 1 ],
position: {
target: this._getMarkerDomElement( marker )
}
Expand All @@ -65,9 +67,9 @@ class CustomStackHighlight {

_getMarkerDomElement( marker ) {
const editing = this.editor.editing;
const viewElement = Array.from( editing.mapper.markerNameToElements( marker.name ).values() )[ 0 ];
const viewRange = editing.mapper.toViewRange( marker.getRange() );

return editing.view.domConverter.mapViewToDom( viewElement );
return editing.view.domConverter.viewRangeToDom( viewRange );
}
}

Expand All @@ -84,11 +86,13 @@ ClassicEditor
const root = editor.model.document.getRoot();

[
{ id: 1, start: [ 1, 5 ], end: [ 1, 26 ] },
{ id: 2, start: [ 5, 5 ], end: [ 5, 26 ] },
{ id: 3, start: [ 10, 5 ], end: [ 10, 26 ] }
{ id: 1, start: [ 1, 5 ], end: [ 1, 26 ], color: 'yellow' },
{ id: 2, start: [ 1, 2 ], end: [ 1, 33 ], color: 'green' },
{ id: 3, start: [ 5, 20 ], end: [ 5, 35 ], color: 'blue' },
{ id: 4, start: [ 5, 15 ], end: [ 5, 40 ], color: 'pink' },
{ id: 5, start: [ 5, 10 ], end: [ 5, 45 ], color: 'yellow' }
].forEach( data => {
writer.addMarker( `highlight:${ data.id }`, {
writer.addMarker( `highlight:${ data.id }:${ data.color }`, {
range: writer.createRange(
writer.createPositionFromPath( root, data.start ),
writer.createPositionFromPath( root, data.end )
Expand Down
10 changes: 9 additions & 1 deletion tests/manual/contextualballoon/contextualballoon.md
Expand Up @@ -8,4 +8,12 @@
## Multiple stacks

1. Select some highlighted text - "View in separate stack." should show up.
2. Switch stacks by clicking navigation buttons - you should switch between toolbar and custom view.
2. Switch stacks by clicking navigation buttons. You should switch between toolbar and custom views.

## Fake panels - min

1. Put the selection before the highlight, start moving selection by right arrow. You should see additional layer under the balloon only when at least 2 stacks are added to the balloon.

## 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.
13 changes: 13 additions & 0 deletions tests/panel/balloon/balloonpanelview.js
Expand Up @@ -192,6 +192,19 @@ describe( 'BalloonPanelView', () => {
} ) );
} );

it( 'should parse optimal position offset to int', () => {
testUtils.sinon.stub( BalloonPanelView, '_getOptimalPosition' ).returns( {
top: 10.345,
left: 10.345,
name: 'position'
} );

view.attachTo( { target, limiter } );

expect( view.top ).to.equal( 10 );
expect( view.left ).to.equal( 10 );
} );

describe( 'limited by limiter element', () => {
beforeEach( () => {
// Mock limiter element dimensions.
Expand Down

0 comments on commit abd05b6

Please sign in to comment.