8
0

balloonpanelview.js 8.4 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. /* globals document, window */
  9. import View from '../../view';
  10. import Template from '../../template';
  11. import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
  12. import toUnit from '@ckeditor/ckeditor5-utils/src/dom/tounit';
  13. const toPx = toUnit( 'px' );
  14. const defaultLimiterElement = 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. See #170.
  145. *
  146. * @param {module:utils/dom/position~Options} options Positioning options compatible with
  147. * {@link module:utils/dom/position~getOptimalPosition}. Default `positions` array is
  148. * {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions}.
  149. */
  150. keepAttachedTo( options ) {
  151. // First we need to attach the balloon panel to the target element.
  152. this.attachTo( options );
  153. const target = options.target;
  154. const limiter = options.limiter || defaultLimiterElement;
  155. // Then we need to listen on scroll event of eny element in the document.
  156. this.listenTo( document, 'scroll', ( evt, domEvt ) => {
  157. // And update position if scrolled element contains related to the balloon elements.
  158. if ( domEvt.target.contains( target ) || domEvt.target.contains( limiter ) ) {
  159. this.attachTo( options );
  160. }
  161. }, { useCapture: true } );
  162. // We need to listen on window resize event and update position.
  163. this.listenTo( window, 'resize', () => this.attachTo( options ) );
  164. // After all we need to clean up the listener.
  165. this.once( 'change:isVisible', () => {
  166. this.stopListening( document, 'scroll' );
  167. this.stopListening( window, 'resize' );
  168. } );
  169. }
  170. }
  171. /**
  172. * A horizontal offset of the arrow tip from the edge of the balloon. Controlled by CSS.
  173. *
  174. * +-----|---------...
  175. * | |
  176. * | |
  177. * | |
  178. * | |
  179. * +--+ | +------...
  180. * \ | /
  181. * \|/
  182. * >|-----|<---------------- horizontal offset
  183. *
  184. * @default 30
  185. * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowHorizontalOffset
  186. */
  187. BalloonPanelView.arrowHorizontalOffset = 30;
  188. /**
  189. * A vertical offset of the arrow from the edge of the balloon. Controlled by CSS.
  190. *
  191. * +-------------...
  192. * |
  193. * |
  194. * | /-- vertical offset
  195. * | V
  196. * +--+ +-----... ---------
  197. * \ / |
  198. * \/ |
  199. * -------------------------------
  200. * ^
  201. *
  202. * @default 15
  203. * @member {Number} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.arrowVerticalOffset
  204. */
  205. BalloonPanelView.arrowVerticalOffset = 15;
  206. /**
  207. * A default set of positioning functions used by the balloon panel view
  208. * when attaching using {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo} method.
  209. *
  210. * The available positioning functions are as follows:
  211. *
  212. * * South east:
  213. *
  214. * [ Target ]
  215. * ^
  216. * +-----------------+
  217. * | Balloon |
  218. * +-----------------+
  219. *
  220. *
  221. * * South west:
  222. *
  223. * [ Target ]
  224. * ^
  225. * +-----------------+
  226. * | Balloon |
  227. * +-----------------+
  228. *
  229. *
  230. * * North east:
  231. *
  232. * +-----------------+
  233. * | Balloon |
  234. * +-----------------+
  235. * V
  236. * [ Target ]
  237. *
  238. *
  239. * * North west:
  240. *
  241. * +-----------------+
  242. * | Balloon |
  243. * +-----------------+
  244. * V
  245. * [ Target ]
  246. *
  247. * See {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView#attachTo}.
  248. *
  249. * Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
  250. *
  251. * The name that position function returns will be reflected in balloon panel's class that
  252. * controls the placement of the "arrow". See {@link #position} to learn more.
  253. *
  254. * @member {Object} module:ui/panel/balloon/balloonpanelview~BalloonPanelView.defaultPositions
  255. */
  256. BalloonPanelView.defaultPositions = {
  257. se: ( targetRect ) => ( {
  258. top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
  259. left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
  260. name: 'se'
  261. } ),
  262. sw: ( targetRect, balloonRect ) => ( {
  263. top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
  264. left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
  265. name: 'sw'
  266. } ),
  267. ne: ( targetRect, balloonRect ) => ( {
  268. top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
  269. left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
  270. name: 'ne'
  271. } ),
  272. nw: ( targetRect, balloonRect ) => ( {
  273. top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
  274. left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
  275. name: 'nw'
  276. } )
  277. };