/** * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ CKEDITOR.plugins.add("floatpanel", { requires: "panel" }); (function() { var panels = {}; function getPanel(editor, doc, parentElement, definition, level) { // Generates the panel key: docId-eleId-skinName-langDir[-uiColor][-CSSs][-level] var key = CKEDITOR.tools.genKey( doc.getUniqueId(), parentElement.getUniqueId(), editor.lang.dir, editor.uiColor || "", definition.css || "", level || "" ), panel = panels[key]; if (!panel) { panel = panels[key] = new CKEDITOR.ui.panel(doc, definition); panel.element = parentElement.append( CKEDITOR.dom.element.createFromHtml(panel.render(editor), doc) ); panel.element.setStyles({ display: "none", position: "absolute" }); } return panel; } /** * Represents a floating panel UI element. * * It is reused by rich combos, color combos, menus, etc. * and it renders its content using {@link CKEDITOR.ui.panel}. * * @class * @todo */ CKEDITOR.ui.floatPanel = CKEDITOR.tools.createClass({ /** * Creates a floatPanel class instance. * * @constructor * @param {CKEDITOR.editor} editor * @param {CKEDITOR.dom.element} parentElement * @param {Object} definition Definition of the panel that will be floating. * @param {Number} level */ $: function(editor, parentElement, definition, level) { definition.forceIFrame = 1; // In case of editor with floating toolbar append panels that should float // to the main UI element. if ( definition.toolbarRelated && editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE ) parentElement = CKEDITOR.document.getById("cke_" + editor.name); var doc = parentElement.getDocument(), panel = getPanel(editor, doc, parentElement, definition, level || 0), element = panel.element, iframe = element.getFirst(), that = this; // Disable native browser menu. (https://dev.ckeditor.com/ticket/4825) element.disableContextMenu(); this.element = element; this._ = { editor: editor, // The panel that will be floating. panel: panel, parentElement: parentElement, definition: definition, document: doc, iframe: iframe, children: [], dir: editor.lang.dir, showBlockParams: null, markFirst: definition.markFirst !== undefined ? definition.markFirst : true }; editor.on("mode", hide); editor.on("resize", hide); // When resize of the window is triggered floatpanel should be repositioned according to new dimensions. // https://dev.ckeditor.com/ticket/11724. Fixes issue with undesired panel hiding on Android and iOS. doc.getWindow().on( "resize", function() { this.reposition(); }, this ); // We need a wrapper because events implementation doesn't allow to attach // one listener more than once for the same event on the same object. // Remember that floatPanel#hide is shared between all instances. function hide() { that.hide(); } }, proto: { /** * @todo */ addBlock: function(name, block) { return this._.panel.addBlock(name, block); }, /** * @todo */ addListBlock: function(name, multiSelect) { return this._.panel.addListBlock(name, multiSelect); }, /** * @todo */ getBlock: function(name) { return this._.panel.getBlock(name); }, /** * Shows the panel block. * * @param {String} name * @param {CKEDITOR.dom.element} offsetParent Positioned parent. * @param {Number} corner * * * For LTR (left to right) oriented editor: * * `1` = top-left * * `2` = top-right * * `3` = bottom-right * * `4` = bottom-left * * For RTL (right to left): * * `1` = top-right * * `2` = top-left * * `3` = bottom-left * * `4` = bottom-right * * @param {Number} [offsetX=0] * @param {Number} [offsetY=0] * @param {Function} [callback] A callback function executed when block positioning is done. * @todo what do exactly these params mean (especially corner)? */ showBlock: function( name, offsetParent, corner, offsetX, offsetY, callback ) { var panel = this._.panel, block = panel.showBlock(name); this._.showBlockParams = [].slice.call(arguments); this.allowBlur(false); // Record from where the focus is when open panel. var editable = this._.editor.editable(); this._.returnFocus = editable.hasFocus ? editable : new CKEDITOR.dom.element(CKEDITOR.document.$.activeElement); this._.hideTimeout = 0; var element = this.element, iframe = this._.iframe, // Edge prefers iframe's window to the iframe, just like the rest of the browsers (https://dev.ckeditor.com/ticket/13143). focused = CKEDITOR.env.ie && !CKEDITOR.env.edge ? iframe : new CKEDITOR.dom.window(iframe.$.contentWindow), doc = element.getDocument(), positionedAncestor = this._.parentElement.getPositionedAncestor(), position = offsetParent.getDocumentPosition(doc), positionedAncestorPosition = positionedAncestor ? positionedAncestor.getDocumentPosition(doc) : { x: 0, y: 0 }, rtl = this._.dir == "rtl", left = position.x + (offsetX || 0) - positionedAncestorPosition.x, top = position.y + (offsetY || 0) - positionedAncestorPosition.y; // Floating panels are off by (-1px, 0px) in RTL mode. (https://dev.ckeditor.com/ticket/3438) if (rtl && (corner == 1 || corner == 4)) left += offsetParent.$.offsetWidth; else if (!rtl && (corner == 2 || corner == 3)) left += offsetParent.$.offsetWidth - 1; if (corner == 3 || corner == 4) top += offsetParent.$.offsetHeight - 1; // Memorize offsetParent by it's ID. this._.panel._.offsetParentId = offsetParent.getId(); element.setStyles({ top: top + "px", left: 0, display: "" }); // Don't use display or visibility style because we need to // calculate the rendering layout later and focus the element. element.setOpacity(0); // To allow the context menu to decrease back their width element.getFirst().removeStyle("width"); // Report to focus manager. this._.editor.focusManager.add(focused); // Configure the IFrame blur event. Do that only once. if (!this._.blurSet) { // With addEventListener compatible browsers, we must // useCapture when registering the focus/blur events to // guarantee they will be firing in all situations. (https://dev.ckeditor.com/ticket/3068, https://dev.ckeditor.com/ticket/3222 ) CKEDITOR.event.useCapture = true; focused.on( "blur", function(ev) { // As we are using capture to register the listener, // the blur event may get fired even when focusing // inside the window itself, so we must ensure the // target is out of it. if ( !this.allowBlur() || ev.data.getPhase() != CKEDITOR.EVENT_PHASE_AT_TARGET ) return; if (this.visible && !this._.activeChild) { // [iOS] Allow hide to be prevented if touch is bound // to any parent of the iframe blur happens before touch (https://dev.ckeditor.com/ticket/10714). if (CKEDITOR.env.iOS) { if (!this._.hideTimeout) this._.hideTimeout = CKEDITOR.tools.setTimeout( doHide, 0, this ); } else { doHide.call(this); } } function doHide() { // Panel close is caused by user's navigating away the focus, e.g. click outside the panel. // DO NOT restore focus in this case. delete this._.returnFocus; this.hide(); } }, this ); focused.on( "focus", function() { this._.focused = true; this.hideChild(); this.allowBlur(true); }, this ); // [iOS] if touch is bound to any parent of the iframe blur // happens twice before touchstart and before touchend (https://dev.ckeditor.com/ticket/10714). if (CKEDITOR.env.iOS) { // Prevent false hiding on blur. // We don't need to return focus here because touchend will fire anyway. // If user scrolls and pointer gets out of the panel area touchend will also fire. focused.on( "touchstart", function() { clearTimeout(this._.hideTimeout); }, this ); // Set focus back to handle blur and hide panel when needed. focused.on( "touchend", function() { this._.hideTimeout = 0; this.focus(); }, this ); } CKEDITOR.event.useCapture = false; this._.blurSet = 1; } panel.onEscape = CKEDITOR.tools.bind(function(keystroke) { if (this.onEscape && this.onEscape(keystroke) === false) return false; }, this); CKEDITOR.tools.setTimeout( function() { var panelLoad = CKEDITOR.tools.bind(function() { var target = element; // Reset panel width as the new content can be narrower // than the old one. (https://dev.ckeditor.com/ticket/9355) target.removeStyle("width"); if (block.autoSize) { var panelDoc = block.element.getDocument(), width = (CKEDITOR.env.webkit || CKEDITOR.env.edge ? block.element : panelDoc.getBody() ).$.scrollWidth; // Account for extra height needed due to IE quirks box model bug: // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug // (https://dev.ckeditor.com/ticket/3426) if (CKEDITOR.env.ie && CKEDITOR.env.quirks && width > 0) width += (target.$.offsetWidth || 0) - (target.$.clientWidth || 0) + 3; // Add some extra pixels to improve the appearance. width += 10; target.setStyle("width", width + "px"); var height = block.element.$.scrollHeight; // Account for extra height needed due to IE quirks box model bug: // http://en.wikipedia.org/wiki/Internet_Explorer_box_model_bug // (https://dev.ckeditor.com/ticket/3426) if (CKEDITOR.env.ie && CKEDITOR.env.quirks && height > 0) height += (target.$.offsetHeight || 0) - (target.$.clientHeight || 0) + 3; target.setStyle("height", height + "px"); // Fix IE < 8 visibility. panel._.currentBlock.element .setStyle("display", "none") .removeStyle("display"); } else { target.removeStyle("height"); } // Flip panel layout horizontally in RTL with known width. if (rtl) left -= element.$.offsetWidth; // Pop the style now for measurement. element.setStyle("left", left + "px"); /* panel layout smartly fit the viewport size. */ var panelElement = panel.element, panelWindow = panelElement.getWindow(), rect = element.$.getBoundingClientRect(), viewportSize = panelWindow.getViewPaneSize(); // Compensation for browsers that dont support "width" and "height". var rectWidth = rect.width || rect.right - rect.left, rectHeight = rect.height || rect.bottom - rect.top; // Check if default horizontal layout is impossible. var spaceAfter = rtl ? rect.right : viewportSize.width - rect.left, spaceBefore = rtl ? viewportSize.width - rect.right : rect.left; if (rtl) { if (spaceAfter < rectWidth) { // Flip to show on right. if (spaceBefore > rectWidth) left += rectWidth; // Align to window left. else if (viewportSize.width > rectWidth) left = left - rect.left; // Align to window right, never cutting the panel at right. else left = left - rect.right + viewportSize.width; } } else if (spaceAfter < rectWidth) { // Flip to show on left. if (spaceBefore > rectWidth) left -= rectWidth; // Align to window right. else if (viewportSize.width > rectWidth) left = left - rect.right + viewportSize.width; // Align to window left, never cutting the panel at left. else left = left - rect.left; } // Check if the default vertical layout is possible. var spaceBelow = viewportSize.height - rect.top, spaceAbove = rect.top; if (spaceBelow < rectHeight) { // Flip to show above. if (spaceAbove > rectHeight) top -= rectHeight; // Align to window bottom. else if (viewportSize.height > rectHeight) top = top - rect.bottom + viewportSize.height; // Align to top, never cutting the panel at top. else top = top - rect.top; } // If IE is in RTL, we have troubles with absolute // position and horizontal scrolls. Here we have a // series of hacks to workaround it. (https://dev.ckeditor.com/ticket/6146) if (CKEDITOR.env.ie && !CKEDITOR.env.edge) { var offsetParent = new CKEDITOR.dom.element( element.$.offsetParent ), scrollParent = offsetParent; // Quirks returns , but standards returns . if (scrollParent.getName() == "html") { scrollParent = scrollParent.getDocument().getBody(); } if (scrollParent.getComputedStyle("direction") == "rtl") { // For IE8, there is not much logic on this, but it works. if (CKEDITOR.env.ie8Compat) { left -= element.getDocument().getDocumentElement().$.scrollLeft * 2; } else { left -= offsetParent.$.scrollWidth - offsetParent.$.clientWidth; } } } // Trigger the onHide event of the previously active panel to prevent // incorrect styles from being applied (https://dev.ckeditor.com/ticket/6170) var innerElement = element.getFirst(), activePanel; if ((activePanel = innerElement.getCustomData("activePanel"))) activePanel.onHide && activePanel.onHide.call(this, 1); innerElement.setCustomData("activePanel", this); element.setStyles({ top: top + "px", left: left + "px" }); element.setOpacity(1); callback && callback(); }, this); panel.isLoaded ? panelLoad() : (panel.onLoad = panelLoad); CKEDITOR.tools.setTimeout( function() { var scrollTop = CKEDITOR.env.webkit && CKEDITOR.document.getWindow().getScrollPosition().y; // Focus the panel frame first, so blur gets fired. this.focus(); // Focus the block now. block.element.focus(); // https://dev.ckeditor.com/ticket/10623, https://dev.ckeditor.com/ticket/10951 - restore the viewport's scroll position after focusing list element. if (CKEDITOR.env.webkit) CKEDITOR.document.getBody().$.scrollTop = scrollTop; // We need this get fired manually because of unfired focus() function. this.allowBlur(true); // Ensure that the first item is focused (https://dev.ckeditor.com/ticket/16804). if (this._.markFirst) { if (CKEDITOR.env.ie) { CKEDITOR.tools.setTimeout(function() { block.markFirstDisplayed ? block.markFirstDisplayed() : block._.markFirstDisplayed(); }, 0); } else { block.markFirstDisplayed ? block.markFirstDisplayed() : block._.markFirstDisplayed(); } } this._.editor.fire("panelShow", this); }, 0, this ); }, CKEDITOR.env.air ? 200 : 0, this ); this.visible = 1; if (this.onShow) this.onShow.call(this); }, /** * Repositions the panel with the same parameters that were used in the last {@link #showBlock} call. * * @since 4.5.4 */ reposition: function() { var blockParams = this._.showBlockParams; if (this.visible && this._.showBlockParams) { this.hide(); this.showBlock.apply(this, blockParams); } }, /** * Restores the last focused element or simply focuses the panel window. */ focus: function() { // Webkit requires to blur any previous focused page element, in // order to properly fire the "focus" event. if (CKEDITOR.env.webkit) { var active = CKEDITOR.document.getActive(); active && !active.equals(this._.iframe) && active.$.blur(); } // Restore last focused element or simply focus panel window. var focus = this._.lastFocused || this._.iframe.getFrameDocument().getWindow(); focus.focus(); }, /** * @todo */ blur: function() { var doc = this._.iframe.getFrameDocument(), active = doc.getActive(); active && active.is("a") && (this._.lastFocused = active); }, /** * Hides the panel. * * @todo */ hide: function(returnFocus) { if (this.visible && (!this.onHide || this.onHide.call(this) !== true)) { this.hideChild(); // Blur previously focused element. (https://dev.ckeditor.com/ticket/6671) CKEDITOR.env.gecko && this._.iframe.getFrameDocument().$.activeElement.blur(); this.element.setStyle("display", "none"); this.visible = 0; this.element.getFirst().removeCustomData("activePanel"); // Return focus properly. (https://dev.ckeditor.com/ticket/6247) var focusReturn = returnFocus && this._.returnFocus; if (focusReturn) { // Webkit requires focus moved out panel iframe first. if (CKEDITOR.env.webkit && focusReturn.type) focusReturn.getWindow().$.focus(); focusReturn.focus(); } delete this._.lastFocused; this._.showBlockParams = null; this._.editor.fire("panelHide", this); } }, /** * @todo */ allowBlur: function(allow) { // Prevent editor from hiding the panel. (https://dev.ckeditor.com/ticket/3222) var panel = this._.panel; if (allow !== undefined) panel.allowBlur = allow; return panel.allowBlur; }, /** * Shows the specified panel as a child of one block of this one. * * @param {CKEDITOR.ui.floatPanel} panel * @param {String} blockName * @param {CKEDITOR.dom.element} offsetParent Positioned parent. * @param {Number} corner * * * For LTR (left to right) oriented editor: * * `1` = top-left * * `2` = top-right * * `3` = bottom-right * * `4` = bottom-left * * For RTL (right to left): * * `1` = top-right * * `2` = top-left * * `3` = bottom-left * * `4` = bottom-right * * @param {Number} [offsetX=0] * @param {Number} [offsetY=0] * @todo */ showAsChild: function( panel, blockName, offsetParent, corner, offsetX, offsetY ) { // Skip reshowing of child which is already visible. if ( this._.activeChild == panel && panel._.panel._.offsetParentId == offsetParent.getId() ) return; this.hideChild(); panel.onHide = CKEDITOR.tools.bind(function() { // Use a timeout, so we give time for this menu to get // potentially focused. CKEDITOR.tools.setTimeout( function() { if (!this._.focused) this.hide(); }, 0, this ); }, this); this._.activeChild = panel; this._.focused = false; panel.showBlock(blockName, offsetParent, corner, offsetX, offsetY); this.blur(); /* https://dev.ckeditor.com/ticket/3767 IE: Second level menu may not have borders */ if (CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat) { setTimeout(function() { panel.element.getChild(0).$.style.cssText += ""; }, 100); } }, /** * @todo */ hideChild: function(restoreFocus) { var activeChild = this._.activeChild; if (activeChild) { delete activeChild.onHide; delete this._.activeChild; activeChild.hide(); // At this point focus should be moved back to parent panel. restoreFocus && this.focus(); } } } }); CKEDITOR.on("instanceDestroyed", function() { var isLastInstance = CKEDITOR.tools.isEmpty(CKEDITOR.instances); for (var i in panels) { var panel = panels[i]; // Safe to destroy it since there're no more instances.(https://dev.ckeditor.com/ticket/4241) if (isLastInstance) panel.destroy(); // Panel might be used by other instances, just hide them.(https://dev.ckeditor.com/ticket/4552) else panel.element.hide(); } // Remove the registration. isLastInstance && (panels = {}); }); })();