toolbarview.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  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 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. * A collection of toolbar items (buttons, dropdowns, etc.).
  57. *
  58. * @readonly
  59. * @member {module:ui/viewcollection~ViewCollection}
  60. */
  61. this.items = this.createCollection();
  62. /**
  63. * Tracks information about the DOM focus in the toolbar.
  64. *
  65. * @readonly
  66. * @member {module:utils/focustracker~FocusTracker}
  67. */
  68. this.focusTracker = new FocusTracker();
  69. /**
  70. * An 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 that expands the 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 configuration.
  217. * @param {module:ui/componentfactory~ComponentFactory} factory A factory producing toolbar items.
  218. */
  219. fillFromConfig( config, factory ) {
  220. config.map( name => {
  221. if ( name == '|' ) {
  222. this.items.add( new ToolbarSeparatorView() );
  223. } else if ( factory.has( name ) ) {
  224. this.items.add( factory.create( name ) );
  225. } else {
  226. /**
  227. * There was a problem processing the configuration of the toolbar. The item with the given
  228. * name does not exist so it was omitted when rendering the toolbar.
  229. *
  230. * This warning usually shows up when the {@link module:core/plugin~Plugin} which is supposed
  231. * to provide a toolbar item has not been loaded or there is a typo in the configuration.
  232. *
  233. * Make sure the plugin responsible for this toolbar item is loaded and the toolbar configuration
  234. * is correct, e.g. {@link module:basic-styles/bold~Bold} is loaded for the `'bold'` toolbar item.
  235. *
  236. * You can use the following snippet to retrieve all available toolbar items:
  237. *
  238. * Array.from( editor.ui.componentFactory.names() );
  239. *
  240. * @error toolbarview-item-unavailable
  241. * @param {String} name The name of the component.
  242. */
  243. console.warn( attachLinkToDocumentation(
  244. 'toolbarview-item-unavailable: The requested toolbar item is unavailable.' ), { name } );
  245. }
  246. } );
  247. }
  248. }
  249. /**
  250. * An inner block of the {@link module:ui/toolbar/toolbarview~ToolbarView} hosting its
  251. * {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  252. *
  253. * @private
  254. * @extends module:ui/view~View
  255. */
  256. class ItemsView extends View {
  257. /**
  258. * @inheritDoc
  259. */
  260. constructor( locale ) {
  261. super( locale );
  262. /**
  263. * A collection of items (buttons, dropdowns, etc.).
  264. *
  265. * @readonly
  266. * @member {module:ui/viewcollection~ViewCollection}
  267. */
  268. this.children = this.createCollection();
  269. this.setTemplate( {
  270. tag: 'div',
  271. attributes: {
  272. class: [
  273. 'ck',
  274. 'ck-toolbar__items'
  275. ],
  276. },
  277. children: this.children
  278. } );
  279. }
  280. }
  281. /**
  282. * A toolbar behavior that makes it static and unresponsive to the changes of the environment.
  283. * At the same time, it also makes it possible to display a toolbar with a vertical layout
  284. * using the {@link module:ui/toolbar/toolbarview~ToolbarView#isVertical} property.
  285. *
  286. * @private
  287. * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
  288. */
  289. class StaticLayout {
  290. /**
  291. * Creates an instance of the {@link module:ui/toolbar/toolbarview~StaticLayout} toolbar
  292. * behavior.
  293. *
  294. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
  295. * is added to.
  296. */
  297. constructor( view ) {
  298. const bind = view.bindTemplate;
  299. // Static toolbar can be vertical when needed.
  300. view.set( 'isVertical', false );
  301. // 1:1 pass–through binding, all ToolbarView#items are visible.
  302. view.itemsView.children.bindTo( view.items ).using( item => item );
  303. // 1:1 pass–through binding, all ToolbarView#items are focusable.
  304. view.focusables.bindTo( view.items ).using( item => item );
  305. view.extendTemplate( {
  306. attributes: {
  307. class: [
  308. // When vertical, the toolbar has an additional CSS class.
  309. bind.if( 'isVertical', 'ck-toolbar_vertical' )
  310. ]
  311. }
  312. } );
  313. }
  314. /**
  315. * @inheritDoc
  316. */
  317. render() {}
  318. /**
  319. * @inheritDoc
  320. */
  321. destroy() {}
  322. }
  323. /**
  324. * A toolbar behavior that makes the items respond to changes in the geometry.
  325. *
  326. * In a nutshell, it groups {@link module:ui/toolbar/toolbarview~ToolbarView#items}
  327. * that do not fit visually into a single row of the toolbar (due to limited space).
  328. * Items that do not fit are aggregated in a dropdown displayed at the end of the toolbar.
  329. *
  330. * ┌──────────────────────────────────────── ToolbarView ──────────────────────────────────────────┐
  331. * | ┌─────────────────────────────────────── #children ─────────────────────────────────────────┐ |
  332. * | | ┌─────── #itemsView ────────┐ ┌──────────────────────┐ ┌── #groupedItemsDropdown ───┐ | |
  333. * | | | #ungroupedItems | | ToolbarSeparatorView | | #groupedItems | | |
  334. * | | └──────────────────────────-┘ └──────────────────────┘ └────────────────────────────┘ | |
  335. * | | \---------- only when toolbar items overflow --------/ | |
  336. * | └───────────────────────────────────────────────────────────────────────────────────────────┘ |
  337. * └───────────────────────────────────────────────────────────────────────────────────────────────┘
  338. *
  339. * @private
  340. * @implements module:ui/toolbar/toolbarview~ToolbarBehavior
  341. */
  342. class DynamicGrouping {
  343. /**
  344. * Creates an instance of the {@link module:ui/toolbar/toolbarview~DynamicGrouping} toolbar
  345. * behavior.
  346. *
  347. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
  348. * is added to.
  349. */
  350. constructor( view ) {
  351. /**
  352. * A collection of toolbar children.
  353. *
  354. * @readonly
  355. * @member {module:ui/viewcollection~ViewCollection}
  356. */
  357. this.viewChildren = view.children;
  358. /**
  359. * A collection of focusable toolbar elements.
  360. *
  361. * @readonly
  362. * @member {module:ui/viewcollection~ViewCollection}
  363. */
  364. this.viewFocusables = view.focusables;
  365. /**
  366. * A view containing toolbar items.
  367. *
  368. * @readonly
  369. * @member {module:ui/toolbar/toolbarview~ItemsView}
  370. */
  371. this.viewItemsView = view.itemsView;
  372. /**
  373. * Toolbar focus tracker.
  374. *
  375. * @readonly
  376. * @member {module:utils/focustracker~FocusTracker}
  377. */
  378. this.viewFocusTracker = view.focusTracker;
  379. /**
  380. * Toolbar locale.
  381. *
  382. * @readonly
  383. * @member {module:utils/locale~Locale}
  384. */
  385. this.viewLocale = view.locale;
  386. /**
  387. * Toolbar element.
  388. *
  389. * @readonly
  390. * @member {HTMLElement} #viewElement
  391. */
  392. /**
  393. * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  394. * Aggregates items that fit into a single row of the toolbar and were not {@link #groupedItems grouped}
  395. * into a {@link #groupedItemsDropdown dropdown}. Items of this collection are displayed in the
  396. * {@link module:ui/toolbar/toolbarview~ToolbarView#itemsView}.
  397. *
  398. * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped, it
  399. * matches the {@link module:ui/toolbar/toolbarview~ToolbarView#items} collection in size and order.
  400. *
  401. * @readonly
  402. * @member {module:ui/viewcollection~ViewCollection}
  403. */
  404. this.ungroupedItems = view.createCollection();
  405. /**
  406. * A subset of toolbar {@link module:ui/toolbar/toolbarview~ToolbarView#items}.
  407. * A collection of the toolbar items that do not fit into a single row of the toolbar.
  408. * Grouped items are displayed in a dedicated {@link #groupedItemsDropdown dropdown}.
  409. *
  410. * When none of the {@link module:ui/toolbar/toolbarview~ToolbarView#items} were grouped,
  411. * this collection is empty.
  412. *
  413. * @readonly
  414. * @member {module:ui/viewcollection~ViewCollection}
  415. */
  416. this.groupedItems = view.createCollection();
  417. /**
  418. * The dropdown that aggregates {@link #groupedItems grouped items} that do not fit into a single
  419. * row of the toolbar. It is displayed on demand as the last of
  420. * {@link module:ui/toolbar/toolbarview~ToolbarView#children toolbar children} and offers another
  421. * (nested) toolbar which displays items that would normally overflow.
  422. *
  423. * @readonly
  424. * @member {module:ui/dropdown/dropdownview~DropdownView}
  425. */
  426. this.groupedItemsDropdown = this._createGroupedItemsDropdown();
  427. /**
  428. * An instance of the resize observer that helps dynamically determine the geometry of the toolbar
  429. * and manage items that do not fit into a single row.
  430. *
  431. * **Note:** Created in {@link #_enableGroupingOnResize}.
  432. *
  433. * @readonly
  434. * @member {module:utils/dom/getresizeobserver~ResizeObserver}
  435. */
  436. this.resizeObserver = null;
  437. /**
  438. * A cached value of the horizontal padding style used by {@link #_updateGrouping}
  439. * to manage the {@link module:ui/toolbar/toolbarview~ToolbarView#items} that do not fit into
  440. * a single toolbar line. This value can be reused between updates because it is unlikely that
  441. * the padding will change and re–using `Window.getComputedStyle()` is expensive.
  442. *
  443. * @readonly
  444. * @member {Number}
  445. */
  446. this.cachedPadding = null;
  447. // Only those items that were not grouped are visible to the user.
  448. view.itemsView.children.bindTo( this.ungroupedItems ).using( item => item );
  449. // Make sure all #items visible in the main space of the toolbar are "focuscycleable".
  450. this.ungroupedItems.on( 'add', this._updateFocusCycleableItems.bind( this ) );
  451. this.ungroupedItems.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
  452. // Make sure the #groupedItemsDropdown is also included in cycling when it appears.
  453. view.children.on( 'add', this._updateFocusCycleableItems.bind( this ) );
  454. view.children.on( 'remove', this._updateFocusCycleableItems.bind( this ) );
  455. // ToolbarView#items is dynamic. When an item is added, it should be automatically
  456. // represented in either grouped or ungrouped items at the right index.
  457. // In other words #items == concat( #ungroupedItems, #groupedItems )
  458. // (in length and order).
  459. view.items.on( 'add', ( evt, item, index ) => {
  460. if ( index > this.ungroupedItems.length ) {
  461. this.groupedItems.add( item, index - this.ungroupedItems.length );
  462. } else {
  463. this.ungroupedItems.add( item, index );
  464. }
  465. // When a new ungrouped item joins in and lands in #ungroupedItems, there's a chance it causes
  466. // the toolbar to overflow.
  467. this._updateGrouping();
  468. } );
  469. // When an item is removed from ToolbarView#items, it should be automatically
  470. // removed from either grouped or ungrouped items.
  471. view.items.on( 'remove', ( evt, item, index ) => {
  472. if ( index > this.ungroupedItems.length ) {
  473. this.groupedItems.remove( item );
  474. } else {
  475. this.ungroupedItems.remove( item );
  476. }
  477. // Whether removed from grouped or ungrouped items, there is a chance
  478. // some new space is available and we could do some ungrouping.
  479. this._updateGrouping();
  480. } );
  481. view.extendTemplate( {
  482. attributes: {
  483. class: [
  484. // To group items dynamically, the toolbar needs a dedicated CSS class.
  485. 'ck-toolbar_grouping'
  486. ]
  487. }
  488. } );
  489. }
  490. /**
  491. * Enables dynamic items grouping based on the dimensions of the toolbar.
  492. *
  493. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior
  494. * is added to.
  495. */
  496. render( view ) {
  497. this.viewElement = view.element;
  498. this._enableGroupingOnResize();
  499. }
  500. /**
  501. * Cleans up the internals used by this behavior.
  502. */
  503. destroy() {
  504. // The dropdown may not be in ToolbarView#children at the moment of toolbar destruction
  505. // so let's make sure it's actually destroyed along with the toolbar.
  506. this.groupedItemsDropdown.destroy();
  507. this.resizeObserver.disconnect();
  508. }
  509. /**
  510. * When called, it will check if any of the {@link #ungroupedItems} do not fit into a single row of the toolbar,
  511. * and it will move them to the {@link #groupedItems} when it happens.
  512. *
  513. * At the same time, it will also check if there is enough space in the toolbar for the first of the
  514. * {@link #groupedItems} to be returned back to {@link #ungroupedItems} and still fit into a single row
  515. * without the toolbar wrapping.
  516. *
  517. * @protected
  518. */
  519. _updateGrouping() {
  520. // Do no grouping–related geometry analysis when the toolbar is detached from visible DOM,
  521. // for instance before #render(), or after render but without a parent or a parent detached
  522. // from DOM. DOMRects won't work anyway and there will be tons of warning in the console and
  523. // nothing else.
  524. if ( !this.viewElement.ownerDocument.body.contains( this.viewElement ) ) {
  525. return;
  526. }
  527. let wereItemsGrouped;
  528. // Group #items as long as some wrap to the next row. This will happen, for instance,
  529. // when the toolbar is getting narrow and there is not enough space to display all items in
  530. // a single row.
  531. while ( this._areItemsOverflowing ) {
  532. this._groupLastItem();
  533. wereItemsGrouped = true;
  534. }
  535. // If none were grouped now but there were some items already grouped before,
  536. // then, what the hell, maybe let's see if some of them can be ungrouped. This happens when,
  537. // for instance, the toolbar is stretching and there's more space in it than before.
  538. if ( !wereItemsGrouped && this.groupedItems.length ) {
  539. // Ungroup items as long as none are overflowing or there are none to ungroup left.
  540. while ( this.groupedItems.length && !this._areItemsOverflowing ) {
  541. this._ungroupFirstItem();
  542. }
  543. // If the ungrouping ended up with some item wrapping to the next row,
  544. // put it back to the group toolbar ("undo the last ungroup"). We don't know whether
  545. // an item will wrap or not until we ungroup it (that's a DOM/CSS thing) so this
  546. // clean–up is vital for the algorithm.
  547. if ( this._areItemsOverflowing ) {
  548. this._groupLastItem();
  549. }
  550. }
  551. }
  552. /**
  553. * Returns `true` when {@link module:ui/toolbar/toolbarview~ToolbarView#element} children visually overflow,
  554. * for instance if the toolbar is narrower than its members. Returns `false` otherwise.
  555. *
  556. * @private
  557. * @type {Boolean}
  558. */
  559. get _areItemsOverflowing() {
  560. // An empty toolbar cannot overflow.
  561. if ( !this.ungroupedItems.length ) {
  562. return false;
  563. }
  564. const element = this.viewElement;
  565. const uiLanguageDirection = this.viewLocale.uiLanguageDirection;
  566. const lastChildRect = new Rect( element.lastChild );
  567. const toolbarRect = new Rect( element );
  568. if ( !this.cachedPadding ) {
  569. const computedStyle = global.window.getComputedStyle( element );
  570. const paddingProperty = uiLanguageDirection === 'ltr' ? 'paddingRight' : 'paddingLeft';
  571. // parseInt() is essential because of quirky floating point numbers logic and DOM.
  572. // If the padding turned out too big because of that, the grouped items dropdown would
  573. // always look (from the Rect perspective) like it overflows (while it's not).
  574. this.cachedPadding = Number.parseInt( computedStyle[ paddingProperty ] );
  575. }
  576. if ( uiLanguageDirection === 'ltr' ) {
  577. return lastChildRect.right > toolbarRect.right - this.cachedPadding;
  578. } else {
  579. return lastChildRect.left < toolbarRect.left + this.cachedPadding;
  580. }
  581. }
  582. /**
  583. * Enables the functionality that prevents {@link #ungroupedItems} from overflowing (wrapping to the next row)
  584. * upon resize when there is little space available. Instead, the toolbar items are moved to the
  585. * {@link #groupedItems} collection and displayed in a dropdown at the end of the row (which has its own nested toolbar).
  586. *
  587. * When called, the toolbar will automatically analyze the location of its {@link #ungroupedItems} and "group"
  588. * them in the dropdown if necessary. It will also observe the browser window for size changes in
  589. * the future and respond to them by grouping more items or reverting already grouped back, depending
  590. * on the visual space available.
  591. *
  592. * @private
  593. */
  594. _enableGroupingOnResize() {
  595. let previousWidth;
  596. // TODO: Consider debounce.
  597. this.resizeObserver = getResizeObserver( ( [ entry ] ) => {
  598. if ( !previousWidth || previousWidth !== entry.contentRect.width ) {
  599. this._updateGrouping();
  600. previousWidth = entry.contentRect.width;
  601. }
  602. } );
  603. this.resizeObserver.observe( this.viewElement );
  604. this._updateGrouping();
  605. }
  606. /**
  607. * When called, it will remove the last item from {@link #ungroupedItems} and move it back
  608. * to the {@link #groupedItems} collection.
  609. *
  610. * The opposite of {@link #_ungroupFirstItem}.
  611. *
  612. * @private
  613. */
  614. _groupLastItem() {
  615. if ( !this.groupedItems.length ) {
  616. this.viewChildren.add( new ToolbarSeparatorView() );
  617. this.viewChildren.add( this.groupedItemsDropdown );
  618. this.viewFocusTracker.add( this.groupedItemsDropdown.element );
  619. }
  620. this.groupedItems.add( this.ungroupedItems.remove( this.ungroupedItems.last ), 0 );
  621. }
  622. /**
  623. * Moves the very first item belonging to {@link #groupedItems} back
  624. * to the {@link #ungroupedItems} collection.
  625. *
  626. * The opposite of {@link #_groupLastItem}.
  627. *
  628. * @private
  629. */
  630. _ungroupFirstItem() {
  631. this.ungroupedItems.add( this.groupedItems.remove( this.groupedItems.first ) );
  632. if ( !this.groupedItems.length ) {
  633. this.viewChildren.remove( this.groupedItemsDropdown );
  634. this.viewChildren.remove( this.viewChildren.last );
  635. this.viewFocusTracker.remove( this.groupedItemsDropdown.element );
  636. }
  637. }
  638. /**
  639. * Creates the {@link #groupedItemsDropdown} that hosts the members of the {@link #groupedItems}
  640. * collection when there is not enough space in the toolbar to display all items in a single row.
  641. *
  642. * @private
  643. * @returns {module:ui/dropdown/dropdownview~DropdownView}
  644. */
  645. _createGroupedItemsDropdown() {
  646. const locale = this.viewLocale;
  647. const t = locale.t;
  648. const dropdown = createDropdown( locale );
  649. dropdown.class = 'ck-toolbar__grouped-dropdown';
  650. // Make sure the dropdown never sticks out to the left/right. It should be under the main toolbar.
  651. // (https://github.com/ckeditor/ckeditor5/issues/5608)
  652. dropdown.panelPosition = locale.uiLanguageDirection === 'ltr' ? 'sw' : 'se';
  653. addToolbarToDropdown( dropdown, [] );
  654. dropdown.buttonView.set( {
  655. label: t( 'Show more items' ),
  656. tooltip: true,
  657. icon: verticalDotsIcon
  658. } );
  659. // 1:1 pass–through binding.
  660. dropdown.toolbarView.items.bindTo( this.groupedItems ).using( item => item );
  661. return dropdown;
  662. }
  663. /**
  664. * Updates the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables focus–cycleable items}
  665. * collection so it represents the up–to–date state of the UI from the perspective of the user.
  666. *
  667. * For instance, the {@link #groupedItemsDropdown} can show up and hide but when it is visible,
  668. * it must be subject to focus cycling in the toolbar.
  669. *
  670. * See the {@link module:ui/toolbar/toolbarview~ToolbarView#focusables collection} documentation
  671. * to learn more about the purpose of this method.
  672. *
  673. * @private
  674. */
  675. _updateFocusCycleableItems() {
  676. this.viewFocusables.clear();
  677. this.ungroupedItems.map( item => {
  678. this.viewFocusables.add( item );
  679. } );
  680. if ( this.groupedItems.length ) {
  681. this.viewFocusables.add( this.groupedItemsDropdown );
  682. }
  683. }
  684. }
  685. /**
  686. * Options passed to the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
  687. *
  688. * @interface module:ui/toolbar/toolbarview~ToolbarOptions
  689. */
  690. /**
  691. * When set to `true`, the toolbar will automatically group {@link module:ui/toolbar/toolbarview~ToolbarView#items} that
  692. * would normally wrap to the next line when there is not enough space to display them in a single row, for
  693. * instance, if the parent container of the toolbar is narrow.
  694. *
  695. * @member {Boolean} module:ui/toolbar/toolbarview~ToolbarOptions#shouldGroupWhenFull
  696. */
  697. /**
  698. * A class interface defining the behavior of the {@link module:ui/toolbar/toolbarview~ToolbarView}.
  699. *
  700. * Toolbar behaviors extend its look and functionality and have an impact on the
  701. * {@link module:ui/toolbar/toolbarview~ToolbarView#element} template or
  702. * {@link module:ui/toolbar/toolbarview~ToolbarView#render rendering}. They can be enabled
  703. * conditionally, e.g. depending on the configuration of the toolbar.
  704. *
  705. * @private
  706. * @interface module:ui/toolbar/toolbarview~ToolbarBehavior
  707. */
  708. /**
  709. * Creates a new toolbar behavior instance.
  710. *
  711. * The instance is created in the {@link module:ui/toolbar/toolbarview~ToolbarView#constructor} of the toolbar.
  712. * This is the right place to extend the {@link module:ui/toolbar/toolbarview~ToolbarView#template} of
  713. * the toolbar, define extra toolbar properties, etc.
  714. *
  715. * @method #constructor
  716. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar that this behavior is added to.
  717. */
  718. /**
  719. * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#render rendered}.
  720. * It can be used to, for example, customize the behavior of the toolbar when its {@link module:ui/toolbar/toolbarview~ToolbarView#element}
  721. * is available.
  722. *
  723. * @readonly
  724. * @member {Function} #render
  725. * @param {module:ui/toolbar/toolbarview~ToolbarView} view An instance of the toolbar being rendered.
  726. */
  727. /**
  728. * A method called after the toolbar has been {@link module:ui/toolbar/toolbarview~ToolbarView#destroy destroyed}.
  729. * It allows cleaning up after the toolbar behavior, for instance, this is the right place to detach
  730. * event listeners, free up references, etc.
  731. *
  732. * @readonly
  733. * @member {Function} #destroy
  734. */