toolbarview.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. /**
  2. * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
  4. */
  5. /**
  6. * @module ui/toolbar/toolbarview
  7. */
  8. /* globals console */
  9. import View from '../view';
  10. import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
  11. import FocusCycler from '../focuscycler';
  12. import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
  13. import ToolbarSeparatorView from './toolbarseparatorview';
  14. import getResizeObserver from '@ckeditor/ckeditor5-utils/src/dom/getresizeobserver';
  15. import preventDefault from '../bindings/preventdefault.js';
  16. import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
  17. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  18. import { createDropdown, addToolbarToDropdown } from '../dropdown/utils';
  19. import { attachLinkToDocumentation } from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
  20. import verticalDotsIcon from '@ckeditor/ckeditor5-core/theme/icons/three-vertical-dots.svg';
  21. import '../../theme/components/toolbar/toolbar.css';
  22. /**
  23. * The toolbar view class.
  24. *
  25. * @extends module:ui/view~View
  26. * @implements module:ui/dropdown/dropdownpanelfocusable~DropdownPanelFocusable
  27. */
  28. export default class ToolbarView extends View {
  29. /**
  30. * Creates an instance of the {@link module:ui/toolbar/toolbarview~ToolbarView} class.
  31. *
  32. * Also see {@link #render}.
  33. *
  34. * @param {module:utils/locale~Locale} locale The localization services instance.
  35. * @param {module:ui/toolbar/toolbarview~ToolbarOptions} [options] Configuration options of the toolbar.
  36. */
  37. constructor( locale, options ) {
  38. super( locale );
  39. const bind = this.bindTemplate;
  40. const t = this.t;
  41. /**
  42. * A reference to the options object passed to the constructor.
  43. *
  44. * @readonly
  45. * @member {module:ui/toolbar/toolbarview~ToolbarOptions}
  46. */
  47. this.options = options || {};
  48. /**
  49. * Label used by assistive technologies to describe this toolbar element.
  50. *
  51. * @default 'Editor toolbar'
  52. * @member {String} #ariaLabel
  53. */
  54. this.set( 'ariaLabel', t( 'Editor toolbar' ) );
  55. /**
  56. * Collection of the toolbar items (buttons, drop–downs, etc.).
  57. *
  58. * @readonly
  59. * @member {module:ui/viewcollection~ViewCollection}
  60. */
  61. this.items = this.createCollection();
  62. /**
  63. * Tracks information about DOM focus in the toolbar.
  64. *
  65. * @readonly
  66. * @member {module:utils/focustracker~FocusTracker}
  67. */
  68. this.focusTracker = new FocusTracker();
  69. /**
  70. * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}
  71. * to handle keyboard navigation in the toolbar.
  72. *
  73. * @readonly
  74. * @member {module:utils/keystrokehandler~KeystrokeHandler}
  75. */
  76. this.keystrokes = new KeystrokeHandler();
  77. /**
  78. * An additional CSS class added to the {@link #element}.
  79. *
  80. * @observable
  81. * @member {String} #class
  82. */
  83. this.set( 'class' );
  84. /**
  85. * A (child) view containing {@link #items toolbar items}.
  86. *
  87. * @readonly
  88. * @member {module:ui/toolbar/toolbarview~ItemsView}
  89. */
  90. this.itemsView = new ItemsView( locale );
  91. /**
  92. * A top–level collection aggregating building blocks of the toolbar.
  93. *
  94. * ┌───────────────── ToolbarView ─────────────────┐
  95. * | ┌──────────────── #children ────────────────┐ |
  96. * | | ┌──────────── #itemsView ───────────┐ | |
  97. * | | | [ item1 ] [ item2 ] ... [ itemN ] | | |
  98. * | | └──────────────────────────────────-┘ | |
  99. * | └───────────────────────────────────────────┘ |
  100. * └───────────────────────────────────────────────┘
  101. *
  102. * By default, it contains the {@link #itemsView} but it can be extended with additional
  103. * UI elements when necessary.
  104. *
  105. * @readonly
  106. * @member {module:ui/viewcollection~ViewCollection}
  107. */
  108. this.children = this.createCollection();
  109. this.children.add( this.itemsView );
  110. /**
  111. * A collection of {@link #items} that take part in the focus cycling
  112. * (i.e. navigation using the keyboard). Usually, it contains a subset of {@link #items} with
  113. * some optional UI elements that also belong to the toolbar and should be focusable
  114. * by the user.
  115. *
  116. * @readonly
  117. * @member {module:ui/viewcollection~ViewCollection}
  118. */
  119. this.focusables = this.createCollection();
  120. /**
  121. * Controls the orientation of toolbar items. Only available when
  122. * {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull dynamic items grouping}
  123. * is **disabled**.
  124. *
  125. * @observable
  126. * @member {Boolean} #isVertical
  127. */
  128. /**
  129. * Helps cycling over {@link #focusables focusable items} in the toolbar.
  130. *
  131. * @readonly
  132. * @protected
  133. * @member {module:ui/focuscycler~FocusCycler}
  134. */
  135. this._focusCycler = new FocusCycler( {
  136. focusables: this.focusables,
  137. focusTracker: this.focusTracker,
  138. keystrokeHandler: this.keystrokes,
  139. actions: {
  140. // Navigate toolbar items backwards using the arrow[left,up] keys.
  141. focusPrevious: [ 'arrowleft', 'arrowup' ],
  142. // Navigate toolbar items forwards using the arrow[right,down] keys.
  143. focusNext: [ 'arrowright', 'arrowdown' ]
  144. }
  145. } );
  146. this.setTemplate( {
  147. tag: 'div',
  148. attributes: {
  149. class: [
  150. 'ck',
  151. 'ck-toolbar',
  152. bind.to( 'class' )
  153. ],
  154. role: 'toolbar',
  155. 'aria-label': bind.to( 'ariaLabel' )
  156. },
  157. children: this.children,
  158. on: {
  159. // https://github.com/ckeditor/ckeditor5-ui/issues/206
  160. mousedown: preventDefault( this )
  161. }
  162. } );
  163. /**
  164. * An instance of the active toolbar behavior that shapes its look and functionality.
  165. *
  166. * See {@link module:ui/toolbar/toolbarview~ToolbarBehavior} to learn more.
  167. *
  168. * @protected
  169. * @readonly
  170. * @member {module:ui/toolbar/toolbarview~ToolbarBehavior}
  171. */
  172. this._behavior = this.options.shouldGroupWhenFull ? new DynamicGrouping( this ) : new StaticLayout( this );
  173. }
  174. /**
  175. * @inheritDoc
  176. */
  177. render() {
  178. super.render();
  179. // Children added before rendering should be known to the #focusTracker.
  180. for ( const item of this.items ) {
  181. this.focusTracker.add( item.element );
  182. }
  183. this.items.on( 'add', ( evt, item ) => {
  184. this.focusTracker.add( item.element );
  185. } );
  186. this.items.on( 'remove', ( evt, item ) => {
  187. this.focusTracker.remove( item.element );
  188. } );
  189. // Start listening for the keystrokes coming from #element.
  190. this.keystrokes.listenTo( this.element );
  191. this._behavior.render( this );
  192. }
  193. /**
  194. * @inheritDoc
  195. */
  196. destroy() {
  197. this._behavior.destroy();
  198. return super.destroy();
  199. }
  200. /**
  201. * Focuses the first focusable in {@link #focusables}.
  202. */
  203. focus() {
  204. this._focusCycler.focusFirst();
  205. }
  206. /**
  207. * Focuses the last focusable in {@link #focusables}.
  208. */
  209. focusLast() {
  210. this._focusCycler.focusLast();
  211. }
  212. /**
  213. * A utility which expands a plain toolbar configuration into
  214. * {@link module:ui/toolbar/toolbarview~ToolbarView#items} using a given component factory.
  215. *
  216. * @param {Array.<String>} config The toolbar items config.
  217. * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
  218. */
  219. fillFromConfig( config, factory ) {
  220. // The toolbar is filled in in the reverse order for the toolbar grouping to work properly.
  221. // If we filled it in in the natural order, items that overflow would be grouped
  222. // in a revere order.
  223. config.map( name => {
  224. if ( name == '|' ) {
  225. this.items.add( new ToolbarSeparatorView() );
  226. } else if ( factory.has( name ) ) {
  227. this.items.add( factory.create( name ) );
  228. } else {
  229. /**
  230. * There was a problem processing the configuration of the toolbar. The item with the given
  231. * name does not exist so it was omitted when rendering the toolbar.
  232. *
  233. * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
  234. * to provide a toolbar item has not been loaded or there is a typo in the configuration.
  235. *
  236. * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
  237. * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
  238. *
  239. * You can use the following snippet to retrieve all available toolbar items:
  240. *
  241. * Array.from( editor.ui.componentFactory.names() );
  242. *
  243. * @error toolbarview-item-unavailable
  244. * @param {String} name The name of the component.
  245. */
  246. console.warn( attachLinkToDocumentation(
  247. 'toolbarview-item-unavailable: The requested toolbar item is unavailable.' ), { name } );
  248. }
  249. } );
  250. }
  251. }
  252. /**
  253. * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
  254. * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  255. *
  256. * @private
  257. * @extends module:ui/view~View
  258. */
  259. class ItemsView extends View {
  260. /**
  261. * @inheritDoc
  262. */
  263. constructor( locale ) {
  264. super( locale );
  265. /**
  266. * Collection of the items (buttons, drop–downs, etc.).
  267. *
  268. * @readonly
  269. * @member {module:ui/viewcollection~ViewCollection}
  270. */
  271. this.children = this.createCollection();
  272. this.setTemplate( {
  273. tag: 'div',
  274. attributes: {
  275. class: [
  276. 'ck',
  277. 'ck-toolbar__items'
  278. ],
  279. },
  280. children: this.children
  281. } );
  282. }
  283. }
  284. /**
  285. * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
  286. * It also allows toolbar with the vertical layout.
  287. *
  288. * @private
  289. * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
  290. */
  291. class StaticLayout {
  292. /**
  293. * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
  294. * behavior.
  295. *
  296. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar this behavior
  297. * is added to.
  298. */
  299. constructor( view ) {
  300. const bind = view.bindTemplate;
  301. // Static toolbar can be vertical when needed.
  302. view.set( 'isVertical', false );
  303. // 1:1 pass–through binding, all ToolbarView#items are visible.
  304. view.itemsView.children.bindTo( view.items ).using( item => item );
  305. // 1:1 pass–through binding, all ToolbarView#items are focusable.
  306. view.focusables.bindTo( view.items ).using( item => item );
  307. view.extendTemplate( {
  308. attributes: {
  309. class: [
  310. // When vertical, the toolbar has an additional CSS class.
  311. bind.if( 'isVertical', 'ck-toolbar_vertical' )
  312. ]
  313. }
  314. } );
  315. }
  316. /**
  317. * @inheritDoc
  318. */
  319. render() {}
  320. /**
  321. * @inheritDoc
  322. */
  323. destroy() {}
  324. }
  325. /**
  326. * A toolbar behavior that makes its items respond to the changes in the geometry.
  327. *
  328. * In a nutshell, it groups {@link module:ui/toolbar/toolbarview~ToolbarView#items}
  329. * that do not fit into visually into a single row of the toolbar (due to limited space).
  330. * Items that do not fit are aggregated in a dropdown displayed at the end of the toolbar.
  331. *
  332. * ┌──────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
  333. * | ┌─────────────────────────────────────── #children ─────────────────────────────────────────┐ |
  334. * | | ┌─────── #itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐ | |
  335. * | | | #ungroupedItems | | ToolbarSeparatorView | | #groupedItems | | |
  336. * | | └──────────────────────────-┘ └──────────────────────┘ └────────────────────────────┘ | |
  337. * | | \---------- only when toolbar items overflow --------/ | |
  338. * | └───────────────────────────────────────────────────────────────────────────────────────────┘ |
  339. * └───────────────────────────────────────────────────────────────────────────────────────────────┘
  340. *
  341. * @private
  342. * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
  343. */
  344. class DynamicGrouping {
  345. /**
  346. * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
  347. * behavior.
  348. *
  349. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar this behavior
  350. * is added to.
  351. */
  352. constructor( view ) {
  353. /**
  354. * Collection of toolbar children.
  355. *
  356. * @readonly
  357. * @member {module:ui/viewcollection~ViewCollection}
  358. */
  359. this.viewChildren = view.children;
  360. /**
  361. * Collection of toolbar focusable elements.
  362. *
  363. * @readonly
  364. * @member {module:ui/viewcollection~ViewCollection}
  365. */
  366. this.viewFocusables = view.focusables;
  367. /**
  368. * Collection of toolbar focusable elements.
  369. *
  370. * @readonly
  371. * @member {module:ui/toolbar/toolbarview~ItemsView}
  372. */
  373. this.viewItemsView = view.itemsView;
  374. /**
  375. * Focus tracker of the toolbar.
  376. *
  377. * @readonly
  378. * @member {module:utils/focustracker~FocusTracker}
  379. */
  380. this.viewFocusTracker = view.focusTracker;
  381. /**
  382. * Locale of the toolbar.
  383. *
  384. * @readonly
  385. * @member {module:utils/locale~Locale}
  386. */
  387. this.viewLocale = view.locale;
  388. /**
  389. * Element of the toolbar.
  390. *
  391. * @readonly
  392. * @member {HTMLElement} #viewElement
  393. */
  394. /**
  395. * A subset of of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  396. * Aggregates items that fit into a single row of the toolbar and were not {@link #groupedItems grouped}
  397. * into a {@link #groupedItemsDropdown dropdown}. Items of this collection are displayed in the
  398. * {@link module:ui/toolbar/toolbarview~ToolbarView#itemsView}.
  399. *
  400. * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped, it
  401. * matches the {@link module:ui/toolbar/toolbarview~ToolbarView#items} collection in size and order.
  402. *
  403. * @readonly
  404. * @member {module:ui/viewcollection~ViewCollection}
  405. */
  406. this.ungroupedItems = view.createCollection();
  407. /**
  408. * A subset of of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  409. * A collection of the toolbar items that do not fit into a single row of the toolbar.
  410. * Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
  411. *
  412. * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped,
  413. * this collection is empty.
  414. *
  415. * @readonly
  416. * @member {module:ui/viewcollection~ViewCollection}
  417. */
  418. this.groupedItems = view.createCollection();
  419. /**
  420. * The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
  421. * row of the toolbar. It is displayed on demand as the last of
  422. * {@link module:ui/toolbar/toolbarview~ToolbarView#children toolbar children} and offers another
  423. * (nested) toolbar which displays items that would normally overflow.
  424. *
  425. * @readonly
  426. * @member {module:ui/dropdown/dropdownview~DropdownView}
  427. */
  428. this.groupedItemsDropdown = this._createGroupedItemsDropdown();
  429. /**
  430. * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
  431. * and manage items that do not fit into a single row.
  432. *
  433. * **Note:** Created in {@link #_enableGroupingOnResize}.
  434. *
  435. * @readonly
  436. * @member {module:utils/dom/getresizeobserver~ResizeObserver}
  437. */
  438. this.resizeObserver = null;
  439. /**
  440. * A cached value of the horizontal padding style used by {@link #_updateGrouping}
  441. * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
  442. * a single toolbar line. This value can be reused between updates because it is unlikely that
  443. * the padding will change and re–using `Window.getComputedStyle()` is expensive.
  444. *
  445. * @readonly
  446. * @member {Number}
  447. */
  448. this.cachedPadding = null;
  449. // Only those items that were not grouped are visible to the user.
  450. view.itemsView.children.bindTo( this.ungroupedItems ).using( item => item );
  451. // Make sure all #items visible in the main space of the toolbar are "focuscycleable".
  452. this.ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
  453. this.ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
  454. // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
  455. view.children.on( 'add', this._updateFocusCycleableItems.bind( this ) );
  456. view.children.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
  457. // ToolbarView#items is dynamic. When an item is added, it should be automatically
  458. // represented in either grouped or ungrouped items at the right index.
  459. // In other words #items == concat( #ungroupedItems, #groupedItems )
  460. // (in length and order).
  461. view.items.on( 'add', ( evt, item, index ) => {
  462. if ( index > this.ungroupedItems.length ) {
  463. this.groupedItems.add( item, index - this.ungroupedItems.length );
  464. } else {
  465. this.ungroupedItems.add( item, index );
  466. }
  467. // When a new ungrouped item joins in and lands in #ungroupedItems, there's a chance it causes
  468. // the toolbar to overflow.
  469. this._updateGrouping();
  470. } );
  471. // When an item is removed from ToolbarView#items, it should be automatically
  472. // removed from either grouped or ungrouped items.
  473. view.items.on( 'remove', ( evt, item, index ) => {
  474. if ( index > this.ungroupedItems.length ) {
  475. this.groupedItems.remove( item );
  476. } else {
  477. this.ungroupedItems.remove( item );
  478. }
  479. // Whether removed from grouped or ungrouped items, there is a chance
  480. // some new space is available and we could do some ungrouping.
  481. this._updateGrouping();
  482. } );
  483. view.extendTemplate( {
  484. attributes: {
  485. class: [
  486. // To group items dynamically, the toolbar needs a dedicated CSS class.
  487. 'ck-toolbar_grouping'
  488. ]
  489. }
  490. } );
  491. }
  492. /**
  493. * Enables dynamic items grouping based on the dimensions of the toolbar.
  494. *
  495. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar this behavior
  496. * is added to.
  497. */
  498. render( view ) {
  499. this.viewElement = view.element;
  500. this._enableGroupingOnResize();
  501. }
  502. /**
  503. * Cleans up the internals used by this behavior.
  504. */
  505. destroy() {
  506. // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
  507. // so let's make sure it's actually destroyed along with the toolbar.
  508. this.groupedItemsDropdown.destroy();
  509. this.resizeObserver.disconnect();
  510. }
  511. /**
  512. * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
  513. * and it will move them to the {@link #groupedItems} when it happens.
  514. *
  515. * At the same time, it will also check if there is enough space in the toolbar for the first of the
  516. * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
  517. * without the toolbar wrapping.
  518. *
  519. * @protected
  520. */
  521. _updateGrouping() {
  522. // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
  523. // for instance before #render(), or after render but without a parent or a parent detached
  524. // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
  525. // nothing else.
  526. if ( !this.viewElement.ownerDocument.body.contains( this.viewElement ) ) {
  527. return;
  528. }
  529. let wereItemsGrouped;
  530. // Group #items as long as some wrap to the next row. This will happen, for instance,
  531. // when the toolbar is getting narrow and there is not enough space to display all items in
  532. // a single row.
  533. while ( this._areItemsOverflowing ) {
  534. this._groupLastItem();
  535. wereItemsGrouped = true;
  536. }
  537. // If none were grouped now but there were some items already grouped before,
  538. // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
  539. // for instance, the toolbar is stretching and there's more space in it than before.
  540. if ( !wereItemsGrouped && this.groupedItems && this.groupedItems.length ) {
  541. // Ungroup items as long as none are overflowing or there are none to ungroup left.
  542. while ( this.groupedItems.length && !this._areItemsOverflowing ) {
  543. this._ungroupFirstItem();
  544. }
  545. // If the ungrouping ended up with some item wrapping to the next row,
  546. // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
  547. // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
  548. // clean–up is vital for the algorithm.
  549. if ( this._areItemsOverflowing ) {
  550. this._groupLastItem();
  551. }
  552. }
  553. }
  554. /**
  555. * Enables the functionality that prevents {@link #ungroupedItems} from overflowing
  556. * (wrapping to the next row) when there is little space available. Instead, the toolbar items are moved to the
  557. * {@link #groupedItems} collection and displayed in a dropdown at the end of the space, which has its own nested toolbar.
  558. *
  559. * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
  560. * them in the dropdown if necessary. It will also observe the browser window for size changes in
  561. * the future and respond to them by grouping more items or reverting already grouped back, depending
  562. * on the visual space available.
  563. *
  564. * @private
  565. */
  566. _enableGroupingOnResize() {
  567. let previousWidth;
  568. // TODO: Consider debounce.
  569. this.resizeObserver = getResizeObserver( ( [ entry ] ) => {
  570. if ( !previousWidth || previousWidth !== entry.contentRect.width ) {
  571. this._updateGrouping();
  572. previousWidth = entry.contentRect.width;
  573. }
  574. } );
  575. this.resizeObserver.observe( this.viewElement );
  576. this._updateGrouping();
  577. }
  578. /**
  579. * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
  580. * for instance if the toolbar is narrower than its members. `false` otherwise.
  581. *
  582. * @private
  583. * @type {Boolean}
  584. */
  585. get _areItemsOverflowing() {
  586. // An empty toolbar cannot overflow.
  587. if ( !this.ungroupedItems.length ) {
  588. return false;
  589. }
  590. const element = this.viewElement;
  591. const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
  592. const lastChildRect = new Rect( element.lastChild );
  593. const toolbarRect = new Rect( element );
  594. if ( !this.cachedPadding ) {
  595. const computedStyle = global.window.getComputedStyle( element );
  596. const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
  597. // parseInt() is essential because of quirky floating point numbers logic and DOM.
  598. // If the padding turned out too big because of that, the grouped items dropdown would
  599. // always look (from the Rect perspective) like it overflows (while it's not).
  600. this.cachedPadding = Number.parseInt( computedStyle[ paddingProperty ] );
  601. }
  602. if ( uiLanguageDirection === 'ltr' ) {
  603. return lastChildRect.right > toolbarRect.right - this.cachedPadding;
  604. } else {
  605. return lastChildRect.left < toolbarRect.left + this.cachedPadding;
  606. }
  607. }
  608. /**
  609. * The opposite of {@link #_ungroupFirstItem}.
  610. *
  611. * When called it will remove the last item from {@link #ungroupedItems} and move it to the
  612. * {@link #groupedItems} collection.
  613. *
  614. * @private
  615. */
  616. _groupLastItem() {
  617. if ( !this.groupedItems.length ) {
  618. this.viewChildren.add( new ToolbarSeparatorView() );
  619. this.viewChildren.add( this.groupedItemsDropdown );
  620. this.viewFocusTracker.add( this.groupedItemsDropdown.element );
  621. }
  622. this.groupedItems.add( this.ungroupedItems.remove( this.ungroupedItems.last ), 0 );
  623. }
  624. /**
  625. * The opposite of {@link #_groupLastItem}.
  626. *
  627. * Moves the very first item from the toolbar belonging to {@link #groupedItems} back
  628. * to the {@link #ungroupedItems} collection.
  629. *
  630. * @private
  631. */
  632. _ungroupFirstItem() {
  633. this.ungroupedItems.add( this.groupedItems.remove( this.groupedItems.first ) );
  634. if ( !this.groupedItems.length ) {
  635. this.viewChildren.remove( this.groupedItemsDropdown );
  636. this.viewChildren.remove( this.viewChildren.last );
  637. this.viewFocusTracker.remove( this.groupedItemsDropdown.element );
  638. }
  639. }
  640. /**
  641. * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
  642. * collection when there is not enough space in the toolbar to display all items in a single row.
  643. *
  644. * @private
  645. * @returns {module:ui/dropdown/dropdownview~DropdownView}
  646. */
  647. _createGroupedItemsDropdown() {
  648. const locale = this.viewLocale;
  649. const t = locale.t;
  650. const dropdown = createDropdown( locale );
  651. dropdown.class = 'ck-toolbar__grouped-dropdown';
  652. addToolbarToDropdown( dropdown, [] );
  653. dropdown.buttonView.set( {
  654. label: t( 'Show more items' ),
  655. tooltip: true,
  656. icon: verticalDotsIcon
  657. } );
  658. // 1:1 pass–through binding.
  659. dropdown.toolbarView.items.bindTo( this.groupedItems ).using( item => item );
  660. return dropdown;
  661. }
  662. /**
  663. * A method that updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
  664. * collection so it represents the up–to–date state of the UI from the perspective of the user.
  665. *
  666. * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
  667. * it must be subject to focus cycling in the toolbar.
  668. *
  669. * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
  670. * to learn more about the purpose of this method.
  671. *
  672. * @private
  673. */
  674. _updateFocusCycleableItems() {
  675. this.viewFocusables.clear();
  676. this.ungroupedItems.map( item => {
  677. this.viewFocusables.add( item );
  678. } );
  679. if ( this.groupedItems.length ) {
  680. this.viewFocusables.add( this.groupedItemsDropdown );
  681. }
  682. }
  683. }
  684. /**
  685. * Options passed to the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
  686. *
  687. * @interface module:ui/toolbar/toolbarview~ToolbarOptions
  688. */
  689. /**
  690. * When set `true`, the toolbar will automatically group {@link module:ui/toolbar/toolbarview~ToolbarView#items} that
  691. * would normally wrap to the next line when there is not enough space to display them in a single row, for
  692. * instance, if the parent container of the toolbar is narrow.
  693. *
  694. * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull
  695. */
  696. /**
  697. * A class interface defining a behavior of the {@link module:ui/toolbar/toolbarview~ToolbarView}.
  698. *
  699. * Toolbar behaviors extend its look and functionality and have an impact on the
  700. * {@link module:ui/toolbar/toolbarview~ToolbarView#element} template or
  701. * {@link module:ui/toolbar/toolbarview~ToolbarView#render rendering}. They can be enabled
  702. * conditionally, e.g. depending on the configuration of the toolbar.
  703. *
  704. * @private
  705. * @interface module:ui/toolbar/toolbarview~ToolbarBehavior
  706. */
  707. /**
  708. * Creates a new toolbar behavior instance.
  709. *
  710. * The instance is created in the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
  711. * This is the right place to extend the {@link module:ui/toolbar/toolbarview~ToolbarView#template} of
  712. * the toolbar, define extra toolbar properties, etc..
  713. *
  714. * @method #constructor
  715. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar this behavior is added to.
  716. */
  717. /**
  718. * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#render rendered}.
  719. * E.g. it can be used to customize the behavior of the toolbar when its {@link module:ui/toolbar/toolbarview~ToolbarView#element}
  720. * is available.
  721. *
  722. * @readonly
  723. * @member {Function} #render
  724. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar being rendered.
  725. */
  726. /**
  727. * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#destroy destroyed}.
  728. * It allows cleaning up after the toolbar behavior, for instance, this is the right place to detach
  729. * event listeners, free up references, etc..
  730. *
  731. * @readonly
  732. * @member {Function} #destroy
  733. */