toolbarview.js 32 KB

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