diff --git a/src/panel/balloon/balloonpanelview.js b/src/panel/balloon/balloonpanelview.js index bbd17f56..b202a8c8 100644 --- a/src/panel/balloon/balloonpanelview.js +++ b/src/panel/balloon/balloonpanelview.js @@ -224,9 +224,13 @@ export default class BalloonPanelView extends View { element: this.element, positions: [ defaultPositions.southArrowNorth, + defaultPositions.southArrowNorthMiddleWest, + defaultPositions.southArrowNorthMiddleEast, defaultPositions.southArrowNorthWest, defaultPositions.southArrowNorthEast, defaultPositions.northArrowSouth, + defaultPositions.northArrowSouthMiddleWest, + defaultPositions.northArrowSouthMiddleEast, defaultPositions.northArrowSouthWest, defaultPositions.northArrowSouthEast ], @@ -435,86 +439,158 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * * The available positioning functions are as follows: * - * **North** - * - * * `northArrowSouth` * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] * - * * `northArrowSouthEast` + * **North west** * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] + * * `northWestArrowSouthWest` * - * * `northArrowSouthWest` + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] + * * `northWestArrowSouthMiddleWest` * - * **North west** + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] * * * `northWestArrowSouth` * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] * - * * `northWestArrowSouthWest` + * * `northWestArrowSouthMiddleEast` * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] * * * `northWestArrowSouthEast` * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * + * + * + * **North** + * + * * `northArrowSouthWest` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * + * * `northArrowSouthMiddleWest` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * + * * `northArrowSouth` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * + * * `northArrowSouthMiddleEast` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * + * * `northArrowSouthEast` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] * * **North east** * + * * `northEastArrowSouthWest` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * + * + * * `northEastArrowSouthMiddleWest` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * * * `northEastArrowSouth` * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] + * + * * `northEastArrowSouthMiddleEast` + * + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] * * * `northEastArrowSouthEast` * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] + * +-----------------+ + * | Balloon | + * +-----------------+ + * V + * [ Target ] * - * * `northEastArrowSouthWest` * - * +-----------------+ - * | Balloon | - * +-----------------+ - * V - * [ Target ] * * **South** * + * + * * `southArrowNorthWest` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ + * + * * `southArrowNorthMiddleWest` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ + * * * `southArrowNorth` * * [ Target ] @@ -523,24 +599,42 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * | Balloon | * +-----------------+ * + * * `southArrowNorthMiddleEast` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ + * * * `southArrowNorthEast` * - * [ Target ] - * ^ + * [ Target ] + * ^ * +-----------------+ * | Balloon | * +-----------------+ * - * * `southArrowNorthWest` * - * [ Target ] - * ^ - * +-----------------+ - * | Balloon | - * +-----------------+ * * **South west** * + * * `southWestArrowNorthWest` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ + * + * * `southWestArrowNorthMiddleWest` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ + * * * `southWestArrowNorth` * * [ Target ] @@ -549,24 +643,41 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * | Balloon | * +-----------------+ * - * * `southWestArrowNorthWest` + * * `southWestArrowNorthMiddleEast` * - * [ Target ] - * ^ + * [ Target ] + * ^ * +-----------------+ * | Balloon | * +-----------------+ * * * `southWestArrowNorthEast` * - * [ Target ] - * ^ + * [ Target ] + * ^ * +-----------------+ * | Balloon | * +-----------------+ * + * + * * **South east** * + * * `southEastArrowNorthWest` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ +* * `southEastArrowNorthMiddleWest` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ + * * * `southEastArrowNorth` * * [ Target ] @@ -575,21 +686,22 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * | Balloon | * +-----------------+ * - * * `southEastArrowNorthEast` + * * `southEastArrowNorthMiddleEast` * - * [ Target ] - * ^ + * [ Target ] + * ^ * +-----------------+ * | Balloon | * +-----------------+ * - * * `southEastArrowNorthWest` + * * `southEastArrowNorthEast` + * + * [ Target ] + * ^ + * +-----------------+ + * | Balloon | + * +-----------------+ * - * [ Target ] - * ^ - * +-----------------+ - * | Balloon | - * +-----------------+ * * See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}. * @@ -601,125 +713,196 @@ BalloonPanelView._getOptimalPosition = getOptimalPosition; * @member {Object} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions */ BalloonPanelView.defaultPositions = { - // ------- North - northArrowSouth: ( targetRect, balloonRect ) => ( { + // ------- North west + + northWestArrowSouthWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, + left: targetRect.left - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_sw' + } ), + + northWestArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_smw' + } ), + + northWestArrowSouth: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - balloonRect.width / 2, name: 'arrow_s' } ), - northArrowSouthEast: ( targetRect, balloonRect ) => ( { + northWestArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, + left: targetRect.left - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_sme' + } ), + + northWestArrowSouthEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, name: 'arrow_se' } ), + // ------- North + northArrowSouthWest: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset, name: 'arrow_sw' } ), - // ------- North west + northArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_smw' + } ), - northWestArrowSouth: ( targetRect, balloonRect ) => ( { + northArrowSouth: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width / 2, + left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, name: 'arrow_s' } ), - northWestArrowSouthWest: ( targetRect, balloonRect ) => ( { + northArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sw' + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_sme' } ), - northWestArrowSouthEast: ( targetRect, balloonRect ) => ( { + northArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, name: 'arrow_se' } ), // ------- North east + northEastArrowSouthWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_sw' + } ), + + northEastArrowSouthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_smw' + } ), northEastArrowSouth: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width / 2, name: 'arrow_s' } ), + northEastArrowSouthMiddleEast: ( targetRect, balloonRect ) => ( { + top: getNorthTop( targetRect, balloonRect ), + left: targetRect.right - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_sme' + } ), + northEastArrowSouthEast: ( targetRect, balloonRect ) => ( { top: getNorthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, name: 'arrow_se' } ), + // ------- South west - northEastArrowSouthWest: ( targetRect, balloonRect ) => ( { - top: getNorthTop( targetRect, balloonRect ), - left: targetRect.right - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_sw' + southWestArrowNorthWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nw' } ), - // ------- South + southWestArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nmw' + } ), - southArrowNorth: ( targetRect, balloonRect ) => ( { + southWestArrowNorth: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, + left: targetRect.left - balloonRect.width / 2, name: 'arrow_n' } ), - southArrowNorthEast: ( targetRect, balloonRect ) => ( { + southWestArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, + left: targetRect.left - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nme' + } ), + + southWestArrowNorthEast: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, name: 'arrow_ne' } ), + // ------- South + southArrowNorthWest: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset, name: 'arrow_nw' } ), + southArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.25 ) - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nmw' + } ), - // ------- South west - - southWestArrowNorth: ( targetRect, balloonRect ) => ( { + southArrowNorth: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width / 2, + left: targetRect.left + targetRect.width / 2 - balloonRect.width / 2, name: 'arrow_n' } ), - southWestArrowNorthWest: ( targetRect, balloonRect ) => ( { + southArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nw' + left: targetRect.left + targetRect.width / 2 - ( balloonRect.width * 0.75 ) + BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nme' } ), - southWestArrowNorthEast: ( targetRect, balloonRect ) => ( { + southArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.left - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, + left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, name: 'arrow_ne' } ), // ------- South east + southEastArrowNorthWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.right - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nw' + } ), + + southEastArrowNorthMiddleWest: ( targetRect, balloonRect ) => ( { + top: getSouthTop( targetRect, balloonRect ), + left: targetRect.right - ( balloonRect.width * .25 ) - BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nmw' + } ), + southEastArrowNorth: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), left: targetRect.right - balloonRect.width / 2, name: 'arrow_n' } ), - southEastArrowNorthEast: ( targetRect, balloonRect ) => ( { + southEastArrowNorthMiddleEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_ne' + left: targetRect.right - ( balloonRect.width * .75 ) + BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_nme' } ), - southEastArrowNorthWest: ( targetRect, balloonRect ) => ( { + southEastArrowNorthEast: ( targetRect, balloonRect ) => ( { top: getSouthTop( targetRect, balloonRect ), - left: targetRect.right - BalloonPanelView.arrowHorizontalOffset, - name: 'arrow_nw' + left: targetRect.right - balloonRect.width + BalloonPanelView.arrowHorizontalOffset, + name: 'arrow_ne' } ) + }; // Returns the top coordinate for positions starting with `north*`. diff --git a/src/toolbar/balloon/balloontoolbar.js b/src/toolbar/balloon/balloontoolbar.js index a593d959..b09e419c 100644 --- a/src/toolbar/balloon/balloontoolbar.js +++ b/src/toolbar/balloon/balloontoolbar.js @@ -15,6 +15,10 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; import normalizeToolbarConfig from '../normalizetoolbarconfig'; import { debounce } from 'lodash-es'; +import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver'; +import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; + +const toPx = toUnit( 'px' ); /** * The contextual toolbar. @@ -44,6 +48,14 @@ export default class BalloonToolbar extends Plugin { constructor( editor ) { super( editor ); + /** + * A cached and normalized `config.balloonToolbar` object. + * + * @type {module:core/editor/editorconfig~EditorConfig#balloonToolbar} + * @private + */ + this._balloonConfig = normalizeToolbarConfig( editor.config.get( 'balloonToolbar' ) ); + /** * The toolbar view displayed in the balloon. * @@ -66,6 +78,20 @@ export default class BalloonToolbar extends Plugin { this.focusTracker.add( this.toolbarView.element ); } ); + /** + * An instance of the resize observer that allows to respond to changes in editable's geometry + * so the toolbar can stay within its boundaries (and group toolbar items that do not fit). + * + * **Note**: Used only when `shouldNotGroupWhenFull` was **not** set in the + * {@link module:core/editor/editorconfig~EditorConfig#balloonToolbar configuration}. + * + * **Note:** Created in {@link #init}. + * + * @protected + * @member {module:utils/dom/resizeobserver~ResizeObserver} + */ + this._resizeObserver = null; + /** * The contextual balloon plugin instance. * @@ -125,6 +151,20 @@ export default class BalloonToolbar extends Plugin { this.show(); } } ); + + if ( !this._balloonConfig.shouldNotGroupWhenFull ) { + this.listenTo( editor, 'ready', () => { + const editableElement = editor.ui.view.editable.element; + + // Set #toolbarView's max-width on the initialization and update it on the editable resize. + this._resizeObserver = new ResizeObserver( editableElement, () => { + // The max-width equals 90% of the editable's width for the best user experience. + // The value keeps the balloon very close to the boundaries of the editable and limits the cases + // when the balloon juts out from the editable element it belongs to. + this.toolbarView.maxWidth = toPx( new Rect( editableElement ).width * .9 ); + } ); + } ); + } } /** @@ -134,10 +174,9 @@ export default class BalloonToolbar extends Plugin { * @inheritDoc */ afterInit() { - const config = normalizeToolbarConfig( this.editor.config.get( 'balloonToolbar' ) ); const factory = this.editor.ui.componentFactory; - this.toolbarView.fillFromConfig( config.items, factory ); + this.toolbarView.fillFromConfig( this._balloonConfig.items, factory ); } /** @@ -147,7 +186,10 @@ export default class BalloonToolbar extends Plugin { * @returns {module:ui/toolbar/toolbarview~ToolbarView} */ _createToolbarView() { - const toolbarView = new ToolbarView( this.editor.locale ); + const shouldGroupWhenFull = !this._balloonConfig.shouldNotGroupWhenFull; + const toolbarView = new ToolbarView( this.editor.locale, { + shouldGroupWhenFull + } ); toolbarView.extendTemplate( { attributes: { @@ -260,6 +302,10 @@ export default class BalloonToolbar extends Plugin { this._fireSelectionChangeDebounced.cancel(); this.toolbarView.destroy(); this.focusTracker.destroy(); + + if ( this._resizeObserver ) { + this._resizeObserver.destroy(); + } } /** @@ -289,16 +335,24 @@ function getBalloonPositions( isBackward ) { defaultPositions.northWestArrowSouth, defaultPositions.northWestArrowSouthWest, defaultPositions.northWestArrowSouthEast, + defaultPositions.northWestArrowSouthMiddleEast, + defaultPositions.northWestArrowSouthMiddleWest, defaultPositions.southWestArrowNorth, defaultPositions.southWestArrowNorthWest, - defaultPositions.southWestArrowNorthEast + defaultPositions.southWestArrowNorthEast, + defaultPositions.southWestArrowNorthMiddleWest, + defaultPositions.southWestArrowNorthMiddleEast ] : [ defaultPositions.southEastArrowNorth, defaultPositions.southEastArrowNorthEast, defaultPositions.southEastArrowNorthWest, + defaultPositions.southEastArrowNorthMiddleEast, + defaultPositions.southEastArrowNorthMiddleWest, defaultPositions.northEastArrowSouth, defaultPositions.northEastArrowSouthEast, - defaultPositions.northEastArrowSouthWest + defaultPositions.northEastArrowSouthWest, + defaultPositions.northEastArrowSouthMiddleEast, + defaultPositions.northEastArrowSouthMiddleWest ]; } @@ -306,6 +360,8 @@ function getBalloonPositions( isBackward ) { * Contextual toolbar configuration. Used by the {@link module:ui/toolbar/balloon/balloontoolbar~BalloonToolbar} * feature. * + * ## Configuring toolbar items + * * const config = { * balloonToolbar: [ 'bold', 'italic', 'undo', 'redo' ] * }; @@ -318,5 +374,16 @@ function getBalloonPositions( isBackward ) { * * Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}. * + * ## Configuring items grouping + * + * You can prevent automatic items grouping by setting the `shouldNotGroupWhenFull` option: + * + * const config = { + * balloonToolbar: { + * items: [ 'bold', 'italic', 'undo', 'redo' ] + * }, + * shouldNotGroupWhenFull: true + * }; + * * @member {Array.|Object} module:core/editor/editorconfig~EditorConfig#balloonToolbar */ diff --git a/src/toolbar/toolbarview.js b/src/toolbar/toolbarview.js index 29496825..7cca8f9f 100644 --- a/src/toolbar/toolbarview.js +++ b/src/toolbar/toolbarview.js @@ -61,6 +61,19 @@ export default class ToolbarView extends View { */ this.set( 'ariaLabel', t( 'Editor toolbar' ) ); + /** + * The maximum width of the toolbar element. + * + * **Note**: When set to a specific value (e.g. `'200px'`), the value will affect the behavior of the + * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull} + * option by changing the number of {@link #items} that will be displayed in the toolbar at a time. + * + * @observable + * @default 'auto' + * @member {String} #maxWidth + */ + this.set( 'maxWidth', 'auto' ); + /** * A collection of toolbar items (buttons, dropdowns, etc.). * @@ -181,7 +194,10 @@ export default class ToolbarView extends View { bind.if( 'isCompact', 'ck-toolbar_compact' ) ], role: 'toolbar', - 'aria-label': bind.to( 'ariaLabel' ) + 'aria-label': bind.to( 'ariaLabel' ), + style: { + maxWidth: bind.to( 'maxWidth' ) + } }, children: this.children, @@ -570,6 +586,7 @@ class DynamicGrouping { this.viewElement = view.element; this._enableGroupingOnResize(); + this._enableGroupingOnMaxWidthChange( view ); } /** @@ -694,6 +711,18 @@ class DynamicGrouping { this._updateGrouping(); } + /** + * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that + * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead. + * + * @private + */ + _enableGroupingOnMaxWidthChange( view ) { + view.on( 'change:maxWidth', () => { + this._updateGrouping(); + } ); + } + /** * When called, it will remove the last item from {@link #ungroupedItems} and move it back * to the {@link #groupedItems} collection. @@ -798,6 +827,8 @@ class DynamicGrouping { * would normally wrap to the next line when there is not enough space to display them in a single row, for * instance, if the parent container of the toolbar is narrow. * + * Also see: {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth}. + * * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull */ diff --git a/tests/manual/panel/balloon/balloonpanelview.html b/tests/manual/panel/balloon/balloonpanelview.html index eb7bca89..28973dff 100644 --- a/tests/manual/panel/balloon/balloonpanelview.html +++ b/tests/manual/panel/balloon/balloonpanelview.html @@ -5,11 +5,18 @@ padding: 5em; } + h1 { + font-size: 22px; + text-align: center; + margin: 70px 0; + } + .target { width: 125px; height: 20px; - background: green; - margin: 0 0 0 100px; + background: lightblue; + transform: translateX(-50%); + margin-left: 50%; } .target + .target { @@ -18,5 +25,7 @@ .ck-balloon-panel { padding: .3em; + width: 250px; + text-align: center; } diff --git a/tests/manual/panel/balloon/balloonpanelview.js b/tests/manual/panel/balloon/balloonpanelview.js index 3aaa3a44..1bac7ee8 100644 --- a/tests/manual/panel/balloon/balloonpanelview.js +++ b/tests/manual/panel/balloon/balloonpanelview.js @@ -10,9 +10,22 @@ import BalloonPanelView from '../../../../src/panel/balloon/balloonpanelview'; const defaultPositions = BalloonPanelView.defaultPositions; const container = document.querySelector( '#container' ); +let currentHeading = ''; + for ( const i in defaultPositions ) { const target = document.createElement( 'div' ); + const heading = document.createElement( 'h1' ); + const headingText = parseHeadingText( i ); + + heading.textContent = headingText; target.classList.add( 'target' ); + + // Lazy heading + if ( currentHeading !== headingText ) { + container.appendChild( heading ); + currentHeading = headingText; + } + container.appendChild( target ); const balloon = new BalloonPanelView(); @@ -27,3 +40,21 @@ for ( const i in defaultPositions ) { ] } ); } + +function parseHeadingText( text ) { + const normalizedText = getNormalizeHeading( text ); + return getCapitalizedHeading( normalizedText ); +} + +// This helper function creates normalize heading text from a full name of the position, +// removing `ArrowXyz` part, like in the example: +// `southEastArrowNorthMiddleEast` -> `south East`. +function getNormalizeHeading( text ) { + return text + .replace( /(w*)arrow\w*/i, '$1' ) + .replace( /([a-z])([A-Z])/, '$1 $2' ); +} + +function getCapitalizedHeading( text ) { + return text.charAt( 0 ).toUpperCase() + text.slice( 1 ); +} diff --git a/tests/manual/panel/balloon/balloonpanelview.md b/tests/manual/panel/balloon/balloonpanelview.md index 980f663e..01337a79 100644 --- a/tests/manual/panel/balloon/balloonpanelview.md +++ b/tests/manual/panel/balloon/balloonpanelview.md @@ -1,5 +1,5 @@ ## `BalloonPanelView` and `defaultPositions` -1. A number of green rectangles should be displayed in the page. +1. A number of colorful rectangles should be displayed in the page. 2. Each rectangle should have a panel attached. 3. Make sure the description in each panel matches the location of the panel. diff --git a/tests/panel/balloon/balloonpanelview.js b/tests/panel/balloon/balloonpanelview.js index f3262bde..028ed6f3 100644 --- a/tests/panel/balloon/balloonpanelview.js +++ b/tests/panel/balloon/balloonpanelview.js @@ -181,9 +181,13 @@ describe( 'BalloonPanelView', () => { target, positions: [ BalloonPanelView.defaultPositions.southArrowNorth, + BalloonPanelView.defaultPositions.southArrowNorthMiddleWest, + BalloonPanelView.defaultPositions.southArrowNorthMiddleEast, BalloonPanelView.defaultPositions.southArrowNorthWest, BalloonPanelView.defaultPositions.southArrowNorthEast, BalloonPanelView.defaultPositions.northArrowSouth, + BalloonPanelView.defaultPositions.northArrowSouthMiddleWest, + BalloonPanelView.defaultPositions.northArrowSouthMiddleEast, BalloonPanelView.defaultPositions.northArrowSouthWest, BalloonPanelView.defaultPositions.northArrowSouthEast ], @@ -711,7 +715,7 @@ describe( 'BalloonPanelView', () => { } ); it( 'should have a proper length', () => { - expect( Object.keys( positions ) ).to.have.length( 18 ); + expect( Object.keys( positions ) ).to.have.length( 30 ); } ); // ------- North @@ -732,6 +736,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "northArrowSouthMiddleEast" position', () => { + expect( positions.northArrowSouthMiddleEast( targetRect, balloonRect ) ).to.deep.equal( { + top: 50 - arrowVOffset, + left: 112.5 + arrowHOffset, + name: 'arrow_sme' + } ); + } ); + it( 'should define the "northArrowSouthWest" position', () => { expect( positions.northArrowSouthWest( targetRect, balloonRect ) ).to.deep.equal( { top: 50 - arrowVOffset, @@ -740,6 +752,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "northArrowSouthMiddleWest" position', () => { + expect( positions.northArrowSouthMiddleWest( targetRect, balloonRect ) ).to.deep.equal( { + top: 50 - arrowVOffset, + left: 137.5 - arrowHOffset, + name: 'arrow_smw' + } ); + } ); + // ------- North west it( 'should define the "northWestArrowSouth" position', () => { @@ -758,6 +778,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "northWestArrowSouthMiddleWest" position', () => { + expect( positions.northWestArrowSouthMiddleWest( targetRect, balloonRect ) ).to.deep.equal( { + top: 50 - arrowVOffset, + left: 87.5 - arrowHOffset, + name: 'arrow_smw' + } ); + } ); + it( 'should define the "northWestArrowSouthEast" position', () => { expect( positions.northWestArrowSouthEast( targetRect, balloonRect ) ).to.deep.equal( { top: 50 - arrowVOffset, @@ -766,6 +794,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "northWestArrowSouthMiddleEast" position', () => { + expect( positions.northWestArrowSouthMiddleEast( targetRect, balloonRect ) ).to.deep.equal( { + top: 50 - arrowVOffset, + left: 62.5 + arrowHOffset, + name: 'arrow_sme' + } ); + } ); + // ------- North east it( 'should define the "northEastArrowSouth" position', () => { @@ -784,6 +820,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "northEastArrowSouthMiddleEast" position', () => { + expect( positions.northEastArrowSouthMiddleEast( targetRect, balloonRect ) ).to.deep.equal( { + top: 50 - arrowVOffset, + left: 162.5 + arrowHOffset, + name: 'arrow_sme' + } ); + } ); + it( 'should define the "northEastArrowSouthWest" position', () => { expect( positions.northEastArrowSouthWest( targetRect, balloonRect ) ).to.deep.equal( { top: 50 - arrowVOffset, @@ -792,6 +836,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "northEastArrowSouthMiddleWest" position', () => { + expect( positions.northEastArrowSouthMiddleWest( targetRect, balloonRect ) ).to.deep.equal( { + top: 50 - arrowVOffset, + left: 187.5 - arrowHOffset, + name: 'arrow_smw' + } ); + } ); + // ------- South it( 'should define the "southArrowNorth" position', () => { @@ -810,6 +862,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "southArrowNorthMiddleEast" position', () => { + expect( positions.southArrowNorthMiddleEast( targetRect, balloonRect ) ).to.deep.equal( { + top: 200 + arrowVOffset, + left: 112.5 + arrowHOffset, + name: 'arrow_nme' + } ); + } ); + it( 'should define the "southArrowNorthWest" position', () => { expect( positions.southArrowNorthWest( targetRect, balloonRect ) ).to.deep.equal( { top: 200 + arrowVOffset, @@ -818,6 +878,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "southArrowNorthMiddleWest" position', () => { + expect( positions.southArrowNorthMiddleWest( targetRect, balloonRect ) ).to.deep.equal( { + top: 200 + arrowVOffset, + left: 137.5 - arrowHOffset, + name: 'arrow_nmw' + } ); + } ); + // ------- South west it( 'should define the "southWestArrowNorth" position', () => { @@ -836,6 +904,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "southWestArrowNorthMiddleWest" position', () => { + expect( positions.southWestArrowNorthMiddleWest( targetRect, balloonRect ) ).to.deep.equal( { + top: 200 + arrowVOffset, + left: 87.5 - arrowHOffset, + name: 'arrow_nmw' + } ); + } ); + it( 'should define the "southWestArrowNorthEast" position', () => { expect( positions.southWestArrowNorthEast( targetRect, balloonRect ) ).to.deep.equal( { top: 200 + arrowVOffset, @@ -844,6 +920,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "southWestArrowNorthMiddleEast" position', () => { + expect( positions.southWestArrowNorthMiddleEast( targetRect, balloonRect ) ).to.deep.equal( { + top: 200 + arrowVOffset, + left: 62.5 + arrowHOffset, + name: 'arrow_nme' + } ); + } ); + // ------- South east it( 'should define the "southEastArrowNorth" position', () => { @@ -862,6 +946,14 @@ describe( 'BalloonPanelView', () => { } ); } ); + it( 'should define the "southEastArrowNorthMiddleEast" position', () => { + expect( positions.southEastArrowNorthMiddleEast( targetRect, balloonRect ) ).to.deep.equal( { + top: 200 + arrowVOffset, + left: 162.5 + arrowHOffset, + name: 'arrow_nme' + } ); + } ); + it( 'should define the "southEastArrowNorthWest" position', () => { expect( positions.southEastArrowNorthWest( targetRect, balloonRect ) ).to.deep.equal( { top: 200 + arrowVOffset, @@ -869,6 +961,14 @@ describe( 'BalloonPanelView', () => { name: 'arrow_nw' } ); } ); + + it( 'should define the "southEastArrowNorthMiddleWest" position', () => { + expect( positions.southEastArrowNorthMiddleWest( targetRect, balloonRect ) ).to.deep.equal( { + top: 200 + arrowVOffset, + left: 187.5 - arrowHOffset, + name: 'arrow_nmw' + } ); + } ); } ); } ); diff --git a/tests/toolbar/balloon/balloontoolbar.js b/tests/toolbar/balloon/balloontoolbar.js index 57277fc0..020b1db3 100644 --- a/tests/toolbar/balloon/balloontoolbar.js +++ b/tests/toolbar/balloon/balloontoolbar.js @@ -14,22 +14,45 @@ import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Italic from '@ckeditor/ckeditor5-basic-styles/src/italic'; import Underline from '@ckeditor/ckeditor5-basic-styles/src/underline'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import global from '@ckeditor/ckeditor5-utils/src/dom/global'; +import ResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/resizeobserver'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import { stringify as viewStringify } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; +import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'; +import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit'; + +const toPx = toUnit( 'px' ); + import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; /* global document, window, Event */ describe( 'BalloonToolbar', () => { let editor, model, selection, editingView, balloonToolbar, balloon, editorElement; + let resizeCallback; + testUtils.createSinonSandbox(); beforeEach( () => { editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); + // Make sure other tests of the editor do not affect tests that follow. + // Without it, if an instance of ResizeObserver already exists somewhere undestroyed + // in DOM, the following DOM mock will have no effect. + ResizeObserver._observerInstance = null; + + testUtils.sinon.stub( global.window, 'ResizeObserver' ).callsFake( callback => { + resizeCallback = callback; + + return { + observe: sinon.spy(), + unobserve: sinon.spy() + }; + } ); + return ClassicTestEditor .create( editorElement, { plugins: [ Paragraph, Bold, Italic, BalloonToolbar ], @@ -102,6 +125,27 @@ describe( 'BalloonToolbar', () => { } ); } ); + it( 'should not group items when the config.shouldNotGroupWhenFull option is enabled', () => { + const editorElement = document.createElement( 'div' ); + document.body.appendChild( editorElement ); + + return ClassicTestEditor.create( editorElement, { + plugins: [ Paragraph, Bold, Italic, Underline, BalloonToolbar ], + balloonToolbar: { + items: [ 'bold', 'italic', 'underline' ], + shouldNotGroupWhenFull: true + } + } ).then( editor => { + const balloonToolbar = editor.plugins.get( BalloonToolbar ); + + expect( balloonToolbar.toolbarView.options.shouldGroupWhenFull ).to.be.false; + + return editor.destroy(); + } ).then( () => { + editorElement.remove(); + } ); + } ); + it( 'should fire internal `_selectionChangeDebounced` event 200 ms after last selection change', () => { const clock = testUtils.sinon.useFakeTimers(); const spy = testUtils.sinon.spy(); @@ -208,9 +252,13 @@ describe( 'BalloonToolbar', () => { defaultPositions.southEastArrowNorth, defaultPositions.southEastArrowNorthEast, defaultPositions.southEastArrowNorthWest, + defaultPositions.southEastArrowNorthMiddleEast, + defaultPositions.southEastArrowNorthMiddleWest, defaultPositions.northEastArrowSouth, defaultPositions.northEastArrowSouthEast, - defaultPositions.northEastArrowSouthWest + defaultPositions.northEastArrowSouthWest, + defaultPositions.northEastArrowSouthMiddleEast, + defaultPositions.northEastArrowSouthMiddleWest ] } } ); @@ -267,9 +315,13 @@ describe( 'BalloonToolbar', () => { defaultPositions.northWestArrowSouth, defaultPositions.northWestArrowSouthWest, defaultPositions.northWestArrowSouthEast, + defaultPositions.northWestArrowSouthMiddleEast, + defaultPositions.northWestArrowSouthMiddleWest, defaultPositions.southWestArrowNorth, defaultPositions.southWestArrowNorthWest, - defaultPositions.southWestArrowNorthEast + defaultPositions.southWestArrowNorthEast, + defaultPositions.southWestArrowNorthMiddleWest, + defaultPositions.southWestArrowNorthMiddleEast ] } } ); @@ -344,6 +396,25 @@ describe( 'BalloonToolbar', () => { balloonToolbar.show(); sinon.assert.calledOnce( balloonAddSpy ); } ); + + it( 'should set the toolbar max-width to 90% of the editable width', () => { + const viewElement = editor.ui.view.editable.element; + + setData( model, 'b[ar]' ); + + expect( global.document.body.contains( viewElement ) ).to.be.true; + viewElement.style.width = '400px'; + + resizeCallback( [ { + target: viewElement, + contentRect: new Rect( viewElement ) + } ] ); + + // The expected width should be 90% of the editor's editable element's width. + const expectedWidth = toPx( new Rect( viewElement ).width * 0.9 ); + + expect( balloonToolbar.toolbarView.maxWidth ).to.be.equal( expectedWidth ); + } ); } ); describe( 'hide()', () => { @@ -403,6 +474,18 @@ describe( 'BalloonToolbar', () => { clock.tick( 200 ); sinon.assert.notCalled( spy ); } ); + + it( 'should destroy #resizeObserver if is available', () => { + const editable = editor.ui.getEditableElement(); + const resizeObserver = new ResizeObserver( editable, () => {} ); + const destroySpy = sinon.spy( resizeObserver, 'destroy' ); + + balloonToolbar._resizeObserver = resizeObserver; + + balloonToolbar.destroy(); + + sinon.assert.calledOnce( destroySpy ); + } ); } ); describe( 'show and hide triggers', () => { diff --git a/tests/toolbar/toolbarview.js b/tests/toolbar/toolbarview.js index f3ad7804..e529289e 100644 --- a/tests/toolbar/toolbarview.js +++ b/tests/toolbar/toolbarview.js @@ -180,6 +180,22 @@ describe( 'ToolbarView', () => { expect( view.element.classList.contains( 'ck-toolbar_compact' ) ).to.be.true; } ); } ); + + describe( 'style', () => { + it( 'reacts on view#maxWidth', () => { + view.maxWidth = '100px'; + expect( view.element.style.maxWidth ).to.equal( '100px' ); + + view.maxWidth = undefined; + expect( view.element.style.maxWidth ).to.equal( '' ); + + view.maxWidth = null; + expect( view.element.style.maxWidth ).to.equal( '' ); + + view.maxWidth = '200px'; + expect( view.element.style.maxWidth ).to.equal( '200px' ); + } ); + } ); } ); describe( 'render()', () => {