Popover overhaul, work in progress
authorAlexander Ebert <ebert@woltlab.com>
Wed, 13 May 2015 14:52:47 +0000 (16:52 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 13 May 2015 14:52:47 +0000 (16:52 +0200)
14 files changed:
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js
wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js
wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js
wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js
wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js
wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js
wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js
wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js
wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js
wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js
wcfsetup/install/files/style/global.less
wcfsetup/install/files/style/popover.less [new file with mode: 0644]

index 4378f6602f5670280dbd1d98093e151e891b1e4e..9c143ac40b70d2beefd8cfcb1f172afcc6dd978a 100644 (file)
                BootstrapFrontend.setup({
                        styleChanger: {if $__wcf->getStyleHandler()->countStyles() > 1}true{else}false{/if}
                });
+               
+               require(['WoltLab/WCF/Controller/Popover'], function(ControllerPopover) {
+                       ControllerPopover.init({
+                               attributeName: 'data-user-id',
+                               className: 'userLink',
+                               identifier: 'com.woltlab.wcf.user',
+                               loadCallback: function(objectId, popover) {
+                                       new WCF.Action.Proxy({
+                                               autoSend: true,
+                                               data: {
+                                                       actionName: 'getUserProfile',
+                                                       className: 'wcf\\data\\user\\UserProfileAction',
+                                                       objectIDs: [ objectId ]
+                                               },
+                                               success: (function(data) {
+                                                       popover.setContent('com.woltlab.wcf.user', objectId, data.returnValues.template);
+                                               }).bind(this),
+                                               failure: (function(data) {
+                                                       // TODO
+                                               }).bind(this)
+                                       });
+                               }
+                       });
+               });
        });
 </script>
 
                
                WCF.System.PageNavigation.init('.pageNavigation');
                WCF.Date.Picker.init();
-               new WCF.User.ProfilePreview();
+               //new WCF.User.ProfilePreview();
                new WCF.Notice.Dismiss();
                WCF.User.Profile.ActivityPointList.init();
                
        });
        //]]>
 </script>
-<!--[IF IE 9]>
-<script data-relocate="true">
-       $(function() {
-               function fixButtonTypeIE9() {
-                       $('button').each(function(index, button) {
-                               var $button = $(button);
-                               if (!$button.attr('type')) {
-                                       $button.attr('type', 'button');
-                               }
-                       });
-               }
-               
-               WCF.DOMNodeInsertedHandler.addCallback('WCF.FixButtonTypeIE9', fixButtonTypeIE9);
-               fixButtonTypeIE9();
-       });
-</script>
-<![ENDIF]-->
 
 {include file='imageViewer'}
index 20fb4ec9e11a493d9d0d980625b2b232e92b49d3..fab271bd75cbfddbbbe05845500e95cf15fb6078 100644 (file)
@@ -51,7 +51,7 @@ define(['Dictionary'], function(Dictionary) {
                 */
                forEach: function(identifier, callback) {
                        var callbacks = this._dictionary.get(identifier);
-                       if (callbacks !== null) {
+                       if (callbacks !== undefined) {
                                callbacks.forEach(callback);
                        }
                }
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js
new file mode 100644 (file)
index 0000000..d45dd95
--- /dev/null
@@ -0,0 +1,227 @@
+define(['Dictionary', 'DOM/Util', 'UI/Alignment'], function(Dictionary, DOMUtil, UIAlignment) {
+       "use strict";
+       
+       var _activeId = 0;
+       var _activeIdentifier = '';
+       var _cache = null;
+       var _handlers = null;
+       var _suspended = false;
+       var _timeoutEnter = null;
+       var _timeoutLeave = null;
+       
+       var _popover = null;
+       var _popoverContent = null;
+       var _popoverLoading = null;
+       
+       /** @const */ var STATE_NONE = 0;
+       /** @const */ var STATE_LOADING = 1;
+       /** @const */ var STATE_READY = 2;
+       
+       /**
+        * @constructor
+        */
+       function ControllerPopover() {};
+       ControllerPopover.prototype = {
+               _setup: function() {
+                       if (_popover !== null) {
+                               return;
+                       }
+                       
+                       _cache = new Dictionary();
+                       _handlers = new Dictionary();
+                       
+                       _popover = document.createElement('div');
+                       _popover.classList.add('popover');
+                       
+                       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';
+                       _popover.appendChild(_popoverLoading);
+                       
+                       _popoverContent = document.createElement('div');
+                       _popoverContent.classList.add('popoverContent');
+                       _popover.appendChild(_popoverContent);
+                       
+                       document.body.appendChild(_popover);
+                       
+                       window.addEventListener('beforeunload', (function() {
+                               _suspended = true;
+                               this._hide(true);
+                       }).bind(this));
+                       
+                       WCF.DOMNodeInsertedHandler.addCallback('WoltLab/WCF/Controller/Popover', this._init.bind(this));
+               },
+               
+               init: function(options) {
+                       if ($.browser.mobile) {
+                               return;
+                       }
+                       
+                       options.attributeName = options.attributeName || 'data-object-id';
+                       
+                       this._setup();
+                       
+                       if (_handlers.has(options.identifier)) {
+                               return;
+                       }
+                       
+                       _cache.set(options.identifier, new Dictionary());
+                       _handlers.set(options.identifier, {
+                               attributeName: options.attributeName,
+                               elements: document.getElementsByClassName(options.className),
+                               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);
+               },
+               
+               _init: function(identifier) {
+                       if (typeof identifier === 'string' && identifier.length) {
+                               this._initElements(identifier, _handlers.get(identifier));
+                       }
+                       else {
+                               _handlers.forEach((function(options, identifier) {
+                                       this._initElements(identifier, options);
+                               }).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);
+                               
+                               if (objectId === 0 || cachedElements.has(objectId)) {
+                                       continue;
+                               }
+                               
+                               element.addEventListener('mouseenter', (function() { this._mouseEnter(identifier, objectId); }).bind(this));
+                               element.addEventListener('mouseleave', (function() { this._mouseLeave(identifier, objectId); }).bind(this));
+                               
+                               if (element.nodeName === 'A' && element.getAttribute('href')) {
+                                       element.addEventListener('click', (function() {
+                                               this._hide(true);
+                                       }).bind(this))
+                               }
+                               
+                               cachedElements.set(objectId, {
+                                       element: element,
+                                       state: STATE_NONE
+                               });
+                       }
+               },
+               
+               _mouseEnter: function(identifier, objectId) {
+                       if (this._timeoutEnter !== null) {
+                               window.clearTimeout(this._timeoutEnter);
+                       }
+                       
+                       this._hoverIdentifier = identifier;
+                       this._hoverId = objectId;
+                       window.setTimeout((function() {
+                               if (this._hoverId === objectId) {
+                                       this._show(identifier, objectId);
+                               }
+                       }).bind(this));
+               },
+               
+               _mouseLeave: function(identifier, objectId) {
+                       
+               },
+               
+               _show: function(identifier, objectId) {
+                       if (this._intervalOut !== null) {
+                               window.clearTimeout(this._intervalOut);
+                       }
+                       
+                       if (_popover.classList.contains('active')) {
+                               this._hide(true);
+                       }
+                       
+                       if (_activeId && _activeId !== objectId) {
+                               var cachedContent = _cache.get(_activeElementId);
+                               while (_popoverContent.childNodes.length) {
+                                       cachedContent.appendChild(_popoverContent.childNodes[0]);
+                               }
+                       }
+                       
+                       var content = _cache.get(identifier).get(objectId);
+                       if (content.state === STATE_READY) {
+                               _popoverContent.classList.remove('loading');
+                               _popoverContent.appendChild(content.element);
+                       }
+                       else if (content.state === STATE_NONE) {
+                               _popoverContent.classList.add('loading');
+                       }
+                       
+                       _activeId = objectId;
+                       _activeIdentifier = identifier;
+                       
+                       if (content.state === STATE_NONE) {
+                               content.state = STATE_LOADING;
+                               
+                               this._load(identifier, objectId);
+                       }
+               },
+               
+               _hide: function(disableAnimation) {
+                       _popover.classList.remove('active');
+                       
+                       if (disableAnimation) {
+                               _popover.classList.add('disableAnimation');
+                       }
+                       
+                       _activeIdentifier = '';
+                       _activeId = null;
+               },
+               
+               _load: function(identifier, objectId) {
+                       _handlers.get(identifier).loadCallback(objectId, this);
+               },
+               
+               _rebuild: function(elementId) {
+                       if (elementId !== _activeElementId) {
+                               return;
+                       }
+                       
+                       _popover.classList.add('active');
+                       _popoverContent.appendChild(_cache.get(elementId));
+                       _popoverContent.classList.remove('loading');
+                       
+                       UIAlignment.set(_popover, document.getElementById(elementId), {
+                               pointer: true
+                       });
+               }
+       };
+       
+       return new ControllerPopover();
+});
index bfed2ea461793308739dd158f70bb3bf84076324..e52e90440ed6bde0b3b83234ffefec7faa8b075b 100644 (file)
@@ -25,6 +25,24 @@ define(function() {
         */
        function DOMUtil() {};
        DOMUtil.prototype = {
+               /**
+                * Returns a DocumentFragment containing the provided HTML string as DOM nodes.
+                * 
+                * @param       {string}        html    HTML string
+                * @return      {DocumentFragment}      fragment containing DOM nodes
+                */
+               createFragmentFromHtml: function(html) {
+                       var tmp = document.createElement('div');
+                       tmp.innerHTML = html;
+                       
+                       var fragment = document.createDocumentFragment();
+                       while (tmp.childNodes.length) {
+                               fragment.appendChild(tmp.childNodes[0]);
+                       }
+                       
+                       return fragment;
+               },
+               
                /**
                 * Returns a unique element id.
                 * 
index 5502686441efde48704d2f66fa25cf6941a72806..b3cd007341eb9508a7ecc04c5e3d2992cd0f07f5 100644 (file)
@@ -25,8 +25,10 @@ define(function() {
                 * @param       {*}             value   value
                 */
                set: function(key, value) {
+                       if (typeof key === 'number') key = key.toString();
+                       
                        if (typeof key !== "string") {
-                               throw new TypeError("Only strings can be used as keys, rejected '" +  + "' (" + typeof key + ").");
+                               throw new TypeError("Only strings can be used as keys, rejected '" + key + "' (" + typeof key + ").");
                        }
                        
                        if (_hasMap) this._dictionary.set(key, value);
@@ -39,6 +41,8 @@ define(function() {
                 * @param       {string}        key     key
                 */
                remove: function(key) {
+                       if (typeof key === 'number') key = key.toString();
+                       
                        if (_hasMap) this._dictionary.remove(key);
                        else this._dictionary[key] = undefined;
                },
@@ -50,6 +54,8 @@ define(function() {
                 * @return      {boolean}       true if key exists and value is not undefined
                 */
                has: function(key) {
+                       if (typeof key === 'number') key = key.toString();
+                       
                        if (_hasMap) return this._dictionary.has(key);
                        else {
                                return (this._dictionary.hasOwnProperty(key) && typeof this._dictionary[key] !== "undefined");
@@ -57,18 +63,20 @@ define(function() {
                },
                
                /**
-                * Retrieves a value by key, returns null if not found or undefined.
+                * Retrieves a value by key, returns undefined if there is no match.
                 * 
                 * @param       {string}        key     key
                 * @return      {*}
                 */
                get: function(key) {
+                       if (typeof key === 'number') key = key.toString();
+                       
                        if (this.has(key)) {
                                if (_hasMap) return this._dictionary.get(key);
                                else return this._dictionary[key];
                        }
                        
-                       return null;
+                       return undefined;
                },
                
                /**
index 47f8ae3b5c1e91f08113a75ac143034155b0d9b5..74720dbc02b90b73e7cf8db754c4c2397698fc2f 100644 (file)
@@ -30,13 +30,13 @@ define(['Dictionary'], function(Dictionary) {
                        }
                        
                        var actions = _listeners.get(identifier);
-                       if (actions === null) {
+                       if (actions === undefined) {
                                actions = new Dictionary();
                                _listeners.set(identifier, actions);
                        }
                        
                        var callbacks = actions.get(action);
-                       if (callbacks === null) {
+                       if (callbacks === undefined) {
                                callbacks = new Dictionary();
                                actions.set(action, callbacks);
                        }
@@ -58,9 +58,9 @@ define(['Dictionary'], function(Dictionary) {
                        data = data || {};
                        
                        var actions = _listeners.get(identifier);
-                       if (actions !== null) {
+                       if (actions !== undefined) {
                                var callbacks = actions.get(action);
-                               if (callbacks !== null) {
+                               if (callbacks !== undefined) {
                                        callbacks.forEach(function(callback) {
                                                callback(data);
                                        });
@@ -77,12 +77,12 @@ define(['Dictionary'], function(Dictionary) {
                 */
                remove: function(identifier, action, uuid) {
                        var actions = _listeners.get(identifier);
-                       if (actions === null) {
+                       if (actions === undefined) {
                                return;
                        }
                        
                        var callbacks = actions.get(action);
-                       if (callbacks === null) {
+                       if (callbacks === undefined) {
                                return;
                        }
                        
@@ -97,8 +97,10 @@ define(['Dictionary'], function(Dictionary) {
                 * @param       {string=}       action          action name
                 */
                removeAll: function(identifier, action) {
+                       if (typeof action !== 'string') action = undefined;
+                       
                        var actions = _listeners.get(identifier);
-                       if (actions === null) {
+                       if (actions === undefined) {
                                return;
                        }
                        
index 21b43b1d5d1791c316ff83cd521323b904fee558..4ca50472d831621c99e5c82e6971b3317d505665 100644 (file)
@@ -182,7 +182,6 @@ define(['Core', 'DOM/Traverse', 'DOM/Util'], function(Core, DOMTraverse, DOMUtil
                                }
                        }
                        else if (align === 'right') {
-                               console.debug(windowWidth + " | " + refOffsets.left + " | " + refDimensions.width);
                                right = windowWidth - (refOffsets.left + refDimensions.width);
                                if (right < 0) {
                                        result = false;
index e4e9c031869d816415d4c1732fed96d2af3b34cf..0c993a26406c72983bb2626219e99f988cd7361e 100644 (file)
@@ -110,7 +110,7 @@ define(['jquery', 'enquire', 'Core', 'Dictionary', 'DOM/Util'], function($, enqu
                 */
                setTitle: function(id, title) {
                        var data = _dialogs.get(id);
-                       if (typeof data === 'undefined') {
+                       if (data === undefined) {
                                throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog.");
                        }
                        
@@ -230,7 +230,7 @@ define(['jquery', 'enquire', 'Core', 'Dictionary', 'DOM/Util'], function($, enqu
                 */
                _updateDialog: function(id, html) {
                        var data = _dialogs.get(id);
-                       if (typeof data === 'undefined') {
+                       if (data === undefined) {
                                throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog.");
                        }
                        
@@ -268,7 +268,7 @@ define(['jquery', 'enquire', 'Core', 'Dictionary', 'DOM/Util'], function($, enqu
                 */
                rebuild: function(id) {
                        var data = _dialogs.get(id);
-                       if (typeof data === 'undefined') {
+                       if (data === undefined) {
                                throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog.");
                        }
                        
@@ -352,7 +352,7 @@ define(['jquery', 'enquire', 'Core', 'Dictionary', 'DOM/Util'], function($, enqu
                 */
                close: function(id) {
                        var data = _dialogs.get(id);
-                       if (typeof data === 'undefined') {
+                       if (data === undefined) {
                                throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog.");
                        }
                        
index c0b42cfdffc9c53d46ca9a6698368c81c7cec431..258cf3b8c384c854d2ee252e13ea74ec5ea9ec9a 100644 (file)
@@ -182,7 +182,7 @@ define(
                 */
                setAlignmentById: function(containerId) {
                        var dropdown = _dropdowns.get(containerId);
-                       if (dropdown === null) {
+                       if (dropdown === undefined) {
                                throw new Error("Unknown dropdown identifier '" + containerId + "'.");
                        }
                        
@@ -198,7 +198,7 @@ define(
                 */
                close: function(containerId) {
                        var dropdown = _dropdowns.get(containerId);
-                       if (dropdown !== null) {
+                       if (dropdown !== undefined) {
                                dropdown.classList.remove('dropdownOpen');
                                _menus.get(containerId).classList.remove('dropdownOpen');
                        }
@@ -314,7 +314,7 @@ define(
                        // check if 'isOverlayDropdownButton' is set which indicates if
                        // the dropdown toggle is in an overlay
                        var dropdown = _dropdowns.get(targetId);
-                       if (dropdown !== null && dropdown.getAttribute('data-is-overlay-dropdown-button') === null) {
+                       if (dropdown !== undefined && dropdown.getAttribute('data-is-overlay-dropdown-button') === null) {
                                var dialogContent = DOMTraverse.parentByClass(dropdown, 'dialogContent');
                                dropdown.setAttribute('data-is-overlay-dropdown-button', (dialogContent !== null));
                                
index 1d34365218e31a8faee649656994ba63d80fb8c8..3c7ff955ad2a79f1bef2da43b41e9b9859f0db51 100644 (file)
@@ -90,7 +90,7 @@ define(['Core', 'Dictionary', 'DOM/Traverse', 'DOM/Util', 'UI/SimpleDropdown'],
                 */
                rebuild: function(containerId) {
                        var container = this._containers.get(containerId);
-                       if (container === null) {
+                       if (container === undefined) {
                                throw "Expected a valid element id, '" + containerId + "' is unknown.";
                        }
                        
@@ -104,7 +104,7 @@ define(['Core', 'Dictionary', 'DOM/Traverse', 'DOM/Util', 'UI/SimpleDropdown'],
                        var items = DOMTraverse.childrenByTag(list, 'LI');
                        var dropdown = this._dropdowns.get(containerId);
                        var dropdownWidth = 0;
-                       if (dropdown !== null) {
+                       if (dropdown !== undefined) {
                                // show all items for calculation
                                for (var i = 0, length = items.length; i < length; i++) {
                                        var item = items[i];
@@ -192,7 +192,7 @@ define(['Core', 'Dictionary', 'DOM/Traverse', 'DOM/Util', 'UI/SimpleDropdown'],
                                dropdownMenu.innerHTML = '';
                                dropdownMenu.appendChild(fragment);
                        }
-                       else if (dropdown !== null && dropdown.parentNode !== null) {
+                       else if (dropdown !== undefined && dropdown.parentNode !== null) {
                                dropdown.parentNode.removeChild(dropdown);
                        }
                }
index 057b2cc915bb57d6f894e5af547223c0597f9be5..fcf06a4907f9f4148ef13fb367c115933153c0e2 100644 (file)
@@ -51,7 +51,7 @@ define(['Dictionary', 'DOM/Util', './TabMenu/Simple'], function(Dictionary, DOMU
                 * Returns a SimpleTabMenu instance for given container id.
                 * 
                 * @param       {string}        containerId     tab menu container id
-                * @return      {SimpleTabMenu} tab menu object
+                * @return      {(SimpleTabMenu|undefined)}     tab menu object
                 */
                getTabMenu: function(containerId) {
                        return _tabMenus.get(containerId);
index c1163bc769c359c790297c80dca4a845c0d2390c..c3f94cba989b42cc60ce71cef9cbb541c3aff5c5 100644 (file)
@@ -83,7 +83,7 @@ define(['jquery', 'Dictionary', 'DOM/Util', 'EventHandler'], function($, Diction
                                }
                                
                                var container = this._containers.get(name);
-                               if (container === null) {
+                               if (container === undefined) {
                                        throw new Error("Expected content element for li[data-name='" + name + "'] (tab menu id: '" + this._containerId + "').");
                                }
                                else if (container.parentNode !== this._container) {
@@ -140,7 +140,7 @@ define(['jquery', 'Dictionary', 'DOM/Util', 'EventHandler'], function($, Diction
                                if (preselect !== false) {
                                        if (preselect !== true) {
                                                var tab = this._tabs.get(preselect);
-                                               if (tab !== null) {
+                                               if (tab !== undefined) {
                                                        this.select(null, tab, true);
                                                }
                                        }
index 476b30f018ada30894f8d9298f8b4d9574eadea1..c80efd7f3eca7616642117498c7fb3d8f5b8632c 100644 (file)
@@ -274,21 +274,8 @@ fieldset {
                }
        }
        
-       .pointer {
-               border-color: @wcfTooltipBackgroundColor transparent;
-               border-style: solid;
-               border-width: 0 5px 5px;
-               left: 50%;
-               position: absolute;
-               top: -5px;
-       }
-       
        .boxShadow(0, 3px, rgba(0, 0, 0, .3), 7px);
        
-       &.inverse > .pointer {
-               border-width: 5px 5px 0;
-       }
-       
        &.active {
                opacity: 1;
                visibility: visible;
@@ -297,70 +284,6 @@ fieldset {
        }
 }
 
-/* popover */
-.popover {
-       background-color: rgba(0, 0, 0, .4);
-       border-radius: 6px;
-       padding: @wcfGapSmall;
-       position: absolute;
-       vertical-align: middle;
-       width: 400px !important;
-       z-index: 500;
-       
-       .boxShadow(0, 1px, rgba(0, 0, 0, .3), 7px);
-       
-       > .icon-spinner {
-               color: white;
-               left: 50%;
-               margin-left: -21px;
-               margin-top: -21px;
-               position: absolute;
-               top: 50%;
-               
-               .textShadow(black);
-       }
-       
-       > .popoverContent {
-               background-color: @wcfContainerBackgroundColor;
-               border-radius: 6px;
-               color: @wcfColor;
-               max-height: 300px;
-               min-height: 16px;
-               opacity: 0;
-               overflow: hidden;
-               padding: @wcfGapSmall @wcfGapMedium;
-       }
-       
-       &::after {
-               border: 10px solid transparent;
-               content: "";
-               display: inline-block;
-               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;
-       }
-}
-
-
 /* ### badges ### */
 /* default values */
 .badge {
diff --git a/wcfsetup/install/files/style/popover.less b/wcfsetup/install/files/style/popover.less
new file mode 100644 (file)
index 0000000..d0e771e
--- /dev/null
@@ -0,0 +1,73 @@
+.popover {
+       background-color: rgba(0, 0, 0, .4);
+       border: 3px solid transparent;
+       border-radius: 3px;
+       position: absolute;
+       vertical-align: middle;
+       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%;
+               
+               .textShadow(black);
+       }
+       
+       > .popoverContent {
+               background-color: @wcfContainerBackgroundColor;
+               border-radius: 3px;
+               color: @wcfColor;
+               max-height: 300px;
+               min-height: 16px;
+               opacity: 0;
+               overflow: hidden;
+               padding: @wcfGapSmall @wcfGapMedium;
+       }
+       
+       > .elementPointer {
+               border-color: rgba(0, 0, 0, .4) transparent;
+               border-style: solid;
+               border-width: 0 5px 5px;
+               top: -3px;
+               
+               &.flipVertical {
+                       border-width: 5px 5px 0;
+                       bottom: -3px;
+               }
+       }
+       
+       &::after {
+               border: 10px solid transparent;
+               content: "";
+               display: inline-block;
+               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;
+       }
+}