Reworked dialogs and tooltips
authorAlexander Ebert <ebert@woltlab.com>
Mon, 11 May 2015 17:51:11 +0000 (19:51 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Mon, 11 May 2015 17:51:11 +0000 (19:51 +0200)
13 files changed:
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
wcfsetup/install/files/js/WCF.User.js
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/js/WoltLab/WCF/Bootstrap.js
wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js
wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js
wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js [new file with mode: 0644]
wcfsetup/install/files/js/require.config.js
wcfsetup/install/files/style/button.less
wcfsetup/install/files/style/dialog.less
wcfsetup/install/files/style/global.less
wcfsetup/setup/db/install.sql

index c310324ff999079c94ce5b6f35e13f4947e7d02f..c6ddd51940189dd102af20c76ba0c2159bc76f68 100644 (file)
        //<![CDATA[
        $(function() {
                new WCF.Effect.SmoothScroll();
-               new WCF.Effect.BalloonTooltip();
                new WCF.Sitemap();
                {if $__wcf->getStyleHandler()->countStyles() > 1}new WCF.Style.Chooser();{/if}
                WCF.System.PageNavigation.init('.pageNavigation');
index 72046114ea16fec7ac6008808f44353e9d79fb61..f66f9d5160f19081bee50263338946de733fa003 100644 (file)
@@ -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 = $('<p class="info" />').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'));
+                       }
+               });
        }
 };
 
index 875b0a8c86a5a87810755e0e31e05e2e2d3811f8..18663caedcfffb844d00774ecc52c3a599e2482c 100755 (executable)
@@ -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 = $('<div id="balloonTooltip" class="balloonTooltip"><span id="balloonTooltipText"></span><span class="pointer"><span></span></span></div>').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 = $('<div class="dialogContainer" />').hide().css({ zIndex: this.options.zIndex }).appendTo(document.body);
-               this._titlebar = $('<header class="dialogTitlebar" />').hide().appendTo(this._container);
-               this._title = $('<span class="dialogTitle" />').hide().appendTo(this._titlebar);
-               this._closeButton = $('<a class="dialogCloseButton jsTooltip" title="' + this.options.closeButtonLabel + '"><span /></a>').click($.proxy(this.close, this)).hide().appendTo(this._titlebar);
-               this._content = $('<div class="dialogContent" />').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 = $('<div id="jsWcfDialogOverlay" class="dialogOverlay" />').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));
        }
 });
 
index 25fb9c7b4ac9b983961949e19d68926ac44bf9b6..e0629c73ced17325ce0189e0e49d2ebf2244568a 100644 (file)
@@ -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);
                }
index 2a5a7e845463f6f13ec3974129d554ea6bc8f544..bfed2ea461793308739dd158f70bb3bf84076324 100644 (file)
@@ -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.
                 * 
index f8100caa35e9d3906d8fb0d53550368251dfd489..21b43b1d5d1791c316ff83cd521323b904fee558 100644 (file)
@@ -6,7 +6,7 @@
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @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 (file)
index 0000000..e8556a8
--- /dev/null
@@ -0,0 +1,382 @@
+/**
+ * Modal dialog handler.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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<string, *>}     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<string, *>}     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 (file)
index 0000000..97d88bd
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Provides enhanced tooltips.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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
index 7b8e4147914e4cdb44e563f17d041ed38cf3c37d..0da554d1159a448075827dc6c57c44040d98bcae 100644 (file)
@@ -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'
                }
        }
index dd0b3a3152d488ad5d8b42d0e016177ff6f2c7ad..b9aac453be4a2e65de8fe141d691e014e2bc8b17 100644 (file)
@@ -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;
index 381f805d5d39b849d3de5b2a8346ff88cf1f6c70..78f107ebc7bac4b7add118c3b762a8c532d4a9e8 100644 (file)
-.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%;
                top: 50%;
        }
        
-       dl:not(.plain) {
-               > dt {
-                       width: 170px;
-               }
-               
-               > dd {
-                       margin-left: 190px;
-               }
-       }
-       
        .formSubmit {
                background-color: @wcfContainerAccentBackgroundColor;
                border-bottom-left-radius: 7px;
        }
 }
 
-@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;
index a12b13a194a9ab36ca4db96c5bf49caf89b80534..476b30f018ada30894f8d9298f8b4d9574eadea1 100644 (file)
@@ -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;
        }
 }
 
index 0c590995afe6ac315a5e3fb8ed9d37fcf200d23b..848cdd055694f9963d58db0ee6611b955419c6ac 100644 (file)
@@ -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)');