From: Alexander Ebert Date: Wed, 13 May 2015 14:52:47 +0000 (+0200) Subject: Popover overhaul, work in progress X-Git-Tag: 3.0.0_Beta_1~2400 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=ab8ebbc41998397bbcbbea7e4cf7db1167dca42a;p=GitHub%2FWoltLab%2FWCF.git Popover overhaul, work in progress --- diff --git a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl index 4378f6602f..9c143ac40b 100644 --- a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl +++ b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl @@ -145,6 +145,30 @@ 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) + }); + } + }); + }); }); @@ -186,7 +210,7 @@ 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(); @@ -211,22 +235,5 @@ }); //]]> - {include file='imageViewer'} diff --git a/wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js b/wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js index 20fb4ec9e1..fab271bd75 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js @@ -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 index 0000000000..d45dd953ca --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js @@ -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(); +}); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js index bfed2ea461..e52e90440e 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js @@ -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. * diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js b/wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js index 5502686441..b3cd007341 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js @@ -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; }, /** diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js index 47f8ae3b5c..74720dbc02 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js @@ -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; } diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js index 21b43b1d5d..4ca50472d8 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js @@ -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; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js index e4e9c03186..0c993a2640 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js @@ -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."); } diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js index c0b42cfdff..258cf3b8c3 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js @@ -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)); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js index 1d34365218..3c7ff955ad 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js @@ -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); } } diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js index 057b2cc915..fcf06a4907 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js @@ -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); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js index c1163bc769..c3f94cba98 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js @@ -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); } } diff --git a/wcfsetup/install/files/style/global.less b/wcfsetup/install/files/style/global.less index 476b30f018..c80efd7f3e 100644 --- a/wcfsetup/install/files/style/global.less +++ b/wcfsetup/install/files/style/global.less @@ -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 index 0000000000..d0e771ecc1 --- /dev/null +++ b/wcfsetup/install/files/style/popover.less @@ -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; + } +}