2 * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
6 CKEDITOR
.plugins
.add( 'floatpanel', {
13 function getPanel( editor
, doc
, parentElement
, definition
, level
) {
14 // Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level]
15 var key
= CKEDITOR
.tools
.genKey( doc
.getUniqueId(), parentElement
.getUniqueId(), editor
.lang
.dir
, editor
.uiColor
|| '', definition
.css
|| '', level
|| '' ),
16 panel
= panels
[ key
];
19 panel
= panels
[ key
] = new CKEDITOR
.ui
.panel( doc
, definition
);
20 panel
.element
= parentElement
.append( CKEDITOR
.dom
.element
.createFromHtml( panel
.render( editor
), doc
) );
22 panel
.element
.setStyles( {
32 * Represents a floating panel UI element.
34 * It is reused by rich combos, color combos, menus, etc.
35 * and it renders its content using {@link CKEDITOR.ui.panel}.
40 CKEDITOR
.ui
.floatPanel
= CKEDITOR
.tools
.createClass( {
42 * Creates a floatPanel class instance.
45 * @param {CKEDITOR.editor} editor
46 * @param {CKEDITOR.dom.element} parentElement
47 * @param {Object} definition Definition of the panel that will be floating.
48 * @param {Number} level
50 $: function( editor
, parentElement
, definition
, level
) {
51 definition
.forceIFrame
= 1;
53 // In case of editor with floating toolbar append panels that should float
54 // to the main UI element.
55 if ( definition
.toolbarRelated
&& editor
.elementMode
== CKEDITOR
.ELEMENT_MODE_INLINE
)
56 parentElement
= CKEDITOR
.document
.getById( 'cke_' + editor
.name
);
58 var doc
= parentElement
.getDocument(),
59 panel
= getPanel( editor
, doc
, parentElement
, definition
, level
|| 0 ),
60 element
= panel
.element
,
61 iframe
= element
.getFirst(),
64 // Disable native browser menu. (http://dev.ckeditor.com/ticket/4825)
65 element
.disableContextMenu();
67 this.element
= element
;
71 // The panel that will be floating.
73 parentElement: parentElement
,
74 definition: definition
,
82 editor
.on( 'mode', hide
);
83 editor
.on( 'resize', hide
);
85 // When resize of the window is triggered floatpanel should be repositioned according to new dimensions.
86 // http://dev.ckeditor.com/ticket/11724. Fixes issue with undesired panel hiding on Android and iOS.
87 doc
.getWindow().on( 'resize', function() {
91 // We need a wrapper because events implementation doesn't allow to attach
92 // one listener more than once for the same event on the same object.
93 // Remember that floatPanel#hide is shared between all instances.
103 addBlock: function( name
, block
) {
104 return this._
.panel
.addBlock( name
, block
);
110 addListBlock: function( name
, multiSelect
) {
111 return this._
.panel
.addListBlock( name
, multiSelect
);
117 getBlock: function( name
) {
118 return this._
.panel
.getBlock( name
);
122 * Shows the panel block.
124 * @param {String} name
125 * @param {CKEDITOR.dom.element} offsetParent Positioned parent.
126 * @param {Number} corner
128 * * For LTR (left to right) oriented editor:
131 * * `3` = bottom-right
132 * * `4` = bottom-left
133 * * For RTL (right to left):
136 * * `3` = bottom-left
137 * * `4` = bottom-right
139 * @param {Number} [offsetX=0]
140 * @param {Number} [offsetY=0]
141 * @param {Function} [callback] A callback function executed when block positioning is done.
142 * @todo what do exactly these params mean (especially corner)?
144 showBlock: function( name
, offsetParent
, corner
, offsetX
, offsetY
, callback
) {
145 var panel
= this._
.panel
,
146 block
= panel
.showBlock( name
);
148 this._
.showBlockParams
= [].slice
.call( arguments
);
149 this.allowBlur( false );
151 // Record from where the focus is when open panel.
152 var editable
= this._
.editor
.editable();
153 this._
.returnFocus
= editable
.hasFocus
? editable : new CKEDITOR
.dom
.element( CKEDITOR
.document
.$.activeElement
);
154 this._
.hideTimeout
= 0;
156 var element
= this.element
,
157 iframe
= this._
.iframe
,
158 // Edge prefers iframe's window to the iframe, just like the rest of the browsers (http://dev.ckeditor.com/ticket/13143).
159 focused
= CKEDITOR
.env
.ie
&& !CKEDITOR
.env
.edge
? iframe : new CKEDITOR
.dom
.window( iframe
.$.contentWindow
),
160 doc
= element
.getDocument(),
161 positionedAncestor
= this._
.parentElement
.getPositionedAncestor(),
162 position
= offsetParent
.getDocumentPosition( doc
),
163 positionedAncestorPosition
= positionedAncestor
? positionedAncestor
.getDocumentPosition( doc
) : { x: 0, y: 0 },
164 rtl
= this._
.dir
== 'rtl',
165 left
= position
.x
+ ( offsetX
|| 0 ) - positionedAncestorPosition
.x
,
166 top
= position
.y
+ ( offsetY
|| 0 ) - positionedAncestorPosition
.y
;
168 // Floating panels are off by (-1px, 0px) in RTL mode. (http://dev.ckeditor.com/ticket/3438)
169 if ( rtl
&& ( corner
== 1 || corner
== 4 ) )
170 left
+= offsetParent
.$.offsetWidth
;
171 else if ( !rtl
&& ( corner
== 2 || corner
== 3 ) )
172 left
+= offsetParent
.$.offsetWidth
- 1;
174 if ( corner
== 3 || corner
== 4 )
175 top
+= offsetParent
.$.offsetHeight
- 1;
177 // Memorize offsetParent by it's ID.
178 this._
.panel
._
.offsetParentId
= offsetParent
.getId();
186 // Don't use display or visibility style because we need to
187 // calculate the rendering layout later and focus the element.
188 element
.setOpacity( 0 );
190 // To allow the context menu to decrease back their width
191 element
.getFirst().removeStyle( 'width' );
193 // Report to focus manager.
194 this._
.editor
.focusManager
.add( focused
);
196 // Configure the IFrame blur event. Do that only once.
197 if ( !this._
.blurSet
) {
199 // With addEventListener compatible browsers, we must
200 // useCapture when registering the focus/blur events to
201 // guarantee they will be firing in all situations. (http://dev.ckeditor.com/ticket/3068, http://dev.ckeditor.com/ticket/3222 )
202 CKEDITOR
.event
.useCapture
= true;
204 focused
.on( 'blur', function( ev
) {
205 // As we are using capture to register the listener,
206 // the blur event may get fired even when focusing
207 // inside the window itself, so we must ensure the
208 // target is out of it.
209 if ( !this.allowBlur() || ev
.data
.getPhase() != CKEDITOR
.EVENT_PHASE_AT_TARGET
)
212 if ( this.visible
&& !this._
.activeChild
) {
213 // [iOS] Allow hide to be prevented if touch is bound
214 // to any parent of the iframe blur happens before touch (http://dev.ckeditor.com/ticket/10714).
215 if ( CKEDITOR
.env
.iOS
) {
216 if ( !this._
.hideTimeout
)
217 this._
.hideTimeout
= CKEDITOR
.tools
.setTimeout( doHide
, 0, this );
224 // Panel close is caused by user's navigating away the focus, e.g. click outside the panel.
225 // DO NOT restore focus in this case.
226 delete this._
.returnFocus
;
231 focused
.on( 'focus', function() {
232 this._
.focused
= true;
234 this.allowBlur( true );
237 // [iOS] if touch is bound to any parent of the iframe blur
238 // happens twice before touchstart and before touchend (http://dev.ckeditor.com/ticket/10714).
239 if ( CKEDITOR
.env
.iOS
) {
240 // Prevent false hiding on blur.
241 // We don't need to return focus here because touchend will fire anyway.
242 // If user scrolls and pointer gets out of the panel area touchend will also fire.
243 focused
.on( 'touchstart', function() {
244 clearTimeout( this._
.hideTimeout
);
247 // Set focus back to handle blur and hide panel when needed.
248 focused
.on( 'touchend', function() {
249 this._
.hideTimeout
= 0;
254 CKEDITOR
.event
.useCapture
= false;
259 panel
.onEscape
= CKEDITOR
.tools
.bind( function( keystroke
) {
260 if ( this.onEscape
&& this.onEscape( keystroke
) === false )
264 CKEDITOR
.tools
.setTimeout( function() {
265 var panelLoad
= CKEDITOR
.tools
.bind( function() {
266 var target
= element
;
268 // Reset panel width as the new content can be narrower
269 // than the old one. (http://dev.ckeditor.com/ticket/9355)
270 target
.removeStyle( 'width' );
272 if ( block
.autoSize
) {
273 var panelDoc
= block
.element
.getDocument(),
274 width
= ( ( CKEDITOR
.env
.webkit
|| CKEDITOR
.env
.edge
) ? block
.element : panelDoc
.getBody() ).$.scrollWidth
;
276 // Account for extra height needed due to IE quirks box model bug:
277 // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
278 // (http://dev.ckeditor.com/ticket/3426)
279 if ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.quirks
&& width
> 0 )
280 width
+= ( target
.$.offsetWidth
|| 0 ) - ( target
.$.clientWidth
|| 0 ) + 3;
282 // Add some extra pixels to improve the appearance.
285 target
.setStyle( 'width', width
+ 'px' );
287 var height
= block
.element
.$.scrollHeight
;
289 // Account for extra height needed due to IE quirks box model bug:
290 // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug
291 // (http://dev.ckeditor.com/ticket/3426)
292 if ( CKEDITOR
.env
.ie
&& CKEDITOR
.env
.quirks
&& height
> 0 )
293 height
+= ( target
.$.offsetHeight
|| 0 ) - ( target
.$.clientHeight
|| 0 ) + 3;
295 target
.setStyle( 'height', height
+ 'px' );
297 // Fix IE < 8 visibility.
298 panel
._
.currentBlock
.element
.setStyle( 'display', 'none' ).removeStyle( 'display' );
300 target
.removeStyle( 'height' );
303 // Flip panel layout horizontally in RTL with known width.
305 left
-= element
.$.offsetWidth
;
307 // Pop the style now for measurement.
308 element
.setStyle( 'left', left
+ 'px' );
310 /* panel layout smartly fit the viewport size. */
311 var panelElement
= panel
.element
,
312 panelWindow
= panelElement
.getWindow(),
313 rect
= element
.$.getBoundingClientRect(),
314 viewportSize
= panelWindow
.getViewPaneSize();
316 // Compensation for browsers that dont support "width" and "height".
317 var rectWidth
= rect
.width
|| rect
.right
- rect
.left
,
318 rectHeight
= rect
.height
|| rect
.bottom
- rect
.top
;
320 // Check if default horizontal layout is impossible.
321 var spaceAfter
= rtl
? rect
.right : viewportSize
.width
- rect
.left
,
322 spaceBefore
= rtl
? viewportSize
.width
- rect
.right : rect
.left
;
325 if ( spaceAfter
< rectWidth
) {
326 // Flip to show on right.
327 if ( spaceBefore
> rectWidth
)
329 // Align to window left.
330 else if ( viewportSize
.width
> rectWidth
)
331 left
= left
- rect
.left
;
332 // Align to window right, never cutting the panel at right.
334 left
= left
- rect
.right
+ viewportSize
.width
;
336 } else if ( spaceAfter
< rectWidth
) {
337 // Flip to show on left.
338 if ( spaceBefore
> rectWidth
)
340 // Align to window right.
341 else if ( viewportSize
.width
> rectWidth
)
342 left
= left
- rect
.right
+ viewportSize
.width
;
343 // Align to window left, never cutting the panel at left.
345 left
= left
- rect
.left
;
349 // Check if the default vertical layout is possible.
350 var spaceBelow
= viewportSize
.height
- rect
.top
,
351 spaceAbove
= rect
.top
;
353 if ( spaceBelow
< rectHeight
) {
354 // Flip to show above.
355 if ( spaceAbove
> rectHeight
)
357 // Align to window bottom.
358 else if ( viewportSize
.height
> rectHeight
)
359 top
= top
- rect
.bottom
+ viewportSize
.height
;
360 // Align to top, never cutting the panel at top.
362 top
= top
- rect
.top
;
365 // If IE is in RTL, we have troubles with absolute
366 // position and horizontal scrolls. Here we have a
367 // series of hacks to workaround it. (http://dev.ckeditor.com/ticket/6146)
368 if ( CKEDITOR
.env
.ie
) {
369 var offsetParent
= new CKEDITOR
.dom
.element( element
.$.offsetParent
),
370 scrollParent
= offsetParent
;
372 // Quirks returns <body>, but standards returns <html>.
373 if ( scrollParent
.getName() == 'html' )
374 scrollParent
= scrollParent
.getDocument().getBody();
376 if ( scrollParent
.getComputedStyle( 'direction' ) == 'rtl' ) {
377 // For IE8, there is not much logic on this, but it works.
378 if ( CKEDITOR
.env
.ie8Compat
)
379 left
-= element
.getDocument().getDocumentElement().$.scrollLeft
* 2;
381 left
-= ( offsetParent
.$.scrollWidth
- offsetParent
.$.clientWidth
);
385 // Trigger the onHide event of the previously active panel to prevent
386 // incorrect styles from being applied (http://dev.ckeditor.com/ticket/6170)
387 var innerElement
= element
.getFirst(),
389 if ( ( activePanel
= innerElement
.getCustomData( 'activePanel' ) ) )
390 activePanel
.onHide
&& activePanel
.onHide
.call( this, 1 );
391 innerElement
.setCustomData( 'activePanel', this );
397 element
.setOpacity( 1 );
399 callback
&& callback();
402 panel
.isLoaded
? panelLoad() : panel
.onLoad
= panelLoad
;
404 CKEDITOR
.tools
.setTimeout( function() {
405 var scrollTop
= CKEDITOR
.env
.webkit
&& CKEDITOR
.document
.getWindow().getScrollPosition().y
;
407 // Focus the panel frame first, so blur gets fired.
410 // Focus the block now.
411 block
.element
.focus();
413 // http://dev.ckeditor.com/ticket/10623, http://dev.ckeditor.com/ticket/10951 - restore the viewport's scroll position after focusing list element.
414 if ( CKEDITOR
.env
.webkit
)
415 CKEDITOR
.document
.getBody().$.scrollTop
= scrollTop
;
417 // We need this get fired manually because of unfired focus() function.
418 this.allowBlur( true );
420 // Ensure that the first item is focused (http://dev.ckeditor.com/ticket/16804).
421 if ( CKEDITOR
.env
.ie
) {
422 CKEDITOR
.tools
.setTimeout( function() {
423 block
.markFirstDisplayed
? block
.markFirstDisplayed() : block
._
.markFirstDisplayed();
426 block
.markFirstDisplayed
? block
.markFirstDisplayed() : block
._
.markFirstDisplayed();
429 this._
.editor
.fire( 'panelShow', this );
431 }, CKEDITOR
.env
.air
? 200 : 0, this );
435 this.onShow
.call( this );
439 * Repositions the panel with the same parameters that were used in the last {@link #showBlock} call.
443 reposition: function() {
444 var blockParams
= this._
.showBlockParams
;
446 if ( this.visible
&& this._
.showBlockParams
) {
448 this.showBlock
.apply( this, blockParams
);
453 * Restores the last focused element or simply focuses the panel window.
456 // Webkit requires to blur any previous focused page element, in
457 // order to properly fire the "focus" event.
458 if ( CKEDITOR
.env
.webkit
) {
459 var active
= CKEDITOR
.document
.getActive();
460 active
&& !active
.equals( this._
.iframe
) && active
.$.blur();
463 // Restore last focused element or simply focus panel window.
464 var focus
= this._
.lastFocused
|| this._
.iframe
.getFrameDocument().getWindow();
472 var doc
= this._
.iframe
.getFrameDocument(),
473 active
= doc
.getActive();
475 active
&& active
.is( 'a' ) && ( this._
.lastFocused
= active
);
483 hide: function( returnFocus
) {
484 if ( this.visible
&& ( !this.onHide
|| this.onHide
.call( this ) !== true ) ) {
486 // Blur previously focused element. (http://dev.ckeditor.com/ticket/6671)
487 CKEDITOR
.env
.gecko
&& this._
.iframe
.getFrameDocument().$.activeElement
.blur();
488 this.element
.setStyle( 'display', 'none' );
490 this.element
.getFirst().removeCustomData( 'activePanel' );
492 // Return focus properly. (http://dev.ckeditor.com/ticket/6247)
493 var focusReturn
= returnFocus
&& this._
.returnFocus
;
495 // Webkit requires focus moved out panel iframe first.
496 if ( CKEDITOR
.env
.webkit
&& focusReturn
.type
)
497 focusReturn
.getWindow().$.focus();
502 delete this._
.lastFocused
;
503 this._
.showBlockParams
= null;
505 this._
.editor
.fire( 'panelHide', this );
512 allowBlur: function( allow
) {
513 // Prevent editor from hiding the panel. (http://dev.ckeditor.com/ticket/3222)
514 var panel
= this._
.panel
;
515 if ( allow
!== undefined )
516 panel
.allowBlur
= allow
;
518 return panel
.allowBlur
;
522 * Shows the specified panel as a child of one block of this one.
524 * @param {CKEDITOR.ui.floatPanel} panel
525 * @param {String} blockName
526 * @param {CKEDITOR.dom.element} offsetParent Positioned parent.
527 * @param {Number} corner
529 * * For LTR (left to right) oriented editor:
532 * * `3` = bottom-right
533 * * `4` = bottom-left
534 * * For RTL (right to left):
537 * * `3` = bottom-left
538 * * `4` = bottom-right
540 * @param {Number} [offsetX=0]
541 * @param {Number} [offsetY=0]
544 showAsChild: function( panel
, blockName
, offsetParent
, corner
, offsetX
, offsetY
) {
545 // Skip reshowing of child which is already visible.
546 if ( this._
.activeChild
== panel
&& panel
._
.panel
._
.offsetParentId
== offsetParent
.getId() )
551 panel
.onHide
= CKEDITOR
.tools
.bind( function() {
552 // Use a timeout, so we give time for this menu to get
553 // potentially focused.
554 CKEDITOR
.tools
.setTimeout( function() {
555 if ( !this._
.focused
)
560 this._
.activeChild
= panel
;
561 this._
.focused
= false;
563 panel
.showBlock( blockName
, offsetParent
, corner
, offsetX
, offsetY
);
566 /* http://dev.ckeditor.com/ticket/3767 IE: Second level menu may not have borders */
567 if ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.ie6Compat
) {
568 setTimeout( function() {
569 panel
.element
.getChild( 0 ).$.style
.cssText
+= '';
577 hideChild: function( restoreFocus
) {
578 var activeChild
= this._
.activeChild
;
581 delete activeChild
.onHide
;
582 delete this._
.activeChild
;
585 // At this point focus should be moved back to parent panel.
586 restoreFocus
&& this.focus();
592 CKEDITOR
.on( 'instanceDestroyed', function() {
593 var isLastInstance
= CKEDITOR
.tools
.isEmpty( CKEDITOR
.instances
);
595 for ( var i
in panels
) {
596 var panel
= panels
[ i
];
597 // Safe to destroy it since there're no more instances.(http://dev.ckeditor.com/ticket/4241)
598 if ( isLastInstance
)
600 // Panel might be used by other instances, just hide them.(http://dev.ckeditor.com/ticket/4552)
602 panel
.element
.hide();
604 // Remove the registration.
605 isLastInstance
&& ( panels
= {} );