ckeditor5-editor-classic-floating 40.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ import { EditorUI, normalizeToolbarConfig } from 'ckeditor5/src/ui';
6
+ import { enablePlaceholder } from 'ckeditor5/src/engine';
7
+ import { ElementReplacer, Rect } from 'ckeditor5/src/utils';
8
+ /**
9
+ * The classic editor UI class.
10
+ */
11
+ export default class ClassicFloatingEditorUI extends EditorUI {
12
+ /**
13
+ * Creates an instance of the classic editor UI class.
14
+ *
15
+ * @param editor The editor instance.
16
+ * @param view The view of the UI.
17
+ */
18
+ constructor(editor, view) {
19
+ super(editor);
20
+ this.view = view;
21
+ this._toolbarConfig = normalizeToolbarConfig(editor.config.get('toolbar'));
22
+ this._elementReplacer = new ElementReplacer();
23
+ this.listenTo(editor.editing.view, 'scrollToTheSelection', this._handleScrollToTheSelectionWithStickyPanel.bind(this));
24
+ }
25
+ /**
26
+ * @inheritDoc
27
+ */
28
+ get element() {
29
+ return this.view.element;
30
+ }
31
+ /**
32
+ * Initializes the UI.
33
+ *
34
+ * @param replacementElement The DOM element that will be the source for the created editor.
35
+ */
36
+ init(replacementElement) {
37
+ const editor = this.editor;
38
+ const view = this.view;
39
+ const editingView = editor.editing.view;
40
+ const editable = view.editable;
41
+ const editingRoot = editingView.document.getRoot();
42
+ // The editable UI and editing root should share the same name. Then name is used
43
+ // to recognize the particular editable, for instance in ARIA attributes.
44
+ editable.name = editingRoot.rootName;
45
+ view.render();
46
+ // The editable UI element in DOM is available for sure only after the editor UI view has been rendered.
47
+ // But it can be available earlier if a DOM element has been passed to BalloonEditor.create().
48
+ const editableElement = editable.element;
49
+ // Register the editable UI view in the editor. A single editor instance can aggregate multiple
50
+ // editable areas (roots) but the classic editor has only one.
51
+ this.setEditableElement(editable.name, editableElement);
52
+ // Let the editable UI element respond to the changes in the global editor focus
53
+ // tracker. It has been added to the same tracker a few lines above but, in reality, there are
54
+ // many focusable areas in the editor, like balloons, toolbars or dropdowns and as long
55
+ // as they have focus, the editable should act like it is focused too (although technically
56
+ // it isn't), e.g. by setting the proper CSS class, visually announcing focus to the user.
57
+ // Doing otherwise will result in editable focus styles disappearing, once e.g. the
58
+ // toolbar gets focused.
59
+ view.editable.bind('isFocused').to(this.focusTracker);
60
+ // Bind the editable UI element to the editing view, making it an end– and entry–point
61
+ // of the editor's engine. This is where the engine meets the UI.
62
+ editingView.attachDomRoot(editableElement);
63
+ // If an element containing the initial data of the editor was provided, replace it with
64
+ // an editor instance's UI in DOM until the editor is destroyed. For instance, a <textarea>
65
+ // can be such element.
66
+ if (replacementElement) {
67
+ this._elementReplacer.replace(replacementElement, this.element);
68
+ }
69
+ this._initPlaceholder();
70
+ this._initToolbar();
71
+ this.fire('ready');
72
+ }
73
+ /**
74
+ * @inheritDoc
75
+ */
76
+ destroy() {
77
+ super.destroy();
78
+ const view = this.view;
79
+ const editingView = this.editor.editing.view;
80
+ this._elementReplacer.restore();
81
+ editingView.detachDomRoot(view.editable.name);
82
+ view.destroy();
83
+ }
84
+ /**
85
+ * Initializes the editor toolbar.
86
+ */
87
+ _initToolbar() {
88
+ const view = this.view;
89
+ // Set–up the sticky panel with toolbar.
90
+ view.stickyPanel.bind('isActive').to(this.focusTracker, 'isFocused');
91
+ view.stickyPanel.limiterElement = view.element;
92
+ view.stickyPanel.bind('viewportTopOffset').to(this, 'viewportOffset', ({ top }) => top || 0);
93
+ view.toolbar.fillFromConfig(this._toolbarConfig, this.componentFactory);
94
+ // Register the toolbar so it becomes available for Alt+F10 and Esc navigation.
95
+ this.addToolbar(view.toolbar);
96
+ }
97
+ /**
98
+ * Enable the placeholder text on the editing root.
99
+ */
100
+ _initPlaceholder() {
101
+ const editor = this.editor;
102
+ const editingView = editor.editing.view;
103
+ const editingRoot = editingView.document.getRoot();
104
+ const sourceElement = editor.sourceElement;
105
+ let placeholderText;
106
+ const placeholder = editor.config.get('placeholder');
107
+ if (placeholder) {
108
+ placeholderText = typeof placeholder === 'string' ? placeholder : placeholder[this.view.editable.name];
109
+ }
110
+ if (!placeholderText && sourceElement && sourceElement.tagName.toLowerCase() === 'textarea') {
111
+ placeholderText = sourceElement.getAttribute('placeholder');
112
+ }
113
+ if (placeholderText) {
114
+ editingRoot.placeholder = placeholderText;
115
+ }
116
+ enablePlaceholder({
117
+ view: editingView,
118
+ element: editingRoot,
119
+ isDirectHost: false,
120
+ keepOnFocus: true
121
+ });
122
+ }
123
+ /**
124
+ * Provides an integration between the sticky toolbar and {@link module:utils/dom/scroll~scrollViewportToShowTarget}.
125
+ * It allows the UI-agnostic engine method to consider the geometry of the
126
+ * {@link module:editor-classic/classicfloatingeditoruiview~FloatingEditorUIView#stickyPanel} that pins to the
127
+ * edge of the viewport and can obscure the user caret after scrolling the window.
128
+ *
129
+ * @param evt The `scrollToTheSelection` event info.
130
+ * @param data The payload carried by the `scrollToTheSelection` event.
131
+ * @param originalArgs The original arguments passed to `scrollViewportToShowTarget()` method (see implementation to learn more).
132
+ */
133
+ _handleScrollToTheSelectionWithStickyPanel(evt, data, originalArgs) {
134
+ const stickyPanel = this.view.stickyPanel;
135
+ if (stickyPanel.isSticky) {
136
+ const stickyPanelHeight = new Rect(stickyPanel.element).height;
137
+ data.viewportOffset.top += stickyPanelHeight;
138
+ }
139
+ else {
140
+ const scrollViewportOnPanelGettingSticky = () => {
141
+ this.editor.editing.view.scrollToTheSelection(originalArgs);
142
+ };
143
+ this.listenTo(stickyPanel, 'change:isSticky', scrollViewportOnPanelGettingSticky);
144
+ // This works as a post-scroll-fixer because it's impossible predict whether the panel will be sticky after scrolling or not.
145
+ // Listen for a short period of time only and if the toolbar does not become sticky very soon, cancel the listener.
146
+ setTimeout(() => {
147
+ this.stopListening(stickyPanel, 'change:isSticky', scrollViewportOnPanelGettingSticky);
148
+ }, 20);
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module editor-classic/classicfloatingeditoruiview
7
+ */
8
+ import StickyPanelView from './panel/StickyPanelView';
9
+ import { BoxedEditorUIView, InlineEditableUIView, ToolbarView } from 'ckeditor5/src/ui';
10
+ import type { Locale } from 'ckeditor5/src/utils';
11
+ import type { View } from 'ckeditor5/src/engine';
12
+ import '../../theme/classicfloatingeditor.css';
13
+ /**
14
+ * Classic editor UI view. Uses an inline editable and a sticky toolbar, all
15
+ * enclosed in a boxed UI view.
16
+ */
17
+ export default class ClassicFloatingEditorUIView extends BoxedEditorUIView {
18
+ /**
19
+ * Sticky panel view instance. This is a parent view of a {@link #toolbar}
20
+ * that makes toolbar sticky.
21
+ */
22
+ readonly stickyPanel: StickyPanelView;
23
+ /**
24
+ * Toolbar view instance.
25
+ */
26
+ readonly toolbar: ToolbarView;
27
+ /**
28
+ * Editable UI view.
29
+ */
30
+ readonly editable: InlineEditableUIView;
31
+ /**
32
+ * Creates an instance of the classic editor UI view.
33
+ *
34
+ * @param locale The {@link module:core/editor/editor~Editor#locale} instance.
35
+ * @param editingView The editing view instance this view is related to.
36
+ * @param options Configuration options for the view instance.
37
+ * @param options.shouldToolbarGroupWhenFull When set `true` enables automatic items grouping
38
+ * in the main {@link module:editor-classic/classicfloatingeditoruiview~ClassicFloatingEditorUIView#toolbar toolbar}.
39
+ * See {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} to learn more.
40
+ */
41
+ constructor(locale: Locale, editingView: View, options?: {
42
+ shouldToolbarGroupWhenFull?: boolean;
43
+ containerElement?: HTMLElement;
44
+ panelAbsolute?: boolean;
45
+ });
46
+ /**
47
+ * @inheritDoc
48
+ */
49
+ render(): void;
50
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module editor-classic/classicfloatingeditoruiview
7
+ */
8
+ import StickyPanelView from './panel/StickyPanelView';
9
+ import { BoxedEditorUIView, InlineEditableUIView, ToolbarView } from 'ckeditor5/src/ui';
10
+ import '../../theme/classicfloatingeditor.css';
11
+ /**
12
+ * Classic editor UI view. Uses an inline editable and a sticky toolbar, all
13
+ * enclosed in a boxed UI view.
14
+ */
15
+ export default class ClassicFloatingEditorUIView extends BoxedEditorUIView {
16
+ /**
17
+ * Creates an instance of the classic editor UI view.
18
+ *
19
+ * @param locale The {@link module:core/editor/editor~Editor#locale} instance.
20
+ * @param editingView The editing view instance this view is related to.
21
+ * @param options Configuration options for the view instance.
22
+ * @param options.shouldToolbarGroupWhenFull When set `true` enables automatic items grouping
23
+ * in the main {@link module:editor-classic/classicfloatingeditoruiview~ClassicFloatingEditorUIView#toolbar toolbar}.
24
+ * See {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} to learn more.
25
+ */
26
+ constructor(locale, editingView, options = {}) {
27
+ super(locale);
28
+ this.stickyPanel = new StickyPanelView(locale, options.containerElement, options.panelAbsolute);
29
+ this.toolbar = new ToolbarView(locale, {
30
+ shouldGroupWhenFull: options.shouldToolbarGroupWhenFull
31
+ });
32
+ this.editable = new InlineEditableUIView(locale, editingView);
33
+ }
34
+ /**
35
+ * @inheritDoc
36
+ */
37
+ render() {
38
+ super.render();
39
+ // Set toolbar as a child of a stickyPanel and makes toolbar sticky.
40
+ this.stickyPanel.content.add(this.toolbar);
41
+ this.top.add(this.stickyPanel);
42
+ this.main.add(this.editable);
43
+ }
44
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ui/panel/sticky/stickypanelview
7
+ */
8
+ import View from '@ckeditor/ckeditor5-ui/src/view';
9
+ import type ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
10
+ import { type Locale } from '@ckeditor/ckeditor5-utils';
11
+ import '../../../theme/stickypanel.css';
12
+ /**
13
+ * The sticky panel view class.
14
+ */
15
+ export default class StickyPanelView extends View {
16
+ /**
17
+ * Collection of the child views which creates balloon panel contents.
18
+ */
19
+ readonly content: ViewCollection;
20
+ containerElement: HTMLElement | Window;
21
+ panelAbsolute: boolean;
22
+ position: string | null;
23
+ panel_height: number;
24
+ panel_width: number;
25
+ /**
26
+ * Controls whether the sticky panel should be active.
27
+ *
28
+ * @readonly
29
+ * @observable
30
+ */
31
+ isActive: boolean;
32
+ /**
33
+ * Controls whether the sticky panel is in the "sticky" state.
34
+ *
35
+ * @readonly
36
+ * @observable
37
+ */
38
+ isSticky: boolean;
39
+ /**
40
+ * The limiter element for the sticky panel instance. Its bounding rect limits
41
+ * the "stickyness" of the panel, i.e. when the panel reaches the bottom
42
+ * edge of the limiter, it becomes sticky to that edge and does not float
43
+ * off the limiter. It is mandatory for the panel to work properly and once
44
+ * set, it cannot be changed.
45
+ *
46
+ * @readonly
47
+ * @observable
48
+ */
49
+ limiterElement: HTMLElement | null;
50
+ /**
51
+ * The offset from the bottom edge of {@link #limiterElement}
52
+ * which stops the panel from stickying any further to prevent limiter's content
53
+ * from being completely covered.
54
+ *
55
+ * @readonly
56
+ * @observable
57
+ * @default 50
58
+ */
59
+ limiterBottomOffset: number;
60
+ /**
61
+ * The offset from the top edge of the web browser's viewport which makes the
62
+ * panel become sticky. The default value is `0`, which means the panel becomes
63
+ * sticky when it's upper edge touches the top of the page viewport.
64
+ *
65
+ * This attribute is useful when the web page has UI elements positioned to the top
66
+ * either using `position: fixed` or `position: sticky`, which would cover the
67
+ * sticky panel or vice–versa (depending on the `z-index` hierarchy).
68
+ *
69
+ * Bound to {@link module:ui/editorui/editorui~EditorUI#viewportOffset `EditorUI#viewportOffset`}.
70
+ *
71
+ * If {@link module:core/editor/editorconfig~EditorConfig#ui `EditorConfig#ui.viewportOffset.top`} is defined, then
72
+ * it will override the default value.
73
+ *
74
+ * @observable
75
+ * @default 0
76
+ */
77
+ viewportTopOffset: number;
78
+ /**
79
+ * Controls the `margin-left` CSS style of the panel.
80
+ *
81
+ * @private
82
+ * @readonly
83
+ * @observable
84
+ */
85
+ _marginLeft: string | null;
86
+ /**
87
+ * Set `true` if the sticky panel reached the bottom edge of the
88
+ * {@link #limiterElement}.
89
+ *
90
+ * @private
91
+ * @readonly
92
+ * @observable
93
+ */
94
+ _isStickyToTheBottomOfLimiter: boolean;
95
+ /**
96
+ * The `top` CSS position of the panel when it is sticky to the top of the viewport or scrollable
97
+ * ancestors of the {@link #limiterElement}.
98
+ *
99
+ * @private
100
+ * @readonly
101
+ * @observable
102
+ */
103
+ _stickyTopOffset: number | null;
104
+ /**
105
+ * The `bottom` CSS position of the panel when it is sticky to the bottom of the {@link #limiterElement}.
106
+ *
107
+ * @private
108
+ * @readonly
109
+ * @observable
110
+ */
111
+ _stickyBottomOffset: number | null;
112
+ /**
113
+ * A dummy element which visually fills the space as long as the
114
+ * actual panel is sticky. It prevents flickering of the UI.
115
+ */
116
+ private _contentPanelPlaceholder;
117
+ /**
118
+ * The panel which accepts children into {@link #content} collection.
119
+ * Also an element which is positioned when {@link #isSticky}.
120
+ */
121
+ private _contentPanel;
122
+ /**
123
+ * @inheritDoc
124
+ */
125
+ constructor(locale?: Locale, containerElement?: HTMLElement, panelAbsolute?: boolean);
126
+ /**
127
+ * @inheritDoc
128
+ */
129
+ render(): void;
130
+ /**
131
+ * Analyzes the environment to decide whether the panel should be sticky or not.
132
+ * Then handles the positioning of the panel.
133
+ */
134
+ checkIfShouldBeSticky(): void;
135
+ /**
136
+ * Sticks the panel at the given CSS `top` offset.
137
+ *
138
+ * @private
139
+ * @param topOffset
140
+ */
141
+ private _stickToTopOfAncestors;
142
+ /**
143
+ * Sticks the panel at the bottom of the limiter with a given CSS `bottom` offset.
144
+ *
145
+ * @private
146
+ * @param stickyBottomOffset
147
+ */
148
+ private _stickToBottomOfLimiter;
149
+ /**
150
+ * Unsticks the panel putting it back to its original position.
151
+ *
152
+ * @private
153
+ */
154
+ private _unstick;
155
+ /**
156
+ * Returns the bounding rect of the {@link #_contentPanel}.
157
+ *
158
+ * @private
159
+ */
160
+ private get _contentPanelRect();
161
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module ui/panel/sticky/stickypanelview
7
+ */
8
+ import View from '@ckeditor/ckeditor5-ui/src/view';
9
+ import Template from '@ckeditor/ckeditor5-ui/src/template';
10
+ import { global, toUnit, Rect } from '@ckeditor/ckeditor5-utils';
11
+ // @if CK_DEBUG_STICKYPANEL // const {
12
+ // @if CK_DEBUG_STICKYPANEL // default: RectDrawer,
13
+ // @if CK_DEBUG_STICKYPANEL // diagonalStylesBlack
14
+ // @if CK_DEBUG_STICKYPANEL // } = require( '@ckeditor/ckeditor5-utils/tests/_utils/rectdrawer' );
15
+ import '../../../theme/stickypanel.css';
16
+ const toPx = toUnit('px');
17
+ /**
18
+ * The sticky panel view class.
19
+ */
20
+ export default class StickyPanelView extends View {
21
+ /**
22
+ * @inheritDoc
23
+ */
24
+ constructor(locale, containerElement, panelAbsolute = false) {
25
+ super(locale);
26
+ const bind = this.bindTemplate;
27
+ this.containerElement = containerElement || global.window;
28
+ this.panelAbsolute = panelAbsolute;
29
+ this.set('position', null);
30
+ this.set('panel_height', 0);
31
+ this.set('panel_width', 0);
32
+ this.set('isActive', false);
33
+ this.set('isSticky', false);
34
+ this.set('limiterElement', null);
35
+ this.set('limiterBottomOffset', 50);
36
+ this.set('viewportTopOffset', 0);
37
+ this.set('_marginLeft', null);
38
+ this.set('_isStickyToTheBottomOfLimiter', false);
39
+ this.set('_stickyTopOffset', null);
40
+ this.set('_stickyBottomOffset', null);
41
+ this.content = this.createCollection();
42
+ this._contentPanelPlaceholder = new Template({
43
+ tag: 'div',
44
+ attributes: {
45
+ class: [
46
+ 'ck',
47
+ 'ck-sticky-panel__placeholder'
48
+ ],
49
+ style: {
50
+ display: bind.to('isSticky', isSticky => isSticky ? 'block' : 'none'),
51
+ height: bind.to('panel_height', value => {
52
+ return toPx(value);
53
+ }),
54
+ width: bind.to('panel_width', value => {
55
+ return toPx(value);
56
+ }),
57
+ }
58
+ }
59
+ }).render();
60
+ this._contentPanel = new Template({
61
+ tag: 'div',
62
+ attributes: {
63
+ class: [
64
+ 'ck',
65
+ 'ck-sticky-panel__content',
66
+ // Toggle class of the panel when "sticky" state changes in the view.
67
+ bind.if('isSticky', 'ck-sticky-panel__content_sticky'),
68
+ bind.if('_isStickyToTheBottomOfLimiter', 'ck-sticky-panel__content_sticky_bottom-limit')
69
+ ],
70
+ style: {
71
+ position: bind.to('position', position => {
72
+ return position;
73
+ }),
74
+ width: bind.to('isSticky', isSticky => {
75
+ return isSticky ? toPx(this._contentPanelPlaceholder.getBoundingClientRect().width) : null;
76
+ }),
77
+ top: bind.to('_stickyTopOffset', value => value ? toPx(value) : value),
78
+ bottom: bind.to('_stickyBottomOffset', value => value ? toPx(value) : value),
79
+ marginLeft: bind.to('_marginLeft')
80
+ }
81
+ },
82
+ children: this.content
83
+ }).render();
84
+ this.setTemplate({
85
+ tag: 'div',
86
+ attributes: {
87
+ class: [
88
+ 'ck',
89
+ 'ck-sticky-panel'
90
+ ]
91
+ },
92
+ children: [
93
+ this._contentPanelPlaceholder,
94
+ this._contentPanel
95
+ ]
96
+ });
97
+ }
98
+ /**
99
+ * @inheritDoc
100
+ */
101
+ render() {
102
+ super.render();
103
+ // Check if the panel should go into the sticky state immediately.
104
+ this.checkIfShouldBeSticky();
105
+ // Update sticky state of the panel as the window and ancestors are being scrolled.
106
+ this.listenTo(this.containerElement, 'scroll', () => {
107
+ this.checkIfShouldBeSticky();
108
+ }, { useCapture: true });
109
+ // Synchronize with `model.isActive` because sticking an inactive panel is pointless.
110
+ this.listenTo(this, 'change:isActive', () => {
111
+ this.checkIfShouldBeSticky();
112
+ });
113
+ }
114
+ /**
115
+ * Analyzes the environment to decide whether the panel should be sticky or not.
116
+ * Then handles the positioning of the panel.
117
+ */
118
+ checkIfShouldBeSticky() {
119
+ // @if CK_DEBUG_STICKYPANEL // RectDrawer.clear();
120
+ if (!this.limiterElement || !this.isActive) {
121
+ this._unstick();
122
+ return;
123
+ }
124
+ this.panel_height = this._contentPanelRect.height;
125
+ this.panel_width = this._contentPanelRect.width;
126
+ const limiterRect = new Rect(this.limiterElement);
127
+ let visibleLimiterRect = limiterRect.getVisible();
128
+ if (visibleLimiterRect) {
129
+ const windowRect = new Rect(this.containerElement);
130
+ limiterRect.top -= windowRect.top;
131
+ limiterRect.bottom -= windowRect.top;
132
+ windowRect.top += this.viewportTopOffset;
133
+ windowRect.height -= this.viewportTopOffset;
134
+ visibleLimiterRect = visibleLimiterRect.getIntersection(windowRect);
135
+ }
136
+ // @if CK_DEBUG_STICKYPANEL // if ( visibleLimiterRect ) {
137
+ // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleLimiterRect,
138
+ // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'red', outlineOffset: '-3px' },
139
+ // @if CK_DEBUG_STICKYPANEL // 'Visible anc'
140
+ // @if CK_DEBUG_STICKYPANEL // );
141
+ // @if CK_DEBUG_STICKYPANEL // }
142
+ // @if CK_DEBUG_STICKYPANEL //
143
+ // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( limiterRect,
144
+ // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'green', outlineOffset: '-3px' },
145
+ // @if CK_DEBUG_STICKYPANEL // 'Limiter'
146
+ // @if CK_DEBUG_STICKYPANEL // );
147
+ // Stick the panel only if
148
+ // * the limiter's ancestors are intersecting with each other so that some of their rects are visible,
149
+ // * and the limiter's top edge is above the visible ancestors' top edge.
150
+ if (visibleLimiterRect && limiterRect.top < visibleLimiterRect.top) {
151
+ // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleLimiterRect,
152
+ // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '3px', opacity: '.8', outlineColor: 'fuchsia', outlineOffset: '-3px',
153
+ // @if CK_DEBUG_STICKYPANEL // backgroundColor: 'rgba(255, 0, 255, .3)' },
154
+ // @if CK_DEBUG_STICKYPANEL // 'Visible limiter'
155
+ // @if CK_DEBUG_STICKYPANEL // );
156
+ const visibleLimiterTop = visibleLimiterRect.top;
157
+ // Check if there's a change the panel can be sticky to the bottom of the limiter.
158
+ if (visibleLimiterTop + this._contentPanelRect.height + this.limiterBottomOffset > visibleLimiterRect.bottom) {
159
+ const stickyBottomOffset = Math.max(limiterRect.bottom - visibleLimiterRect.bottom, 0) + this.limiterBottomOffset;
160
+ // @if CK_DEBUG_STICKYPANEL // const stickyBottomOffsetRect = new Rect( {
161
+ // @if CK_DEBUG_STICKYPANEL // top: limiterRect.bottom - stickyBottomOffset, left: 0, right: 2000,
162
+ // @if CK_DEBUG_STICKYPANEL // bottom: limiterRect.bottom - stickyBottomOffset, width: 2000, height: 1
163
+ // @if CK_DEBUG_STICKYPANEL // } );
164
+ // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( stickyBottomOffsetRect,
165
+ // @if CK_DEBUG_STICKYPANEL // { outlineWidth: '1px', opacity: '.8', outlineColor: 'black' },
166
+ // @if CK_DEBUG_STICKYPANEL // 'Sticky bottom offset'
167
+ // @if CK_DEBUG_STICKYPANEL // );
168
+ // Check if sticking the panel to the bottom of the limiter does not cause it to suddenly
169
+ // move upwards if there's not enough space for it.
170
+ if (limiterRect.bottom - stickyBottomOffset > limiterRect.top + this._contentPanelRect.height) {
171
+ if (this.panelAbsolute) {
172
+ }
173
+ else {
174
+ this._stickToBottomOfLimiter(stickyBottomOffset);
175
+ }
176
+ }
177
+ else {
178
+ this._unstick();
179
+ }
180
+ }
181
+ else {
182
+ if (this._contentPanelRect.height + this.limiterBottomOffset < limiterRect.height) {
183
+ if (this.panelAbsolute) {
184
+ if (limiterRect.top < 0 && -limiterRect.top < limiterRect.height - this.limiterBottomOffset) {
185
+ this._stickToTopOfAncestors(-limiterRect.top, 'absolute');
186
+ }
187
+ else {
188
+ this._unstick();
189
+ }
190
+ }
191
+ else {
192
+ this._stickToTopOfAncestors(visibleLimiterTop, null);
193
+ }
194
+ }
195
+ else {
196
+ this._unstick();
197
+ }
198
+ }
199
+ }
200
+ else {
201
+ this._unstick();
202
+ }
203
+ // @if CK_DEBUG_STICKYPANEL // console.clear();
204
+ // @if CK_DEBUG_STICKYPANEL // console.log( 'isSticky', this.isSticky );
205
+ // @if CK_DEBUG_STICKYPANEL // console.log( '_isStickyToTheBottomOfLimiter', this._isStickyToTheBottomOfLimiter );
206
+ // @if CK_DEBUG_STICKYPANEL // console.log( '_stickyTopOffset', this._stickyTopOffset );
207
+ // @if CK_DEBUG_STICKYPANEL // console.log( '_stickyBottomOffset', this._stickyBottomOffset );
208
+ // @if CK_DEBUG_STICKYPANEL // if ( visibleLimiterRect ) {
209
+ // @if CK_DEBUG_STICKYPANEL // RectDrawer.draw( visibleLimiterRect,
210
+ // @if CK_DEBUG_STICKYPANEL // { ...diagonalStylesBlack,
211
+ // @if CK_DEBUG_STICKYPANEL // outlineWidth: '3px', opacity: '.8', outlineColor: 'orange', outlineOffset: '-3px',
212
+ // @if CK_DEBUG_STICKYPANEL // backgroundColor: 'rgba(0, 0, 255, .2)' },
213
+ // @if CK_DEBUG_STICKYPANEL // 'visibleLimiterRect'
214
+ // @if CK_DEBUG_STICKYPANEL // );
215
+ // @if CK_DEBUG_STICKYPANEL // }
216
+ }
217
+ /**
218
+ * Sticks the panel at the given CSS `top` offset.
219
+ *
220
+ * @private
221
+ * @param topOffset
222
+ */
223
+ _stickToTopOfAncestors(topOffset, position) {
224
+ this.isSticky = true;
225
+ this._isStickyToTheBottomOfLimiter = false;
226
+ this._stickyBottomOffset = null;
227
+ this._stickyTopOffset = topOffset;
228
+ this._marginLeft = toPx(-global.window.scrollX);
229
+ this.position = position;
230
+ }
231
+ /**
232
+ * Sticks the panel at the bottom of the limiter with a given CSS `bottom` offset.
233
+ *
234
+ * @private
235
+ * @param stickyBottomOffset
236
+ */
237
+ _stickToBottomOfLimiter(stickyBottomOffset) {
238
+ this.isSticky = true;
239
+ this._isStickyToTheBottomOfLimiter = true;
240
+ this._stickyTopOffset = null;
241
+ this._stickyBottomOffset = stickyBottomOffset;
242
+ this._marginLeft = toPx(-global.window.scrollX);
243
+ this.position = null;
244
+ }
245
+ /**
246
+ * Unsticks the panel putting it back to its original position.
247
+ *
248
+ * @private
249
+ */
250
+ _unstick() {
251
+ this.isSticky = false;
252
+ this._isStickyToTheBottomOfLimiter = false;
253
+ this._stickyTopOffset = null;
254
+ this._stickyBottomOffset = null;
255
+ this._marginLeft = null;
256
+ this.position = null;
257
+ }
258
+ /**
259
+ * Returns the bounding rect of the {@link #_contentPanel}.
260
+ *
261
+ * @private
262
+ */
263
+ get _contentPanelRect() {
264
+ return new Rect(this._contentPanel);
265
+ }
266
+ }