toolbarview.js 28 KB

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