plugin.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /**
  2. * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md or http://ckeditor.com/license
  4. */
  5. CKEDITOR.plugins.add( 'floatpanel', {
  6. requires: 'panel'
  7. } );
  8. ( function() {
  9. var panels = {};
  10. function getPanel( editor, doc, parentElement, definition, level ) {
  11. // Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level]
  12. var key = CKEDITOR.tools.genKey( doc.getUniqueId(), parentElement.getUniqueId(), editor.lang.dir, editor.uiColor || '', definition.css || '', level || '' ),
  13. panel = panels[ key ];
  14. if ( !panel ) {
  15. panel = panels[ key ] = new CKEDITOR.ui.panel( doc, definition );
  16. panel.element = parentElement.append( CKEDITOR.dom.element.createFromHtml( panel.render( editor ), doc ) );
  17. panel.element.setStyles( {
  18. display: 'none',
  19. position: 'absolute'
  20. } );
  21. }
  22. return panel;
  23. }
  24. /**
  25. * Represents a floating panel UI element.
  26. *
  27. * It's reused by rich combos, color combos, menus, etc.
  28. * and it renders its content using {@link CKEDITOR.ui.panel}.
  29. *
  30. * @class
  31. * @todo
  32. */
  33. CKEDITOR.ui.floatPanel = CKEDITOR.tools.createClass( {
  34. /**
  35. * Creates a floatPanel class instance.
  36. *
  37. * @constructor
  38. * @param {CKEDITOR.editor} editor
  39. * @param {CKEDITOR.dom.element} parentElement
  40. * @param {Object} definition Definition of the panel that will be floating.
  41. * @param {Number} level
  42. */
  43. $: function( editor, parentElement, definition, level ) {
  44. definition.forceIFrame = 1;
  45. // In case of editor with floating toolbar append panels that should float
  46. // to the main UI element.
  47. if ( definition.toolbarRelated && editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE )
  48. parentElement = CKEDITOR.document.getById( 'cke_' + editor.name );
  49. var doc = parentElement.getDocument(),
  50. panel = getPanel( editor, doc, parentElement, definition, level || 0 ),
  51. element = panel.element,
  52. iframe = element.getFirst(),
  53. that = this;
  54. // Disable native browser menu. (#4825)
  55. element.disableContextMenu();
  56. this.element = element;
  57. this._ = {
  58. editor: editor,
  59. // The panel that will be floating.
  60. panel: panel,
  61. parentElement: parentElement,
  62. definition: definition,
  63. document: doc,
  64. iframe: iframe,
  65. children: [],
  66. dir: editor.lang.dir
  67. };
  68. editor.on( 'mode', hide );
  69. editor.on( 'resize', hide );
  70. // Window resize doesn't cause hide on blur. (#9800)
  71. // [iOS] Poping up keyboard triggers window resize
  72. // which leads to undesired panel hides.
  73. if ( !CKEDITOR.env.iOS )
  74. doc.getWindow().on( 'resize', hide );
  75. // We need a wrapper because events implementation doesn't allow to attach
  76. // one listener more than once for the same event on the same object.
  77. // Remember that floatPanel#hide is shared between all instances.
  78. function hide() {
  79. that.hide();
  80. }
  81. },
  82. proto: {
  83. /**
  84. * @todo
  85. */
  86. addBlock: function( name, block ) {
  87. return this._.panel.addBlock( name, block );
  88. },
  89. /**
  90. * @todo
  91. */
  92. addListBlock: function( name, multiSelect ) {
  93. return this._.panel.addListBlock( name, multiSelect );
  94. },
  95. /**
  96. * @todo
  97. */
  98. getBlock: function( name ) {
  99. return this._.panel.getBlock( name );
  100. },
  101. /**
  102. * Shows panel block.
  103. *
  104. * @param {String} name
  105. * @param {CKEDITOR.dom.element} offsetParent Positioned parent.
  106. * @param {Number} corner
  107. *
  108. * * For LTR (left to right) oriented editor:
  109. * * `1` = top-left
  110. * * `2` = top-right
  111. * * `3` = bottom-right
  112. * * `4` = bottom-left
  113. * * For RTL (right to left):
  114. * * `1` = top-right
  115. * * `2` = top-left
  116. * * `3` = bottom-left
  117. * * `4` = bottom-right
  118. *
  119. * @param {Number} [offsetX=0]
  120. * @param {Number} [offsetY=0]
  121. * @param {Function} [callback] A callback function executed when block positioning is done.
  122. * @todo what do exactly these params mean (especially corner)?
  123. */
  124. showBlock: function( name, offsetParent, corner, offsetX, offsetY, callback ) {
  125. var panel = this._.panel,
  126. block = panel.showBlock( name );
  127. this.allowBlur( false );
  128. // Record from where the focus is when open panel.
  129. var editable = this._.editor.editable();
  130. this._.returnFocus = editable.hasFocus ? editable : new CKEDITOR.dom.element( CKEDITOR.document.$.activeElement );
  131. this._.hideTimeout = 0;
  132. var element = this.element,
  133. iframe = this._.iframe,
  134. // Edge prefers iframe's window to the iframe, just like the rest of the browsers (#13143).
  135. focused = CKEDITOR.env.ie && !CKEDITOR.env.edge ? iframe : new CKEDITOR.dom.window( iframe.$.contentWindow ),
  136. doc = element.getDocument(),
  137. positionedAncestor = this._.parentElement.getPositionedAncestor(),
  138. position = offsetParent.getDocumentPosition( doc ),
  139. positionedAncestorPosition = positionedAncestor ? positionedAncestor.getDocumentPosition( doc ) : { x: 0, y: 0 },
  140. rtl = this._.dir == 'rtl',
  141. left = position.x + ( offsetX || 0 ) - positionedAncestorPosition.x,
  142. top = position.y + ( offsetY || 0 ) - positionedAncestorPosition.y;
  143. // Floating panels are off by (-1px, 0px) in RTL mode. (#3438)
  144. if ( rtl && ( corner == 1 || corner == 4 ) )
  145. left += offsetParent.$.offsetWidth;
  146. else if ( !rtl && ( corner == 2 || corner == 3 ) )
  147. left += offsetParent.$.offsetWidth - 1;
  148. if ( corner == 3 || corner == 4 )
  149. top += offsetParent.$.offsetHeight - 1;
  150. // Memorize offsetParent by it's ID.
  151. this._.panel._.offsetParentId = offsetParent.getId();
  152. element.setStyles( {
  153. top: top + 'px',
  154. left: 0,
  155. display: ''
  156. } );
  157. // Don't use display or visibility style because we need to
  158. // calculate the rendering layout later and focus the element.
  159. element.setOpacity( 0 );
  160. // To allow the context menu to decrease back their width
  161. element.getFirst().removeStyle( 'width' );
  162. // Report to focus manager.
  163. this._.editor.focusManager.add( focused );
  164. // Configure the IFrame blur event. Do that only once.
  165. if ( !this._.blurSet ) {
  166. // With addEventListener compatible browsers, we must
  167. // useCapture when registering the focus/blur events to
  168. // guarantee they will be firing in all situations. (#3068, #3222 )
  169. CKEDITOR.event.useCapture = true;
  170. focused.on( 'blur', function( ev ) {
  171. // As we are using capture to register the listener,
  172. // the blur event may get fired even when focusing
  173. // inside the window itself, so we must ensure the
  174. // target is out of it.
  175. if ( !this.allowBlur() || ev.data.getPhase() != CKEDITOR.EVENT_PHASE_AT_TARGET )
  176. return;
  177. if ( this.visible && !this._.activeChild ) {
  178. // [iOS] Allow hide to be prevented if touch is bound
  179. // to any parent of the iframe blur happens before touch (#10714).
  180. if ( CKEDITOR.env.iOS ) {
  181. if ( !this._.hideTimeout )
  182. this._.hideTimeout = CKEDITOR.tools.setTimeout( doHide, 0, this );
  183. } else {
  184. doHide.call( this );
  185. }
  186. }
  187. function doHide() {
  188. // Panel close is caused by user's navigating away the focus, e.g. click outside the panel.
  189. // DO NOT restore focus in this case.
  190. delete this._.returnFocus;
  191. this.hide();
  192. }
  193. }, this );
  194. focused.on( 'focus', function() {
  195. this._.focused = true;
  196. this.hideChild();
  197. this.allowBlur( true );
  198. }, this );
  199. // [iOS] if touch is bound to any parent of the iframe blur
  200. // happens twice before touchstart and before touchend (#10714).
  201. if ( CKEDITOR.env.iOS ) {
  202. // Prevent false hiding on blur.
  203. // We don't need to return focus here because touchend will fire anyway.
  204. // If user scrolls and pointer gets out of the panel area touchend will also fire.
  205. focused.on( 'touchstart', function() {
  206. clearTimeout( this._.hideTimeout );
  207. }, this );
  208. // Set focus back to handle blur and hide panel when needed.
  209. focused.on( 'touchend', function() {
  210. this._.hideTimeout = 0;
  211. this.focus();
  212. }, this );
  213. }
  214. CKEDITOR.event.useCapture = false;
  215. this._.blurSet = 1;
  216. }
  217. panel.onEscape = CKEDITOR.tools.bind( function( keystroke ) {
  218. if ( this.onEscape && this.onEscape( keystroke ) === false )
  219. return false;
  220. }, this );
  221. CKEDITOR.tools.setTimeout( function() {
  222. var panelLoad = CKEDITOR.tools.bind( function() {
  223. var target = element;
  224. // Reset panel width as the new content can be narrower
  225. // than the old one. (#9355)
  226. target.removeStyle( 'width' );
  227. if ( block.autoSize ) {
  228. var panelDoc = block.element.getDocument();
  229. var width = ( CKEDITOR.env.webkit ? block.element : panelDoc.getBody() ).$.scrollWidth;
  230. // Account for extra height needed due to IE quirks box model bug:
  231. // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
  232. // (#3426)
  233. if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && width > 0 )
  234. width += ( target.$.offsetWidth || 0 ) - ( target.$.clientWidth || 0 ) + 3;
  235. // Add some extra pixels to improve the appearance.
  236. width += 10;
  237. target.setStyle( 'width', width + 'px' );
  238. var height = block.element.$.scrollHeight;
  239. // Account for extra height needed due to IE quirks box model bug:
  240. // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
  241. // (#3426)
  242. if ( CKEDITOR.env.ie && CKEDITOR.env.quirks && height > 0 )
  243. height += ( target.$.offsetHeight || 0 ) - ( target.$.clientHeight || 0 ) + 3;
  244. target.setStyle( 'height', height + 'px' );
  245. // Fix IE < 8 visibility.
  246. panel._.currentBlock.element.setStyle( 'display', 'none' ).removeStyle( 'display' );
  247. } else {
  248. target.removeStyle( 'height' );
  249. }
  250. // Flip panel layout horizontally in RTL with known width.
  251. if ( rtl )
  252. left -= element.$.offsetWidth;
  253. // Pop the style now for measurement.
  254. element.setStyle( 'left', left + 'px' );
  255. /* panel layout smartly fit the viewport size. */
  256. var panelElement = panel.element,
  257. panelWindow = panelElement.getWindow(),
  258. rect = element.$.getBoundingClientRect(),
  259. viewportSize = panelWindow.getViewPaneSize();
  260. // Compensation for browsers that dont support "width" and "height".
  261. var rectWidth = rect.width || rect.right - rect.left,
  262. rectHeight = rect.height || rect.bottom - rect.top;
  263. // Check if default horizontal layout is impossible.
  264. var spaceAfter = rtl ? rect.right : viewportSize.width - rect.left,
  265. spaceBefore = rtl ? viewportSize.width - rect.right : rect.left;
  266. if ( rtl ) {
  267. if ( spaceAfter < rectWidth ) {
  268. // Flip to show on right.
  269. if ( spaceBefore > rectWidth )
  270. left += rectWidth;
  271. // Align to window left.
  272. else if ( viewportSize.width > rectWidth )
  273. left = left - rect.left;
  274. // Align to window right, never cutting the panel at right.
  275. else
  276. left = left - rect.right + viewportSize.width;
  277. }
  278. } else if ( spaceAfter < rectWidth ) {
  279. // Flip to show on left.
  280. if ( spaceBefore > rectWidth )
  281. left -= rectWidth;
  282. // Align to window right.
  283. else if ( viewportSize.width > rectWidth )
  284. left = left - rect.right + viewportSize.width;
  285. // Align to window left, never cutting the panel at left.
  286. else
  287. left = left - rect.left;
  288. }
  289. // Check if the default vertical layout is possible.
  290. var spaceBelow = viewportSize.height - rect.top,
  291. spaceAbove = rect.top;
  292. if ( spaceBelow < rectHeight ) {
  293. // Flip to show above.
  294. if ( spaceAbove > rectHeight )
  295. top -= rectHeight;
  296. // Align to window bottom.
  297. else if ( viewportSize.height > rectHeight )
  298. top = top - rect.bottom + viewportSize.height;
  299. // Align to top, never cutting the panel at top.
  300. else
  301. top = top - rect.top;
  302. }
  303. // If IE is in RTL, we have troubles with absolute
  304. // position and horizontal scrolls. Here we have a
  305. // series of hacks to workaround it. (#6146)
  306. if ( CKEDITOR.env.ie ) {
  307. var offsetParent = new CKEDITOR.dom.element( element.$.offsetParent ),
  308. scrollParent = offsetParent;
  309. // Quirks returns <body>, but standards returns <html>.
  310. if ( scrollParent.getName() == 'html' )
  311. scrollParent = scrollParent.getDocument().getBody();
  312. if ( scrollParent.getComputedStyle( 'direction' ) == 'rtl' ) {
  313. // For IE8, there is not much logic on this, but it works.
  314. if ( CKEDITOR.env.ie8Compat )
  315. left -= element.getDocument().getDocumentElement().$.scrollLeft * 2;
  316. else
  317. left -= ( offsetParent.$.scrollWidth - offsetParent.$.clientWidth );
  318. }
  319. }
  320. // Trigger the onHide event of the previously active panel to prevent
  321. // incorrect styles from being applied (#6170)
  322. var innerElement = element.getFirst(),
  323. activePanel;
  324. if ( ( activePanel = innerElement.getCustomData( 'activePanel' ) ) )
  325. activePanel.onHide && activePanel.onHide.call( this, 1 );
  326. innerElement.setCustomData( 'activePanel', this );
  327. element.setStyles( {
  328. top: top + 'px',
  329. left: left + 'px'
  330. } );
  331. element.setOpacity( 1 );
  332. callback && callback();
  333. }, this );
  334. panel.isLoaded ? panelLoad() : panel.onLoad = panelLoad;
  335. CKEDITOR.tools.setTimeout( function() {
  336. var scrollTop = CKEDITOR.env.webkit && CKEDITOR.document.getWindow().getScrollPosition().y;
  337. // Focus the panel frame first, so blur gets fired.
  338. this.focus();
  339. // Focus the block now.
  340. block.element.focus();
  341. // #10623, #10951 - restore the viewport's scroll position after focusing list element.
  342. if ( CKEDITOR.env.webkit )
  343. CKEDITOR.document.getBody().$.scrollTop = scrollTop;
  344. // We need this get fired manually because of unfired focus() function.
  345. this.allowBlur( true );
  346. this._.editor.fire( 'panelShow', this );
  347. }, 0, this );
  348. }, CKEDITOR.env.air ? 200 : 0, this );
  349. this.visible = 1;
  350. if ( this.onShow )
  351. this.onShow.call( this );
  352. },
  353. /**
  354. * Restores last focused element or simply focus panel window.
  355. */
  356. focus: function() {
  357. // Webkit requires to blur any previous focused page element, in
  358. // order to properly fire the "focus" event.
  359. if ( CKEDITOR.env.webkit ) {
  360. var active = CKEDITOR.document.getActive();
  361. active && !active.equals( this._.iframe ) && active.$.blur();
  362. }
  363. // Restore last focused element or simply focus panel window.
  364. var focus = this._.lastFocused || this._.iframe.getFrameDocument().getWindow();
  365. focus.focus();
  366. },
  367. /**
  368. * @todo
  369. */
  370. blur: function() {
  371. var doc = this._.iframe.getFrameDocument(),
  372. active = doc.getActive();
  373. active && active.is( 'a' ) && ( this._.lastFocused = active );
  374. },
  375. /**
  376. * Hides panel.
  377. *
  378. * @todo
  379. */
  380. hide: function( returnFocus ) {
  381. if ( this.visible && ( !this.onHide || this.onHide.call( this ) !== true ) ) {
  382. this.hideChild();
  383. // Blur previously focused element. (#6671)
  384. CKEDITOR.env.gecko && this._.iframe.getFrameDocument().$.activeElement.blur();
  385. this.element.setStyle( 'display', 'none' );
  386. this.visible = 0;
  387. this.element.getFirst().removeCustomData( 'activePanel' );
  388. // Return focus properly. (#6247)
  389. var focusReturn = returnFocus && this._.returnFocus;
  390. if ( focusReturn ) {
  391. // Webkit requires focus moved out panel iframe first.
  392. if ( CKEDITOR.env.webkit && focusReturn.type )
  393. focusReturn.getWindow().$.focus();
  394. focusReturn.focus();
  395. }
  396. delete this._.lastFocused;
  397. this._.editor.fire( 'panelHide', this );
  398. }
  399. },
  400. /**
  401. * @todo
  402. */
  403. allowBlur: function( allow ) {
  404. // Prevent editor from hiding the panel. (#3222)
  405. var panel = this._.panel;
  406. if ( allow !== undefined )
  407. panel.allowBlur = allow;
  408. return panel.allowBlur;
  409. },
  410. /**
  411. * Shows specified panel as a child of one block of this one.
  412. *
  413. * @param {CKEDITOR.ui.floatPanel} panel
  414. * @param {String} blockName
  415. * @param {CKEDITOR.dom.element} offsetParent Positioned parent.
  416. * @param {Number} corner
  417. *
  418. * * For LTR (left to right) oriented editor:
  419. * * `1` = top-left
  420. * * `2` = top-right
  421. * * `3` = bottom-right
  422. * * `4` = bottom-left
  423. * * For RTL (right to left):
  424. * * `1` = top-right
  425. * * `2` = top-left
  426. * * `3` = bottom-left
  427. * * `4` = bottom-right
  428. *
  429. * @param {Number} [offsetX=0]
  430. * @param {Number} [offsetY=0]
  431. * @todo
  432. */
  433. showAsChild: function( panel, blockName, offsetParent, corner, offsetX, offsetY ) {
  434. // Skip reshowing of child which is already visible.
  435. if ( this._.activeChild == panel && panel._.panel._.offsetParentId == offsetParent.getId() )
  436. return;
  437. this.hideChild();
  438. panel.onHide = CKEDITOR.tools.bind( function() {
  439. // Use a timeout, so we give time for this menu to get
  440. // potentially focused.
  441. CKEDITOR.tools.setTimeout( function() {
  442. if ( !this._.focused )
  443. this.hide();
  444. }, 0, this );
  445. }, this );
  446. this._.activeChild = panel;
  447. this._.focused = false;
  448. panel.showBlock( blockName, offsetParent, corner, offsetX, offsetY );
  449. this.blur();
  450. /* #3767 IE: Second level menu may not have borders */
  451. if ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) {
  452. setTimeout( function() {
  453. panel.element.getChild( 0 ).$.style.cssText += '';
  454. }, 100 );
  455. }
  456. },
  457. /**
  458. * @todo
  459. */
  460. hideChild: function( restoreFocus ) {
  461. var activeChild = this._.activeChild;
  462. if ( activeChild ) {
  463. delete activeChild.onHide;
  464. delete this._.activeChild;
  465. activeChild.hide();
  466. // At this point focus should be moved back to parent panel.
  467. restoreFocus && this.focus();
  468. }
  469. }
  470. }
  471. } );
  472. CKEDITOR.on( 'instanceDestroyed', function() {
  473. var isLastInstance = CKEDITOR.tools.isEmpty( CKEDITOR.instances );
  474. for ( var i in panels ) {
  475. var panel = panels[ i ];
  476. // Safe to destroy it since there're no more instances.(#4241)
  477. if ( isLastInstance )
  478. panel.destroy();
  479. // Panel might be used by other instances, just hide them.(#4552)
  480. else
  481. panel.element.hide();
  482. }
  483. // Remove the registration.
  484. isLastInstance && ( panels = {} );
  485. } );
  486. } )();