8
0

toolbarview.js 31 KB

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