*/
_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.
// 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);
}
});
+/**
+ * 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;
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);
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();
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
});
}
};
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
.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); }
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);
}
}