Overhauled popover implementation and added backward compatibility
authorAlexander Ebert <ebert@woltlab.com>
Sat, 16 May 2015 17:06:36 +0000 (19:06 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sat, 16 May 2015 17:06:36 +0000 (19:06 +0200)
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js
wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js
wcfsetup/install/files/style/icon.less
wcfsetup/install/files/style/popover.less

index ec7153c0026db71cb09a8e2fab87f623a3fa5d94..b4fec2bfba65dc7c20343325f6b840ba82e32255 100755 (executable)
@@ -8584,104 +8584,8 @@ WCF.Popover = Class.extend({
         */
        _activeElementID: '',
        
-       /**
-        * cancels popover
-        * @var boolean
-        */
-       _cancelPopover: false,
-       
-       /**
-        * element data
-        * @var object
-        */
-       _data: { },
-       
-       /**
-        * default dimensions, should reflect the estimated size
-        * @var object
-        */
-       _defaultDimensions: {
-               height: 150,
-               width: 450
-       },
-       
-       /**
-        * default orientation, may be a combintion of left/right and bottom/top
-        * @var object
-        */
-       _defaultOrientation: {
-               x: 'right',
-               y: 'top'
-       },
-       
-       /**
-        * delay to show or hide popover, values in miliseconds
-        * @var object
-        */
-       _delay: {
-               show: 800,
-               hide: 500
-       },
-       
-       /**
-        * true, if an element is being hovered
-        * @var boolean
-        */
-       _hoverElement: false,
-       
-       /**
-        * element id of element being hovered
-        * @var string
-        */
-       _hoverElementID: '',
-       
-       /**
-        * true, if popover is being hovered
-        * @var boolean
-        */
-       _hoverPopover: false,
-       
-       /**
-        * minimum margin (all directions) for popover
-        * @var integer
-        */
-       _margin: 20,
-       
-       /**
-        * periodical executer once element or popover is no longer being hovered
-        * @var WCF.PeriodicalExecuter
-        */
-       _peOut: null,
-       
-       /**
-        * periodical executer once an element is being hovered
-        * @var WCF.PeriodicalExecuter
-        */
-       _peOverElement: null,
-       
-       /**
-        * popover object
-        * @var jQuery
-        */
-       _popover: null,
-       
-       /**
-        * popover content
-        * @var jQuery
-        */
-       _popoverContent: null,
-       
-       /**
-        * popover horizontal offset
-        * @var integer
-        */
-       _popoverOffset: 10,
-       
-       /**
-        * element selector
-        * @var string
-        */
-       _selector: '',
+       _identifier: '',
+       _popoverObj: null,
        
        /**
         * Initializes a new WCF.Popover object.
@@ -8693,446 +8597,30 @@ WCF.Popover = Class.extend({
                
                // assign default values
                this._activeElementID = '';
-               this._cancelPopover = false;
-               this._data = { };
-               this._defaultDimensions = {
-                       height: 150,
-                       width: 450
-               };
-               this._defaultOrientation = {
-                       x: (WCF.Language.get('wcf.global.pageDirection') === 'rtl' ? 'left' : 'right'),
-                       y: 'top'
-               };
-               this._delay = {
-                       show: 800,
-                       hide: 500
-               };
-               this._hoverElement = false;
-               this._hoverElementID = '';
-               this._hoverPopover = false;
-               this._margin = 20;
-               this._peOut = null;
-               this._peOverElement = null;
-               this._popoverOffset = 10;
-               this._selector = selector;
-               
-               this._popover = $('<div class="popover"><span class="icon icon48 icon-spinner"></span><div class="popoverContent"></div></div>').hide().appendTo(document.body);
-               this._popoverContent = this._popover.children('.popoverContent:eq(0)');
-               this._popover.hover($.proxy(this._overPopover, this), $.proxy(this._out, this));
-               
-               this._initContainers();
-               WCF.DOMNodeInsertedHandler.addCallback('WCF.Popover.'+selector, $.proxy(this._initContainers, this));
-               
-               $(window).on('beforeunload', (function() {
-                       this._cancelPopover = true;
-                       this._hide(true);
-               }).bind(this));
-       },
-       
-       /**
-        * Initializes all element triggers.
-        */
-       _initContainers: function() {
-               if ($.browser.mobile) return;
-               
-               var $elements = $(this._selector);
-               if (!$elements.length) {
-                       return;
-               }
-               
-               $elements.each($.proxy(function(index, element) {
-                       var $element = $(element);
-                       var $elementID = $element.wcfIdentify();
-                       
-                       if (!this._data[$elementID]) {
-                               this._data[$elementID] = {
-                                       'content': null,
-                                       'isLoading': false
-                               };
-                               
-                               $element.hover($.proxy(this._overElement, this), $.proxy(this._out, this));
-                               
-                               if ($element.is('a') && $element.attr('href')) {
-                                       $element.click((function() {
-                                               this._hide(true);
-                                       }).bind(this));
-                               }
-                       }
-               }, this));
-       },
-       
-       /**
-        * Triggered once an element is being hovered.
-        * 
-        * @param       object          event
-        */
-       _overElement: function(event) {
-               if (this._cancelPopover) {
-                       return;
-               }
-               
-               if (this._peOverElement !== null) {
-                       this._peOverElement.stop();
-               }
-               
-               var $elementID = $(event.currentTarget).wcfIdentify();
-               this._hoverElementID = $elementID;
-               this._peOverElement = new WCF.PeriodicalExecuter($.proxy(function(pe) {
-                       pe.stop();
-                       
-                       // still above the same element
-                       if (this._hoverElementID === $elementID) {
-                               this._activeElementID = $elementID;
-                               this._prepare();
-                       }
-               }, this), this._delay.show);
-               
-               this._hoverElement = true;
-               this._hoverPopover = false;
-       },
-       
-       /**
-        * Prepares popover to be displayed.
-        */
-       _prepare: function() {
-               if (this._cancelPopover) {
-                       return;
-               }
-               
-               if (this._peOut !== null) {
-                       this._peOut.stop();
-               }
-               
-               // hide and reset
-               if (this._popover.is(':visible')) {
-                       this._hide(true);
-               }
-               
-               // insert html
-               if (!this._data[this._activeElementID].loading && this._data[this._activeElementID].content) {
-                       this._popoverContent.html(this._data[this._activeElementID].content);
-                       
-                       WCF.DOMNodeInsertedHandler.execute();
-               }
-               else {
-                       this._data[this._activeElementID].loading = true;
-               }
-               
-               // get dimensions
-               var $dimensions = this._popover.show().getDimensions();
-               if (this._data[this._activeElementID].loading) {
-                       $dimensions = {
-                               height: Math.max($dimensions.height, this._defaultDimensions.height),
-                               width: Math.max($dimensions.width, this._defaultDimensions.width)
-                       };
-               }
-               else {
-                       $dimensions = this._fixElementDimensions(this._popover, $dimensions);
-               }
-               this._popover.hide();
-               
-               // get orientation
-               var $orientation = this._getOrientation($dimensions.height, $dimensions.width);
-               this._popover.css(this._getCSS($orientation.x, $orientation.y));
-               
-               // apply orientation to popover
-               this._popover.removeClass('bottom left right top').addClass($orientation.x).addClass($orientation.y);
-               
-               this._show();
-       },
-       
-       /**
-        * Displays the popover.
-        */
-       _show: function() {
-               if (this._cancelPopover) {
-                       return;
-               }
-               
-               this._popover.stop().show().css({ opacity: 1 }).wcfFadeIn();
-               
-               if (this._data[this._activeElementID].loading) {
-                       this._popover.children('span').show();
-                       this._loadContent();
-               }
-               else {
-                       this._popover.children('span').hide();
-                       this._popoverContent.css({ opacity: 1 });
-               }
-       },
-       
-       /**
-        * Loads content, should be overwritten by child classes.
-        */
-       _loadContent: function() { },
-       
-       /**
-        * Inserts content and animating transition.
-        * 
-        * @param       string          elementID
-        * @param       boolean         animate
-        */
-       _insertContent: function(elementID, content, animate) {
-               this._data[elementID] = {
-                       content: content,
-                       loading: false
-               };
-               
-               // only update content if element id is active
-               if (this._activeElementID === elementID) {
-                       if (animate) {
-                               // get current dimensions
-                               var $dimensions = this._popoverContent.getDimensions();
-                               
-                               // insert new content
-                               this._popoverContent.css({
-                                       height: 'auto',
-                                       width: 'auto'
-                               });
-                               this._popoverContent.html(this._data[elementID].content);
-                               var $newDimensions = this._popoverContent.getDimensions();
-                               
-                               // enforce current dimensions and remove HTML
-                               this._popoverContent.html('').css({
-                                       height: $dimensions.height + 'px',
-                                       width: $dimensions.width + 'px'
-                               });
-                               
-                               // animate to new dimensons
-                               var self = this;
-                               this._popoverContent.animate({
-                                       height: $newDimensions.height + 'px',
-                                       width: $newDimensions.width + 'px'
-                               }, 300, function() {
-                                       self._popover.children('span').hide();
-                                       self._popoverContent.html(self._data[elementID].content).css({ opacity: 0 }).animate({ opacity: 1 }, 200);
-                                       
-                                       WCF.DOMNodeInsertedHandler.execute();
-                               });
-                       }
-                       else {
-                               // insert new content
-                               this._popover.children('span').hide();
-                               this._popoverContent.html(this._data[elementID].content);
-                               
-                               WCF.DOMNodeInsertedHandler.execute();
-                       }
-               }
-       },
-       
-       /**
-        * Hides the popover.
-        */
-       _hide: function(disableAnimation) {
-               var self = this;
-               this._popoverContent.stop();
-               this._popover.stop();
-               
-               if (disableAnimation) {
-                       self._popover.css({ opacity: 0 }).hide();
-                       self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' });
-               }
-               else {
-                       this._popover.wcfFadeOut(function() {
-                               self._popoverContent.empty().css({ height: 'auto', opacity: 0, width: 'auto' });
-                               self._popover.hide();
+               this._identifier = selector;
+               
+               require(['WoltLab/WCF/Controller/Popover'], (function(popover) {
+                       popover.init({
+                               attributeName: 'legacy',
+                               className: selector,
+                               identifier: this._identifier,
+                               legacy: true,
+                               loadCallback: this._legacyLoad.bind(this)
                        });
-               }
-       },
-       
-       /**
-        * Triggered once popover is being hovered.
-        */
-       _overPopover: function() {
-               if (this._peOut !== null) {
-                       this._peOut.stop();
-               }
-               
-               this._hoverElement = false;
-               this._hoverPopover = true;
-       },
-       
-       /**
-        * Triggered once element *or* popover is now longer hovered.
-        */
-       _out: function(event) {
-               if (this._cancelPopover) {
-                       return;
-               }
-               
-               this._hoverElementID = '';
-               this._hoverElement = false;
-               this._hoverPopover = false;
-               
-               this._peOut = new WCF.PeriodicalExecuter($.proxy(function(pe) {
-                       pe.stop();
-                       
-                       // hide popover is neither element nor popover was hovered given time
-                       if (!this._hoverElement && !this._hoverPopover) {
-                               this._hide(false);
-                       }
-               }, this), this._delay.hide);
-       },
-       
-       /**
-        * Resolves popover orientation, tries to use default orientation first.
-        * 
-        * @param       integer         height
-        * @param       integer         width
-        * @return      object
-        */
-       _getOrientation: function(height, width) {
-               // get offsets and dimensions
-               var $element = $('#' + this._activeElementID);
-               var $offsets = $element.getOffsets('offset');
-               var $elementDimensions = $element.getDimensions();
-               var $documentDimensions = $(document).getDimensions();
-               
-               // try default orientation first
-               var $orientationX = (this._defaultOrientation.x === 'left') ? 'left' : 'right';
-               var $orientationY = (this._defaultOrientation.y === 'bottom') ? 'bottom' : 'top';
-               var $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
-               
-               if ($result.flawed) {
-                       // try flipping orientationX
-                       $orientationX = ($orientationX === 'left') ? 'right' : 'left';
-                       $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
-                       
-                       if ($result.flawed) {
-                               // try flipping orientationY while maintaing original orientationX
-                               $orientationX = ($orientationX === 'right') ? 'left' : 'right';
-                               $orientationY = ($orientationY === 'bottom') ? 'top' : 'bottom';
-                               $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
-                               
-                               if ($result.flawed) {
-                                       // try flipping both orientationX and orientationY compared to default values
-                                       $orientationX = ($orientationX === 'left') ? 'right' : 'left';
-                                       $result = this._evaluateOrientation($orientationX, $orientationY, $offsets, $elementDimensions, $documentDimensions, height, width);
-                                       
-                                       if ($result.flawed) {
-                                               // fuck this shit, we will use the default orientation
-                                               $orientationX = (this._defaultOrientationX === 'left') ? 'left' : 'right';
-                                               $orientationY = (this._defaultOrientationY === 'bottom') ? 'bottom' : 'top';
-                                       }
-                               }
-                       }
-               }
-               
-               return {
-                       x: $orientationX,
-                       y: $orientationY
-               };
+               }).bind(this));
        },
        
-       /**
-        * Evaluates if popover fits into given orientation.
-        * 
-        * @param       string          orientationX
-        * @param       string          orientationY
-        * @param       object          offsets
-        * @param       object          elementDimensions
-        * @param       object          documentDimensions
-        * @param       integer         height
-        * @param       integer         width
-        * @return      object
-        */
-       _evaluateOrientation: function(orientationX, orientationY, offsets, elementDimensions, documentDimensions, height, width) {
-               var $heightDifference = 0, $widthDifference = 0;
-               switch (orientationX) {
-                       case 'left':
-                               $widthDifference = offsets.left - width;
-                       break;
-                       
-                       case 'right':
-                               $widthDifference = documentDimensions.width - (offsets.left + width);
-                       break;
-               }
-               
-               switch (orientationY) {
-                       case 'bottom':
-                               $heightDifference = documentDimensions.height - (offsets.top + elementDimensions.height + this._popoverOffset + height);
-                       break;
-                       
-                       case 'top':
-                               $heightDifference = offsets.top - (height - this._popoverOffset);
-                       break;
-               }
-               
-               // check if both difference are above margin
-               var $flawed = false;
-               if ($heightDifference < this._margin || $widthDifference < this._margin) {
-                       $flawed = true;
-               }
-               
-               return {
-                       flawed: $flawed,
-                       x: $widthDifference,
-                       y: $heightDifference
-               };
-       },
+       _initContainers: function() {},
        
-       /**
-        * Computes CSS for popover.
-        * 
-        * @param       string          orientationX
-        * @param       string          orientationY
-        * @return      object
-        */
-       _getCSS: function(orientationX, orientationY) {
-               var $css = {
-                       bottom: 'auto',
-                       left: 'auto',
-                       right: 'auto',
-                       top: 'auto'
-               };
-               
-               var $element = $('#' + this._activeElementID);
-               var $offsets = $element.getOffsets('offset');
-               var $elementDimensions = this._fixElementDimensions($element, $element.getDimensions());
-               var $windowDimensions = $(window).getDimensions();
-               
-               switch (orientationX) {
-                       case 'left':
-                               $css.right = $windowDimensions.width - ($offsets.left + $elementDimensions.width);
-                       break;
-                       
-                       case 'right':
-                               $css.left = $offsets.left;
-                       break;
-               }
+       _legacyLoad: function(objectId, popover) {
+               this._activeElementID = objectId;
+               this._popoverObj = popover;
                
-               switch (orientationY) {
-                       case 'bottom':
-                               $css.top = $offsets.top + ($elementDimensions.height + this._popoverOffset);
-                       break;
-                       
-                       case 'top':
-                               $css.bottom = $windowDimensions.height - ($offsets.top - this._popoverOffset);
-                       break;
-               }
-               
-               return $css;
+               this._loadContent();
        },
        
-       /**
-        * Tries to fix dimensions if element is partially hidden (overflow: hidden).
-        * 
-        * @param       jQuery          element
-        * @param       object          dimensions
-        * @return      dimensions
-        */
-       _fixElementDimensions: function(element, dimensions) {
-               var $parentDimensions = element.parent().getDimensions();
-               
-               if ($parentDimensions.height < dimensions.height) {
-                       dimensions.height = $parentDimensions.height;
-               }
-               
-               if ($parentDimensions.width < dimensions.width) {
-                       dimensions.width = $parentDimensions.width;
-               }
-               
-               return dimensions;
+       _insertContent: function(elementId, template) {
+               this._popoverObj.setContent(this._identifier, elementId, template);
        }
 });
 
index d45dd953ca9c66def03a4241aa9dc8e5f070ec89..bf8b5a6fb5a17e7c72fb7c38230a75a4a56f1a7b 100644 (file)
@@ -1,10 +1,20 @@
+/**
+ * Versatile popover manager.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/Controller/Popover
+ */
 define(['Dictionary', 'DOM/Util', 'UI/Alignment'], function(Dictionary, DOMUtil, UIAlignment) {
        "use strict";
        
-       var _activeId = 0;
-       var _activeIdentifier = '';
+       var _activeId = null;
+       var _baseHeight = 0;
        var _cache = null;
+       var _elements = null;
        var _handlers = null;
+       var _hoverId = null;
        var _suspended = false;
        var _timeoutEnter = null;
        var _timeoutLeave = null;
@@ -13,41 +23,68 @@ define(['Dictionary', 'DOM/Util', 'UI/Alignment'], function(Dictionary, DOMUtil,
        var _popoverContent = null;
        var _popoverLoading = null;
        
+       var _callbackClick = null;
+       var _callbackHide = null;
+       var _callbackMouseEnter = null;
+       var _callbackMouseLeave = null;
+       
        /** @const */ var STATE_NONE = 0;
        /** @const */ var STATE_LOADING = 1;
        /** @const */ var STATE_READY = 2;
        
+       /** @const */ var DELAY_SHOW = 800;
+       /** @const */ var DELAY_HIDE = 500;
+       
        /**
         * @constructor
         */
        function ControllerPopover() {};
        ControllerPopover.prototype = {
+               /**
+                * Builds popover DOM elements and binds event listeners.
+                */
                _setup: function() {
                        if (_popover !== null) {
                                return;
                        }
                        
                        _cache = new Dictionary();
+                       _elements = new Dictionary();
                        _handlers = new Dictionary();
                        
                        _popover = document.createElement('div');
                        _popover.classList.add('popover');
                        
+                       _popoverContent = document.createElement('div');
+                       _popoverContent.classList.add('popoverContent');
+                       _popover.appendChild(_popoverContent);
+                       
                        var pointer = document.createElement('span');
                        pointer.classList.add('elementPointer');
                        pointer.appendChild(document.createElement('span'));
                        _popover.appendChild(pointer);
                        
                        _popoverLoading = document.createElement('span');
-                       _popoverLoading.className = 'icon icon48 fa-spinner';
+                       _popoverLoading.className = 'icon icon32 fa-spinner';
                        _popover.appendChild(_popoverLoading);
                        
-                       _popoverContent = document.createElement('div');
-                       _popoverContent.classList.add('popoverContent');
-                       _popover.appendChild(_popoverContent);
-                       
                        document.body.appendChild(_popover);
                        
+                       // static binding for callbacks (they don't change anyway and binding each time is expensive)
+                       _callbackClick = this._hide.bind(this);
+                       _callbackMouseEnter = this._mouseEnter.bind(this);
+                       _callbackMouseLeave = this._mouseLeave.bind(this);
+                       
+                       // event listener
+                       _popover.addEventListener('mouseenter', this._popoverMouseEnter.bind(this));
+                       _popover.addEventListener('mouseleave', _callbackMouseLeave);
+                       
+                       _popoverContent.addEventListener('transitionend', function(event) {
+                               if (event.propertyName === 'height') {
+                                       _popoverContent.classList.remove('loading');
+                               }
+                       });
+                       
                        window.addEventListener('beforeunload', (function() {
                                _suspended = true;
                                this._hide(true);
@@ -56,12 +93,31 @@ define(['Dictionary', 'DOM/Util', 'UI/Alignment'], function(Dictionary, DOMUtil,
                        WCF.DOMNodeInsertedHandler.addCallback('WoltLab/WCF/Controller/Popover', this._init.bind(this));
                },
                
+               /**
+                * Initializes a popover handler.
+                * 
+                * Usage:
+                * ControllerPopover.init({
+                *      attributeName: 'data-object-id',
+                *      className: 'fooLink',
+                *      identifier: 'com.example.bar.foo',
+                *      loadCallback: function(objectId, popover) {
+                *              // request data for object id (e.g. via WCF.Action.Proxy)
+                *              
+                *              // then call this to set the content
+                *              popover.setContent('com.example.bar.foo', objectId, htmlTemplateString);
+                *      }
+                * });
+                * 
+                * @param       {object<string, *>}     options         handler options
+                */
                init: function(options) {
                        if ($.browser.mobile) {
                                return;
                        }
                        
                        options.attributeName = options.attributeName || 'data-object-id';
+                       options.legacy = (options.legacy === true);
                        
                        this._setup();
                        
@@ -69,156 +125,265 @@ define(['Dictionary', 'DOM/Util', 'UI/Alignment'], function(Dictionary, DOMUtil,
                                return;
                        }
                        
-                       _cache.set(options.identifier, new Dictionary());
                        _handlers.set(options.identifier, {
                                attributeName: options.attributeName,
-                               elements: document.getElementsByClassName(options.className),
+                               elements: options.legacy ? options.className : document.getElementsByClassName(options.className),
+                               legacy: options.legacy,
                                loadCallback: options.loadCallback
                        });
                        
                        this._init(options.identifier)
                },
                
-               setContent: function(identifier, objectId, content) {
-                       content = (typeof content === 'string') ? content.trim() : '';
-                       if (content.length === 0) {
-                               throw new Error("Expected a non-empty HTML string for '" + objectId + "' (identifier: '" + identifier + "').");
-                       }
-                       
-                       var objects = _cache.get(identifier);
-                       if (objects === undefined) {
-                               throw new Error("Expected a valid identifier, '" + identifier + "' is invalid.");
-                       }
-                       
-                       var obj = objects.get(objectId);
-                       if (obj === undefined) {
-                               throw new Error("Expected a valid object id, '" + objectId + "' is invalid (identifier: '" + identifier + "').");
-                       }
-                       
-                       obj.element = DOMUtil.createFragmentFromHtml(content);
-                       obj.state = STATE_READY;
-                       console.debug(obj);
-                       this._show(identifier, objectId);
-               },
-               
+               /**
+                * Initializes a popover handler.
+                * 
+                * @param       {string}        identifier      handler identifier
+                */
                _init: function(identifier) {
                        if (typeof identifier === 'string' && identifier.length) {
-                               this._initElements(identifier, _handlers.get(identifier));
+                               this._initElements(_handlers.get(identifier), identifier);
                        }
                        else {
-                               _handlers.forEach((function(options, identifier) {
-                                       this._initElements(identifier, options);
-                               }).bind(this));
+                               _handlers.forEach(this._initElements.bind(this));
                        }
                },
                
-               _initElements: function(identifier, options) {
-                       var cachedElements = _cache.get(identifier);
-                       console.debug(identifier);
-                       console.debug(options);
-                       for (var i = 0, length = options.elements.length; i < length; i++) {
-                               var element = options.elements[i];
-                               var objectId = ~~element.getAttribute(options.attributeName);
+               /**
+                * Binds event listeners for popover-enabled elements.
+                * 
+                * @param       {object<string, *>}     options         handler options
+                * @param       {string}                identifier      handler identifier
+                */
+               _initElements: function(options, identifier) {
+                       var elements = options.legacy ? document.querySelectorAll(options.elements) : options.elements;
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               var element = elements[i];
+                               
+                               var id = DOMUtil.identify(element);
+                               if (_cache.has(id)) {
+                                       return;
+                               }
                                
-                               if (objectId === 0 || cachedElements.has(objectId)) {
+                               var objectId = (options.legacy) ? id : ~~element.getAttribute(options.attributeName);
+                               if (objectId === 0) {
                                        continue;
                                }
                                
-                               element.addEventListener('mouseenter', (function() { this._mouseEnter(identifier, objectId); }).bind(this));
-                               element.addEventListener('mouseleave', (function() { this._mouseLeave(identifier, objectId); }).bind(this));
+                               element.addEventListener('mouseenter', _callbackMouseEnter);
+                               element.addEventListener('mouseleave', _callbackMouseLeave);
                                
                                if (element.nodeName === 'A' && element.getAttribute('href')) {
-                                       element.addEventListener('click', (function() {
-                                               this._hide(true);
-                                       }).bind(this))
+                                       element.addEventListener('click', _callbackClick)
                                }
                                
-                               cachedElements.set(objectId, {
+                               var cacheId = identifier + "-" + objectId;
+                               element.setAttribute('data-cache-id', cacheId);
+                               
+                               _elements.set(id, {
                                        element: element,
-                                       state: STATE_NONE
+                                       identifier: identifier,
+                                       objectId: objectId
                                });
+                               
+                               if (!_cache.has(cacheId)) {
+                                       _cache.set(identifier + "-" + objectId, {
+                                               content: null,
+                                               state: STATE_NONE
+                                       });
+                               }
                        }
                },
                
-               _mouseEnter: function(identifier, objectId) {
-                       if (this._timeoutEnter !== null) {
-                               window.clearTimeout(this._timeoutEnter);
+               /**
+                * Sets the content for given identifier and object id.
+                * 
+                * @param       {string}        identifier      handler identifier
+                * @param       {integer}       objectId        object id
+                * @param       {string}        content         HTML string
+                */
+               setContent: function(identifier, objectId, content) {
+                       var cacheId = identifier + "-" + objectId;
+                       var data = _cache.get(cacheId);
+                       if (data === undefined) {
+                               throw new Error("Unable to find element for object id '" + objectId + "' (identifier: '" + identifier + "').");
                        }
                        
-                       this._hoverIdentifier = identifier;
-                       this._hoverId = objectId;
-                       window.setTimeout((function() {
-                               if (this._hoverId === objectId) {
-                                       this._show(identifier, objectId);
+                       data.content = DOMUtil.createFragmentFromHtml(content);
+                       data.state = STATE_READY;
+                       
+                       if (_activeId) {
+                               var activeElement = _elements.get(_activeId).element;
+                               
+                               if (activeElement.getAttribute('data-cache-id') === cacheId) {
+                                       this._show();
                                }
-                       }).bind(this));
+                       }
                },
                
-               _mouseLeave: function(identifier, objectId) {
+               /**
+                * Handles the mouse start hovering the popover-enabled element.
+                * 
+                * @param       {object}        event   event object
+                */
+               _mouseEnter: function(event) {
+                       if (_timeoutEnter !== null) {
+                               window.clearTimeout(_timeoutEnter);
+                               _timeoutEnter = null;
+                       }
                        
+                       var id = DOMUtil.identify(event.currentTarget);
+                       if (_activeId === id && _timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
+                       }
+                       
+                       _hoverId = id;
+                       
+                       _timeoutEnter = window.setTimeout((function() {
+                               _timeoutEnter = null;
+                               
+                               if (_hoverId === id) {
+                                       this._show();
+                               }
+                       }).bind(this), DELAY_SHOW);
                },
                
-               _show: function(identifier, objectId) {
-                       if (this._intervalOut !== null) {
-                               window.clearTimeout(this._intervalOut);
+               /**
+                * Handles the mouse leaving the popover-enabled element or the popover itself.
+                * 
+                * @param       {object}        event   event object
+                */
+               _mouseLeave: function(event) {
+                       _hoverId = null;
+                       
+                       if (_timeoutLeave !== null) {
+                               return;
                        }
                        
-                       if (_popover.classList.contains('active')) {
-                               this._hide(true);
+                       if (_callbackHide === null) {
+                               _callbackHide = this._hide.bind(this);
+                       }
+                       
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                       }
+                       
+                       _timeoutLeave = window.setTimeout(_callbackHide, DELAY_HIDE);
+               },
+               
+               /**
+                * Handles the mouse start hovering the popover element.
+                * 
+                * @param       {object}        event   event object
+                */
+               _popoverMouseEnter: function(event) {
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
+                       }
+               },
+               
+               /**
+                * Shows the popover and loads content on-the-fly.
+                */
+               _show: function() {
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
                        }
                        
-                       if (_activeId && _activeId !== objectId) {
-                               var cachedContent = _cache.get(_activeElementId);
+                       var disableAnimation = (_activeId !== null && _activeId !== _hoverId);
+                       if (disableAnimation) {
+                               var activeElData = _cache.get(_elements.get(_activeId).element.getAttribute('data-cache-id'));
                                while (_popoverContent.childNodes.length) {
-                                       cachedContent.appendChild(_popoverContent.childNodes[0]);
+                                       activeElData.content.appendChild(_popoverContent.childNodes[0]);
                                }
                        }
                        
-                       var content = _cache.get(identifier).get(objectId);
-                       if (content.state === STATE_READY) {
-                               _popoverContent.classList.remove('loading');
-                               _popoverContent.appendChild(content.element);
+                       if (_popover.classList.contains('active')) {
+                               this._hide(disableAnimation);
                        }
-                       else if (content.state === STATE_NONE) {
+                       
+                       _activeId = _hoverId;
+                       
+                       var elData = _elements.get(_activeId);
+                       var data = _cache.get(elData.element.getAttribute('data-cache-id'));
+                       
+                       if (data.state === STATE_READY) {
+                               _popoverContent.appendChild(data.content);
+                       }
+                       else if (data.state === STATE_NONE) {
                                _popoverContent.classList.add('loading');
                        }
                        
-                       _activeId = objectId;
-                       _activeIdentifier = identifier;
+                       this._rebuild(_activeId);
                        
-                       if (content.state === STATE_NONE) {
-                               content.state = STATE_LOADING;
+                       if (data.state === STATE_NONE) {
+                               data.state = STATE_LOADING;
                                
-                               this._load(identifier, objectId);
+                               _handlers.get(elData.identifier).loadCallback(elData.objectId, this);
                        }
                },
                
-               _hide: function(disableAnimation) {
+               /**
+                * Hides the popover element.
+                * 
+                * @param       {(object|boolean)}      event   event object or boolean if popover should be forced hidden
+                */
+               _hide: function(event) {
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
+                       }
+                       
                        _popover.classList.remove('active');
                        
-                       if (disableAnimation) {
+                       if (typeof event === 'boolean' && event === true) {
                                _popover.classList.add('disableAnimation');
+                               
+                               // force reflow
+                               _popover.offsetHeight;
                        }
-                       
-                       _activeIdentifier = '';
-                       _activeId = null;
-               },
-               
-               _load: function(identifier, objectId) {
-                       _handlers.get(identifier).loadCallback(objectId, this);
                },
                
-               _rebuild: function(elementId) {
-                       if (elementId !== _activeElementId) {
+               /**
+                * Rebuilds the popover.
+                */
+               _rebuild: function() {
+                       if (_popover.classList.contains('active')) {
                                return;
                        }
                        
                        _popover.classList.add('active');
-                       _popoverContent.appendChild(_cache.get(elementId));
-                       _popoverContent.classList.remove('loading');
+                       _popover.classList.remove('disableAnimation');
+                       if (_popoverContent.classList.contains('loading')) {
+                               if (_popoverContent.childElementCount === 0) {
+                                       if (_baseHeight === 0) {
+                                               _baseHeight = _popoverContent.offsetHeight;
+                                       }
+                                       
+                                       _popoverContent.style.setProperty('height', _baseHeight + 'px');
+                               }
+                               else {
+                                       _popoverContent.style.removeProperty('height');
+                                       
+                                       var height = _popoverContent.offsetHeight;
+                                       console.debug(_baseHeight);
+                                       console.debug(height);
+                                       _popoverContent.style.setProperty('height', _baseHeight + 'px');
+                                       
+                                       // force reflow
+                                       _popoverContent.offsetHeight;
+                                       
+                                       _popoverContent.style.setProperty('height', height + 'px');
+                               }
+                       }
                        
-                       UIAlignment.set(_popover, document.getElementById(elementId), {
-                               pointer: true
+                       UIAlignment.set(_popover, _elements.get(_activeId).element, {
+                               pointer: true,
+                               vertical: 'top',
+                               verticalOffset: 3
                        });
                }
        };
index 4ca50472d831621c99e5c82e6971b3317d505665..b6b6be2a09126fd2f02fd9563155bc94f7494320 100644 (file)
@@ -48,7 +48,7 @@ define(['Core', 'DOM/Traverse', 'DOM/Util'], function(Core, DOMTraverse, DOMUtil
                        
                        if (!Array.isArray(options.pointerClassNames) || options.pointerClassNames.length !== (options.pointer ? 1 : 2)) options.pointerClassNames = [];
                        if (['left', 'right', 'center'].indexOf(options.horizontal) === -1) options.horizontal = 'left';
-                       if (options.vertical !== 'bottom') options.horizontal = 'top';
+                       if (options.vertical !== 'bottom') options.vertical = 'top';
                        if (['both', 'horizontal', 'vertical', 'none'].indexOf(options.allowFlip) === -1) options.allowFlip = 'both';
                        
                        // place element in the upper left corner to prevent calculation issues due to possible scrollbars
index e17b9e8dc78ed9b46eff67bd99efe1517fa0d153..1cb86694ed94d7baf82b1a84914bd8a2ecd6f2c2 100644 (file)
@@ -120,10 +120,10 @@ a > span.fa:not(.pointer) {
 .fa-spinner {
        height: auto;
        
-       -moz-animation: spin 2s infinite linear;
-       -o-animation: spin 2s infinite linear;
-       -webkit-animation: spin 2s infinite linear;
-       animation: spin 2s infinite linear;
+       -moz-animation: spin 1s infinite steps(8);
+       -o-animation: spin 1s infinite steps(8);
+       -webkit-animation: spin 1s infinite steps(8);
+       animation: spin 1s infinite steps(8);
 }
 @-moz-keyframes spin {
        0% { -moz-transform: rotate(0deg); }
index d0e771ecc18b5e573b864530d08c28e0bfc3c146..196a668f6310fd008789bcf15d0f6410536d031c 100644 (file)
        background-color: rgba(0, 0, 0, .4);
        border: 3px solid transparent;
        border-radius: 3px;
+       opacity: 0;
        position: absolute;
        vertical-align: middle;
+       visibility: hidden;
        width: 400px !important;
        z-index: 500;
        
        .boxShadow(0, 1px, rgba(0, 0, 0, .3), 7px);
        
-       > .fa-spinner {
-               color: white;
-               left: 50%;
-               margin-left: -21px;
-               margin-top: -21px;
-               position: absolute;
-               top: 50%;
+       transition: visibility 0s linear .3s, opacity .3s linear;
+       
+       &.active {
+               opacity: 1;
+               visibility: visible;
                
-               .textShadow(black);
+               transition-delay: 0s;
+       }
+       
+       &.disableAnimation {
+               transition: none !important;
+               
+               > .popoverContent {
+                       transition: none !important;
+               }
+               
+               > .elementPointer > span {
+                       transition: none !important;
+               }
        }
        
        > .popoverContent {
                background-color: @wcfContainerBackgroundColor;
                border-radius: 3px;
+               box-sizing: border-box;
                color: @wcfColor;
-               max-height: 300px;
-               min-height: 16px;
-               opacity: 0;
+               max-height: 300px + (@wcfGapSmall + @wcfGapSmall);
+               min-height: 16px + (@wcfGapSmall + @wcfGapSmall);
+               opacity: 1;
                overflow: hidden;
                padding: @wcfGapSmall @wcfGapMedium;
+               
+               transition: opacity .3s linear;
+               
+               &:not(.loading) {
+                       ~ .fa-spinner {
+                               display: none;
+                       }
+                       
+                       ~ .elementPointer {
+                               > span {
+                                       border-color: @wcfContainerBackgroundColor transparent;
+                                       border-style: solid;
+                                       border-width: 0 5px 5px;
+                                       left: -5px;
+                                       opacity: 1;
+                                       position: absolute;
+                                       top: 3px;
+                                       
+                                       transition: opacity .3s linear;
+                               }
+                               
+                               &.flipVertical > span {
+                                       border-width: 5px 5px 0;
+                                       bottom: 3px;
+                                       top: auto;
+                               }
+                       }
+               }
+               
+               &.loading {
+                       opacity: 0;
+                       transition: height .3s linear, opacity 0s;
+                       
+                       ~ .elementPointer > span {
+                               opacity: 0;
+                               
+                               transition: opacity 0s;
+                       }
+               }
        }
        
        > .elementPointer {
                border-color: rgba(0, 0, 0, .4) transparent;
                border-style: solid;
-               border-width: 0 5px 5px;
-               top: -3px;
+               border-width: 0 6px 6px;
+               top: -2px;
                
                &.flipVertical {
-                       border-width: 5px 5px 0;
-                       bottom: -3px;
+                       border-width: 6px 6px 0;
+                       bottom: -2px;
+                       top: auto;
                }
        }
        
-       &::after {
-               border: 10px solid transparent;
-               content: "";
-               display: inline-block;
+       > .fa-spinner {
+               color: rgba(255, 255, 255, 1);
+               left: 50%;
+               margin-left: -14px;
+               margin-top: -14px;
                position: absolute;
-               z-index: 100;
-       }
-       
-       &.top::after {
-               border-bottom-width: 0;
-               border-top-color: rgba(0, 0, 0, .3);
-               bottom: -10px;
-       }
-       
-       &.bottom::after {
-               border-bottom-color: rgba(0, 0, 0, .3);
-               border-top-width: 0;
-               top: -10px;
-       }
-       
-       &.right::after {
-               left: 10px;
-       }
-       
-       &.left::after {
-               right: 10px;
+               top: 50%;
+               
+               .textShadow(black);
        }
 }