diff --git a/src/inlineeditorui.js b/src/inlineeditorui.js index 00a62c9..4488027 100644 --- a/src/inlineeditorui.js +++ b/src/inlineeditorui.js @@ -10,6 +10,7 @@ import ComponentFactory from '@ckeditor/ckeditor5-ui/src/componentfactory'; import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import enableToolbarKeyboardFocus from '@ckeditor/ckeditor5-ui/src/toolbar/enabletoolbarkeyboardfocus'; +import normalizeToolbarConfig from '@ckeditor/ckeditor5-ui/src/toolbar/normalizetoolbarconfig'; /** * The inline editor UI class. @@ -44,9 +45,21 @@ export default class InlineEditorUI { */ this.focusTracker = new FocusTracker(); + /** + * A normalized `config.toolbar` object. + * + * @type {Object} + * @private + */ + this._toolbarConfig = normalizeToolbarConfig( editor.config.get( 'toolbar' ) ); + // Set–up the view#panel. view.panel.bind( 'isVisible' ).to( this.focusTracker, 'isFocused' ); + if ( this._toolbarConfig && this._toolbarConfig.viewportTopOffset ) { + view.viewportTopOffset = this._toolbarConfig.viewportTopOffset; + } + // https://github.com/ckeditor/ckeditor5-editor-inline/issues/4 view.listenTo( editor.editing.view, 'render', () => { // Don't pin if the panel is not already visible. It prevents the panel @@ -78,7 +91,10 @@ export default class InlineEditorUI { const editor = this.editor; this.view.init(); - this.view.toolbar.fillFromConfig( editor.config.get( 'toolbar' ), this.componentFactory ); + + if ( this._toolbarConfig ) { + this.view.toolbar.fillFromConfig( this._toolbarConfig.items, this.componentFactory ); + } enableToolbarKeyboardFocus( { origin: editor.editing.view, diff --git a/src/inlineeditoruiview.js b/src/inlineeditoruiview.js index 96ae2f1..aeba33d 100644 --- a/src/inlineeditoruiview.js +++ b/src/inlineeditoruiview.js @@ -13,28 +13,6 @@ import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpa import ToolbarView from '@ckeditor/ckeditor5-ui/src/toolbar/toolbarview'; import Template from '@ckeditor/ckeditor5-ui/src/template'; -// A set of positioning functions used by the -// {@link module:editor-inline/inlineeditoruiview~InlineEditableUIView#panel}. -// -// @private -// @type {module:utils/dom/position~Options#positions} -const panelPositions = [ - ( editableRect, panelRect ) => { - return { - top: getPanelPositionTop( editableRect, panelRect ), - left: editableRect.left, - name: 'toolbar_west' - }; - }, - ( editableRect, panelRect ) => { - return { - top: getPanelPositionTop( editableRect, panelRect ), - left: editableRect.left + editableRect.width - panelRect.width, - name: 'toolbar_east' - }; - } -]; - /** * Inline editor UI view. Uses inline editable and floating toolbar. * @@ -57,6 +35,21 @@ export default class InlineEditorUIView extends EditorUIView { */ this.toolbar = new ToolbarView( locale ); + /** + * The offset from the top edge of the web browser's viewport which makes the + * UI become sticky. The default value is `0`, which means the UI 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 + * UI or vice–versa (depending on the `z-index` hierarchy). + * + * @readonly + * @observable + * @member {Number} #viewportTopOffset + */ + this.set( 'viewportTopOffset', 0 ); + Template.extend( this.toolbar.template, { attributes: { class: [ @@ -78,6 +71,53 @@ export default class InlineEditorUIView extends EditorUIView { this.panel.withArrow = false; + /** + * A set of positioning functions used by the {@link #panel} to float around + * {@link #editableElement}. + * + * The positioning functions are as follows: + * + * * West: + * + * [ Panel ] + * +------------------+ + * | #editableElement | + * +------------------+ + * + * +------------------+ + * | #editableElement | + * |[ Panel ] | + * | | + * +------------------+ + * + * +------------------+ + * | #editableElement | + * +------------------+ + * [ Panel ] + * + * * East: + * + * [ Panel ] + * +------------------+ + * | #editableElement | + * +------------------+ + * + * +------------------+ + * | #editableElement | + * | [ Panel ]| + * | | + * +------------------+ + * + * +------------------+ + * | #editableElement | + * +------------------+ + * [ Panel ] + * + * @readonly + * @type {module:utils/dom/position~Options#positions} + */ + this.panelPositions = this._getPanelPositions(); + Template.extend( this.panel.template, { attributes: { class: 'ck-toolbar-container' @@ -113,73 +153,50 @@ export default class InlineEditorUIView extends EditorUIView { } /** - * A set of positioning functions used by the {@link #panel} to float around - * {@link #editableElement}. - * - * The positioning functions are as follows: - * - * * West: - * - * [ Panel ] - * +------------------+ - * | #editableElement | - * +------------------+ - * - * +------------------+ - * | #editableElement | - * |[ Panel ] | - * | | - * +------------------+ - * - * +------------------+ - * | #editableElement | - * +------------------+ - * [ Panel ] - * - * * East: + * Determines panel top position of the {@link #panel} in {@link #panelPositions}. * - * [ Panel ] - * +------------------+ - * | #editableElement | - * +------------------+ - * - * +------------------+ - * | #editableElement | - * | [ Panel ]| - * | | - * +------------------+ - * - * +------------------+ - * | #editableElement | - * +------------------+ - * [ Panel ] - * - * @readonly - * @type {module:utils/dom/position~Options#positions} + * @private + * @param {module:utils/dom/rect~Rect} editableRect Rect of the + * {@link module:editor-inline/inlineeditoruiview~InlineEditableUIView#editableElement}. + * @param {module:utils/dom/rect~Rect} panelRect Rect of the + * {@link module:editor-inline/inlineeditoruiview~InlineEditableUIView#panel}. */ - get panelPositions() { - return panelPositions; + _getPanelPositionTop( editableRect, panelRect ) { + let top; + + if ( editableRect.top > panelRect.height + this.viewportTopOffset ) { + top = editableRect.top - panelRect.height; + } else if ( editableRect.bottom > panelRect.height + this.viewportTopOffset + 50 ) { + top = this.viewportTopOffset; + } else { + top = editableRect.bottom; + } + + return top; } -} -// Determines panel top position for -// {@link module:editor-inline/inlineeditoruiview~InlineEditableUIView#panelPositions} -// -// @private -// @param {module:utils/dom/rect~Rect} editableRect Rect of the -// {@link module:editor-inline/inlineeditoruiview~InlineEditableUIView#editableElement}. -// @param {module:utils/dom/rect~Rect} panelRect Rect of the -// {@link module:editor-inline/inlineeditoruiview~InlineEditableUIView#panel}. -function getPanelPositionTop( editableRect, panelRect ) { - let top; - - if ( editableRect.top > panelRect.height ) { - top = editableRect.top - panelRect.height; - } else if ( editableRect.bottom > panelRect.height + 50 ) { - top = 0; - } else { - top = editableRect.bottom; + /** + * Returns the positions for {@link #panelPositions}. + * + * @private + * @returns module:utils/dom/position~Options#positions + */ + _getPanelPositions() { + return [ + ( editableRect, panelRect ) => { + return { + top: this._getPanelPositionTop( editableRect, panelRect ), + left: editableRect.left, + name: 'toolbar_west' + }; + }, + ( editableRect, panelRect ) => { + return { + top: this._getPanelPositionTop( editableRect, panelRect ), + left: editableRect.left + editableRect.width - panelRect.width, + name: 'toolbar_east' + }; + } + ]; } - - return top; } diff --git a/tests/inlineeditorui.js b/tests/inlineeditorui.js index d65a7f4..54c6f3e 100644 --- a/tests/inlineeditorui.js +++ b/tests/inlineeditorui.js @@ -27,16 +27,24 @@ describe( 'InlineEditorUI', () => { editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); - editor = new ClassicTestEditor( editorElement, { + return ClassicTestEditor.create( editorElement, { toolbar: [ 'foo', 'bar' ] - } ); + } ) + .then( newEditor => { + editor = newEditor; - view = new InlineEditorUIView( editor.locale ); - ui = new InlineEditorUI( editor, view ); - editable = editor.editing.view.getRoot(); + view = new InlineEditorUIView( editor.locale ); + ui = new InlineEditorUI( editor, view ); + editable = editor.editing.view.getRoot(); - ui.componentFactory.add( 'foo', viewCreator( 'foo' ) ); - ui.componentFactory.add( 'bar', viewCreator( 'bar' ) ); + ui.componentFactory.add( 'foo', viewCreator( 'foo' ) ); + ui.componentFactory.add( 'bar', viewCreator( 'bar' ) ); + } ); + } ); + + afterEach( () => { + editorElement.remove(); + editor.destroy(); } ); describe( 'constructor()', () => { @@ -65,6 +73,35 @@ describe( 'InlineEditorUI', () => { expect( view.panel.isVisible ).to.be.true; } ); + it( 'doesn\'t set the view#viewportTopOffset, if not specified in the config', () => { + expect( view.viewportTopOffset ).to.equal( 0 ); + } ); + + it( 'sets view#viewportTopOffset, if specified', () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor.create( editorElement, { + toolbar: { + items: [ 'foo', 'bar' ], + viewportTopOffset: 100 + } + } ) + .then( editor => { + view = new InlineEditorUIView( editor.locale ); + ui = new InlineEditorUI( editor, view ); + editable = editor.editing.view.getRoot(); + + ui.componentFactory.add( 'foo', viewCreator( 'foo' ) ); + ui.componentFactory.add( 'bar', viewCreator( 'bar' ) ); + + expect( view.viewportTopOffset ).to.equal( 100 ); + + editorElement.remove(); + return editor.destroy(); + } ); + } ); + // https://github.com/ckeditor/ckeditor5-editor-inline/issues/4 it( 'pin() is called on editor.editable.view#render', () => { const spy = sinon.spy( view.panel, 'pin' ); @@ -133,11 +170,40 @@ describe( 'InlineEditorUI', () => { sinon.assert.calledOnce( spy ); } ); - it( 'fills view.toolbar#items with editor config', () => { - const spy = testUtils.sinon.spy( view.toolbar, 'fillFromConfig' ); + describe( 'view.toolbar#items', () => { + it( 'are filled with the config.toolbar (specified as an Array)', () => { + const spy = testUtils.sinon.spy( view.toolbar, 'fillFromConfig' ); - ui.init(); - sinon.assert.calledWithExactly( spy, editor.config.get( 'toolbar' ), ui.componentFactory ); + ui.init(); + sinon.assert.calledWithExactly( spy, editor.config.get( 'toolbar' ), ui.componentFactory ); + } ); + + it( 'are filled with the config.toolbar (specified as an Object)', () => { + editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor.create( editorElement, { + toolbar: { + items: [ 'foo', 'bar' ], + viewportTopOffset: 100 + } + } ) + .then( editor => { + view = new InlineEditorUIView( editor.locale ); + ui = new InlineEditorUI( editor, view ); + + ui.componentFactory.add( 'foo', viewCreator( 'foo' ) ); + ui.componentFactory.add( 'bar', viewCreator( 'bar' ) ); + + const spy = testUtils.sinon.spy( view.toolbar, 'fillFromConfig' ); + + ui.init(); + sinon.assert.calledWithExactly( spy, + editor.config.get( 'toolbar.items' ), + ui.componentFactory + ); + } ); + } ); } ); it( 'initializes keyboard navigation between view#toolbar and view#editable', () => { diff --git a/tests/inlineeditoruiview.js b/tests/inlineeditoruiview.js index 7b8b752..7a4d687 100644 --- a/tests/inlineeditoruiview.js +++ b/tests/inlineeditoruiview.js @@ -31,6 +31,10 @@ describe( 'InlineEditorUIView', () => { expect( view.toolbar.element.classList.contains( 'ck-editor-toolbar' ) ).to.be.true; expect( view.toolbar.element.classList.contains( 'ck-toolbar_floating' ) ).to.be.true; } ); + + it( 'sets the default value of the #viewportTopOffset attribute', () => { + expect( view.viewportTopOffset ).to.equal( 0 ); + } ); } ); describe( '#panel', () => { @@ -226,6 +230,54 @@ describe( 'InlineEditorUIView', () => { expect( top ).to.equal( 150 ); expect( left ).to.equal( expectedLeft ); } ); + + describe( 'view#viewportTopOffset', () => { + it( 'sticks the panel to the offset when there\'s not enough space above', () => { + view.viewportTopOffset = 50; + + const position = view.panelPositions[ positionIndex ]; + const editableRect = { + top: 0, // ! + bottom: 200, + left: 100, + right: 100, + width: 100, + height: 200 + }; + const panelRect = { + width: 50, + height: 50 + }; + + const { top, left } = position( editableRect, panelRect ); + + expect( top ).to.equal( 50 ); + expect( left ).to.equal( expectedLeft ); + } ); + + it( 'positions the panel below the editable when there\'s not enough space above/over', () => { + view.viewportTopOffset = 50; + + const position = view.panelPositions[ positionIndex ]; + const editableRect = { + top: 100, + bottom: 150, + left: 100, + right: 100, + width: 100, + height: 50 + }; + const panelRect = { + width: 50, + height: 80 + }; + + const { top, left } = position( editableRect, panelRect ); + + expect( top ).to.equal( 150 ); + expect( left ).to.equal( expectedLeft ); + } ); + } ); } } ); } ); diff --git a/tests/manual/tickets/23/1.html b/tests/manual/tickets/23/1.html new file mode 100644 index 0000000..1f32c0b --- /dev/null +++ b/tests/manual/tickets/23/1.html @@ -0,0 +1,37 @@ +
This is an editor instance.
+This is an editor instance.
+This is an editor instance.
+This is an editor instance.
+This is an editor instance.
+This is an editor instance.
+