8
0

dropdownview.js 12 KB

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