balloonpanelview.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module ui/panel/balloon/balloonpanelview
  7. */
  8. import View from '../../view';
  9. import Template from '../../template';
  10. import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
  11. import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
  12. import global from '@ckeditor/ckeditor5-utils/src/dom/global';
  13. const toPx = toUnit( 'px' );
  14. const defaultLimiterElement = global.document.body;
  15. /**
  16. * The balloon panel view class.
  17. *
  18. * @extends module:ui/view~View
  19. */
  20. export default class BalloonPanelView extends View {
  21. /**
  22. * @inheritDoc
  23. */
  24. constructor( locale ) {
  25. super( locale );
  26. const bind = this.bindTemplate;
  27. /**
  28. * The absolute top position of the balloon panel in pixels.
  29. *
  30. * @observable
  31. * @default 0
  32. * @member {Number} #top
  33. */
  34. this.set( 'top', 0 );
  35. /**
  36. * The absolute left position of the balloon panel in pixels.
  37. *
  38. * @observable
  39. * @default 0
  40. * @member {Number} #left
  41. */
  42. this.set( 'left', 0 );
  43. /**
  44. * Balloon panel's current position. The position name is reflected in the CSS class set
  45. * to the balloon, i.e. `.ck-balloon-panel_arrow_se` for "se" position. The class
  46. * controls the minor aspects of the balloon's visual appearance like placement
  47. * of the "arrow". To support a new position, an additional CSS must be created.
  48. *
  49. * Default position names correspond with
  50. * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
  51. *
  52. * @observable
  53. * @default 'se'
  54. * @member {'se'|'sw'|'ne'|'nw'} #position
  55. */
  56. this.set( 'position', 'se' );
  57. /**
  58. * Controls whether the balloon panel is visible or not.
  59. *
  60. * @observable
  61. * @default false
  62. * @member {Boolean} #isVisible
  63. */
  64. this.set( 'isVisible', false );
  65. /**
  66. * Max width of the balloon panel, as in CSS.
  67. *
  68. * @observable
  69. * @member {Number} #maxWidth
  70. */
  71. /**
  72. * Collection of the child views which creates balloon panel contents.
  73. *
  74. * @readonly
  75. * @member {module:ui/viewcollection~ViewCollection}
  76. */
  77. this.content = this.createCollection();
  78. this.template = new Template( {
  79. tag: 'div',
  80. attributes: {
  81. class: [
  82. 'ck-balloon-panel',
  83. bind.to( 'position', ( value ) => `ck-balloon-panel_arrow_${ value }` ),
  84. bind.if( 'isVisible', 'ck-balloon-panel_visible' )
  85. ],
  86. style: {
  87. top: bind.to( 'top', toPx ),
  88. left: bind.to( 'left', toPx ),
  89. maxWidth: bind.to( 'maxWidth', toPx )
  90. },
  91. // Make this element `focusable` to be available for adding to FocusTracker.
  92. tabindex: -1
  93. },
  94. children: this.content
  95. } );
  96. }
  97. /**
  98. * Shows the balloon panel.
  99. *
  100. * See {@link #isVisible}.
  101. */
  102. show() {
  103. this.isVisible = true;
  104. }
  105. /**
  106. * Hides the balloon panel.
  107. *
  108. * See {@link #isVisible}.
  109. */
  110. hide() {
  111. this.isVisible = false;
  112. }
  113. /**
  114. * Attaches the balloon panel to a specified DOM element or range with a smart heuristics.
  115. *
  116. * See {@link @link module:utils/dom/position~getOptimalPosition}.
  117. *
  118. * TODO: More docs and examples.
  119. *
  120. * @param {module:utils/dom/position~Options} options Positioning options compatible with
  121. * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
  122. * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
  123. */
  124. attachTo( options ) {
  125. this.show();
  126. const defaultPositions = BalloonPanelView.defaultPositions;
  127. const positionOptions = Object.assign( {}, {
  128. element: this.element,
  129. positions: [
  130. defaultPositions.se,
  131. defaultPositions.sw,
  132. defaultPositions.ne,
  133. defaultPositions.nw
  134. ],
  135. limiter: defaultLimiterElement,
  136. fitInViewport: true
  137. }, options );
  138. const { top, left, name: position } = getOptimalPosition( positionOptions );
  139. Object.assign( this, { top, left, position } );
  140. }
  141. /**
  142. * Works exactly the same as {module:ui/panel/balloon/balloonpanelview~BalloonPanelView.attachTo} with one exception.
  143. * Position of attached panel is constantly updated when any of parents of the target or limiter elements are scrolled
  144. * or when browser window is resized. Thanks to this panel always sticks to the target element.
  145. * See https://github.com/ckeditor/ckeditor5-ui/issues/170.
  146. *
  147. * @param {module:utils/dom/position~Options} options Positioning options compatible with
  148. * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
  149. * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
  150. */
  151. keepAttachedTo( options ) {
  152. // First we need to attach the balloon panel to the target element.
  153. this.attachTo( options );
  154. const target = options.target;
  155. const limiter = options.limiter || defaultLimiterElement;
  156. // Then we need to listen on scroll event of eny element in the document.
  157. this.listenTo( global.document, 'scroll', ( evt, domEvt ) => {
  158. // And update position if scrolled element contains related to the balloon elements.
  159. if ( domEvt.target.contains( target ) || domEvt.target.contains( limiter ) ) {
  160. this.attachTo( options );
  161. }
  162. }, { useCapture: true } );
  163. // We need to listen on window resize event and update position.
  164. this.listenTo( global.window, 'resize', () => this.attachTo( options ) );
  165. // After all we need to clean up the listener.
  166. this.once( 'change:isVisible', () => {
  167. this.stopListening( global.document, 'scroll' );
  168. this.stopListening( global.window, 'resize' );
  169. } );
  170. }
  171. }
  172. /**
  173. * A horizontal offset of the arrow tip from the edge of the balloon. Controlled by CSS.
  174. *
  175. * +-----|---------...
  176. * | |
  177. * | |
  178. * | |
  179. * | |
  180. * +--+ | +------...
  181. * \ | /
  182. * \|/
  183. * >|-----|<---------------- horizontal offset
  184. *
  185. * @default 30
  186. * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHorizontalOffset
  187. */
  188. BalloonPanelView.arrowHorizontalOffset = 30;
  189. /**
  190. * A vertical offset of the arrow from the edge of the balloon. Controlled by CSS.
  191. *
  192. * +-------------...
  193. * |
  194. * |
  195. * | /-- vertical offset
  196. * | V
  197. * +--+ +-----... ---------
  198. * \ / |
  199. * \/ |
  200. * -------------------------------
  201. * ^
  202. *
  203. * @default 15
  204. * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowVerticalOffset
  205. */
  206. BalloonPanelView.arrowVerticalOffset = 15;
  207. /**
  208. * A default set of positioning functions used by the balloon panel view
  209. * when attaching using {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo} method.
  210. *
  211. * The available positioning functions are as follows:
  212. *
  213. * * South east:
  214. *
  215. * [ Target ]
  216. * ^
  217. * +-----------------+
  218. * | Balloon |
  219. * +-----------------+
  220. *
  221. *
  222. * * South west:
  223. *
  224. * [ Target ]
  225. * ^
  226. * +-----------------+
  227. * | Balloon |
  228. * +-----------------+
  229. *
  230. *
  231. * * North east:
  232. *
  233. * +-----------------+
  234. * | Balloon |
  235. * +-----------------+
  236. * V
  237. * [ Target ]
  238. *
  239. *
  240. * * North west:
  241. *
  242. * +-----------------+
  243. * | Balloon |
  244. * +-----------------+
  245. * V
  246. * [ Target ]
  247. *
  248. * See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}.
  249. *
  250. * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
  251. *
  252. * The name that position function returns will be reflected in balloon panel's class that
  253. * controls the placement of the "arrow". See {@link #position} to learn more.
  254. *
  255. * @member {Object} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions
  256. */
  257. BalloonPanelView.defaultPositions = {
  258. se: ( targetRect ) => ( {
  259. top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
  260. left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
  261. name: 'se'
  262. } ),
  263. sw: ( targetRect, balloonRect ) => ( {
  264. top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
  265. left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
  266. name: 'sw'
  267. } ),
  268. ne: ( targetRect, balloonRect ) => ( {
  269. top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
  270. left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
  271. name: 'ne'
  272. } ),
  273. nw: ( targetRect, balloonRect ) => ( {
  274. top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
  275. left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
  276. name: 'nw'
  277. } )
  278. };