diff --git a/src/panel/balloon/balloonpanelview.js b/src/panel/balloon/balloonpanelview.js index 21d62b68..a2529457 100644 --- a/src/panel/balloon/balloonpanelview.js +++ b/src/panel/balloon/balloonpanelview.js @@ -52,18 +52,22 @@ export default class BalloonPanelView extends View { /** * Balloon panel's current position. The position name is reflected in the CSS class set - * to the balloon, i.e. `.ck-balloon-panel_arrow_se` for "se" position. The class + * to the balloon, i.e. `.ck-balloon-panel_arrow_se` for "arrow_se" position. The class * controls the minor aspects of the balloon's visual appearance like placement * of the "arrow". To support a new position, an additional CSS must be created. * * Default position names correspond with * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}. * + * See {@link #attachTo} to learn about custom balloon positions. + * + * See {@link #withArrow}. + * * @observable - * @default 'se' - * @member {'se'|'sw'|'ne'|'nw'} #position + * @default 'arrow_se' + * @member {'arrow_se'|'arrow_sw'|'arrow_ne'|'arrow_nw'} #position */ - this.set( 'position', 'se' ); + this.set( 'position', 'arrow_se' ); /** * Controls whether the balloon panel is visible or not. @@ -74,6 +78,16 @@ export default class BalloonPanelView extends View { */ this.set( 'isVisible', false ); + /** + * Controls whether the balloon panel has an arrow. The presence of the arrow + * is reflected in `ck-balloon-panel_with-arrow` CSS class. + * + * @observable + * @default true + * @member {Boolean} #withArrow + */ + this.set( 'withArrow', true ); + /** * Max width of the balloon panel, as in CSS. * @@ -81,6 +95,14 @@ export default class BalloonPanelView extends View { * @member {Number} #maxWidth */ + /** + * A callback that starts pining the panel when {@link #isVisible} gets + * `true`. Used by {@link #pin}. + * + * @private + * @member {Function} #_pinWhenIsVisibleCallback + */ + /** * Collection of the child views which creates balloon panel contents. * @@ -94,8 +116,9 @@ export default class BalloonPanelView extends View { attributes: { class: [ 'ck-balloon-panel', - bind.to( 'position', ( value ) => `ck-balloon-panel_arrow_${ value }` ), - bind.if( 'isVisible', 'ck-balloon-panel_visible' ) + bind.to( 'position', ( value ) => `ck-balloon-panel_${ value }` ), + bind.if( 'isVisible', 'ck-balloon-panel_visible' ), + bind.if( 'withArrow', 'ck-balloon-panel_with-arrow' ) ], style: { @@ -170,14 +193,57 @@ export default class BalloonPanelView extends View { * * Thanks to this, the panel always sticks to the {@link module:utils/dom/position~Options#target}. * - * See https://github.com/ckeditor/ckeditor5-ui/issues/170. + * See: {@link #unpin}. * * @param {module:utils/dom/position~Options} options Positioning options compatible with * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}. */ - keepAttachedTo( options ) { - // First we need to attach the balloon panel to the target element. + pin( options ) { + this.unpin(); + + this._pinWhenIsVisibleCallback = () => { + if ( this.isVisible ) { + this._startPinning( options ); + } else { + this._stopPinning(); + } + }; + + this._startPinning( options ); + + // Control the state of the listeners depending on whether the panel is visible + // or not. + // TODO: Use on() (https://github.com/ckeditor/ckeditor5-utils/issues/144). + this.listenTo( this, 'change:isVisible', this._pinWhenIsVisibleCallback ); + } + + /** + * Stops pinning the panel, as set up by {@link #pin}. + */ + unpin() { + if ( this._pinWhenIsVisibleCallback ) { + // Deactivate listeners attached by pin(). + this._stopPinning(); + + // Deactivate the panel pin() control logic. + // TODO: Use off() (https://github.com/ckeditor/ckeditor5-utils/issues/144). + this.stopListening( this, 'change:isVisible', this._pinWhenIsVisibleCallback ); + + this._pinWhenIsVisibleCallback = null; + + this.hide(); + } + } + + /** + * Starts managing the pinned state of the panel. See {@link #pin}. + * + * @private + * @param {module:utils/dom/position~Options} options Positioning options compatible with + * {@link module:utils/dom/position~getOptimalPosition}. + */ + _startPinning( options ) { this.attachTo( options ); const limiter = options.limiter || defaultLimiterElement; @@ -199,23 +265,19 @@ export default class BalloonPanelView extends View { }, { useCapture: true } ); // We need to listen on window resize event and update position. - this.listenTo( global.window, 'resize', () => this.attachTo( options ) ); - - // After all we need to clean up the listeners. - this.once( 'change:isVisible', () => { - this.stopListening( global.document, 'scroll' ); - this.stopListening( global.window, 'resize' ); + this.listenTo( global.window, 'resize', () => { + this.attachTo( options ); } ); } /** - * @inheritDoc + * Stops managing the pinned state of the panel. See {@link #pin}. + * + * @private */ - destroy() { + _stopPinning() { this.stopListening( global.document, 'scroll' ); this.stopListening( global.window, 'resize' ); - - return super.destroy(); } } @@ -310,24 +372,24 @@ BalloonPanelView.defaultPositions = { se: ( targetRect ) => ( { top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset, left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset, - name: 'se' + name: 'arrow_se' } ), sw: ( targetRect, balloonRect ) => ( { top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset, left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'sw' + name: 'arrow_sw' } ), ne: ( targetRect, balloonRect ) => ( { top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset, left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset, - name: 'ne' + name: 'arrow_ne' } ), nw: ( targetRect, balloonRect ) => ( { top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset, left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'nw' + name: 'arrow_nw' } ) }; diff --git a/src/panel/floating/floatingpanelview.js b/src/panel/floating/floatingpanelview.js deleted file mode 100644 index 042be7cd..00000000 --- a/src/panel/floating/floatingpanelview.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/** - * @module ui/panel/floating/floatingpanelview - */ - -import global from '@ckeditor/ckeditor5-utils/src/dom/global'; -import Template from '../../template'; -import View from '../../view'; -import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; -import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position'; - -const toPx = toUnit( 'px' ); - -/** - * The floating panel view class. It floats around the - * {@link module:ui/panel/floating/floatingpanelview~FloatingPanelView#targetElement} in DOM - * to remain visible in the browser viewport. - * - * See {@link module:ui/panel/floating/floatingpanelview~FloatingPanelView.defaultPositions} - * to learn about the positioning. - * - * @extends module:ui/view~View - */ -export default class FloatingPanelView extends View { - /** - * @inheritDoc - */ - constructor( locale ) { - super( locale ); - - const bind = this.bindTemplate; - - /** - * Controls whether the floating panel is active. When any editable - * is focused in the editor, panel becomes active. - * - * @readonly - * @observable - * @member {Boolean} #isActive - */ - this.set( 'isActive', false ); - - /** - * The absolute top position of the panel, in pixels. - * - * @observable - * @default 0 - * @member {Number} #top - */ - this.set( 'top', 0 ); - - /** - * The absolute left position of the panel, in pixels. - * - * @observable - * @default 0 - * @member {Number} #left - */ - this.set( 'left', 0 ); - - /** - * An element with respect to which the panel is positioned. - * - * @readonly - * @observable - * @member {HTMLElement} #targetElement - */ - this.set( 'targetElement', null ); - - /** - * Collection of the child views which creates balloon panel contents. - * - * @readonly - * @member {module:ui/viewcollection~ViewCollection} - */ - this.content = this.createCollection(); - - this.template = new Template( { - tag: 'div', - attributes: { - class: [ - 'ck-floating-panel', - bind.if( 'isActive', 'ck-floating-panel_active' ), - ], - style: { - top: bind.to( 'top', toPx ), - left: bind.to( 'left', toPx ), - } - }, - - children: this.content - } ); - } - - /** - * @inheritDoc - */ - init() { - this.listenTo( global.window, 'scroll', () => this._updatePosition() ); - this.listenTo( this, 'change:isActive', () => this._updatePosition() ); - - return super.init(); - } - - /** - * Analyzes the environment to decide where the panel should - * be positioned. - * - * @protected - */ - _updatePosition() { - if ( !this.isActive ) { - return; - } - - const { nw, sw, ne, se } = FloatingPanelView.defaultPositions; - const { top, left } = getOptimalPosition( { - element: this.element, - target: this.targetElement, - positions: [ nw, sw, ne, se ], - limiter: global.document.body, - fitInViewport: true - } ); - - Object.assign( this, { top, left } ); - } -} - -/** - * A default set of positioning functions used by the panel view to float - * around {@link module:ui/panel/floating/floatingpanelview~FloatingPanelView#targetElement}. - * - * The available positioning functions are as follows: - * - * * South east: - * - * +----------------+ - * | #targetElement | - * +----------------+ - * [ Panel ] - * - * * South west: - * - * +----------------+ - * | #targetElement | - * +----------------+ - * [ Panel ] - * - * * North east: - * - * [ Panel ] - * +----------------+ - * | #targetElement | - * +----------------+ - * - * - * * North west: - * - * [ Panel ] - * +----------------+ - * | #targetElement | - * +----------------+ - * - * Positioning functions must be compatible with {@link module:utils/dom/position~Position}. - * - * @member {Object} module:ui/panel/floating/floatingpanelview~FloatingPanelView.defaultPositions - */ -FloatingPanelView.defaultPositions = { - nw: ( targetRect, panelRect ) => ( { - top: targetRect.top - panelRect.height, - left: targetRect.left, - name: 'nw' - } ), - - sw: ( targetRect ) => ( { - top: targetRect.bottom, - left: targetRect.left, - name: 'sw' - } ), - - ne: ( targetRect, panelRect ) => ( { - top: targetRect.top - panelRect.height, - left: targetRect.left + targetRect.width - panelRect.width, - name: 'ne' - } ), - - se: ( targetRect, panelRect ) => ( { - top: targetRect.bottom, - left: targetRect.left + targetRect.width - panelRect.width, - name: 'se' - } ) -}; diff --git a/tests/manual/contextualballoon/contextualballoon.js b/tests/manual/contextualballoon/contextualballoon.js index 7ecbc4e5..c3697aa8 100644 --- a/tests/manual/contextualballoon/contextualballoon.js +++ b/tests/manual/contextualballoon/contextualballoon.js @@ -230,13 +230,13 @@ function createContextualToolbar( editor ) { forwardSelection: ( targetRect, balloonRect ) => ( { top: targetRect.bottom + arrowVOffset, left: targetRect.right - balloonRect.width / 2, - name: 's' + name: 'arrow_s' } ), backwardSelection: ( targetRect, balloonRect ) => ( { top: targetRect.top - balloonRect.height - arrowVOffset, left: targetRect.left - balloonRect.width / 2, - name: 'n' + name: 'arrow_n' } ) }; diff --git a/tests/manual/contextualtoolbar/contextualtoolbar.js b/tests/manual/contextualtoolbar/contextualtoolbar.js index ba6cbf54..414152f3 100644 --- a/tests/manual/contextualtoolbar/contextualtoolbar.js +++ b/tests/manual/contextualtoolbar/contextualtoolbar.js @@ -28,7 +28,7 @@ const positions = { forwardSelection: ( targetRect, balloonRect ) => ( { top: targetRect.bottom + arrowVOffset, left: targetRect.right - balloonRect.width / 2, - name: 's' + name: 'arrow_s' } ), // +-----------------+ @@ -39,7 +39,7 @@ const positions = { backwardSelection: ( targetRect, balloonRect ) => ( { top: targetRect.top - balloonRect.height - arrowVOffset, left: targetRect.left - balloonRect.width / 2, - name: 'n' + name: 'arrow_n' } ) }; diff --git a/tests/manual/tickets/170/1.js b/tests/manual/tickets/170/1.js index ccfa0362..ac00b1d7 100644 --- a/tests/manual/tickets/170/1.js +++ b/tests/manual/tickets/170/1.js @@ -52,7 +52,7 @@ ClassicEditor.create( document.querySelector( '#editor-stick' ), { editor.ui.view.element.querySelector( '.ck-editor__editable' ).scrollTop = 360; panel.init().then( () => { - panel.keepAttachedTo( { + panel.pin( { target: editor.ui.view.element.querySelector( '.ck-editor__editable p strong' ), limiter: editor.ui.view.editableElement } ); diff --git a/tests/manual/tickets/170/1.md b/tests/manual/tickets/170/1.md index 904f1286..bf692040 100644 --- a/tests/manual/tickets/170/1.md +++ b/tests/manual/tickets/170/1.md @@ -1,4 +1,4 @@ -## BalloonPanelView `attachTo` vs `keepAttachedTo` +## BalloonPanelView `attachTo` vs `pin` Scroll editable elements and container (horizontally as well). Balloon in the left editor should float but balloon in the right editor should stick to the target element. diff --git a/tests/panel/balloon/balloonpanelview.js b/tests/panel/balloon/balloonpanelview.js index 89ebe225..00bfb396 100644 --- a/tests/panel/balloon/balloonpanelview.js +++ b/tests/panel/balloon/balloonpanelview.js @@ -25,6 +25,12 @@ describe( 'BalloonPanelView', () => { return view.init(); } ); + afterEach( () => { + if ( view ) { + return view.destroy(); + } + } ); + describe( 'constructor()', () => { it( 'should create element from template', () => { expect( view.element.tagName ).to.equal( 'DIV' ); @@ -35,8 +41,9 @@ describe( 'BalloonPanelView', () => { it( 'should set default values', () => { expect( view.top ).to.equal( 0 ); expect( view.left ).to.equal( 0 ); - expect( view.position ).to.equal( 'se' ); + expect( view.position ).to.equal( 'arrow_se' ); expect( view.isVisible ).to.equal( false ); + expect( view.withArrow ).to.equal( true ); } ); it( 'creates view#content collection', () => { @@ -49,10 +56,18 @@ describe( 'BalloonPanelView', () => { it( 'should react on view#position', () => { expect( view.element.classList.contains( 'ck-balloon-panel_arrow_se' ) ).to.true; - view.position = 'sw'; + view.position = 'arrow_sw'; expect( view.element.classList.contains( 'ck-balloon-panel_arrow_sw' ) ).to.true; } ); + + it( 'should react on view#withArrow', () => { + expect( view.element.classList.contains( 'ck-balloon-panel_with-arrow' ) ).to.be.true; + + view.withArrow = false; + + expect( view.element.classList.contains( 'ck-balloon-panel_with-arrow' ) ).to.be.false; + } ); } ); describe( 'isVisible', () => { @@ -96,9 +111,10 @@ describe( 'BalloonPanelView', () => { expect( view.element.childNodes.length ).to.equal( 0 ); const button = new ButtonView( { t() {} } ); - view.content.add( button ); - expect( view.element.childNodes.length ).to.equal( 1 ); + return view.content.add( button ).then( () => { + expect( view.element.childNodes.length ).to.equal( 1 ); + } ); } ); } ); } ); @@ -203,7 +219,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'se' ); + expect( view.position ).to.equal( 'arrow_se' ); } ); it( 'should put balloon on the `south east` side of the target element when target is on the top left side of the limiter', () => { @@ -216,7 +232,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'se' ); + expect( view.position ).to.equal( 'arrow_se' ); } ); it( 'should put balloon on the `south west` side of the target element when target is on the right side of the limiter', () => { @@ -229,7 +245,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'sw' ); + expect( view.position ).to.equal( 'arrow_sw' ); } ); it( 'should put balloon on the `north east` side of the target element when target is on the bottom of the limiter ', () => { @@ -242,7 +258,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'ne' ); + expect( view.position ).to.equal( 'arrow_ne' ); } ); it( 'should put balloon on the `north west` side of the target element when target is on the bottom right of the limiter', () => { @@ -255,7 +271,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'nw' ); + expect( view.position ).to.equal( 'arrow_nw' ); } ); // https://github.com/ckeditor/ckeditor5-ui-default/issues/126 @@ -336,7 +352,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'sw' ); + expect( view.position ).to.equal( 'arrow_sw' ); } ); it( 'should put balloon on the `south east` position when `south west` is limited', () => { @@ -356,7 +372,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'se' ); + expect( view.position ).to.equal( 'arrow_se' ); } ); it( 'should put balloon on the `north east` position when `south east` is limited', () => { @@ -380,7 +396,7 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'ne' ); + expect( view.position ).to.equal( 'arrow_ne' ); } ); it( 'should put balloon on the `south east` position when `north east` is limited', () => { @@ -400,21 +416,23 @@ describe( 'BalloonPanelView', () => { view.attachTo( { target, limiter } ); - expect( view.position ).to.equal( 'se' ); + expect( view.position ).to.equal( 'arrow_se' ); } ); } ); } ); - describe( 'keepAttachedTo()', () => { + describe( 'pin() and unpin()', () => { let attachToSpy, target, targetParent, limiter, notRelatedElement; beforeEach( () => { - attachToSpy = testUtils.sinon.spy( view, 'attachTo' ); + attachToSpy = sinon.spy( view, 'attachTo' ); limiter = document.createElement( 'div' ); targetParent = document.createElement( 'div' ); target = document.createElement( 'div' ); notRelatedElement = document.createElement( 'div' ); + view.show(); + targetParent.appendChild( target ); document.body.appendChild( targetParent ); document.body.appendChild( limiter ); @@ -422,112 +440,194 @@ describe( 'BalloonPanelView', () => { } ); afterEach( () => { - attachToSpy.restore(); + targetParent.remove(); limiter.remove(); notRelatedElement.remove(); } ); - it( 'should keep the balloon attached to the target when any of the related elements is scrolled', () => { - view.keepAttachedTo( { target, limiter } ); + describe( 'pin()', () => { + it( 'should show the balloon', () => { + const spy = sinon.spy( view, 'show' ); + + view.hide(); - sinon.assert.calledOnce( attachToSpy ); - sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); + view.pin( { target, limiter } ); + sinon.assert.calledOnce( spy ); + } ); - targetParent.dispatchEvent( new Event( 'scroll' ) ); + it( 'should start pinning when the balloon is visible', () => { + view.pin( { target, limiter } ); + sinon.assert.calledOnce( attachToSpy ); - sinon.assert.calledTwice( attachToSpy ); - sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); + view.hide(); + targetParent.dispatchEvent( new Event( 'scroll' ) ); - limiter.dispatchEvent( new Event( 'scroll' ) ); + view.show(); + sinon.assert.calledTwice( attachToSpy ); - sinon.assert.calledThrice( attachToSpy ); - sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); + targetParent.dispatchEvent( new Event( 'scroll' ) ); + sinon.assert.calledThrice( attachToSpy ); + } ); - notRelatedElement.dispatchEvent( new Event( 'scroll' ) ); + it( 'should stop pinning when the balloon becomes invisible', () => { + view.show(); - // Nothing's changed. - sinon.assert.calledThrice( attachToSpy ); - sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); - } ); + view.pin( { target, limiter } ); + sinon.assert.calledOnce( attachToSpy ); - it( 'should keep the balloon attached to the target when the browser window is being resized', () => { - view.keepAttachedTo( { target, limiter } ); + view.hide(); - sinon.assert.calledOnce( attachToSpy ); - sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); + targetParent.dispatchEvent( new Event( 'scroll' ) ); + sinon.assert.calledOnce( attachToSpy ); + } ); - window.dispatchEvent( new Event( 'resize' ) ); + it( 'should unpin if already pinned', () => { + const unpinSpy = testUtils.sinon.spy( view, 'unpin' ); - sinon.assert.calledTwice( attachToSpy ); - sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); - } ); + view.show(); + sinon.assert.notCalled( attachToSpy ); - it( 'should stop attaching when the balloon is hidden', () => { - view.keepAttachedTo( { target, limiter } ); + view.pin( { target, limiter } ); + sinon.assert.calledOnce( attachToSpy ); - sinon.assert.calledOnce( attachToSpy ); + view.pin( { target, limiter } ); + sinon.assert.calledTwice( unpinSpy ); - view.hide(); + targetParent.dispatchEvent( new Event( 'scroll' ) ); + sinon.assert.calledThrice( attachToSpy ); + } ); - window.dispatchEvent( new Event( 'resize' ) ); - window.dispatchEvent( new Event( 'scroll' ) ); + it( 'should keep the balloon pinned to the target when any of the related elements is scrolled', () => { + view.pin( { target, limiter } ); - // Still once. - sinon.assert.calledOnce( attachToSpy ); - } ); + sinon.assert.calledOnce( attachToSpy ); + sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); - it( 'should stop attaching once the view is destroyed', () => { - view.keepAttachedTo( { target, limiter } ); + targetParent.dispatchEvent( new Event( 'scroll' ) ); - sinon.assert.calledOnce( attachToSpy ); + sinon.assert.calledTwice( attachToSpy ); + sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); - view.destroy(); + limiter.dispatchEvent( new Event( 'scroll' ) ); - window.dispatchEvent( new Event( 'resize' ) ); - window.dispatchEvent( new Event( 'scroll' ) ); + sinon.assert.calledThrice( attachToSpy ); + sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); - // Still once. - sinon.assert.calledOnce( attachToSpy ); - } ); + notRelatedElement.dispatchEvent( new Event( 'scroll' ) ); - it( 'should set document.body as the default limiter', () => { - view.keepAttachedTo( { target } ); + // Nothing's changed. + sinon.assert.calledThrice( attachToSpy ); + sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); + } ); - sinon.assert.calledOnce( attachToSpy ); + it( 'should keep the balloon pinned to the target when the browser window is being resized', () => { + view.pin( { target, limiter } ); - document.body.dispatchEvent( new Event( 'scroll' ) ); + sinon.assert.calledOnce( attachToSpy ); + sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); - sinon.assert.calledTwice( attachToSpy ); - } ); + window.dispatchEvent( new Event( 'resize' ) ); + + sinon.assert.calledTwice( attachToSpy ); + sinon.assert.calledWith( attachToSpy.lastCall, { target, limiter } ); + } ); + + it( 'should stop attaching when the balloon is hidden', () => { + view.pin( { target, limiter } ); + + sinon.assert.calledOnce( attachToSpy ); + + view.hide(); - it( 'should work for Range as a target', () => { - const element = document.createElement( 'div' ); - const range = document.createRange(); + window.dispatchEvent( new Event( 'resize' ) ); + window.dispatchEvent( new Event( 'scroll' ) ); - element.appendChild( document.createTextNode( 'foo bar' ) ); - document.body.appendChild( element ); - range.selectNodeContents( element ); + // Still once. + sinon.assert.calledOnce( attachToSpy ); + } ); + + it( 'should stop attaching once the view is destroyed', () => { + view.pin( { target, limiter } ); + + sinon.assert.calledOnce( attachToSpy ); + + return view.destroy().then( () => { + view = null; + + window.dispatchEvent( new Event( 'resize' ) ); + window.dispatchEvent( new Event( 'scroll' ) ); + + // Still once. + sinon.assert.calledOnce( attachToSpy ); + } ); + } ); + + it( 'should set document.body as the default limiter', () => { + view.pin( { target } ); + + sinon.assert.calledOnce( attachToSpy ); + + document.body.dispatchEvent( new Event( 'scroll' ) ); + + sinon.assert.calledTwice( attachToSpy ); + } ); - view.keepAttachedTo( { target: range } ); + it( 'should work for Range as a target', () => { + const element = document.createElement( 'div' ); + const range = document.createRange(); - sinon.assert.calledOnce( attachToSpy ); + element.appendChild( document.createTextNode( 'foo bar' ) ); + document.body.appendChild( element ); + range.selectNodeContents( element ); - element.dispatchEvent( new Event( 'scroll' ) ); + view.pin( { target: range } ); - sinon.assert.calledTwice( attachToSpy ); + sinon.assert.calledOnce( attachToSpy ); + + element.dispatchEvent( new Event( 'scroll' ) ); + + sinon.assert.calledTwice( attachToSpy ); + } ); + + it( 'should work for rect as a target', () => { + // Just check if this normally works without errors. + const rect = {}; + + view.pin( { target: rect, limiter } ); + + sinon.assert.calledOnce( attachToSpy ); + + limiter.dispatchEvent( new Event( 'scroll' ) ); + + sinon.assert.calledTwice( attachToSpy ); + } ); } ); - it( 'should work for rect as a target', () => { - // Just check if this normally works without errors. - const rect = {}; + describe( 'unpin()', () => { + it( 'should hide the balloon if pinned', () => { + const spy = sinon.spy( view, 'hide' ); + + view.pin( { target, limiter } ); + view.unpin(); - view.keepAttachedTo( { target: rect, limiter } ); + sinon.assert.calledOnce( spy ); + } ); + + it( 'should stop attaching', () => { + view.pin( { target, limiter } ); + sinon.assert.calledOnce( attachToSpy ); - sinon.assert.calledOnce( attachToSpy ); + view.unpin(); - limiter.dispatchEvent( new Event( 'scroll' ) ); + view.hide(); + window.dispatchEvent( new Event( 'resize' ) ); + document.dispatchEvent( new Event( 'scroll' ) ); + view.show(); + window.dispatchEvent( new Event( 'resize' ) ); + document.dispatchEvent( new Event( 'scroll' ) ); - sinon.assert.calledTwice( attachToSpy ); + sinon.assert.calledOnce( attachToSpy ); + } ); } ); } ); } ); diff --git a/tests/panel/floating/floatingpanelview.js b/tests/panel/floating/floatingpanelview.js deleted file mode 100644 index 5c3f02e0..00000000 --- a/tests/panel/floating/floatingpanelview.js +++ /dev/null @@ -1,229 +0,0 @@ -/** - * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. - * For licensing, see LICENSE.md. - */ - -/* global document, Event */ - -import global from '@ckeditor/ckeditor5-utils/src/dom/global'; -import FloatingPanelView from '../../../src/panel/floating/floatingpanelview'; -import View from '../../../src/view'; -import ViewCollection from '../../../src/viewcollection'; -import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; -import * as positionUtils from '@ckeditor/ckeditor5-utils/src/dom/position'; - -testUtils.createSinonSandbox(); - -describe( 'FloatingPanelView', () => { - let locale, view, target; - - beforeEach( () => { - locale = {}; - view = new FloatingPanelView( locale ); - - target = global.document.createElement( 'a' ); - - global.document.body.appendChild( view.element ); - global.document.body.appendChild( target ); - - view.targetElement = target; - - return view.init(); - } ); - - afterEach( () => { - view.element.remove(); - - return view.destroy(); - } ); - - describe( 'constructor()', () => { - it( 'should inherit from View', () => { - expect( view ).to.be.instanceOf( View ); - } ); - - it( 'should set view#locale', () => { - expect( view.locale ).to.equal( locale ); - } ); - - it( 'should set #isActive', () => { - expect( view.isActive ).to.be.false; - } ); - - it( 'should set #top', () => { - expect( view.top ).to.equal( 0 ); - } ); - - it( 'should set #left', () => { - expect( view.left ).to.equal( 0 ); - } ); - - it( 'should set #targetElement', () => { - view = new FloatingPanelView( locale ); - - expect( view.targetElement ).to.be.null; - } ); - - it( 'creates view#content collection', () => { - expect( view.content ).to.be.instanceOf( ViewCollection ); - } ); - } ); - - describe( 'template', () => { - it( 'should create element from template', () => { - expect( view.element.classList.contains( 'ck-floating-panel' ) ).to.be.true; - } ); - - describe( 'bindings', () => { - describe( 'class', () => { - it( 'reacts on #isActive', () => { - view.isActive = false; - expect( view.element.classList.contains( 'ck-floating-panel_active' ) ).to.be.false; - - view.isActive = true; - expect( view.element.classList.contains( 'ck-floating-panel_active' ) ).to.be.true; - } ); - } ); - - describe( 'style', () => { - it( 'reacts on #top', () => { - view.top = 30; - expect( view.element.style.top ).to.equal( '30px' ); - } ); - - it( 'reacts on #left', () => { - view.left = 20; - expect( view.element.style.left ).to.equal( '20px' ); - } ); - } ); - - describe( 'children', () => { - it( 'should react on view#content', () => { - expect( view.element.childNodes.length ).to.equal( 0 ); - - const item = new View(); - item.element = document.createElement( 'div' ); - view.content.add( item ); - - expect( view.element.childNodes.length ).to.equal( 1 ); - } ); - } ); - } ); - } ); - - describe( 'init()', () => { - it( 'calls #_updatePosition on window.scroll', () => { - const spy = sinon.spy( view, '_updatePosition' ); - - global.window.dispatchEvent( new Event( 'scroll', { bubbles: true } ) ); - sinon.assert.calledOnce( spy ); - } ); - - it( 'calls #_updatePosition on #change:isActive', () => { - view.isActive = false; - - const spy = sinon.spy( view, '_updatePosition' ); - - view.isActive = true; - sinon.assert.calledOnce( spy ); - - view.isActive = false; - sinon.assert.calledTwice( spy ); - } ); - } ); - - describe( '_updatePosition()', () => { - it( 'does not update when not #isActive', () => { - const spy = testUtils.sinon.spy( positionUtils, 'getOptimalPosition' ); - - view.isActive = false; - - view._updatePosition(); - sinon.assert.notCalled( spy ); - - view.isActive = true; - - view._updatePosition(); - - // Note: #_updatePosition() is called on #change:isActive. - sinon.assert.calledTwice( spy ); - } ); - - it( 'uses getOptimalPosition() utility', () => { - const { nw, sw, ne, se } = FloatingPanelView.defaultPositions; - - view.isActive = true; - - const spy = testUtils.sinon.stub( positionUtils, 'getOptimalPosition' ).returns( { - top: 5, - left: 10 - } ); - - view._updatePosition(); - - sinon.assert.calledWithExactly( spy, { - element: view.element, - target: target, - positions: [ nw, sw, ne, se ], - limiter: global.document.body, - fitInViewport: true - } ); - - expect( view.top ).to.equal( 5 ); - expect( view.left ).to.equal( 10 ); - } ); - } ); - - describe( 'defaultPositions', () => { - let targetRect, panelRect, defaultPositions; - - beforeEach( () => { - defaultPositions = FloatingPanelView.defaultPositions; - - targetRect = { - top: 10, - width: 100, - left: 10, - height: 10, - bottom: 20 - }; - - panelRect = { - width: 20, - height: 10 - }; - } ); - - it( 'should provide "nw" position', () => { - expect( defaultPositions.nw( targetRect, panelRect ) ).to.deep.equal( { - top: 0, - left: 10, - name: 'nw' - } ); - } ); - - it( 'should provide "sw" position', () => { - expect( defaultPositions.sw( targetRect, panelRect ) ).to.deep.equal( { - top: 20, - left: 10, - name: 'sw' - } ); - } ); - - it( 'should provide "ne" position', () => { - expect( defaultPositions.ne( targetRect, panelRect ) ).to.deep.equal( { - top: 0, - left: 90, - name: 'ne' - } ); - } ); - - it( 'should provide "se" position', () => { - expect( defaultPositions.se( targetRect, panelRect ) ).to.deep.equal( { - top: 20, - left: 90, - name: 'se' - } ); - } ); - } ); -} ); diff --git a/theme/components/panel/balloonpanel.scss b/theme/components/panel/balloonpanel.scss index 736bed48..ca578498 100644 --- a/theme/components/panel/balloonpanel.scss +++ b/theme/components/panel/balloonpanel.scss @@ -9,25 +9,15 @@ z-index: ck-z( 'modal' ); - &:before, - &:after { - content: ""; - position: absolute; - } - - &:before { - z-index: ck-z(); - } - - &:after { - z-index: ck-z( 'default', 1 ); - } + &.ck-balloon-panel_with-arrow { + &:before, + &:after { + content: ""; + position: absolute; + } - &_arrow_s, - &_arrow_se, - &_arrow_sw { &:before { - z-index: ck-z( 'default' ); + z-index: ck-z(); } &:after { @@ -35,15 +25,29 @@ } } - &_arrow_n, - &_arrow_ne, - &_arrow_nw { - &:before { - z-index: ck-z( 'default' ); + &.ck-balloon-panel_arrow { + &_s, + &_se, + &_sw { + &:before { + z-index: ck-z( 'default' ); + } + + &:after { + z-index: ck-z( 'default', 1 ); + } } - &:after { - z-index: ck-z( 'default', 1 ); + &_n, + &_ne, + &_nw { + &:before { + z-index: ck-z( 'default' ); + } + + &:after { + z-index: ck-z( 'default', 1 ); + } } }