From 09f7100ba1e817d9a35e80b9440b7b35937a86f4 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Mon, 11 May 2015 19:51:11 +0200 Subject: [PATCH] Reworked dialogs and tooltips --- .../templates/headIncludeJavaScript.tpl | 1 - wcfsetup/install/files/js/WCF.User.js | 76 +-- wcfsetup/install/files/js/WCF.js | 632 +----------------- .../install/files/js/WoltLab/WCF/Bootstrap.js | 10 +- .../install/files/js/WoltLab/WCF/DOM/Util.js | 15 + .../files/js/WoltLab/WCF/UI/Alignment.js | 103 ++- .../install/files/js/WoltLab/WCF/UI/Dialog.js | 382 +++++++++++ .../files/js/WoltLab/WCF/UI/Tooltip.js | 118 ++++ wcfsetup/install/files/js/require.config.js | 1 + wcfsetup/install/files/style/button.less | 1 + wcfsetup/install/files/style/dialog.less | 215 +++--- wcfsetup/install/files/style/global.less | 57 +- wcfsetup/setup/db/install.sql | 2 +- 13 files changed, 805 insertions(+), 808 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js diff --git a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl index c310324ff9..c6ddd51940 100644 --- a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl +++ b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl @@ -181,7 +181,6 @@ //getStyleHandler()->countStyles() > 1}new WCF.Style.Chooser();{/if} WCF.System.PageNavigation.init('.pageNavigation'); diff --git a/wcfsetup/install/files/js/WCF.User.js b/wcfsetup/install/files/js/WCF.User.js index 72046114ea..f66f9d5160 100644 --- a/wcfsetup/install/files/js/WCF.User.js +++ b/wcfsetup/install/files/js/WCF.User.js @@ -552,69 +552,31 @@ WCF.User.Panel.UserMenu = WCF.User.Panel.Abstract.extend({ * Quick login box */ WCF.User.QuickLogin = { - /** - * dialog overlay - * @var jQuery - */ - _dialog: null, - - /** - * login message container - * @var jQuery - */ - _loginMessage: null, - /** * Initializes the quick login box */ init: function() { - $('.loginLink').click($.proxy(this._render, this)); - - // prepend protocol and hostname - $('#loginForm input[name=url]').val(function(index, value) { - return window.location.protocol + '//' + window.location.host + value; - }); - }, - - /** - * Displays the quick login box with a info message - * - * @param string message - */ - show: function(message) { - if (message) { - if (this._loginMessage === null) { - this._loginMessage = $('

').hide().prependTo($('#loginForm > form')); + require(['UI/Dialog'], function(UIDialog) { + var loginForm = document.getElementById('loginForm'); + + var links = document.getElementsByClassName('loginLink'); + for (var i = 0, length = links.length; i < length; i++) { + links[i].addEventListener('click', function(event) { + event.preventDefault(); + + loginForm.style.removeProperty('display'); + + UIDialog.open('loginForm', null, { + title: WCF.Language.get('wcf.user.login') + }); + }); } - this._loginMessage.show().text(message); - } - else if (this._loginMessage !== null) { - this._loginMessage.hide(); - } - - this._render(); - }, - - /** - * Renders the dialog - * - * @param jQuery.Event event - */ - _render: function(event) { - if (event !== undefined) { - event.preventDefault(); - } - - if (this._dialog === null) { - this._dialog = $('#loginForm').wcfDialog({ - title: WCF.Language.get('wcf.user.login') - }); - this._dialog.find('#username').focus(); - } - else { - this._dialog.wcfDialog('open'); - } + var input = loginForm.querySelector('#loginForm input[name=url]'); + if (input !== null) { + input.setAttribute('value', window.location.protocol + '//' + window.location.host + input.getAttribute('value')); + } + }); } }; diff --git a/wcfsetup/install/files/js/WCF.js b/wcfsetup/install/files/js/WCF.js index 875b0a8c86..18663caedc 100755 --- a/wcfsetup/install/files/js/WCF.js +++ b/wcfsetup/install/files/js/WCF.js @@ -5123,211 +5123,6 @@ WCF.Effect.SmoothScroll = WCF.Effect.Scroll.extend({ } }); -/** - * Creates the balloon tool-tip. - */ -WCF.Effect.BalloonTooltip = Class.extend({ - /** - * initialization state - * @var boolean - */ - _didInit: false, - - /** - * tooltip element - * @var jQuery - */ - _tooltip: null, - - /** - * cache viewport dimensions - * @var object - */ - _viewportDimensions: { }, - - /** - * Initializes tooltips. - */ - init: function() { - if (jQuery.browser.mobile) return; - - if (!this._didInit) { - // create empty div - this._tooltip = $('

').appendTo($('body')).hide(); - - // get viewport dimensions - this._updateViewportDimensions(); - - // update viewport dimensions on resize - $(window).resize($.proxy(this._updateViewportDimensions, this)); - - // observe DOM changes - WCF.DOMNodeInsertedHandler.addCallback('WCF.Effect.BalloonTooltip', $.proxy(this.init, this)); - - this._didInit = true; - } - - // init elements - $('.jsTooltip').each($.proxy(this._initTooltip, this)); - }, - - /** - * Updates cached viewport dimensions. - */ - _updateViewportDimensions: function() { - this._viewportDimensions = $(document).getDimensions(); - }, - - /** - * Initializes a tooltip element. - * - * @param integer index - * @param object element - */ - _initTooltip: function(index, element) { - var $element = $(element); - - if ($element.hasClass('jsTooltip')) { - $element.removeClass('jsTooltip'); - var $title = $element.attr('title'); - - // ignore empty elements - if ($title !== '') { - $element.data('tooltip', $title); - $element.removeAttr('title'); - - $element.hover( - $.proxy(this._mouseEnterHandler, this), - $.proxy(this._mouseLeaveHandler, this) - ); - $element.click($.proxy(this._mouseLeaveHandler, this)); - } - } - }, - - /** - * Shows tooltip on hover. - * - * @param object event - */ - _mouseEnterHandler: function(event) { - var $top, $left; - var $element = $(event.currentTarget); - - var $title = $element.attr('title'); - if ($title && $title !== '') { - $element.data('tooltip', $title); - $element.removeAttr('title'); - } - - // reset tooltip position - this._tooltip.css({ - top: "0px", - left: "0px" - }); - - // empty tooltip, skip - if (!$element.data('tooltip')) { - this._tooltip.hide(); - return; - } - - // update text - this._tooltip.children('span:eq(0)').text($element.data('tooltip')); - - // get arrow - var $arrow = this._tooltip.find('.pointer'); - - // get arrow width - this._tooltip.show(); - var $arrowWidth = $arrow.outerWidth(); - this._tooltip.hide(); - - // calculate position - var $elementOffsets = $element.getOffsets('offset'); - var $elementDimensions = $element.getDimensions('outer'); - var $tooltipDimensions = this._tooltip.getDimensions('outer'); - var $tooltipDimensionsInner = this._tooltip.getDimensions('inner'); - - var $elementCenter = $elementOffsets.left + Math.ceil($elementDimensions.width / 2); - var $tooltipHalfWidth = Math.ceil($tooltipDimensions.width / 2); - - // determine alignment - var $alignment = 'center'; - if (($elementCenter - $tooltipHalfWidth) < 5) { - $alignment = 'left'; - } - else if ((this._viewportDimensions.width - 5) < ($elementCenter + $tooltipHalfWidth)) { - $alignment = 'right'; - } - - // calculate top offset - if ($elementOffsets.top + $elementDimensions.height + $tooltipDimensions.height - $(document).scrollTop() < $(window).height()) { - $top = $elementOffsets.top + $elementDimensions.height + 7; - this._tooltip.removeClass('inverse'); - $arrow.css('top', -5); - } - else { - $top = $elementOffsets.top - $tooltipDimensions.height - 7; - this._tooltip.addClass('inverse'); - $arrow.css('top', $tooltipDimensions.height); - } - - var $property = (WCF.Language.get('wcf.global.pageDirection') == 'rtl' ? 'right' : 'left'); - - // calculate left offset - switch ($alignment) { - case 'center': - $left = Math.round($elementOffsets.left - $tooltipHalfWidth + ($elementDimensions.width / 2)); - - $arrow.css($property, ($tooltipDimensionsInner.width / 2 - $arrowWidth / 2) + 'px'); - break; - - case 'left': - $left = $elementOffsets.left; - - if ($property === 'right') { - $arrow.css($property, ($tooltipDimensionsInner.width - $arrowWidth - 5) + 'px'); - } - else { - $arrow.css($property, '5px'); - } - break; - - case 'right': - $left = $elementOffsets.left + $elementDimensions.width - $tooltipDimensions.width; - - if ($property === 'right') { - $arrow.css($property, '5px'); - } - else { - $arrow.css($property, ($tooltipDimensionsInner.width - $arrowWidth - 5) + 'px'); - } - break; - } - - // move tooltip - this._tooltip.css({ - top: $top + "px", - left: $left + "px" - }); - - // show tooltip - this._tooltip.wcfFadeIn(); - }, - - /** - * Hides tooltip once cursor left the element. - * - * @param object event - */ - _mouseLeaveHandler: function(event) { - this._tooltip.stop().hide().css({ - opacity: 1 - }); - } -}); - /** * Handles clicks outside an overlay, hitting body-tag through bubbling. * @@ -10181,427 +9976,28 @@ WCF.UserPanel = Class.extend({ _after: function(dropdownMenu) { } }); -/** - * WCF implementation for dialogs, based upon ideas by jQuery UI. - */ -$.widget('ui.wcfDialog', { - /** - * close button - * @var jQuery - */ - _closeButton: null, - - /** - * dialog container - * @var jQuery - */ - _container: null, - - /** - * dialog content - * @var jQuery - */ - _content: null, - - /** - * modal overlay - * @var jQuery - */ - _overlay: null, - - /** - * plain html for title - * @var string - */ - _title: null, - - /** - * title bar - * @var jQuery - */ - _titlebar: null, - - /** - * dialog visibility state - * @var boolean - */ - _isOpen: false, - - /** - * option list - * @var object - */ - options: { - // dialog - autoOpen: true, - closable: true, - closeButtonLabel: null, - closeConfirmMessage: null, - closeViaModal: true, - hideTitle: false, - modal: true, - title: '', - zIndex: 400, - - // event callbacks - onClose: null, - onShow: null - }, - - /** - * @see $.widget._createWidget() - */ - _createWidget: function(options, element) { - // ignore script tags - if ($(element).getTagName() === 'script') { - console.debug("[ui.wcfDialog] Ignored script tag"); - this.element = false; - return null; - } - - $.Widget.prototype._createWidget.apply(this, arguments); - }, - - /** - * Initializes a new dialog. - */ - _init: function() { - if (this.options.autoOpen) { - this.open(); - } - - // act on resize - $(window).resize($.proxy(this._resize, this)); - }, - - /** - * Creates a new dialog instance. - */ - _create: function() { - if (this.options.closeButtonLabel === null) { - this.options.closeButtonLabel = WCF.Language.get('wcf.global.button.close'); - } - - // create dialog container - this._container = $('
').hide().css({ zIndex: this.options.zIndex }).appendTo(document.body); - this._titlebar = $('
').hide().appendTo(this._container); - this._title = $('').hide().appendTo(this._titlebar); - this._closeButton = $('').click($.proxy(this.close, this)).hide().appendTo(this._titlebar); - this._content = $('
').appendTo(this._container); - - this._setOption('title', this.options.title); - this._setOption('closable', this.options.closable); - - // move target element into content - var $content = this.element.detach(); - this._content.html($content); - - // create modal view - if (this.options.modal) { - this._overlay = $('#jsWcfDialogOverlay'); - if (!this._overlay.length) { - this._overlay = $('
').css({ height: '100%', zIndex: 399 }).hide().appendTo(document.body); - } - - if (this.options.closable && this.options.closeViaModal) { - this._overlay.click($.proxy(this.close, this)); - - $(document).keyup($.proxy(function(event) { - if (event.keyCode && event.keyCode === $.ui.keyCode.ESCAPE) { - this.close(); - event.preventDefault(); - } - }, this)); - } - } - - WCF.DOMNodeInsertedHandler.execute(); - }, - - /** - * Sets the given option to the given value. - * See the jQuery UI widget documentation for more. - */ - _setOption: function(key, value) { - this.options[key] = value; - - if (key == 'hideTitle' || key == 'title') { - if (!this.options.hideTitle && this.options.title != '') { - this._title.html(this.options.title).show(); - } else { - this._title.html(''); - } - } else if (key == 'closable' || key == 'closeButtonLabel') { - if (this.options.closable) { - this._closeButton.attr('title', this.options.closeButtonLabel).show().find('span').html(this.options.closeButtonLabel); - - WCF.DOMNodeInsertedHandler.execute(); - } else { - this._closeButton.hide(); - } - } - - if ((!this.options.hideTitle && this.options.title != '') || this.options.closable) { - this._titlebar.show(); - } else { - this._titlebar.hide(); - } - - return this; - }, - - /** - * Opens this dialog. - */ - open: function() { - // ignore script tags - if (this.element === false) { - return; - } - - if (this.isOpen()) { - return; - } +jQuery.fn.extend({ + wcfDialog: function(method) { + var args = arguments; - if (this._overlay !== null) { - WCF.activeDialogs++; + require(['DOM/Util', 'UI/Dialog'], (function(DOMUtil, UIDialog) { + var id = DOMUtil.identify(this[0]); - if (WCF.activeDialogs === 1) { - this._overlay.show(); + if (method === 'close') { + UIDialog.close(id); } - } - - this.render(); - this._isOpen = true; - - this._content.find('.jsDialogAutoFocus:visible:first').focus(); - }, - - /** - * Returns true if dialog is visible. - * - * @return boolean - */ - isOpen: function() { - return this._isOpen; - }, - - /** - * Closes this dialog. - * - * This function can be manually called, even if the dialog is set as not - * closable by the user. - * - * @param object event - */ - close: function(event) { - if (!this.isOpen()) { - return; - } - - if (this.options.closeConfirmMessage) { - WCF.System.Confirmation.show(this.options.closeConfirmMessage, $.proxy(function(action) { - if (action === 'confirm') { - this._close(); - } - }, this)); - } - else { - this._close(); - } - - if (event !== undefined) { - event.preventDefault(); - } - }, - - /** - * Handles dialog closing, should never be called directly. - * - * @see $.ui.wcfDialog.close() - */ - _close: function() { - this._isOpen = false; - this._container.wcfFadeOut(); - - if (this._container.data('wcfDialogScrollOffset')) { - window.scrollTo(0, this._container.data('wcfDialogScrollOffset')); - } - - if (this._overlay !== null) { - WCF.activeDialogs--; - - if (WCF.activeDialogs === 0) { - this._overlay.hide(); + else if (method === 'render') { + UIDialog.rebuild(id); } - } - - if (this.options.onClose !== null) { - this.options.onClose(); - } - }, - - /** - * Renders dialog on resize if visible. - */ - _resize: function() { - if (this.isOpen()) { - this.render(); - } - }, - - /** - * Renders this dialog, should be called whenever content is updated. - */ - render: function() { - // check if this if dialog was previously hidden and container is fixed - // at 0px (mobile optimization), in this case scroll to top - if (!this._container.is(':visible') && this._container.css('top') === '0px') { - // save scrolling - this._container.data('wcfDialogScrollOffset', $(window).scrollTop()); - - window.scrollTo(0, 0); - } - - // force dialog and it's contents to be visible - this._container.show(); - this._content.children().show(); - - // remove fixed content dimensions for calculation - this._content.css({ - height: 'auto', - width: 'auto' - }); - - // terminate concurrent rendering processes - this._container.stop(); - this._content.stop(); - - // set dialog to be fully opaque, prevents weird bugs in WebKit - this._container.show().css('opacity', 1.0); - - // handle positioning of form submit controls - var $heightDifference = 0; - if (this._content.find('.formSubmit').length) { - $heightDifference = this._content.find('.formSubmit').outerHeight(); - - this._content.addClass('dialogForm').css({ marginBottom: $heightDifference + 'px' }); - } - else { - this._content.removeClass('dialogForm').css({ marginBottom: '0px' }); - } - - // force 800px or 90% width - var $windowDimensions = $(window).getDimensions(); - if ($windowDimensions.width * 0.9 > 800) { - this._container.css('maxWidth', '800px'); - } - - // calculate dimensions - var $containerDimensions = this._container.getDimensions('outer'); - var $contentDimensions = this._content.getDimensions(); - - // calculate maximum content height - var $heightDifference = $containerDimensions.height - $contentDimensions.height; - var $maximumHeight = $windowDimensions.height - $heightDifference - 120; - this._content.css({ maxHeight: $maximumHeight + 'px' }); - - this._determineOverflow(); - - // calculate new dimensions - $containerDimensions = this._container.getDimensions('outer'); - - // move container - var $leftOffset = Math.round(($windowDimensions.width - $containerDimensions.width) / 2); - var $topOffset = Math.round(($windowDimensions.height - $containerDimensions.height) / 2); - - // place container at 20% height if possible - var $desiredTopOffset = Math.round(($windowDimensions.height / 100) * 20); - if ($desiredTopOffset < $topOffset) { - $topOffset = $desiredTopOffset; - } - - // apply offset - this._container.css({ - left: $leftOffset + 'px', - top: $topOffset + 'px' - }); - - // remove static dimensions - this._content.css({ - height: 'auto', - width: 'auto' - }); - - if (!this.isOpen()) { - // hide container again - this._container.hide(); - - // fade in container - this._container.wcfFadeIn($.proxy(function() { - if (this.options.onShow !== null) { - this.options.onShow(); + else if (method === 'option') { + if (args.length === 3 && args[1] === 'title' && typeof args[2] === 'string') { + UIDialog.setTitle(id, args[2]); } - }, this)); - } - }, - - /** - * Determines content overflow based upon static dimensions. - */ - _determineOverflow: function() { - var $max = $(window).getDimensions(); - var $maxHeight = this._content.css('maxHeight'); - this._content.css('maxHeight', 'none'); - var $dialog = this._container.getDimensions('outer'); - - var $overflow = 'visible'; - if (($max.height * 0.8 < $dialog.height) || ($max.width * 0.8 < $dialog.width)) { - $overflow = 'auto'; - } - - this._content.css('overflow', $overflow); - this._content.css('maxHeight', $maxHeight); - - if ($overflow === 'visible') { - // content may already overflow, even though the overall height is still below the threshold - var $contentHeight = 0; - this._content.children().each(function(index, child) { - $contentHeight += $(child).outerHeight(); - }); - - if (this._content.height() < $contentHeight) { - $overflow = 'auto'; - this._content.css('overflow', 'auto'); - } - } - - // Firefox ignores padding-bottom for elements within an overflowing container - if ($.browser.mozilla && !$.browser.mobile) { - if ($overflow === 'auto') { - this._content.children('div').css('margin-bottom', this._content.css('padding-bottom')); } else { - this._content.children('div').css('margin-bottom', false); + UIDialog.open(id, null, (args.length === 1 && typeof args[0] === 'object') ? args[0] : {}); } - } - }, - - /** - * Returns calculated content dimensions. - * - * @param integer maximumHeight - * @return object - */ - _getContentDimensions: function(maximumHeight) { - var $contentDimensions = this._content.getDimensions(); - - // set height to maximum height if exceeded - if (maximumHeight && $contentDimensions.height > maximumHeight) { - $contentDimensions.height = maximumHeight; - } - - return $contentDimensions; + }).bind(this)); } }); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Bootstrap.js b/wcfsetup/install/files/js/WoltLab/WCF/Bootstrap.js index 25fb9c7b4a..e0629c73ce 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Bootstrap.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Bootstrap.js @@ -9,8 +9,8 @@ * @module WoltLab/WCF/Bootstrap */ define( - [ 'jquery', 'favico', 'enquire', 'WoltLab/WCF/Date/Time/Relative', 'UI/SimpleDropdown', 'WoltLab/WCF/UI/Mobile', 'WoltLab/WCF/UI/TabMenu', 'WoltLab/WCF/UI/FlexibleMenu'], - function($, favico, enquire, relativeTime, simpleDropdown, uiMobile, TabMenu, UIFlexibleMenu) + [ 'jquery', 'favico', 'enquire', 'WoltLab/WCF/Date/Time/Relative', 'UI/SimpleDropdown', 'WoltLab/WCF/UI/Mobile', 'WoltLab/WCF/UI/TabMenu', 'WoltLab/WCF/UI/FlexibleMenu', 'UI/Dialog', 'WoltLab/WCF/UI/Tooltip'], + function($, favico, enquire, relativeTime, simpleDropdown, UIMobile, UITabMenu, UIFlexibleMenu, UIDialog, UITooltip) { "use strict"; @@ -28,9 +28,11 @@ define( setup: function() { relativeTime.setup(); simpleDropdown.setup(); - uiMobile.setup(); - TabMenu.setup(); + UIMobile.setup(); + UITabMenu.setup(); UIFlexibleMenu.setup(); + UIDialog.setup(); + UITooltip.setup(); $.holdReady(false); } diff --git a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js index 2a5a7e8454..bfed2ea461 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js @@ -135,6 +135,21 @@ define(function() { }; }, + /** + * Prepends an element to a parent element. + * + * @param {Element} el element to prepend + * @param {Element} parentEl future containing element + */ + prepend: function(el, parentEl) { + if (parentEl.childElementCount === 0) { + parentEl.appendChild(el); + } + else { + parentEl.insertBefore(el, parentEl.children[0]); + } + }, + /** * Applies a list of CSS properties to an element. * diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js index f8100caa35..21b43b1d5d 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js @@ -6,7 +6,7 @@ * @license GNU Lesser General Public License * @module WoltLab/WCF/UI/Alignment */ -define(['Core', 'DOM/Util'], function(Core, DOMUtil) { +define(['Core', 'DOM/Traverse', 'DOM/Util'], function(Core, DOMTraverse, DOMUtil) { "use strict"; /** @@ -29,13 +29,16 @@ define(['Core', 'DOM/Util'], function(Core, DOMUtil) { // align the pointer element, expects .pointer as a direct child of given element pointer: false, + // offset from/left side, ignored for center alignment + pointerOffset: 4, + // use static pointer positions, expects two items: class to move it to the bottom and the second to move it to the right pointerClassNames: [], // alternate element used to calculate dimensions refDimensionsElement: null, - // preferred alignment, possible values: left/right and top/bottom + // preferred alignment, possible values: left/right/center and top/bottom horizontal: 'left', vertical: 'bottom', @@ -43,36 +46,58 @@ define(['Core', 'DOM/Util'], function(Core, DOMUtil) { allowFlip: 'both' }, options); - if (!Array.isArray(options.pointerClassNames) || options.pointerClassNames.length !== 2) options.pointerClassNames = []; - if (options.horizontal !== 'right') options.horizontal = 'left'; + 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 (['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 DOMUtil.setStyles(el, { - bottom: 'auto', - left: '0px', - right: 'auto', - top: '0px' + bottom: 'auto !important', + left: '0 !important', + right: 'auto !important', + top: '0 !important' }); var elDimensions = DOMUtil.outerDimensions(el); var refDimensions = DOMUtil.outerDimensions((options.refDimensionsElement instanceof Element ? options.refDimensionsElement : ref)); var refOffsets = DOMUtil.offset(ref); var windowHeight = window.innerHeight; - var windowWidth = window.innerWidth; + var windowWidth = document.body.clientWidth; + + var horizontal = { result: null }; + var alignCenter = false; + if (options.horizontal === 'center') { + alignCenter = true; + horizontal = this._tryAlignmentHorizontal(options.horizontal, elDimensions, refDimensions, refOffsets, windowWidth); + + if (!horizontal.result) { + if (options.allowFlip === 'both' || options.allowFlip === 'horizontal') { + options.horizontal = 'left'; + } + else { + horizontal.result = true; + } + } + } // in rtl languages we simply swap the value for 'horizontal' if (WCF.Language.get('wcf.global.pageDirection') === 'rtl') { options.horizontal = (options.horizontal === 'left') ? 'right' : 'left'; } - var horizontal = this._tryAlignmentHorizontal(options.horizontal, elDimensions, refDimensions, refOffsets, windowWidth); - if (!horizontal.result && (options.allowFlip === 'both' || options.allowFlip === 'horizontal')) { - var horizontalFlipped = this._tryAlignmentHorizontal((options.horizontal === 'left' ? 'right' : 'left'), elDimensions, refDimensions, refOffsets, windowWidth); - // only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction - if (horizontalFlipped.result) { - horizontal = horizontalFlipped; + if (!horizontal.result) { + var horizontalCenter = horizontal; + horizontal = this._tryAlignmentHorizontal(options.horizontal, elDimensions, refDimensions, refOffsets, windowWidth); + if (!horizontal.result && (options.allowFlip === 'both' || options.allowFlip === 'horizontal')) { + var horizontalFlipped = this._tryAlignmentHorizontal((options.horizontal === 'left' ? 'right' : 'left'), elDimensions, refDimensions, refOffsets, windowWidth); + // only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction + if (horizontalFlipped.result) { + horizontal = horizontalFlipped; + } + else if (alignCenter) { + horizontal = horizontalCenter; + } } } @@ -93,9 +118,31 @@ define(['Core', 'DOM/Util'], function(Core, DOMUtil) { // set pointer position if (options.pointer) { - //var pointer = null; - // TODO: implement pointer support, e.g. for interactive dropdowns - console.debug("TODO"); + var pointer = DOMTraverse.childrenByClass(el, 'elementPointer'); + pointer = pointer[0] || null; + if (pointer === null) { + throw new Error("Expected the .elementPointer element to be a direct children."); + } + + if (horizontal.align === 'center') { + pointer.classList.add('center'); + + pointer.classList.remove('left'); + pointer.classList.remove('right'); + } + else { + pointer.classList.add(horizontal.align); + + pointer.classList.remove('center'); + pointer.classList.remove(horizontal.align === 'left' ? 'right' : 'left'); + } + + if (vertical.align === 'top') { + pointer.classList.add('flipVertical'); + } + else { + pointer.classList.remove('flipVertical'); + } } else if (options.pointerClassNames.length === 2) { var pointerRight = 0; @@ -134,14 +181,24 @@ define(['Core', 'DOM/Util'], function(Core, DOMUtil) { result = false; } } + else if (align === 'right') { + console.debug(windowWidth + " | " + refOffsets.left + " | " + refDimensions.width); + right = windowWidth - (refOffsets.left + refDimensions.width); + if (right < 0) { + result = false; + } + } else { - right = refOffsets.left + refDimensions.width; - if (right - elDimensions.width < 0) { + left = refOffsets.left + (refDimensions.width / 2) - (elDimensions.width / 2); + left = ~~left; + + if (left < 0 || left + elDimensions.width > windowWidth) { result = false; } } return { + align: align, left: left, right: right, result: result @@ -165,8 +222,9 @@ define(['Core', 'DOM/Util'], function(Core, DOMUtil) { var result = true; if (align === 'top') { - bottom = refOffsets.top + verticalOffset; - if (bottom - elDimensions.height < 0) { + var bodyHeight = document.body.clientHeight; + bottom = (bodyHeight - refOffsets.top) + verticalOffset; + if (bottom + elDimensions.height > document.body.clientHeight) { result = false; } } @@ -178,6 +236,7 @@ define(['Core', 'DOM/Util'], function(Core, DOMUtil) { } return { + align: align, bottom: bottom, top: top, result: result diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js new file mode 100644 index 0000000000..e8556a8653 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js @@ -0,0 +1,382 @@ +/** + * Modal dialog handler. + * + * @author Alexander Ebert + * @copyright 2001-2015 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/UI/Dialog + */ +define(['jquery', 'Core', 'Dictionary', 'DOM/Util'], function($, Core, Dictionary, DOMUtil) { + "use strict"; + + var _activeDialog = null; + var _container = null; + var _dialogs = null; + var _keyupListener = null; + + /** + * @constructor + */ + function UIDialog() {}; + UIDialog.prototype = { + /** + * Sets up global container and internal variables. + */ + setup: function() { + _container = document.createElement('div'); + _container.classList.add('dialogOverlay'); + _container.setAttribute('aria-hidden', 'true'); + _container.addEventListener('click', this._closeOnBackdrop.bind(this)); + + document.body.appendChild(_container); + + _dialogs = new Dictionary(); + + _keyupListener = (function(event) { + if (event.keyCode === 27) { + if (event.target.nodeName !== 'INPUT' && event.target.nodeName !== 'TEXTAREA') { + this.close(_activeDialog); + + return false; + } + } + + return true; + }).bind(this); + }, + + /** + * Opens an dialog, if the dialog is already open the content container + * will be replaced by the HTML string contained in the parameter html. + * + * If id is an existing element id, html will be ignored and the referenced + * element will be appended to the content element instead. + * + * @param {string} id element id, if exists the html parameter is ignored in favor of the existing element + * @param {?string} html content html + * @param {object} options list of options, is completely ignored if the dialog already exists + */ + open: function(id, html, options) { + if (_dialogs.has(id)) { + this._updateDialog(id, html); + } + else { + options = Core.extend({ + backdropCloseOnClick: true, + closable: true, + closeButtonLabel: WCF.Language.get('wcf.global.button.close'), + closeConfirmMessage: '', + disposeOnClose: false, + title: '', + + // callbacks + onBeforeClose: null, + onClose: null, + onShow: null + }, options); + + if (!options.closable) options.backdropCloseOnClick = false; + if (options.closeConfirmMessage) { + options.onBeforeClose = (function(id) { + WCF.System.Confirmation.show(options.closeConfirmMessage, (function(action) { + if (action === 'confirm') { + this.close(id); + } + }).bind(this)); + }).bind(this); + } + + this._createDialog(id, html, options); + } + }, + + /** + * Sets the dialog title. + * + * @param {string} id element id + * @param {string} title dialog title + */ + setTitle: function(id, title) { + var data = _dialogs.get(id); + if (typeof data === 'undefined') { + throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog."); + } + + var header = DOMTraverse.childrenByTag(data.dialog, 'HEADER'); + DOMTraverse.childrenByTag(header[0], 'SPAN').textContent = title; + }, + + /** + * Creates the DOM for a new dialog and opens it. + * + * @param {string} id element id, if exists the html parameter is ignored in favor of the existing element + * @param {?string} html content html + * @param {object} options list of options + */ + _createDialog: function(id, html, options) { + var element = null; + if (html === null) { + element = document.getElementById(id); + if (element === null) { + throw new Error("Expected either a HTML string or an existing element id."); + } + } + + var dialog = document.createElement('div'); + dialog.classList.add('dialogContainer'); + dialog.setAttribute('aria-hidden', 'true'); + dialog.setAttribute('role', 'dialog') + dialog.setAttribute('data-id', id); + + if (options.disposeOnClose) { + dialog.setAttribute('data-dispose-on-close', true); + } + + var header = document.createElement('header'); + dialog.appendChild(header); + + if (options.title) { + var titleId = DOMUtil.getUniqueId(); + dialog.setAttribute('aria-labelledby', titleId); + + var title = document.createElement('span'); + title.classList.add('dialogTitle'); + title.textContent = options.title; + title.setAttribute('id', titleId); + header.appendChild(title); + } + + if (options.closable) { + var closeButton = document.createElement('a'); + closeButton.className = 'dialogCloseButton jsTooltip'; + closeButton.setAttribute('title', options.closeButtonLabel); + closeButton.setAttribute('aria-label', options.closeButtonLabel); + closeButton.addEventListener('click', this._close.bind(this)); + header.appendChild(closeButton); + + var span = document.createElement('span'); + span.textContent = options.closeButtonLabel; + closeButton.appendChild(span); + } + + var contentContainer = document.createElement('div'); + contentContainer.classList.add('dialogContent'); + dialog.appendChild(contentContainer); + + var content; + if (element === null) { + content = document.createElement('div'); + content.setAttribute('id', id); + content.innerHTML = html; + } + else { + content = element; + } + + contentContainer.appendChild(element); + + _dialogs.set(id, { + backdropCloseOnClick: options.backdropCloseOnClick, + content: content, + dialog: dialog, + header: header, + onBeforeClose: options.onBeforeClose, + onClose: options.onClose, + onShow: options.onShow + }); + + if (_container.getAttribute('aria-hidden') === 'true') { + window.addEventListener('keyup', _keyupListener); + } + + DOMUtil.prepend(dialog, _container); + _container.setAttribute('aria-hidden', 'false'); + _container.setAttribute('data-close-on-click', (options.backdropCloseOnClick ? 'true' : 'false')); + dialog.setAttribute('aria-hidden', 'false'); + + this.rebuild(id); + + _activeDialog = id; + + if (typeof options.onShow === 'function') { + options.onShow(id); + } + + WCF.DOMNodeInsertedHandler.execute(); + }, + + /** + * Updates the dialog's content element. + * + * @param {string} id element id + * @param {?string} html content html, prevent changes by passing null + */ + _updateDialog: function(id, html) { + var data = _dialogs.get(id); + if (typeof data === 'undefined') { + throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog."); + } + + if (typeof html === 'string') { + data.content.innerHTML = ''; + + var content = document.createElement('div'); + content.innerHTML = html; + + data.content.appendChild(content); + } + + if (data.dialog.getAttribute('aria-hidden') === 'true') { + data.dialog.setAttribute('aria-hidden', 'false'); + _container.setAttribute('aria-hidden', 'false'); + _container.setAttribute('data-close-on-click', (data.backdropCloseOnClick ? 'true' : 'false')); + _activeDialog = id; + + window.addEventListener('keyup', _keyupListener); + + this.rebuild(id); + + if (typeof data.onShow === 'function') { + data.onShow(id); + } + } + + WCF.DOMNodeInsertedHandler.execute(); + }, + + rebuild: function(id) { + var data = _dialogs.get(id); + if (typeof data === 'undefined') { + throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog."); + } + + // ignore non-active dialogs + if (data.dialog.getAttribute('aria-hidden') === 'true') { + return; + } + + // fix for a calculation bug in Chrome causing the scrollbar to overlap the border + if ($.browser.chrome) { + data.content.style.setProperty('margin-right', '-1px'); + } + + var contentContainer = data.content.parentNode; + + var formSubmit = data.content.querySelector('.formSubmit'); + var unavailableHeight = 0; + if (formSubmit !== null) { + contentContainer.classList.add('dialogForm'); + formSubmit.classList.add('dialogFormSubmit'); + + unavailableHeight += DOMUtil.outerHeight(formSubmit); + contentContainer.style.setProperty('margin-bottom', unavailableHeight + 'px'); + } + else { + contentContainer.classList.remove('dialogForm'); + } + + unavailableHeight += DOMUtil.outerHeight(data.header); + + var maximumHeight = (window.innerHeight * 0.8) - unavailableHeight; + contentContainer.style.setProperty('max-height', ~~maximumHeight + 'px'); + }, + + /** + * Handles clicks on the close button or the backdrop if enabled. + * + * @param {object} event click event + * @return {boolean} false if the event should be cancelled + */ + _close: function(event) { + event.preventDefault(); + + var data = _dialogs.get(_activeDialog); + if (typeof data.onBeforeClose === 'function') { + data.onBeforeClose(_activeDialog); + + return false; + } + + this.close(_activeDialog); + }, + + /** + * Closes the current active dialog by clicks on the backdrop. + * + * @param {object} event event object + */ + _closeOnBackdrop: function(event) { + if (event.target !== _container) { + return true; + } + + if (_container.getAttribute('data-close-on-click') === 'true') { + this._close(event); + } + else { + event.preventDefault(); + } + }, + + /** + * Closes a dialog identified by given id. + * + * @param {string} id element id + */ + close: function(id) { + var data = _dialogs.get(id); + if (typeof data === 'undefined') { + throw new Error("Expected a valid dialog id, '" + id + "' does not match any active dialog."); + } + + if (typeof data.onClose === 'function') { + data.onClose(id); + } + + if (data.dialog.getAttribute('data-dispose-on-close')) { + setTimeout(function() { + if (data.dialog.getAttribute('aria-hidden') === 'true') { + _container.removeChild(data.dialog); + _dialogs.remove(id); + } + }, 5000); + } + else { + data.dialog.setAttribute('aria-hidden', 'true'); + } + + // get next active dialog + _activeDialog = null; + for (var i = 0; i < _container.childElementCount; i++) { + var child = _container.children[i]; + if (child.getAttribute('aria-hidden') === 'false') { + _activeDialog = child.getAttribute('data-id'); + break; + } + } + + if (_activeDialog === null) { + _container.setAttribute('aria-hidden', 'true'); + _container.setAttribute('data-close-on-click', 'false'); + + window.removeEventListener('keyup', _keyupListener); + } + else { + data = _dialogs.get(_activeDialog); + _container.setAttribute('data-close-on-click', (data.backdropCloseOnClick ? 'true' : 'false')); + } + }, + + /** + * Returns the dialog data for given element id. + * + * @param {string} id element id + * @return {(object|undefined)} dialog data or undefined if element id is unknown + */ + getDialog: function(id) { + return _dialogs.get(id); + } + }; + + return new UIDialog(); +}); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js new file mode 100644 index 0000000000..97d88bd895 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js @@ -0,0 +1,118 @@ +/** + * Provides enhanced tooltips. + * + * @author Alexander Ebert + * @copyright 2001-2015 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/UI/Tooltip + */ +define(['jquery', 'UI/Alignment'], function($, UIAlignment) { + "use strict"; + + var _elements = null; + var _pointer = null; + var _text = null; + var _tooltip = null; + + /** + * @constructor + */ + function UITooltip() {}; + UITooltip.prototype = { + /** + * Initializes the tooltip element and binds event listener. + */ + setup: function() { + if ($.browser.mobile) return; + + _tooltip = document.createElement('div'); + _tooltip.setAttribute('id', 'balloonTooltip'); + _tooltip.classList.add('balloonTooltip'); + + _text = document.createElement('span'); + _text.setAttribute('id', 'balloonTooltipText'); + _tooltip.appendChild(_text); + + _pointer = document.createElement('span'); + _pointer.classList.add('elementPointer'); + _pointer.appendChild(document.createElement('span')); + _tooltip.appendChild(_pointer); + + document.body.appendChild(_tooltip); + + _elements = document.getElementsByClassName('jsTooltip'); + + this.init(); + + WCF.DOMNodeInsertedHandler.addCallback('WoltLab/WCF/UI/Tooltip', this.init.bind(this)); + }, + + /** + * Initializes tooltip elements. + */ + init: function() { + while (_elements.length) { + var element = _elements[0]; + element.classList.remove('jsTooltip'); + + var title = element.getAttribute('title'); + if (title.length) { + element.setAttribute('data-tooltip', title); + element.removeAttribute('title'); + + element.addEventListener('mouseenter', this._mouseEnter.bind(this)); + element.addEventListener('mouseleave', this._mouseLeave.bind(this)); + element.addEventListener('click', this._mouseLeave.bind(this)); + } + } + }, + + /** + * Displays the tooltip on mouse enter. + * + * @param {object} event event object + */ + _mouseEnter: function(event) { + var element = event.currentTarget; + var title = element.getAttribute('title'); + if (typeof title === 'string' && title !== '') { + element.setAttribute('data-tooltip', title); + element.removeAttribute('title'); + } + + title = element.getAttribute('data-tooltip'); + + // reset tooltip position + _tooltip.style.removeProperty('top'); + _tooltip.style.removeProperty('left'); + + // ignore empty tooltip + if (!title.length) { + _tooltip.classList.remove('active'); + return; + } + else { + _tooltip.classList.add('active'); + } + + _text.textContent = title; + + UIAlignment.set(_tooltip, element, { + horizontal: 'center', + pointer: true, + pointerClassNames: ['inverse'] + }); + }, + + /** + * Hides the tooltip once the mouse leaves the element. + * + * @param {object} event event object + */ + _mouseLeave: function(event) { + _tooltip.classList.remove('active'); + } + }; + + return new UITooltip(); +}); \ No newline at end of file diff --git a/wcfsetup/install/files/js/require.config.js b/wcfsetup/install/files/js/require.config.js index 7b8e414791..0da554d115 100644 --- a/wcfsetup/install/files/js/require.config.js +++ b/wcfsetup/install/files/js/require.config.js @@ -12,6 +12,7 @@ requirejs.config({ 'DOM/Util': 'WoltLab/WCF/DOM/Util', 'EventHandler': 'WoltLab/WCF/Event/Handler', 'UI/Alignment': 'WoltLab/WCF/UI/Alignment', + 'UI/Dialog': 'WoltLab/WCF/UI/Dialog', 'UI/SimpleDropdown': 'WoltLab/WCF/UI/Dropdown/Simple' } } diff --git a/wcfsetup/install/files/style/button.less b/wcfsetup/install/files/style/button.less index dd0b3a3152..b9aac453be 100644 --- a/wcfsetup/install/files/style/button.less +++ b/wcfsetup/install/files/style/button.less @@ -9,6 +9,7 @@ input[type='button'], border-width: 1px; cursor: pointer; display: inline-block; + line-height: @wcfBaseLineHeight; margin: 0 4px; padding: 5px 13px; position: relative; diff --git a/wcfsetup/install/files/style/dialog.less b/wcfsetup/install/files/style/dialog.less index 381f805d5d..78f107ebc7 100644 --- a/wcfsetup/install/files/style/dialog.less +++ b/wcfsetup/install/files/style/dialog.less @@ -1,93 +1,140 @@ -.dialogContainer { - background: rgba(0, 0, 0, .4); - border: 14px solid transparent; - border-radius: 15px; - margin-left: auto; - margin-right: auto; - max-width: 90%; - min-width: 500px; +.dialogOverlay { + background-color: transparent; + bottom: 0; + left: 0; position: fixed; + right: 0; + top: 0; + visibility: hidden; + z-index: 399; - .boxShadow(0, 1px, rgba(0, 0, 0, .3), 23px); + transition: visibility 0s linear .3s; + + &[aria-hidden=false] { + /* do not animate opacity or background-color, the transition is anything but smooth due to the large area covered */ + background-color: rgba(255, 255, 255, .4); + visibility: visible; + + transition-delay: 0s; + } } -@media only screen and (max-width: 800px) { - .dialogContainer { - border: 0; - border-radius: 0; - left: 0 !important; - max-width: none; - min-width: 0; - position: absolute; - top: 0 !important; - width: 100%; - } +@-webkit-keyframes wcfDialog { + 0% { visibility: visible; opacity: 0; top: 8%; } + 100% { visibility: visible; opacity: 1; top: 10%; } +} + +@-webkit-keyframes wcfDialogOut { + 0% { visibility: visible; opacity: 1; top: 10%; } + 100% { visibility: hidden; opacity: 0; top: 12%; } } -.dialogTitlebar { - background-color: @wcfTabularBoxBackgroundColor; - border-bottom: 1px solid rgba(0, 0, 0, .1); - border-top-left-radius: 7px; - border-top-right-radius: 7px; - display: block; - padding: 10px 20px; - min-height: 27px; - position: relative; +.dialogContainer { + background-color: rgba(0, 0, 0, .4); + border: 3px solid transparent; + border-radius: 3px; + box-shadow: 0 1px 15px 0 rgba(0, 0, 0, .3); + box-sizing: border-box; + left: 50%; + max-height: 80%; + max-width: 80%; + min-width: 400px; + position: absolute; + top: 10%; + transform: translateX(-50%); + + -webkit-animation: wcfDialogOut .3s; + -webkit-animation-fill-mode: forwards; + + &[aria-hidden=false] { + -webkit-animation: wcfDialog .3s; + -webkit-animation-fill-mode: forwards; + } - .dialogTitle { + > header { + background: linear-gradient(to right, @wcfTabularBoxBackgroundColor, lighten(@wcfTabularBoxBackgroundColor, 10%)); + border-top-left-radius: 3px; + border-top-right-radius: 3px; color: @wcfTabularBoxColor; - display: block; - font-size: @wcfHeadlineFontSize; - font-weight: bold; - margin-right: 28px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + display: flex; + padding: 7px 10px; .textShadow(@wcfTabularBoxBackgroundColor); + + > span { + flex: 1; + font-size: 1.2rem; + } + + > a { + color: @wcfTabularBoxColor; + flex: 0 0 20px; + font-family: FontAwesome; + font-size: 18px; + text-align: right; + text-decoration: none; + + &:before { + content: @fa-var-times-circle; + } + + > span { + display: none; + } + } } - .dialogCloseButton { - color: @wcfTabularBoxColor; - cursor: pointer; - display: inline-block; - font-family: FontAwesome; - font-size: 28px; - height: 32px; - position: absolute; - right: 10px; - text-align: center; - text-decoration: none; - top: 7px; - width: 32px; + > .dialogContent { + background-color: @wcfContainerBackgroundColor; + box-sizing: border-box; + color: @wcfColor; + overflow: auto; + padding: 10px; + padding-bottom: 0; - .textShadow(@wcfTabularBoxBackgroundColor); + &:after { + content: ""; + display: block; + height: 10px; + } + + &.dialogForm:after { + height: 17px; + } + + &:not(.dialogForm) { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + } - &::before { - content: "\f057"; + dl:not(.plain) { + > dt { + width: 170px; + } + + > dd { + margin-left: 190px; + } } - span { - display: none; + .dialogFormSubmit { + background-color: @wcfContainerAccentBackgroundColor; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + bottom: 0; + left: 0; + padding: 7px 10px; + position: absolute; + right: 0; } } } @media only screen and (max-width: 800px) { - .dialogTitlebar { - border-radius: 0; - } } -.dialogContent { - background-color: @wcfContainerBackgroundColor; - color: @wcfColor; - padding: 10px 20px 20px; - - &:not(.dialogForm) { - border-bottom-left-radius: 7px; - border-bottom-right-radius: 7px; - } + +.dialogContentX { > .icon-spinner { left: 50%; @@ -97,16 +144,6 @@ top: 50%; } - dl:not(.plain) { - > dt { - width: 170px; - } - - > dd { - margin-left: 190px; - } - } - .formSubmit { background-color: @wcfContainerAccentBackgroundColor; border-bottom-left-radius: 7px; @@ -120,30 +157,6 @@ } } -@media only screen and (max-width: 800px) { - .dialogContent { - max-height: none !important; - max-width: none !important; - - &:not(.dialogForm) { - border-radius: 0; - } - - .formSubmit { - border-radius: 0; - } - } -} - -.dialogOverlay { - background-color: rgba(0, 0, 0, .5); - bottom: 0; - left: 0; - position: fixed; - right: 0; - top: 0; -} - /* package (un-)installation */ #packageInstallationDialogContainer > .boxHeadline { margin-top: 0; diff --git a/wcfsetup/install/files/style/global.less b/wcfsetup/install/files/style/global.less index a12b13a194..476b30f018 100644 --- a/wcfsetup/install/files/style/global.less +++ b/wcfsetup/install/files/style/global.less @@ -8,6 +8,7 @@ body { color: @wcfColor; font-family: @wcfBaseFontFamily; line-height: @wcfBaseLineHeight; + position: relative; word-wrap: break-word; } @@ -219,6 +220,35 @@ fieldset { } } +.elementPointer { + position: absolute; + top: 0; + transform: translateY(-100%); + + &.center { + left: 50%; + transform: translateX(-50%) translateY(-100%); + } + + &.left { + left: 4px; + } + + &.right { + right: 4px; + } + + &.flipVertical { + bottom: 0; + top: auto; + transform: translateY(100%); + + &.center { + transform: translateX(-50%) translateY(100%); + } + } +} + /* balloon tooltips */ .balloonTooltip { background-color: @wcfTooltipBackgroundColor; @@ -226,10 +256,24 @@ fieldset { color: @wcfTooltipColor; font-size: @wcfSmallFontSize; max-width: 300px; + opacity: 0; padding: 5px 10px 7px; position: absolute; + visibility: hidden; z-index: 800; + transition: visibility 0s linear .2s, opacity .2s linear .2s; + + > .elementPointer { + border-color: @wcfTooltipBackgroundColor transparent; + border-style: solid; + border-width: 0 5px 5px; + + &.flipVertical { + border-width: 5px 5px 0; + } + } + .pointer { border-color: @wcfTooltipBackgroundColor transparent; border-style: solid; @@ -241,10 +285,15 @@ fieldset { .boxShadow(0, 3px, rgba(0, 0, 0, .3), 7px); - &.inverse { - .pointer { - border-width: 5px 5px 0; - } + &.inverse > .pointer { + border-width: 5px 5px 0; + } + + &.active { + opacity: 1; + visibility: visible; + + transition-delay: 0s; } } diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 0c590995af..848cdd0556 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1791,7 +1791,7 @@ INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfUserPan INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfUserPanelHoverColor', 'rgba(255, 255, 255, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfButtonBackgroundColor', 'rgba(249, 249, 249, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfButtonBorderColor', 'rgba(221, 221, 221, 1)'); -INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfButtonBorderRadius', '15px'); +INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfButtonBorderRadius', '3px'); INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfSmallButtonBorderRadius', '3px'); INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfButtonColor', 'rgba(102, 102, 102, 1)'); INSERT INTO wcf1_style_variable (variableName, defaultValue) VALUES ('wcfButtonPrimaryBackgroundColor', 'rgba(211, 232, 254, 1)'); -- 2.20.1