UNPKG

@steroidsjs/ckeditor5

Version:

The development environment of CKEditor 5 – the best browser-based rich text editor.

284 lines (250 loc) 8.32 kB
/** * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ /** * @module ui/panel/sticky/stickypanelview */ import global from '@ckeditor/ckeditor5-utils/src/dom/global'; import View from '../../view'; import Template from '../../template'; import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; import '../../../theme/components/panel/stickypanel.css'; const toPx = toUnit( 'px' ); /** * The sticky panel view class. */ export default class StickyPanelView extends View { /** * @inheritDoc */ constructor( locale ) { super( locale ); const bind = this.bindTemplate; /** * Controls whether the sticky panel should be active. * * @readonly * @observable * @member {Boolean} #isActive */ this.set( 'isActive', false ); /** * Controls whether the sticky panel is in the "sticky" state. * * @readonly * @observable * @member {Boolean} #isSticky */ this.set( 'isSticky', false ); /** * The limiter element for the sticky panel instance. Its bounding rect limits * the "stickyness" of the panel, i.e. when the panel reaches the bottom * edge of the limiter, it becomes sticky to that edge and does not float * off the limiter. It is mandatory for the panel to work properly and once * set, it cannot be changed. * * @readonly * @observable * @member {HTMLElement} #limiterElement */ this.set( 'limiterElement', null ); /** * The offset from the bottom edge of {@link #limiterElement} * which stops the panel from stickying any further to prevent limiter's content * from being completely covered. * * @readonly * @observable * @default 50 * @member {Number} #limiterBottomOffset */ this.set( 'limiterBottomOffset', 50 ); /** * The offset from the top edge of the web browser's viewport which makes the * panel become sticky. The default value is `0`, which means the panel becomes * sticky when it's upper edge touches the top of the page viewport. * * This attribute is useful when the web page has UI elements positioned to the top * either using `position: fixed` or `position: sticky`, which would cover the * sticky panel or vice–versa (depending on the `z-index` hierarchy). * * @readonly * @observable * @default 0 * @member {Number} #viewportTopOffset */ this.set( 'viewportTopOffset', 0 ); /** * Controls the `margin-left` CSS style of the panel. * * @protected * @readonly * @observable * @member {String} #_marginLeft */ this.set( '_marginLeft', null ); /** * Set `true` if the sticky panel reached the bottom edge of the * {@link #limiterElement}. * * @protected * @readonly * @observable * @member {Boolean} #_isStickyToTheLimiter */ this.set( '_isStickyToTheLimiter', false ); /** * Set `true` if the sticky panel uses the {@link #viewportTopOffset}, * i.e. not {@link #_isStickyToTheLimiter} and the {@link #viewportTopOffset} * is not `0`. * * @protected * @readonly * @observable * @member {Boolean} #_hasViewportTopOffset */ this.set( '_hasViewportTopOffset', false ); /** * Collection of the child views which creates balloon panel contents. * * @readonly * @member {module:ui/viewcollection~ViewCollection} */ this.content = this.createCollection(); /** * The DOM bounding client rect of the {@link module:ui/view~View#element} of the panel. * * @protected * @member {Object} #_panelRect */ /** * The DOM bounding client rect of the {@link #limiterElement} * of the panel. * * @protected * @member {Object} #_limiterRect */ /** * A dummy element which visually fills the space as long as the * actual panel is sticky. It prevents flickering of the UI. * * @protected * @property {HTMLElement} */ this._contentPanelPlaceholder = new Template( { tag: 'div', attributes: { class: [ 'ck', 'ck-sticky-panel__placeholder' ], style: { display: bind.to( 'isSticky', isSticky => isSticky ? 'block' : 'none' ), height: bind.to( 'isSticky', isSticky => { return isSticky ? toPx( this._panelRect.height ) : null; } ) } } } ).render(); /** * The panel which accepts children into {@link #content} collection. * Also an element which is positioned when {@link #isSticky}. * * @protected * @property {HTMLElement} */ this._contentPanel = new Template( { tag: 'div', attributes: { class: [ 'ck', 'ck-sticky-panel__content', // Toggle class of the panel when "sticky" state changes in the view. bind.if( 'isSticky', 'ck-sticky-panel__content_sticky' ), bind.if( '_isStickyToTheLimiter', 'ck-sticky-panel__content_sticky_bottom-limit' ) ], style: { width: bind.to( 'isSticky', isSticky => { return isSticky ? toPx( this._contentPanelPlaceholder.getBoundingClientRect().width ) : null; } ), top: bind.to( '_hasViewportTopOffset', _hasViewportTopOffset => { return _hasViewportTopOffset ? toPx( this.viewportTopOffset ) : null; } ), bottom: bind.to( '_isStickyToTheLimiter', _isStickyToTheLimiter => { return _isStickyToTheLimiter ? toPx( this.limiterBottomOffset ) : null; } ), marginLeft: bind.to( '_marginLeft' ) } }, children: this.content } ).render(); this.setTemplate( { tag: 'div', attributes: { class: [ 'ck', 'ck-sticky-panel' ] }, children: [ this._contentPanelPlaceholder, this._contentPanel ] } ); } /** * @inheritDoc */ render() { super.render(); // Check if the panel should go into the sticky state immediately. this._checkIfShouldBeSticky(); // Update sticky state of the panel as the window is being scrolled. this.listenTo( global.window, 'scroll', () => { this._checkIfShouldBeSticky(); } ); // Synchronize with `model.isActive` because sticking an inactive panel is pointless. this.listenTo( this, 'change:isActive', () => { this._checkIfShouldBeSticky(); } ); } /** * Analyzes the environment to decide whether the panel should * be sticky or not. * * @protected */ _checkIfShouldBeSticky() { const panelRect = this._panelRect = this._contentPanel.getBoundingClientRect(); let limiterRect; if ( !this.limiterElement ) { this.isSticky = false; } else { limiterRect = this._limiterRect = this.limiterElement.getBoundingClientRect(); // The panel must be active to become sticky. this.isSticky = this.isActive && // The limiter's top edge must be beyond the upper edge of the visible viewport (+the viewportTopOffset). limiterRect.top < this.viewportTopOffset && // The model#limiterElement's height mustn't be smaller than the panel's height and model#limiterBottomOffset. // There's no point in entering the sticky mode if the model#limiterElement is very, very small, because // it would immediately set model#_isStickyToTheLimiter true and, given model#limiterBottomOffset, the panel // would be positioned before the model#limiterElement. this._panelRect.height + this.limiterBottomOffset < limiterRect.height; } // Stick the panel to the top edge of the viewport simulating CSS position:sticky. // TODO: Possibly replaced by CSS in the future http://caniuse.com/#feat=css-sticky if ( this.isSticky ) { this._isStickyToTheLimiter = limiterRect.bottom < panelRect.height + this.limiterBottomOffset + this.viewportTopOffset; this._hasViewportTopOffset = !this._isStickyToTheLimiter && !!this.viewportTopOffset; this._marginLeft = this._isStickyToTheLimiter ? null : toPx( -global.window.scrollX ); } // Detach the panel from the top edge of the viewport. else { this._isStickyToTheLimiter = false; this._hasViewportTopOffset = false; this._marginLeft = null; } } }