| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663 |
- /**
- * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module ui/panel/balloon/balloonpanelview
- */
- import View from '../../view';
- import Template from '../../template';
- import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
- import isRange from '@ckeditor/ckeditor5-utils/src/dom/isrange';
- import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement';
- import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
- import preventDefault from '../../bindings/preventdefault.js';
- import global from '@ckeditor/ckeditor5-utils/src/dom/global';
- const toPx = toUnit( 'px' );
- const defaultLimiterElement = global.document.body;
- /**
- * The balloon panel view class.
- *
- * @extends module:ui/view~View
- */
- export default class BalloonPanelView extends View {
- /**
- * @inheritDoc
- */
- constructor( locale ) {
- super( locale );
- const bind = this.bindTemplate;
- /**
- * The absolute top position of the balloon panel in pixels.
- *
- * @observable
- * @default 0
- * @member {Number} #top
- */
- this.set( 'top', 0 );
- /**
- * The absolute left position of the balloon panel in pixels.
- *
- * @observable
- * @default 0
- * @member {Number} #left
- */
- this.set( 'left', 0 );
- /**
- * Balloon panel's current position. The position name is reflected in the CSS class set
- * to the balloon, i.e. `.ck-balloon-panel_arrow_nw` for "arrow_nw" 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 'arrow_nw'
- * @member {'arrow_nw'|'arrow_ne'|'arrow_sw'|'arrow_se'} #position
- */
- this.set( 'position', 'arrow_nw' );
- /**
- * Controls whether the balloon panel is visible or not.
- *
- * @observable
- * @default false
- * @member {Boolean} #isVisible
- */
- 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 );
- /**
- * Additional css class added to the {#element}.
- *
- * @observable
- * @member {String} #className
- */
- this.set( 'className' );
- /**
- * 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.
- *
- * @readonly
- * @member {module:ui/viewcollection~ViewCollection}
- */
- this.content = this.createCollection();
- this.template = new Template( {
- tag: 'div',
- attributes: {
- class: [
- 'ck-balloon-panel',
- bind.to( 'position', value => `ck-balloon-panel_${ value }` ),
- bind.if( 'isVisible', 'ck-balloon-panel_visible' ),
- bind.if( 'withArrow', 'ck-balloon-panel_with-arrow' ),
- bind.to( 'className' )
- ],
- style: {
- top: bind.to( 'top', toPx ),
- left: bind.to( 'left', toPx )
- }
- },
- children: this.content,
- on: {
- // https://github.com/ckeditor/ckeditor5-ui/issues/206
- mousedown: preventDefault( this ),
- // https://github.com/ckeditor/ckeditor5-ui/issues/243
- selectstart: bind.to( evt => evt.preventDefault() )
- }
- } );
- }
- /**
- * Shows the balloon panel.
- *
- * See {@link #isVisible}.
- */
- show() {
- this.isVisible = true;
- }
- /**
- * Hides the balloon panel.
- *
- * See {@link #isVisible}.
- */
- hide() {
- this.isVisible = false;
- }
- /**
- * Attaches the balloon panel to a specified DOM element or range with a smart heuristics.
- *
- * See {@link @link module:utils/dom/position~getOptimalPosition}.
- *
- * TODO: More docs and examples.
- *
- * @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}.
- */
- attachTo( options ) {
- this.show();
- const defaultPositions = BalloonPanelView.defaultPositions;
- const positionOptions = Object.assign( {}, {
- element: this.element,
- positions: [
- defaultPositions.southArrowNorthWest,
- defaultPositions.southArrowNorthEast,
- defaultPositions.northArrowSouthWest,
- defaultPositions.northArrowSouthEast
- ],
- limiter: defaultLimiterElement,
- fitInViewport: true
- }, options );
- const { top, left, name: position } = getOptimalPosition( positionOptions );
- Object.assign( this, { top, left, position } );
- }
- /**
- * Works the same way as {module:ui/panel/balloon/balloonpanelview~BalloonPanelView.attachTo}
- * except that the position of the panel is continuously updated when any ancestor of the
- * {@link module:utils/dom/position~Options#target} or {@link module:utils/dom/position~Options#limiter}
- * is being scrolled or when the browser window is being resized.
- *
- * Thanks to this, the panel always sticks to the {@link module:utils/dom/position~Options#target}.
- *
- * 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}.
- */
- 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 targetElement = getDomElement( options.target );
- const limiterElement = options.limiter ? getDomElement( options.limiter ) : defaultLimiterElement;
- // Then we need to listen on scroll event of eny element in the document.
- this.listenTo( global.document, 'scroll', ( evt, domEvt ) => {
- const scrollTarget = domEvt.target;
- // The position needs to be updated if the positioning target is within the scrolled element.
- const isWithinScrollTarget = targetElement && scrollTarget.contains( targetElement );
- // The position needs to be updated if the positioning limiter is within the scrolled element.
- const isLimiterWithinScrollTarget = limiterElement && scrollTarget.contains( limiterElement );
- // The positioning target and/or limiter can be a Rect, object etc..
- // There's no way to optimize the listener then.
- if ( isWithinScrollTarget || isLimiterWithinScrollTarget || !targetElement || !limiterElement ) {
- this.attachTo( options );
- }
- }, { useCapture: true } );
- // We need to listen on window resize event and update position.
- this.listenTo( global.window, 'resize', () => {
- this.attachTo( options );
- } );
- }
- /**
- * Stops managing the pinned state of the panel. See {@link #pin}.
- *
- * @private
- */
- _stopPinning() {
- this.stopListening( global.document, 'scroll' );
- this.stopListening( global.window, 'resize' );
- }
- }
- // Returns the DOM element for given object or null, if there's none,
- // e.g. when passed object is a Rect instance or so.
- //
- // @private
- // @param {*} object
- // @returns {HTMLElement|null}
- function getDomElement( object ) {
- if ( isElement( object ) ) {
- return object;
- }
- if ( isRange( object ) ) {
- return object.commonAncestorContainer;
- }
- if ( typeof object == 'function' ) {
- return getDomElement( object() );
- }
- return null;
- }
- /**
- * A horizontal offset of the arrow tip from the edge of the balloon. Controlled by CSS.
- *
- * +-----|---------...
- * | |
- * | |
- * | |
- * | |
- * +--+ | +------...
- * \ | /
- * \|/
- * >|-----|<---------------- horizontal offset
- *
- * @default 30
- * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHorizontalOffset
- */
- BalloonPanelView.arrowHorizontalOffset = 30;
- /**
- * A vertical offset of the arrow from the edge of the balloon. Controlled by CSS.
- *
- * +-------------...
- * |
- * |
- * | /-- vertical offset
- * | V
- * +--+ +-----... ---------
- * \ / |
- * \/ |
- * -------------------------------
- * ^
- *
- * @default 15
- * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowVerticalOffset
- */
- BalloonPanelView.arrowVerticalOffset = 15;
- /**
- * A default set of positioning functions used by the balloon panel view
- * when attaching using {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo} method.
- *
- * The available positioning functions are as follow:
- *
- * **North**
- *
- * * `northArrowSouth`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * * `northArrowSouthEast`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * * `northArrowSouthWest`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * **North west**
- *
- * * `northWestArrowSouth`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * * `northWestArrowSouthWest`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * * `northWestArrowSouthEast`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * **North east**
- *
- * * `northEastArrowSouth`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * * `northEastArrowSouthEast`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * * `northEastArrowSouthWest`
- *
- * +-----------------+
- * | Balloon |
- * +-----------------+
- * V
- * [ Target ]
- *
- * **South**
- *
- * * `southArrowNorth`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * * `southArrowNorthEast`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * * `southArrowNorthWest`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * **South west**
- *
- * * `southWestArrowNorth`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * * `southWestArrowNorthWest`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * * `southWestArrowNorthEast`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * **South east**
- *
- * * `southEastArrowNorth`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * * `southEastArrowNorthEast`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * * `southEastArrowNorthWest`
- *
- * [ Target ]
- * ^
- * +-----------------+
- * | Balloon |
- * +-----------------+
- *
- * See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}.
- *
- * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
- *
- * The name that position function returns will be reflected in balloon panel's class that
- * controls the placement of the "arrow". See {@link #position} to learn more.
- *
- * @member {Object} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions
- */
- BalloonPanelView.defaultPositions = {
- // ------- North
- northArrowSouth: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
- name: 'arrow_s'
- } ),
- northArrowSouthEast: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_se'
- } ),
- northArrowSouthWest: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_sw'
- } ),
- // ------- North west
- northWestArrowSouth: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.left - balloonRect.width / 2,
- name: 'arrow_s'
- } ),
- northWestArrowSouthWest: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.left - BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_sw'
- } ),
- northWestArrowSouthEast: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_se'
- } ),
- // ------- North east
- northEastArrowSouth: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.right - balloonRect.width / 2,
- name: 'arrow_s'
- } ),
- northEastArrowSouthEast: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_se'
- } ),
- northEastArrowSouthWest: ( targetRect, balloonRect ) => ( {
- top: getNorthTop( targetRect, balloonRect ),
- left: targetRect.right - BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_sw'
- } ),
- // ------- South
- southArrowNorth: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2,
- name: 'arrow_n'
- } ),
- southArrowNorthEast: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_ne'
- } ),
- southArrowNorthWest: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_nw'
- } ),
- // ------- South west
- southWestArrowNorth: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.left - balloonRect.width / 2,
- name: 'arrow_n'
- } ),
- southWestArrowNorthWest: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.left - BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_nw'
- } ),
- southWestArrowNorthEast: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_ne'
- } ),
- // ------- South east
- southEastArrowNorth: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.right - balloonRect.width / 2,
- name: 'arrow_n'
- } ),
- southEastArrowNorthEast: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_ne'
- } ),
- southEastArrowNorthWest: ( targetRect, balloonRect ) => ( {
- top: getSouthTop( targetRect, balloonRect ),
- left: targetRect.right - BalloonPanelView.arrowHorizontalOffset,
- name: 'arrow_nw'
- } ),
- };
- // Returns the top coordinate for positions starting with `north*`.
- //
- // @private
- // @param {utils/dom/rect~Rect} targetRect A rect of the target.
- // @param {utils/dom/rect~Rect} elementRect A rect of the balloon.
- // @returns {Number}
- function getNorthTop( targetRect, balloonRect ) {
- return targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset;
- }
- // Returns the top coordinate for positions starting with `south*`.
- //
- // @private
- // @param {utils/dom/rect~Rect} targetRect A rect of the target.
- // @param {utils/dom/rect~Rect} elementRect A rect of the balloon.
- // @returns {Number}
- function getSouthTop( targetRect ) {
- return targetRect.bottom + BalloonPanelView.arrowVerticalOffset;
- }
|