8
0

toolbarview.js 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  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( 'toolbarview-item-unavailable' ), { name } );
  268. }
  269. } ).filter( item => item !== undefined ) );
  270. }
  271. /**
  272. * Fired when some toolbar {@link #items} were grouped or ungrouped as a result of some change
  273. * in the toolbar geometry.
  274. *
  275. * **Note**: This event is always fired **once** regardless of the number of items that were be
  276. * grouped or ungrouped at a time.
  277. *
  278. * **Note**: This event is fired only if the items grouping functionality was enabled in
  279. * the first place (see {@link module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull}).
  280. *
  281. * @event groupedItemsUpdate
  282. */
  283. }
  284. /**
  285. * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
  286. * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  287. *
  288. * @private
  289. * @extends module:ui/view~View
  290. */
  291. class ItemsView extends View {
  292. /**
  293. * @inheritDoc
  294. */
  295. constructor( locale ) {
  296. super( locale );
  297. /**
  298. * A collection of items (buttons, dropdowns, etc.).
  299. *
  300. * @readonly
  301. * @member {module:ui/viewcollection~ViewCollection}
  302. */
  303. this.children = this.createCollection();
  304. this.setTemplate( {
  305. tag: 'div',
  306. attributes: {
  307. class: [
  308. 'ck',
  309. 'ck-toolbar__items'
  310. ]
  311. },
  312. children: this.children
  313. } );
  314. }
  315. }
  316. /**
  317. * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
  318. * At the same time, it also makes it possible to display a toolbar with a vertical layout
  319. * using the {@link module:ui/toolbar/toolbarview~ToolbarView#isVertical} property.
  320. *
  321. * @private
  322. * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
  323. */
  324. class StaticLayout {
  325. /**
  326. * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
  327. * behavior.
  328. *
  329. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
  330. * is added to.
  331. */
  332. constructor( view ) {
  333. const bind = view.bindTemplate;
  334. // Static toolbar can be vertical when needed.
  335. view.set( 'isVertical', false );
  336. // 1:1 pass–through binding, all ToolbarView#items are visible.
  337. view.itemsView.children.bindTo( view.items ).using( item => item );
  338. // 1:1 pass–through binding, all ToolbarView#items are focusable.
  339. view.focusables.bindTo( view.items ).using( item => item );
  340. view.extendTemplate( {
  341. attributes: {
  342. class: [
  343. // When vertical, the toolbar has an additional CSS class.
  344. bind.if( 'isVertical', 'ck-toolbar_vertical' )
  345. ]
  346. }
  347. } );
  348. }
  349. /**
  350. * @inheritDoc
  351. */
  352. render() {}
  353. /**
  354. * @inheritDoc
  355. */
  356. destroy() {}
  357. }
  358. /**
  359. * A toolbar behavior that makes the items respond to changes in the geometry.
  360. *
  361. * In a nutshell, it groups {@link module:ui/toolbar/toolbarview~ToolbarView#items}
  362. * that do not fit visually into a single row of the toolbar (due to limited space).
  363. * Items that do not fit are aggregated in a dropdown displayed at the end of the toolbar.
  364. *
  365. * ┌──────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
  366. * | ┌─────────────────────────────────────── #children ─────────────────────────────────────────┐ |
  367. * | | ┌─────── #itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐ | |
  368. * | | | #ungroupedItems | | ToolbarSeparatorView | | #groupedItems | | |
  369. * | | └──────────────────────────-┘ └──────────────────────┘ └────────────────────────────┘ | |
  370. * | | \---------- only when toolbar items overflow --------/ | |
  371. * | └───────────────────────────────────────────────────────────────────────────────────────────┘ |
  372. * └───────────────────────────────────────────────────────────────────────────────────────────────┘
  373. *
  374. * @private
  375. * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
  376. */
  377. class DynamicGrouping {
  378. /**
  379. * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
  380. * behavior.
  381. *
  382. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
  383. * is added to.
  384. */
  385. constructor( view ) {
  386. /**
  387. * A toolbar view this behavior belongs to.
  388. *
  389. * @readonly
  390. * @member {module:ui/toolbar~ToolbarView}
  391. */
  392. this.view = view;
  393. /**
  394. * A collection of toolbar children.
  395. *
  396. * @readonly
  397. * @member {module:ui/viewcollection~ViewCollection}
  398. */
  399. this.viewChildren = view.children;
  400. /**
  401. * A collection of focusable toolbar elements.
  402. *
  403. * @readonly
  404. * @member {module:ui/viewcollection~ViewCollection}
  405. */
  406. this.viewFocusables = view.focusables;
  407. /**
  408. * A view containing toolbar items.
  409. *
  410. * @readonly
  411. * @member {module:ui/toolbar/toolbarview~ItemsView}
  412. */
  413. this.viewItemsView = view.itemsView;
  414. /**
  415. * Toolbar focus tracker.
  416. *
  417. * @readonly
  418. * @member {module:utils/focustracker~FocusTracker}
  419. */
  420. this.viewFocusTracker = view.focusTracker;
  421. /**
  422. * Toolbar locale.
  423. *
  424. * @readonly
  425. * @member {module:utils/locale~Locale}
  426. */
  427. this.viewLocale = view.locale;
  428. /**
  429. * Toolbar element.
  430. *
  431. * @readonly
  432. * @member {HTMLElement} #viewElement
  433. */
  434. /**
  435. * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  436. * Aggregates items that fit into a single row of the toolbar and were not {@link #groupedItems grouped}
  437. * into a {@link #groupedItemsDropdown dropdown}. Items of this collection are displayed in the
  438. * {@link module:ui/toolbar/toolbarview~ToolbarView#itemsView}.
  439. *
  440. * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped, it
  441. * matches the {@link module:ui/toolbar/toolbarview~ToolbarView#items} collection in size and order.
  442. *
  443. * @readonly
  444. * @member {module:ui/viewcollection~ViewCollection}
  445. */
  446. this.ungroupedItems = view.createCollection();
  447. /**
  448. * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  449. * A collection of the toolbar items that do not fit into a single row of the toolbar.
  450. * Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
  451. *
  452. * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped,
  453. * this collection is empty.
  454. *
  455. * @readonly
  456. * @member {module:ui/viewcollection~ViewCollection}
  457. */
  458. this.groupedItems = view.createCollection();
  459. /**
  460. * The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
  461. * row of the toolbar. It is displayed on demand as the last of
  462. * {@link module:ui/toolbar/toolbarview~ToolbarView#children toolbar children} and offers another
  463. * (nested) toolbar which displays items that would normally overflow.
  464. *
  465. * @readonly
  466. * @member {module:ui/dropdown/dropdownview~DropdownView}
  467. */
  468. this.groupedItemsDropdown = this._createGroupedItemsDropdown();
  469. /**
  470. * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
  471. * and manage items that do not fit into a single row.
  472. *
  473. * **Note:** Created in {@link #_enableGroupingOnResize}.
  474. *
  475. * @readonly
  476. * @member {module:utils/dom/resizeobserver~ResizeObserver}
  477. */
  478. this.resizeObserver = null;
  479. /**
  480. * A cached value of the horizontal padding style used by {@link #_updateGrouping}
  481. * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
  482. * a single toolbar line. This value can be reused between updates because it is unlikely that
  483. * the padding will change and re–using `Window.getComputedStyle()` is expensive.
  484. *
  485. * @readonly
  486. * @member {Number}
  487. */
  488. this.cachedPadding = null;
  489. /**
  490. * A flag indicating that an items grouping update has been queued (e.g. due to the toolbar being visible)
  491. * and should be executed immediately the next time the toolbar shows up.
  492. *
  493. * @readonly
  494. * @member {Boolean}
  495. */
  496. this.shouldUpdateGroupingOnNextResize = false;
  497. // Only those items that were not grouped are visible to the user.
  498. view.itemsView.children.bindTo( this.ungroupedItems ).using( item => item );
  499. // Make sure all #items visible in the main space of the toolbar are "focuscycleable".
  500. this.ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
  501. this.ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
  502. // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
  503. view.children.on( 'add', this._updateFocusCycleableItems.bind( this ) );
  504. view.children.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
  505. // ToolbarView#items is dynamic. When an item is added or removed, it should be automatically
  506. // represented in either grouped or ungrouped items at the right index.
  507. // In other words #items == concat( #ungroupedItems, #groupedItems )
  508. // (in length and order).
  509. view.items.on( 'change', ( evt, changeData ) => {
  510. const index = changeData.index;
  511. // Removing.
  512. for ( const removedItem of changeData.removed ) {
  513. if ( index >= this.ungroupedItems.length ) {
  514. this.groupedItems.remove( removedItem );
  515. } else {
  516. this.ungroupedItems.remove( removedItem );
  517. }
  518. }
  519. // Adding.
  520. for ( let currentIndex = index; currentIndex < index + changeData.added.length; currentIndex++ ) {
  521. const addedItem = changeData.added[ currentIndex - index ];
  522. if ( currentIndex > this.ungroupedItems.length ) {
  523. this.groupedItems.add( addedItem, currentIndex - this.ungroupedItems.length );
  524. } else {
  525. this.ungroupedItems.add( addedItem, currentIndex );
  526. }
  527. }
  528. // When new ungrouped items join in and land in #ungroupedItems, there's a chance it causes
  529. // the toolbar to overflow.
  530. // Consequently if removed from grouped or ungrouped items, there is a chance
  531. // some new space is available and we could do some ungrouping.
  532. this._updateGrouping();
  533. } );
  534. view.extendTemplate( {
  535. attributes: {
  536. class: [
  537. // To group items dynamically, the toolbar needs a dedicated CSS class.
  538. 'ck-toolbar_grouping'
  539. ]
  540. }
  541. } );
  542. }
  543. /**
  544. * Enables dynamic items grouping based on the dimensions of the toolbar.
  545. *
  546. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
  547. * is added to.
  548. */
  549. render( view ) {
  550. this.viewElement = view.element;
  551. this._enableGroupingOnResize();
  552. this._enableGroupingOnMaxWidthChange( view );
  553. }
  554. /**
  555. * Cleans up the internals used by this behavior.
  556. */
  557. destroy() {
  558. // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
  559. // so let's make sure it's actually destroyed along with the toolbar.
  560. this.groupedItemsDropdown.destroy();
  561. this.resizeObserver.destroy();
  562. }
  563. /**
  564. * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
  565. * and it will move them to the {@link #groupedItems} when it happens.
  566. *
  567. * At the same time, it will also check if there is enough space in the toolbar for the first of the
  568. * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
  569. * without the toolbar wrapping.
  570. *
  571. * @protected
  572. */
  573. _updateGrouping() {
  574. // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
  575. // for instance before #render(), or after render but without a parent or a parent detached
  576. // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
  577. // nothing else. This happens, for instance, when the toolbar is detached from DOM and
  578. // some logic adds or removes its #items.
  579. if ( !this.viewElement.ownerDocument.body.contains( this.viewElement ) ) {
  580. return;
  581. }
  582. // Do not update grouping when the element is invisible. Such toolbar has DOMRect filled with zeros
  583. // and that would cause all items to be grouped. Instead, queue the grouping so it runs next time
  584. // the toolbar is visible (the next ResizeObserver callback execution). This is handy because
  585. // the grouping could be caused by increasing the #maxWidth when the toolbar was invisible and the next
  586. // time it shows up, some items could actually be ungrouped (https://github.com/ckeditor/ckeditor5/issues/6575).
  587. if ( !this.viewElement.offsetParent ) {
  588. this.shouldUpdateGroupingOnNextResize = true;
  589. return;
  590. }
  591. // Remember how many items were initially grouped so at the it is possible to figure out if the number
  592. // of grouped items has changed. If the number has changed, geometry of the toolbar has also changed.
  593. const initialGroupedItemsCount = this.groupedItems.length;
  594. let wereItemsGrouped;
  595. // Group #items as long as some wrap to the next row. This will happen, for instance,
  596. // when the toolbar is getting narrow and there is not enough space to display all items in
  597. // a single row.
  598. while ( this._areItemsOverflowing ) {
  599. this._groupLastItem();
  600. wereItemsGrouped = true;
  601. }
  602. // If none were grouped now but there were some items already grouped before,
  603. // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
  604. // for instance, the toolbar is stretching and there's more space in it than before.
  605. if ( !wereItemsGrouped && this.groupedItems.length ) {
  606. // Ungroup items as long as none are overflowing or there are none to ungroup left.
  607. while ( this.groupedItems.length && !this._areItemsOverflowing ) {
  608. this._ungroupFirstItem();
  609. }
  610. // If the ungrouping ended up with some item wrapping to the next row,
  611. // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
  612. // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
  613. // clean–up is vital for the algorithm.
  614. if ( this._areItemsOverflowing ) {
  615. this._groupLastItem();
  616. }
  617. }
  618. if ( this.groupedItems.length !== initialGroupedItemsCount ) {
  619. this.view.fire( 'groupedItemsUpdate' );
  620. }
  621. }
  622. /**
  623. * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
  624. * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
  625. *
  626. * @private
  627. * @type {Boolean}
  628. */
  629. get _areItemsOverflowing() {
  630. // An empty toolbar cannot overflow.
  631. if ( !this.ungroupedItems.length ) {
  632. return false;
  633. }
  634. const element = this.viewElement;
  635. const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
  636. const lastChildRect = new Rect( element.lastChild );
  637. const toolbarRect = new Rect( element );
  638. if ( !this.cachedPadding ) {
  639. const computedStyle = global.window.getComputedStyle( element );
  640. const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
  641. // parseInt() is essential because of quirky floating point numbers logic and DOM.
  642. // If the padding turned out too big because of that, the grouped items dropdown would
  643. // always look (from the Rect perspective) like it overflows (while it's not).
  644. this.cachedPadding = Number.parseInt( computedStyle[ paddingProperty ] );
  645. }
  646. if ( uiLanguageDirection === 'ltr' ) {
  647. return lastChildRect.right > toolbarRect.right - this.cachedPadding;
  648. } else {
  649. return lastChildRect.left < toolbarRect.left + this.cachedPadding;
  650. }
  651. }
  652. /**
  653. * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
  654. * upon resize when there is little space available. Instead, the toolbar items are moved to the
  655. * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
  656. *
  657. * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
  658. * them in the dropdown if necessary. It will also observe the browser window for size changes in
  659. * the future and respond to them by grouping more items or reverting already grouped back, depending
  660. * on the visual space available.
  661. *
  662. * @private
  663. */
  664. _enableGroupingOnResize() {
  665. let previousWidth;
  666. // TODO: Consider debounce.
  667. this.resizeObserver = new ResizeObserver( this.viewElement, entry => {
  668. if ( !previousWidth || previousWidth !== entry.contentRect.width || this.shouldUpdateGroupingOnNextResize ) {
  669. this.shouldUpdateGroupingOnNextResize = false;
  670. this._updateGrouping();
  671. previousWidth = entry.contentRect.width;
  672. }
  673. } );
  674. this._updateGrouping();
  675. }
  676. /**
  677. * Enables the grouping functionality, just like {@link #_enableGroupingOnResize} but the difference is that
  678. * it listens to the changes of {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth} instead.
  679. *
  680. * @private
  681. */
  682. _enableGroupingOnMaxWidthChange( view ) {
  683. view.on( 'change:maxWidth', () => {
  684. this._updateGrouping();
  685. } );
  686. }
  687. /**
  688. * When called, it will remove the last item from {@link #ungroupedItems} and move it back
  689. * to the {@link #groupedItems} collection.
  690. *
  691. * The opposite of {@link #_ungroupFirstItem}.
  692. *
  693. * @private
  694. */
  695. _groupLastItem() {
  696. if ( !this.groupedItems.length ) {
  697. this.viewChildren.add( new ToolbarSeparatorView() );
  698. this.viewChildren.add( this.groupedItemsDropdown );
  699. this.viewFocusTracker.add( this.groupedItemsDropdown.element );
  700. }
  701. this.groupedItems.add( this.ungroupedItems.remove( this.ungroupedItems.last ), 0 );
  702. }
  703. /**
  704. * Moves the very first item belonging to {@link #groupedItems} back
  705. * to the {@link #ungroupedItems} collection.
  706. *
  707. * The opposite of {@link #_groupLastItem}.
  708. *
  709. * @private
  710. */
  711. _ungroupFirstItem() {
  712. this.ungroupedItems.add( this.groupedItems.remove( this.groupedItems.first ) );
  713. if ( !this.groupedItems.length ) {
  714. this.viewChildren.remove( this.groupedItemsDropdown );
  715. this.viewChildren.remove( this.viewChildren.last );
  716. this.viewFocusTracker.remove( this.groupedItemsDropdown.element );
  717. }
  718. }
  719. /**
  720. * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
  721. * collection when there is not enough space in the toolbar to display all items in a single row.
  722. *
  723. * @private
  724. * @returns {module:ui/dropdown/dropdownview~DropdownView}
  725. */
  726. _createGroupedItemsDropdown() {
  727. const locale = this.viewLocale;
  728. const t = locale.t;
  729. const dropdown = createDropdown( locale );
  730. dropdown.class = 'ck-toolbar__grouped-dropdown';
  731. // Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
  732. // (https://github.com/ckeditor/ckeditor5/issues/5608)
  733. dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';
  734. addToolbarToDropdown( dropdown, [] );
  735. dropdown.buttonView.set( {
  736. label: t( 'Show more items' ),
  737. tooltip: true,
  738. icon: verticalDotsIcon
  739. } );
  740. // 1:1 pass–through binding.
  741. dropdown.toolbarView.items.bindTo( this.groupedItems ).using( item => item );
  742. return dropdown;
  743. }
  744. /**
  745. * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
  746. * collection so it represents the up–to–date state of the UI from the perspective of the user.
  747. *
  748. * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
  749. * it must be subject to focus cycling in the toolbar.
  750. *
  751. * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
  752. * to learn more about the purpose of this method.
  753. *
  754. * @private
  755. */
  756. _updateFocusCycleableItems() {
  757. this.viewFocusables.clear();
  758. this.ungroupedItems.map( item => {
  759. this.viewFocusables.add( item );
  760. } );
  761. if ( this.groupedItems.length ) {
  762. this.viewFocusables.add( this.groupedItemsDropdown );
  763. }
  764. }
  765. }
  766. /**
  767. * Options passed to the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
  768. *
  769. * @interface module:ui/toolbar/toolbarview~ToolbarOptions
  770. */
  771. /**
  772. * When set to `true`, the toolbar will automatically group {@link module:ui/toolbar/toolbarview~ToolbarView#items} that
  773. * would normally wrap to the next line when there is not enough space to display them in a single row, for
  774. * instance, if the parent container of the toolbar is narrow.
  775. *
  776. * Also see: {@link module:ui/toolbar/toolbarview~ToolbarView#maxWidth}.
  777. *
  778. * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull
  779. */
  780. /**
  781. * A class interface defining the behavior of the {@link module:ui/toolbar/toolbarview~ToolbarView}.
  782. *
  783. * Toolbar behaviors extend its look and functionality and have an impact on the
  784. * {@link module:ui/toolbar/toolbarview~ToolbarView#element} template or
  785. * {@link module:ui/toolbar/toolbarview~ToolbarView#render rendering}. They can be enabled
  786. * conditionally, e.g. depending on the configuration of the toolbar.
  787. *
  788. * @private
  789. * @interface module:ui/toolbar/toolbarview~ToolbarBehavior
  790. */
  791. /**
  792. * Creates a new toolbar behavior instance.
  793. *
  794. * The instance is created in the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
  795. * This is the right place to extend the {@link module:ui/toolbar/toolbarview~ToolbarView#template} of
  796. * the toolbar, define extra toolbar properties, etc.
  797. *
  798. * @method #constructor
  799. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior is added to.
  800. */
  801. /**
  802. * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#render rendered}.
  803. * It can be used to, for example, customize the behavior of the toolbar when its {@link module:ui/toolbar/toolbarview~ToolbarView#element}
  804. * is available.
  805. *
  806. * @readonly
  807. * @member {Function} #render
  808. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar being rendered.
  809. */
  810. /**
  811. * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#destroy destroyed}.
  812. * It allows cleaning up after the toolbar behavior, for instance, this is the right place to detach
  813. * event listeners, free up references, etc.
  814. *
  815. * @readonly
  816. * @member {Function} #destroy
  817. */