From b8d16c73f00a2a36feee6d2508e12ad1bc60dfa9 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sat, 16 May 2015 19:06:36 +0200 Subject: [PATCH] Overhauled popover implementation and added backward compatibility --- wcfsetup/install/files/js/WCF.js | 550 +----------------- .../js/WoltLab/WCF/Controller/Popover.js | 351 ++++++++--- .../files/js/WoltLab/WCF/UI/Alignment.js | 2 +- wcfsetup/install/files/style/icon.less | 8 +- wcfsetup/install/files/style/popover.less | 116 ++-- 5 files changed, 358 insertions(+), 669 deletions(-) diff --git a/wcfsetup/install/files/js/WCF.js b/wcfsetup/install/files/js/WCF.js index ec7153c002..b4fec2bfba 100755 --- a/wcfsetup/install/files/js/WCF.js +++ b/wcfsetup/install/files/js/WCF.js @@ -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 = $('
').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); } }); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js index d45dd953ca..bf8b5a6fb5 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js @@ -1,10 +1,20 @@ +/** + * Versatile popover manager. + * + * @author Alexander Ebert + * @copyright 2001-2015 WoltLab GmbH + * @license GNU Lesser General Public License + * @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} 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} 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 }); } }; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js index 4ca50472d8..b6b6be2a09 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js @@ -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 diff --git a/wcfsetup/install/files/style/icon.less b/wcfsetup/install/files/style/icon.less index e17b9e8dc7..1cb86694ed 100644 --- a/wcfsetup/install/files/style/icon.less +++ b/wcfsetup/install/files/style/icon.less @@ -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); } diff --git a/wcfsetup/install/files/style/popover.less b/wcfsetup/install/files/style/popover.less index d0e771ecc1..196a668f63 100644 --- a/wcfsetup/install/files/style/popover.less +++ b/wcfsetup/install/files/style/popover.less @@ -2,72 +2,108 @@ 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); } } -- 2.20.1