toolbarview.js 27 KB

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