From 96ea5f8ad78fee9fad7495a81968e6f27dc3cbf1 Mon Sep 17 00:00:00 2001 From: Bruno Beghelli Date: Mon, 24 Sep 2012 18:24:00 -0300 Subject: [PATCH] *7918* Fixed tinyMCE and multilingual popover, introduced possibility to perform many outside click checks --- js/controllers/SiteHandler.js | 128 +++++++++++------ js/controllers/form/MultilingualInputHandler.js | 175 +++++++++++++++-------- 2 files changed, 193 insertions(+), 110 deletions(-) diff --git a/js/controllers/SiteHandler.js b/js/controllers/SiteHandler.js index 9fdb8ea..ef05e7a 100644 --- a/js/controllers/SiteHandler.js +++ b/js/controllers/SiteHandler.js @@ -40,6 +40,7 @@ jQuery.pkp.controllers = jQuery.pkp.controllers || { }; this.bind('updateHeader', this.updateHeaderHandler_); this.bind('updateSidebar', this.updateSidebarHandler_); this.bind('callWhenClickOutside', this.callWhenClickOutsideHandler_); + this.bind('mousedown', this.mouseDownHandler_); // Listen for grid initialized events so the inline help // can be shown or hidden. @@ -74,6 +75,8 @@ jQuery.pkp.controllers = jQuery.pkp.controllers || { }; this.unregisterUnsavedFormElement_)); this.bind('unregisterAllForms', this.callbackWrapper( this.unregisterAllFormElements_)); + + this.outsideClickChecks_ = {}; }; $.pkp.classes.Helper.inherits( $.pkp.controllers.SiteHandler, $.pkp.classes.Handler); @@ -91,6 +94,16 @@ jQuery.pkp.controllers = jQuery.pkp.controllers || { }; /** + * Object with data to be used when checking if user + * clicked outside a site element. See callWhenClickOutsideHandler_() + * to check the expected check options. + * @private + * @type {Object} + */ + $.pkp.controllers.SiteHandler.prototype.outsideClickChecks_ = null; + + + /** * A state variable to store the form elements that have unsaved data. * @private * @type {Array} @@ -99,7 +112,7 @@ jQuery.pkp.controllers = jQuery.pkp.controllers || { }; // - // Public static method. + // Public static methods. // /** * Callback used by the tinyMCE plugin to trigger the tinyMCEInitialized @@ -304,8 +317,9 @@ jQuery.pkp.controllers = jQuery.pkp.controllers || { }; /** - * Binds a click event to this element so we can track if user - * clicked outside the passed element or not. + * Call when click outside event handler. Stores the event + * parameters as checks to be used later by mouse down handler so we + * can track if user clicked outside the passed element or not. * @param {HTMLElement} sourceElement The element that issued the * callWhenClickOutside event. * @param {Event} event The "call when click outside" event. @@ -320,75 +334,95 @@ jQuery.pkp.controllers = jQuery.pkp.controllers || { }; */ $.pkp.controllers.SiteHandler.prototype.callWhenClickOutsideHandler_ = function(sourceElement, event, eventParams) { - if (this.callWhenClickOutsideEventParams_ !== undefined) { - throw Error('Another widget is already using this structure.'); + // Check the required parameters. + if (eventParams.container == undefined) { + return; + } + + if (eventParams.callback == undefined) { + return; } - this.callWhenClickOutsideEventParams_ = eventParams; - setTimeout(this.callbackWrapper(function() { - this.bind('mousedown', this.checkOutsideClickHandler_); - }), 25); + var id = eventParams.container.attr('id'); + this.outsideClickChecks_[id] = eventParams; }; /** - * Mouse down event handler, used by the callWhenClickOutside event handler - * to test if user clicked outside an element or not. If true, will - * callback a function. Can optionally avoid the callback - * when a modal widget is loaded. + * Mouse down event handler attached to the site element. * @param {HTMLElement} sourceElement The element that issued the * click event. * @param {Event} event The "mousedown" event. * @return {boolean} Event handling status. * @private */ - $.pkp.controllers.SiteHandler.prototype.checkOutsideClickHandler_ = + $.pkp.controllers.SiteHandler.prototype.mouseDownHandler_ = function(sourceElement, event) { var $container, callback; - if (this.callWhenClickOutsideEventParams_ !== undefined) { - // Start checking the paramenters. - if (this.callWhenClickOutsideEventParams_.container !== undefined) { - // Store the container element. - $container = this.callWhenClickOutsideEventParams_.container; - } else { - // Need a container, return. - return false; + if (!$.isEmptyObject(this.outsideClickChecks_)) { + for (var id in this.outsideClickChecks_) { + this.processOutsideClickCheck_( + this.outsideClickChecks_[id], event); } + } - if (this.callWhenClickOutsideEventParams_.callback !== undefined) { - // Store the callback. - callback = this.callWhenClickOutsideEventParams_.callback; - } else { - // Need the callback, return. - return false; - } + return true; + }; - if (this.callWhenClickOutsideEventParams_.skipWhenVisibleModals !== - undefined) { - if (this.callWhenClickOutsideEventParams_.skipWhenVisibleModals) { - if (this.getHtmlElement().find('div.ui-dialog').length > 0) { - // Found a modal, return. - return false; - } - } - } - // Do the click origin checking. - if ($container.has(event.target).length === 0) { - // Unbind this click handler. - this.unbind('mousedown', this.checkOutsideClickHandler_); + /** + * Check if the passed event target is outside the element + * inside the passed check data. If true and no other check + * option avoids it, use the callback. + * @param {Object} checkOptions Object with data to be used to + * check the click. + * @param {Event} event The click event to be checked. + * @returns {Boolean} Whether the check was processed or not. + */ + $.pkp.controllers.SiteHandler.prototype.processOutsideClickCheck_ = + function(checkOptions, event) { + + // Make sure we have a click event. + if (event.type !== 'click' && + event.type !== 'mousedown' && event.type !== 'mouseup') { + throw Error('Can not check outside click with the passed event: ' + + event.type + '.'); + return false; + } + + // Get the container element. + var $container = checkOptions.container; - // Clean the original event parameters data. - this.callWhenClickOutsideEventParams_ = undefined; + // Doesn't make sense to check an outside click + // with an invisible element, so skip test if + // container is hidden. + if ($container.is(':hidden')) { + return false; + } - if (!$container.is(':hidden')) { - // Only considered outside if the container is visible. - callback(); + // Check for the visible modals option. + if (checkOptions.skipWhenVisibleModals !== + undefined) { + if (checkOptions.skipWhenVisibleModals) { + if (this.getHtmlElement().find('div.ui-dialog').length > 0) { + // Found a modal, return. + return false; } } } + // Do the click origin checking. + if ($container.has(event.target).length === 0) { + + // Once the check was processed, delete it. + delete this.outsideClickChecks_[$container.attr('id')]; + + checkOptions.callback(); + + return true; + } + return false; }; diff --git a/js/controllers/form/MultilingualInputHandler.js b/js/controllers/form/MultilingualInputHandler.js index a7310a7..8c44fb9 100644 --- a/js/controllers/form/MultilingualInputHandler.js +++ b/js/controllers/form/MultilingualInputHandler.js @@ -36,14 +36,14 @@ } $popoverNode - .focus(this.callbackWrapper(this.multilingualShow)); + .focus(this.callbackWrapper(this.focusHandler_)); // Bind to the blur of any of the inputs to to check if we should close. $popover.find(':input'). - blur(this.callbackWrapper(this.multilingualHide)); + blur(this.callbackWrapper(this.blurHandler_)); this.publishEvent('tinyMCEInitialized'); - this.bind('tinyMCEInitialized', this.handleTinyMCEEvents_); + this.bind('tinyMCEInitialized', this.tinyMCEInitHandler_); }; $.pkp.classes.Helper.inherits( $.pkp.controllers.form.MultilingualInputHandler, @@ -51,32 +51,63 @@ // - // Private properties + // Private helper methods. // /** - * This timer is used to control closing the - * popover when blur events are detected. - * @private - * @type {Object} + * Focus event handler. This is attached to all primary inputs. + * + * @param {HTMLElement} multilingualInput The primary multilingual + * element. + * @param {Event} event The focus event. */ - $.pkp.controllers.form.MultilingualInputHandler.prototype. - popoverCloseTimer_ = null; + $.pkp.controllers.form.MultilingualInputHandler.prototype.focusHandler_ = + function(multilingualInput, event) { + + this.showPopover_(); + }; - // - // Public methods - // /** - * Internal callback called to show additional languages for a - * multilingual input + * Blur event handler. This is attached to all inputs inside this + * popover element. * - * @param {HTMLElement} multilingualInput The primary multilingual - * element in the set to show. + * @param {HTMLElement} multilingualInput The element in the + * multilingual set to hide. * @param {Event} event The event that triggered the action. + * @return {Boolean} Return true to continue the event handling. */ - $.pkp.controllers.form.MultilingualInputHandler.prototype.multilingualShow = + $.pkp.controllers.form.MultilingualInputHandler.prototype.blurHandler_ = function(multilingualInput, event) { + // Use a timeout to give the other element a chance to acquire the focus. + setTimeout(this.callbackWrapper(function() { + if (!this.hasElementInFocus_()) { + this.hidePopover_(); + } + }), 0); + + return true; + }; + + + /** + * Hide this popover. + * @private + */ + $.pkp.controllers.form.MultilingualInputHandler.prototype.hidePopover_ = + function() { + var $popover = this.getHtmlElement(); + $popover.removeClass('localization_popover_container_focus'); + $popover.find('.localization_popover').hide(); + }; + + + /** + * Show this popover. + * @private + */ + $.pkp.controllers.form.MultilingualInputHandler.prototype.showPopover_ = + function() { var $popover = this.getHtmlElement(); $popover.addClass('localization_popover_container_focus'); @@ -89,53 +120,70 @@ /** - * Internal callback called to hide additional languages for a - * multilingual input - * - * @param {HTMLElement} multilingualInput The element in the - * multilingual set to hide. - * @param {Event} event The event that triggered the action. + * Test if any of the elements inside this popover has focus. + * @return {boolean} */ - $.pkp.controllers.form.MultilingualInputHandler.prototype.multilingualHide = - function(multilingualInput, event) { + $.pkp.controllers.form.MultilingualInputHandler.prototype.hasElementInFocus_ = + function() { - // Use a timeout to give the other element a chance to acquire the focus. - setTimeout(this.callbackWrapper(function() { - var $popover = this.getHtmlElement(); - var found = false; - // Test if any of the other elements has the focus. - $popover.find(':input').each(function(index, elem) { - if (elem === document.activeElement) { - found = true; - } - }); - // If none of them have the focus, we can hide the pop over. - if (!found) { - $popover.removeClass('localization_popover_container_focus'); - $popover.find('.localization_popover').hide(); - } - }), 0); + var $popover = this.getHtmlElement(); + + // Do the test. + if ($popover.has(document.activeElement).length) { + return true; + } else { + return false; + } }; /** - * tinyMCE initialized event handler, it will attach focus and blur - * event handlers to the tinyMCE window element. + * TinyMCE initialized event handler, it will attach focus and blur + * event handlers to the tinyMCE window element, and it will also + * fix some small issues related to the way tinyMCE editor behaves + * across different browsers. * @param {HTMLElement} input The input element that triggered the * event. * @param {Event} event The tinyMCE initialized event. * @param {Object} tinyMCEObject The tinyMCE object inside this * multilingual element handler that was initialized. */ - $.pkp.controllers.form.MultilingualInputHandler.prototype.handleTinyMCEEvents_ = + $.pkp.controllers.form.MultilingualInputHandler.prototype.tinyMCEInitHandler_ = function(input, event, tinyMCEObject) { var editorId = tinyMCEObject.editorId; + + // This hack is needed so the focus event is triggered correctly in IE8. + // We just adjust the body element height inside the tinyMCE editor + // instance to a percent of the original text area height, so when users + // click inside an empty tinyMCE editor the target will be the body element + // and the focus event will be triggered. + var textAreaHeight = $('#' + tinyMCEObject.editorId).height(); + $(tinyMCEObject.getBody()).height((textAreaHeight / 100) * 78); + $(tinyMCEObject.getWin()).focus( this.callbackWrapper(function() { + // We need also to close the multilingual popover when user clicks + // outside the popover element. The blur event is not enough because + // sometimes (with text selected in editor) Chrome will consider the + // tinyMCE editor as still active and that will avoid the popover to + // close (see the first check of the blur handler, just above). + // + // Firefox will also not completely focus on tinyMCE editors after + // comming back from fullscreen mode (the callback to focus the + // editor when set content will only trigger the focus handler that + // we attach here, but will not move the cursor inside the tinyMCE + // editor). Then, if user clicks outside the popover, it will not + // close because no blur event will be triggered. + this.trigger('callWhenClickOutside', { + container: this.getHtmlElement(), + callback: this.callbackWrapper(this.hidePopover_), + skipWhenVisibleModals: false + }); + // Create a callback for the set content event, so we can - // still show the multilingual input if user is back from an - // image insertion, html edit or fullscreen mode. + // still show the multilingual input if user is back from + // fullscreen mode. var setContentCallback = this.callbackWrapper( function(tinyMCEObject) { var $tinyWindow = $(tinyMCEObject.getWin()); @@ -151,25 +199,26 @@ // Add the set content callback. tinyMCEObject.onSetContent.add(setContentCallback); - clearTimeout(this.popoverTimer); - var $popoverContainer = this.getHtmlElement(); - $popoverContainer. - addClass('localization_popover_container_focus'); - var $localizationPopover = $popoverContainer.find('.localization_popover'); - - $localizationPopover.find('iframe').width($popoverContainer.width() -1); - $localizationPopover.show(); + this.showPopover_(); })); + $(tinyMCEObject.getWin()).blur( this.callbackWrapper(function() { - // set a short timer to prevent the next popover from closing. - // this allows time for the next click event from the - // TinyMCE editor to cancel the timer. - this.popoverTimer = setTimeout(this.callbackWrapper( - function() { - this.getHtmlElement(). - removeClass('localization_popover_container_focus'); - $('.localization_popover', this.getHtmlElement()).hide(); + + // Check if the active document element is still the tinyMCE + // editor. If true, return false. This will avoid closing the + // popover if user is just inserting an image or editing the + // html source, for example (both actions open a new window). + if ($(tinyMCEObject.getContainer()).find('iframe').attr('id') == + $(document.activeElement).attr('id')) { + return false; + } + + // Use a timeout to give the other element a chance to acquire the focus. + setTimeout(this.callbackWrapper(function() { + if (!this.hasElementInFocus_()) { + this.hidePopover_(); + } }), 0); })); }; -- 1.7.5.4