dropdownview.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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/dropdown/dropdownview
  7. */
  8. import View from '../view';
  9. import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
  10. import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
  11. import '../../theme/components/dropdown/dropdown.css';
  12. import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
  13. /**
  14. * The dropdown view class. It manages the dropdown button and dropdown panel.
  15. *
  16. * In most cases, the easiest way to create a dropdown is by using the {@link module:ui/dropdown/utils~createDropdown}
  17. * util:
  18. *
  19. * const dropdown = createDropdown( locale );
  20. *
  21. * // Configure dropdown's button properties:
  22. * dropdown.buttonView.set( {
  23. * label: 'A dropdown',
  24. * withText: true
  25. * } );
  26. *
  27. * dropdown.render();
  28. *
  29. * dropdown.panelView.element.textContent = 'Content of the panel';
  30. *
  31. * // Will render a dropdown with a panel containing a "Content of the panel" text.
  32. * document.body.appendChild( dropdown.element );
  33. *
  34. * If you want to add a richer content to the dropdown panel, you can use the {@link module:ui/dropdown/utils~addListToDropdown}
  35. * and {@link module:ui/dropdown/utils~addToolbarToDropdown} helpers. See more examples in
  36. * {@link module:ui/dropdown/utils~createDropdown} documentation.
  37. *
  38. * If you want to create a completely custom dropdown, then you can compose it manually:
  39. *
  40. * const button = new DropdownButtonView( locale );
  41. * const panel = new DropdownPanelView( locale );
  42. * const dropdown = new DropdownView( locale, button, panel );
  43. *
  44. * button.set( {
  45. * label: 'A dropdown',
  46. * withText: true
  47. * } );
  48. *
  49. * dropdown.render();
  50. *
  51. * panel.element.textContent = 'Content of the panel';
  52. *
  53. * // Will render a dropdown with a panel containing a "Content of the panel" text.
  54. * document.body.appendChild( dropdown.element );
  55. *
  56. * However, dropdown created this way will contain little behavior. You will need to implement handlers for actions
  57. * such as {@link module:ui/bindings/clickoutsidehandler~clickOutsideHandler clicking outside an open dropdown}
  58. * (which should close it) and support for arrow keys inside the panel. Therefore, unless you really know what
  59. * you do and you really need to do it, it is recommended to use the {@link module:ui/dropdown/utils~createDropdown} helper.
  60. *
  61. * @extends module:ui/view~View
  62. */
  63. export default class DropdownView extends View {
  64. /**
  65. * Creates an instance of the dropdown.
  66. *
  67. * Also see {@link #render}.
  68. *
  69. * @param {module:utils/locale~Locale} [locale] The localization services instance.
  70. * @param {module:ui/dropdown/button/dropdownbutton~DropdownButton} buttonView
  71. * @param {module:ui/dropdown/dropdownpanelview~DropdownPanelView} panelView
  72. */
  73. constructor( locale, buttonView, panelView ) {
  74. super( locale );
  75. const bind = this.bindTemplate;
  76. /**
  77. * Button of the dropdown view. Clicking the button opens the {@link #panelView}.
  78. *
  79. * @readonly
  80. * @member {module:ui/button/buttonview~ButtonView} #buttonView
  81. */
  82. this.buttonView = buttonView;
  83. /**
  84. * Panel of the dropdown. It opens when the {@link #buttonView} is
  85. * {@link module:ui/button/buttonview~ButtonView#event:execute executed} (i.e. clicked).
  86. *
  87. * Child views can be added to the panel's `children` collection:
  88. *
  89. * dropdown.panelView.children.add( childView );
  90. *
  91. * See {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView#children} and
  92. * {@link module:ui/viewcollection~ViewCollection#add}.
  93. *
  94. * @readonly
  95. * @member {module:ui/dropdown/dropdownpanelview~DropdownPanelView} #panelView
  96. */
  97. this.panelView = panelView;
  98. /**
  99. * Controls whether the dropdown view is open, i.e. shows or hides the {@link #panelView panel}.
  100. *
  101. * @observable
  102. * @member {Boolean} #isOpen
  103. */
  104. this.set( 'isOpen', false );
  105. /**
  106. * Controls whether the dropdown is enabled, i.e. it can be clicked and execute an action.
  107. *
  108. * See {@link module:ui/button/buttonview~ButtonView#isEnabled}.
  109. *
  110. * @observable
  111. * @member {Boolean} #isEnabled
  112. */
  113. this.set( 'isEnabled', true );
  114. /**
  115. * (Optional) The additional CSS class set on the dropdown {@link #element}.
  116. *
  117. * @observable
  118. * @member {String} #class
  119. */
  120. this.set( 'class' );
  121. /**
  122. * The position of the panel, relative to the dropdown.
  123. *
  124. * **Note**: When `'auto'`, the panel will use one of the remaining positions to stay
  125. * in the viewport, visible to the user. The positions correspond directly to
  126. * {@link module:ui/dropdown/dropdownview~DropdownView.defaultPanelPositions default panel positions}.
  127. *
  128. * **Note**: This value has an impact on the
  129. * {@link module:ui/dropdown/dropdownpanelview~DropdownPanelView#position} property
  130. * each time the panel becomes {@link #isOpen open}.
  131. *
  132. * @observable
  133. * @default 'auto'
  134. * @member {'auto'|'se'|'sw'|'ne'|'nw'} #panelPosition
  135. */
  136. this.set( 'panelPosition', 'auto' );
  137. /**
  138. * Tracks information about DOM focus in the dropdown.
  139. *
  140. * @readonly
  141. * @member {module:utils/focustracker~FocusTracker}
  142. */
  143. this.focusTracker = new FocusTracker();
  144. /**
  145. * Instance of the {@link module:utils/keystrokehandler~KeystrokeHandler}. It manages
  146. * keystrokes of the dropdown:
  147. *
  148. * * <kbd>▼</kbd> opens the dropdown,
  149. * * <kbd>◀</kbd> and <kbd>Esc</kbd> closes the dropdown.
  150. *
  151. * @readonly
  152. * @member {module:utils/keystrokehandler~KeystrokeHandler}
  153. */
  154. this.keystrokes = new KeystrokeHandler();
  155. this.setTemplate( {
  156. tag: 'div',
  157. attributes: {
  158. class: [
  159. 'ck',
  160. 'ck-dropdown',
  161. bind.to( 'class' ),
  162. bind.if( 'isEnabled', 'ck-disabled', value => !value )
  163. ]
  164. },
  165. children: [
  166. buttonView,
  167. panelView
  168. ]
  169. } );
  170. buttonView.extendTemplate( {
  171. attributes: {
  172. class: [
  173. 'ck-dropdown__button',
  174. ]
  175. }
  176. } );
  177. /**
  178. * A child {@link module:ui/list/listview~ListView list view} of the dropdown located
  179. * in its {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
  180. *
  181. * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addListToDropdown}.
  182. *
  183. * @readonly
  184. * @member {module:ui/list/listview~ListView} #listView
  185. */
  186. /**
  187. * A child toolbar of the dropdown located in the
  188. * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel}.
  189. *
  190. * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addToolbarToDropdown}.
  191. *
  192. * @readonly
  193. * @member {module:ui/toolbar/toolbarview~ToolbarView} #toolbarView
  194. */
  195. /**
  196. * Fired when the toolbar button or list item is executed.
  197. *
  198. * For {@link #listView} It fires when a child of some {@link module:ui/list/listitemview~ListItemView}
  199. * fired `execute`.
  200. *
  201. * For {@link #toolbarView} It fires when one of the buttons has been
  202. * {@link module:ui/button/buttonview~ButtonView#event:execute executed}.
  203. *
  204. * **Note**: Only supported when dropdown has list view added using {@link module:ui/dropdown/utils~addListToDropdown}
  205. * or {@link module:ui/dropdown/utils~addToolbarToDropdown}.
  206. *
  207. * @event execute
  208. */
  209. }
  210. /**
  211. * @inheritDoc
  212. */
  213. render() {
  214. super.render();
  215. // Toggle the dropdown when its button has been clicked.
  216. this.listenTo( this.buttonView, 'open', () => {
  217. this.isOpen = !this.isOpen;
  218. } );
  219. // Toggle the visibility of the panel when the dropdown becomes open.
  220. this.panelView.bind( 'isVisible' ).to( this, 'isOpen' );
  221. // Let the dropdown control the position of the panel. The position must
  222. // be updated every time the dropdown is open.
  223. this.on( 'change:isOpen', () => {
  224. if ( !this.isOpen ) {
  225. return;
  226. }
  227. // If "auto", find the best position of the panel to fit into the viewport.
  228. // Otherwise, simply assign the static position.
  229. if ( this.panelPosition === 'auto' ) {
  230. this.panelView.position = DropdownView._getOptimalPosition( {
  231. element: this.panelView.element,
  232. target: this.buttonView.element,
  233. fitInViewport: true,
  234. positions: this._panelPositions
  235. } ).name;
  236. } else {
  237. this.panelView.position = this.panelPosition;
  238. }
  239. } );
  240. // Listen for keystrokes coming from within #element.
  241. this.keystrokes.listenTo( this.element );
  242. // Register #element in the focus tracker.
  243. this.focusTracker.add( this.element );
  244. const closeDropdown = ( data, cancel ) => {
  245. if ( this.isOpen ) {
  246. this.buttonView.focus();
  247. this.isOpen = false;
  248. cancel();
  249. }
  250. };
  251. // Open the dropdown panel using the arrow down key, just like with return or space.
  252. this.keystrokes.set( 'arrowdown', ( data, cancel ) => {
  253. // Don't open if the dropdown is disabled or already open.
  254. if ( this.buttonView.isEnabled && !this.isOpen ) {
  255. this.isOpen = true;
  256. cancel();
  257. }
  258. } );
  259. // Block the right arrow key (until nested dropdowns are implemented).
  260. this.keystrokes.set( 'arrowright', ( data, cancel ) => {
  261. if ( this.isOpen ) {
  262. cancel();
  263. }
  264. } );
  265. // Close the dropdown using the arrow left/escape key.
  266. this.keystrokes.set( 'arrowleft', closeDropdown );
  267. this.keystrokes.set( 'esc', closeDropdown );
  268. }
  269. /**
  270. * Focuses the {@link #buttonView}.
  271. */
  272. focus() {
  273. this.buttonView.focus();
  274. }
  275. /**
  276. * Returns {@link #panelView panel} positions to be used by the
  277. * {@link module:utils/dom/position~getOptimalPosition `getOptimalPosition()`}
  278. * utility considering the direction of the language the UI of the editor is displayed in.
  279. *
  280. * @type {module:utils/dom/position~Options#positions}
  281. * @private
  282. */
  283. get _panelPositions() {
  284. const { southEast, southWest, northEast, northWest } = DropdownView.defaultPanelPositions;
  285. if ( this.locale.languageDirection === 'ltr' ) {
  286. return [ southEast, southWest, northEast, northWest ];
  287. } else {
  288. return [ southWest, southEast, northWest, northEast ];
  289. }
  290. }
  291. }
  292. /**
  293. * A set of positioning functions used by the dropdown view to determine
  294. * the optimal position (i.e. fitting into the browser viewport) of its
  295. * {@link module:ui/dropdown/dropdownview~DropdownView#panelView panel} when
  296. * {@link module:ui/dropdown/dropdownview~DropdownView#panelPosition} is set to 'auto'`.
  297. *
  298. * The available positioning functions are as follow:
  299. *
  300. * **South**
  301. *
  302. * * `southEast`
  303. *
  304. * [ Button ]
  305. * +-----------------+
  306. * | Panel |
  307. * +-----------------+
  308. *
  309. * * `southWest`
  310. *
  311. * [ Button ]
  312. * +-----------------+
  313. * | Panel |
  314. * +-----------------+
  315. *
  316. * **North**
  317. *
  318. * * `northEast`
  319. *
  320. * +-----------------+
  321. * | Panel |
  322. * +-----------------+
  323. * [ Button ]
  324. *
  325. * * `northWest`
  326. *
  327. * +-----------------+
  328. * | Panel |
  329. * +-----------------+
  330. * [ Button ]
  331. *
  332. * Positioning functions are compatible with {@link module:utils/dom/position~Position}.
  333. *
  334. * The name that position function returns will be reflected in dropdown panel's class that
  335. * controls its placement. See {@link module:ui/dropdown/dropdownview~DropdownView#panelPosition}
  336. * to learn more.
  337. *
  338. * @member {Object} module:ui/dropdown/dropdownview~DropdownView.defaultPanelPositions
  339. */
  340. DropdownView.defaultPanelPositions = {
  341. southEast: buttonRect => {
  342. return {
  343. top: buttonRect.bottom,
  344. left: buttonRect.left,
  345. name: 'se'
  346. };
  347. },
  348. southWest: ( buttonRect, panelRect ) => {
  349. return {
  350. top: buttonRect.bottom,
  351. left: buttonRect.left - panelRect.width + buttonRect.width,
  352. name: 'sw'
  353. };
  354. },
  355. northEast: ( buttonRect, panelRect ) => {
  356. return {
  357. top: buttonRect.top - panelRect.height,
  358. left: buttonRect.left,
  359. name: 'ne'
  360. };
  361. },
  362. northWest: ( buttonRect, panelRect ) => {
  363. return {
  364. top: buttonRect.bottom - panelRect.height,
  365. left: buttonRect.left - panelRect.width + buttonRect.width,
  366. name: 'nw'
  367. };
  368. }
  369. };
  370. /**
  371. * A function used to calculate the optimal position for the dropdown panel.
  372. *
  373. * @protected
  374. * @member {Function} module:ui/dropdown/dropdownview~DropdownView._getOptimalPosition
  375. */
  376. DropdownView._getOptimalPosition = getOptimalPosition;