Merge branch '5.3'
authorTim Düsterhus <duesterhus@woltlab.com>
Mon, 23 Nov 2020 10:36:46 +0000 (11:36 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Mon, 23 Nov 2020 10:36:46 +0000 (11:36 +0100)
- Dropped update_com.woltlab.wcf_5.3_orphanedComments.php
- Replaced ts/WoltLabSuite/Core/Acp/Ui/Option/EmailSmtpTest.js with the file
  from 5.3, replacing WCF_CLICK_EVENT with 'click' and regenerated the compiled
  JavaScript.
- Manually applied 4ac5f76b4ee5804919a832729a7ab384ea9d9a4d to the already
  converted WoltLabSuite/Core/Ui/Screen.ts

1  2 
com.woltlab.wcf/package.xml
wcfsetup/install/files/acp/templates/styleAdd.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Option/EmailSmtpTest.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Screen.js
wcfsetup/install/files/lib/system/WCF.class.php
wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Ui/Option/EmailSmtpTest.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Screen.ts

Simple merge
index ea1bdd9038dec640127dcc5764e7dba599c8c44a,3b2d8d6c72e71484ace97d06f2c1bcecf74e3f14..e006a1c20447b4434905069f5aa49bf0e86e1f1b
 - * 
+ /**
+  * Simple SMTP connection testing.
 -define(['Ajax', 'Core', 'Language'], function(Ajax, Core, Language) {
 -      "use strict";
 -      
 -      var _buttonRunTest = null;
 -      var _container = null;
 -      
 -      return {
 -              init: function () {
 -                      var smtpCheckbox = null;
 -                      var methods = elBySelAll('input[name="values[mail_send_method]"]', undefined, (function (radioCheckbox) {
 -                              radioCheckbox.addEventListener('change', this._onChange.bind(this));
 -                              
 -                              if (radioCheckbox.value === 'smtp') smtpCheckbox = radioCheckbox;
 -                      }).bind(this));
 -                      
 -                      // This configuration part is unavailable when running in enterprise mode.
 -                      if (methods.length === 0) {
 -                              return;
 -                      }
 -                      
 -                      Core.triggerEvent(smtpCheckbox, 'change');
 -              },
 -              
 -              _onChange: function (event) {
 -                      var checkbox = event.currentTarget;
 -                      
 -                      if (checkbox.value === 'smtp' && checkbox.checked) {
 -                              if (_container === null) this._initUi(checkbox);
 -                              
 -                              elShow(_container);
 -                      }
 -                      else if (_container !== null) {
 -                              elHide(_container);
 -                      }
 -              },
 -              
 -              _initUi: function (checkbox) {
 -                      var html = '<dt>' + Language.get('wcf.acp.email.smtp.test') + '</dt>';
 -                      html += '<dd>';
 -                      html += '<a href="#" class="button">' + Language.get('wcf.acp.email.smtp.test.run') + '</a>';
 -                      html += '<small>' + Language.get('wcf.acp.email.smtp.test.description') + '</small>';
 -                      html += '</dd>';
 -                      
 -                      _container = elCreate('dl');
 -                      _container.innerHTML = html;
 -                      
 -                      _buttonRunTest = elBySel('a', _container);
 -                      _buttonRunTest.addEventListener(WCF_CLICK_EVENT, this._onClick.bind(this));
 -                      
 -                      var insertAfter = checkbox.closest('dl');
 -                      insertAfter.parentNode.insertBefore(_container, insertAfter.nextSibling);
 -              },
 -              
 -              _onClick: function (event) {
 -                      event.preventDefault();
 -                      
 -                      _buttonRunTest.disabled = true;
 -                      _buttonRunTest.innerHTML = '<span class="icon icon16 fa-spinner"></span> ' + Language.get('wcf.global.loading');
 -                      
 -                      elInnerError(_buttonRunTest, false);
 -                      
 -                      window.setTimeout((function () {
 -                              var startTls = elBySel('input[name="values[mail_smtp_starttls]"]:checked');
 -                              
 -                              Ajax.api(this, {
 -                                      parameters: {
 -                                              host: elById('mail_smtp_host').value,
 -                                              port: elById('mail_smtp_port').value,
 -                                              startTls: (startTls) ? startTls.value : '',
 -                                              user: elById('mail_smtp_user').value,
 -                                              password: elById('mail_smtp_password').value
 -                                      }
 -                              });
 -                      }).bind(this), 100);
 -              },
 -              
 -              _ajaxSuccess: function (data) {
 -                      var result = data.returnValues.validationResult;
 -                      if (result === '') {
 -                              this._resetButton(true);
 -                      }
 -                      else {
 -                              this._resetButton(false, result);
 -                      }
 -              },
 -              
 -              _ajaxFailure: function (data) {
 -                      var result = '';
 -                      if (data && data.returnValues && data.returnValues.fieldName) {
 -                              result = Language.get('wcf.acp.email.smtp.test.error.empty.' + data.returnValues.fieldName);
 -                      }
 -                      
 -                      this._resetButton(false, result);
 -                      
 -                      return (result === '');
 -              },
 -              
 -              _resetButton: function (success, errorMessage) {
 -                      _buttonRunTest.disabled = false;
 -                      
 -                      if (success) _buttonRunTest.innerHTML = '<span class="icon icon16 fa-check green"></span> ' + Language.get('wcf.acp.email.smtp.test.run.success');
 -                      else _buttonRunTest.innerHTML = Language.get('wcf.acp.email.smtp.test.run');
 -                      
 -                      if (errorMessage) elInnerError(_buttonRunTest, errorMessage);
 -              },
 -              
 -              _ajaxSetup: function () {
 -                      return {
 -                              data: {
 -                                      actionName: 'emailSmtpTest',
 -                                      className: 'wcf\\data\\option\\OptionAction'
 -                              },
 -                              silent: true
 -                      };
 -              }
 -      };
++ *
+  * @author    Alexander Ebert
+  * @copyright 2001-2018 WoltLab GmbH
+  * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+  * @module    WoltLabSuite/Core/Acp/Ui/Option/EmailSmtpTest
+  */
 +define(['Ajax', 'Core', 'Language'], function (Ajax, Core, Language) {
 +    "use strict";
 +    var _buttonRunTest = null;
 +    var _container = null;
 +    return {
 +        init: function () {
 +            var smtpCheckbox = null;
 +            var methods = elBySelAll('input[name="values[mail_send_method]"]', undefined, (function (radioCheckbox) {
 +                radioCheckbox.addEventListener('change', this._onChange.bind(this));
 +                if (radioCheckbox.value === 'smtp')
 +                    smtpCheckbox = radioCheckbox;
 +            }).bind(this));
 +            // This configuration part is unavailable when running in enterprise mode.
 +            if (methods.length === 0) {
 +                return;
 +            }
 +            Core.triggerEvent(smtpCheckbox, 'change');
 +        },
 +        _onChange: function (event) {
 +            var checkbox = event.currentTarget;
 +            if (checkbox.value === 'smtp' && checkbox.checked) {
 +                if (_container === null)
 +                    this._initUi(checkbox);
 +                elShow(_container);
 +            }
 +            else if (_container !== null) {
 +                elHide(_container);
 +            }
 +        },
 +        _initUi: function (checkbox) {
 +            var html = '<dt>' + Language.get('wcf.acp.email.smtp.test') + '</dt>';
 +            html += '<dd>';
 +            html += '<a href="#" class="button">' + Language.get('wcf.acp.email.smtp.test.run') + '</a>';
 +            html += '<small>' + Language.get('wcf.acp.email.smtp.test.description') + '</small>';
 +            html += '</dd>';
 +            _container = elCreate('dl');
 +            _container.innerHTML = html;
 +            _buttonRunTest = elBySel('a', _container);
 +            _buttonRunTest.addEventListener('click', this._onClick.bind(this));
 +            var insertAfter = checkbox.closest('dl');
 +            insertAfter.parentNode.insertBefore(_container, insertAfter.nextSibling);
 +        },
 +        _onClick: function (event) {
 +            event.preventDefault();
 +            _buttonRunTest.disabled = true;
 +            _buttonRunTest.innerHTML = '<span class="icon icon16 fa-spinner"></span> ' + Language.get('wcf.global.loading');
 +            elInnerError(_buttonRunTest, false);
 +            window.setTimeout((function () {
 +                var startTls = elBySel('input[name="values[mail_smtp_starttls]"]:checked');
 +                Ajax.api(this, {
 +                    parameters: {
 +                        host: elById('mail_smtp_host').value,
 +                        port: elById('mail_smtp_port').value,
 +                        startTls: (startTls) ? startTls.value : '',
 +                        user: elById('mail_smtp_user').value,
 +                        password: elById('mail_smtp_password').value
 +                    }
 +                });
 +            }).bind(this), 100);
 +        },
 +        _ajaxSuccess: function (data) {
 +            var result = data.returnValues.validationResult;
 +            if (result === '') {
 +                this._resetButton(true);
 +            }
 +            else {
 +                this._resetButton(false, result);
 +            }
 +        },
 +        _ajaxFailure: function (data) {
 +            var result = '';
 +            if (data && data.returnValues && data.returnValues.fieldName) {
 +                result = Language.get('wcf.acp.email.smtp.test.error.empty.' + data.returnValues.fieldName);
 +            }
 +            this._resetButton(false, result);
 +            return (result === '');
 +        },
 +        _resetButton: function (success, errorMessage) {
 +            _buttonRunTest.disabled = false;
 +            if (success)
 +                _buttonRunTest.innerHTML = '<span class="icon icon16 fa-check green"></span> ' + Language.get('wcf.acp.email.smtp.test.run.success');
 +            else
 +                _buttonRunTest.innerHTML = Language.get('wcf.acp.email.smtp.test.run');
 +            if (errorMessage)
 +                elInnerError(_buttonRunTest, errorMessage);
 +        },
 +        _ajaxSetup: function () {
 +            return {
 +                data: {
 +                    actionName: 'emailSmtpTest',
 +                    className: 'wcf\\data\\option\\OptionAction'
 +                },
 +                silent: true
 +            };
 +        }
 +    };
  });
index 7bf7547bce271dd10c8edbeb373d0d6af774c9ea,307d315e637981798aada5563d31c3f030d777c5..26cae7dd847990781294e30c92648186c6228d34
  /**
   * Provides consistent support for media queries and body scrolling.
 - * 
 - * @author    Alexander Ebert
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @module    Ui/Screen (alias)
 - * @module    WoltLabSuite/Core/Ui/Screen
 + *
 + * @author  Alexander Ebert
 + * @copyright  2001-2019 WoltLab GmbH
 + * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  Ui/Screen (alias)
 + * @module  WoltLabSuite/Core/Ui/Screen
   */
 -define(['Core', 'Dictionary', 'Environment'], function(Core, Dictionary, Environment) {
 -      "use strict";
 -      
 -      var _dialogContainer = null;
 -      var _mql = new Dictionary();
 -      var _scrollDisableCounter = 0;
 -      var _scrollOffsetFrom = null;
 -      var _scrollTop = 0;
 -      var _pageOverlayCounter = 0;
 -      
 -      var _mqMap = Dictionary.fromObject({
 -              'screen-xs': '(max-width: 544px)',                               /* smartphone */
 -              'screen-sm': '(min-width: 545px) and (max-width: 768px)',        /* tablet (portrait) */
 -              'screen-sm-down': '(max-width: 768px)',                          /* smartphone + tablet (portrait) */
 -              'screen-sm-up': '(min-width: 545px)',                            /* tablet (portrait) + tablet (landscape) + desktop */
 -              'screen-sm-md': '(min-width: 545px) and (max-width: 1024px)',    /* tablet (portrait) + tablet (landscape) */
 -              'screen-md': '(min-width: 769px) and (max-width: 1024px)',       /* tablet (landscape) */
 -              'screen-md-down': '(max-width: 1024px)',                         /* smartphone + tablet (portrait) + tablet (landscape) */
 -              'screen-md-up': '(min-width: 769px)',                            /* tablet (landscape) + desktop */
 -              'screen-lg': '(min-width: 1025px)',                              /* desktop */
 -              'screen-lg-only': '(min-width: 1025px) and (max-width: 1280px)',
 -              'screen-lg-down': '(max-width: 1280px)',
 -              'screen-xl': '(min-width: 1281px)'
 -      });
 -      
 -      // Microsoft Edge rewrites the media queries to whatever it
 -      // pleases, causing the input and output query to mismatch
 -      var _mqMapEdge = new Dictionary();
 -      
 -      /**
 -       * @exports     WoltLabSuite/Core/Ui/Screen
 -       */
 -      return {
 -              /**
 -               * Registers event listeners for media query match/unmatch.
 -               * 
 -               * The `callbacks` object may contain the following keys:
 -               *  - `match`, triggered when media query matches
 -               *  - `unmatch`, triggered when media query no longer matches
 -               *  - `setup`, invoked when media query first matches
 -               * 
 -               * Returns a UUID that is used to internal identify the callbacks, can be used
 -               * to remove binding by calling the `remove` method.
 -               * 
 -               * @param       {string}        query           media query
 -               * @param       {object}        callbacks       callback functions
 -               * @return      {string}        UUID for listener removal
 -               */
 -              on: function(query, callbacks) {
 -                      var uuid = Core.getUuid(), queryObject = this._getQueryObject(query);
 -                      
 -                      if (typeof callbacks.match === 'function') {
 -                              queryObject.callbacksMatch.set(uuid, callbacks.match);
 -                      }
 -                      
 -                      if (typeof callbacks.unmatch === 'function') {
 -                              queryObject.callbacksUnmatch.set(uuid, callbacks.unmatch);
 -                      }
 -                      
 -                      if (typeof callbacks.setup === 'function') {
 -                              if (queryObject.mql.matches) {
 -                                      callbacks.setup();
 -                              }
 -                              else {
 -                                      queryObject.callbacksSetup.set(uuid, callbacks.setup);
 -                              }
 -                      }
 -                      
 -                      return uuid;
 -              },
 -              
 -              /**
 -               * Removes all listeners identified by their common UUID.
 -               *
 -               * @param       {string}        query   must match the `query` argument used when calling `on()`
 -               * @param       {string}        uuid    UUID received when calling `on()`
 -               */
 -              remove: function(query, uuid) {
 -                      var queryObject = this._getQueryObject(query);
 -                      
 -                      queryObject.callbacksMatch.delete(uuid);
 -                      queryObject.callbacksUnmatch.delete(uuid);
 -                      queryObject.callbacksSetup.delete(uuid);
 -              },
 -              
 -              /**
 -               * Returns a boolean value if a media query expression currently matches.
 -               * 
 -               * @param       {string}        query   CSS media query
 -               * @returns     {boolean}       true if query matches
 -               */
 -              is: function(query) {
 -                      return this._getQueryObject(query).mql.matches;
 -              },
 -              
 -              /**
 -               * Disables scrolling of body element.
 -               */
 -              scrollDisable: function() {
 -                      if (_scrollDisableCounter === 0) {
 -                              _scrollTop = document.body.scrollTop;
 -                              _scrollOffsetFrom = 'body';
 -                              if (!_scrollTop) {
 -                                      _scrollTop = document.documentElement.scrollTop;
 -                                      _scrollOffsetFrom = 'documentElement';
 -                              }
 -                              
 -                              var pageContainer = elById('pageContainer');
 -                              
 -                              // setting translateY causes Mobile Safari to snap
 -                              if (Environment.platform() === 'ios') {
 -                                      pageContainer.style.setProperty('position', 'relative', '');
 -                                      pageContainer.style.setProperty('top', '-' + _scrollTop + 'px', '');
 -                              }
 -                              else {
 -                                      pageContainer.style.setProperty('margin-top', '-' + _scrollTop + 'px', '');
 -                              }
 -                              
 -                              document.documentElement.classList.add('disableScrolling');
 -                      }
 -                      
 -                      _scrollDisableCounter++;
 -              },
 -              
 -              /**
 -               * Re-enables scrolling of body element.
 -               */
 -              scrollEnable: function() {
 -                      if (_scrollDisableCounter) {
 -                              _scrollDisableCounter--;
 -                              
 -                              if (_scrollDisableCounter === 0) {
 -                                      document.documentElement.classList.remove('disableScrolling');
 -                                      
 -                                      var pageContainer = elById('pageContainer');
 -                                      if (Environment.platform() === 'ios') {
 -                                              pageContainer.style.removeProperty('position');
 -                                              pageContainer.style.removeProperty('top');
 -                                      }
 -                                      else {
 -                                              pageContainer.style.removeProperty('margin-top');
 -                                      }
 -                                      
 -                                      if (_scrollTop) {
 -                                              document[_scrollOffsetFrom].scrollTop = ~~_scrollTop;
 -                                      }
 -                              }
 -                      }
 -              },
 -              
 -              /**
 -               * Indicates that at least one page overlay is currently open.
 -               */
 -              pageOverlayOpen: function() {
 -                      if (_pageOverlayCounter === 0) {
 -                              document.documentElement.classList.add('pageOverlayActive');
 -                      }
 -                      
 -                      _pageOverlayCounter++;
 -              },
 -              
 -              /**
 -               * Marks one page overlay as closed.
 -               */
 -              pageOverlayClose: function() {
 -                      if (_pageOverlayCounter) {
 -                              _pageOverlayCounter--;
 -                              
 -                              if (_pageOverlayCounter === 0) {
 -                                      document.documentElement.classList.remove('pageOverlayActive');
 -                              }
 -                      }
 -              },
 -              
 -              /**
 -               * Returns true if at least one page overlay is currently open.
 -               * 
 -               * @returns {boolean}
 -               */
 -              pageOverlayIsActive: function() {
 -                      return _pageOverlayCounter > 0;
 -              },
 -              
 -              /**
 -               * Sets the dialog container element. This method is used to
 -               * circumvent a possible circular dependency, due to `Ui/Dialog`
 -               * requiring the `Ui/Screen` module itself.
 -               * 
 -               * @param       {Element}       container       dialog container element
 -               */
 -              setDialogContainer: function (container) {
 -                      _dialogContainer = container;
 -              },
 -              
 -              /**
 -               * 
 -               * @param       {string}        query   CSS media query
 -               * @return      {Object}        object containing callbacks and MediaQueryList
 -               * @protected
 -               */
 -              _getQueryObject: function(query) {
 -                      if (typeof query !== 'string' || query.trim() === '') {
 -                              throw new TypeError("Expected a non-empty string for parameter 'query'.");
 -                      }
 -                      
 -                      // Microsoft Edge rewrites the media queries to whatever it
 -                      // pleases, causing the input and output query to mismatch
 -                      if (_mqMapEdge.has(query)) query = _mqMapEdge.get(query);
 -                      
 -                      if (_mqMap.has(query)) query = _mqMap.get(query);
 -                      
 -                      var queryObject = _mql.get(query);
 -                      if (!queryObject) {
 -                              queryObject = {
 -                                      callbacksMatch: new Dictionary(),
 -                                      callbacksUnmatch: new Dictionary(),
 -                                      callbacksSetup: new Dictionary(),
 -                                      mql: window.matchMedia(query)
 -                              };
 -                              queryObject.mql.addListener(this._mqlChange.bind(this));
 -                              
 -                              _mql.set(query, queryObject);
 -                              
 -                              if (query !== queryObject.mql.media) {
 -                                      _mqMapEdge.set(queryObject.mql.media, query);
 -                              }
 -                      }
 -                      
 -                      return queryObject;
 -              },
 -              
 -              /**
 -               * Triggered whenever a registered media query now matches or no longer matches.
 -               * 
 -               * @param       {Event} event   event object
 -               * @protected
 -               */
 -              _mqlChange: function(event) {
 -                      var queryObject = this._getQueryObject(event.media);
 -                      if (event.matches) {
 -                              if (queryObject.callbacksSetup.size) {
 -                                      queryObject.callbacksSetup.forEach(function(callback) {
 -                                              callback();
 -                                      });
 -                                      
 -                                      // discard all setup callbacks after execution
 -                                      queryObject.callbacksSetup = new Dictionary();
 -                              }
 -                              else {
 -                                      queryObject.callbacksMatch.forEach(function (callback) {
 -                                              callback();
 -                                      });
 -                              }
 -                      }
 -                      else {
 -                              // Chromium based browsers running on Windows suffer from a bug when
 -                              // used with the responsive mode of the DevTools. Enabling and
 -                              // disabling it will trigger some media queries to report a change
 -                              // even when there isn't really one. This cause errors when invoking
 -                              // "unmatch" handlers that rely on the setup being executed before.
 -                              if (queryObject.callbacksSetup.size) {
 -                                      return;
 -                              }
 -                              
 -                              queryObject.callbacksUnmatch.forEach(function(callback) {
 -                                      callback();
 -                              });
 -                      }
 -              }
 -      };
 +define(["require", "exports", "tslib", "../Core", "../Environment"], function (require, exports, tslib_1, Core, Environment) {
 +    "use strict";
 +    Object.defineProperty(exports, "__esModule", { value: true });
 +    exports.setDialogContainer = exports.pageOverlayIsActive = exports.pageOverlayClose = exports.pageOverlayOpen = exports.scrollEnable = exports.scrollDisable = exports.is = exports.remove = exports.on = void 0;
 +    Core = tslib_1.__importStar(Core);
 +    Environment = tslib_1.__importStar(Environment);
 +    const _mql = new Map();
 +    let _scrollDisableCounter = 0;
 +    let _scrollOffsetFrom;
 +    let _scrollTop = 0;
 +    let _pageOverlayCounter = 0;
 +    const _mqMap = new Map(Object.entries({
 +        "screen-xs": "(max-width: 544px)" /* smartphone */,
 +        "screen-sm": "(min-width: 545px) and (max-width: 768px)" /* tablet (portrait) */,
 +        "screen-sm-down": "(max-width: 768px)" /* smartphone + tablet (portrait) */,
 +        "screen-sm-up": "(min-width: 545px)" /* tablet (portrait) + tablet (landscape) + desktop */,
 +        "screen-sm-md": "(min-width: 545px) and (max-width: 1024px)" /* tablet (portrait) + tablet (landscape) */,
 +        "screen-md": "(min-width: 769px) and (max-width: 1024px)" /* tablet (landscape) */,
 +        "screen-md-down": "(max-width: 1024px)" /* smartphone + tablet (portrait) + tablet (landscape) */,
 +        "screen-md-up": "(min-width: 769px)" /* tablet (landscape) + desktop */,
 +        "screen-lg": "(min-width: 1025px)" /* desktop */,
 +        "screen-lg-only": "(min-width: 1025px) and (max-width: 1280px)",
 +        "screen-lg-down": "(max-width: 1280px)",
 +        "screen-xl": "(min-width: 1281px)",
 +    }));
 +    // Microsoft Edge rewrites the media queries to whatever it
 +    // pleases, causing the input and output query to mismatch
 +    const _mqMapEdge = new Map();
 +    /**
 +     * Registers event listeners for media query match/unmatch.
 +     *
 +     * The `callbacks` object may contain the following keys:
 +     *  - `match`, triggered when media query matches
 +     *  - `unmatch`, triggered when media query no longer matches
 +     *  - `setup`, invoked when media query first matches
 +     *
 +     * Returns a UUID that is used to internal identify the callbacks, can be used
 +     * to remove binding by calling the `remove` method.
 +     */
 +    function on(query, callbacks) {
 +        const uuid = Core.getUuid(), queryObject = _getQueryObject(query);
 +        if (typeof callbacks.match === "function") {
 +            queryObject.callbacksMatch.set(uuid, callbacks.match);
 +        }
 +        if (typeof callbacks.unmatch === "function") {
 +            queryObject.callbacksUnmatch.set(uuid, callbacks.unmatch);
 +        }
 +        if (typeof callbacks.setup === "function") {
 +            if (queryObject.mql.matches) {
 +                callbacks.setup();
 +            }
 +            else {
 +                queryObject.callbacksSetup.set(uuid, callbacks.setup);
 +            }
 +        }
 +        return uuid;
 +    }
 +    exports.on = on;
 +    /**
 +     * Removes all listeners identified by their common UUID.
 +     */
 +    function remove(query, uuid) {
 +        const queryObject = _getQueryObject(query);
 +        queryObject.callbacksMatch.delete(uuid);
 +        queryObject.callbacksUnmatch.delete(uuid);
 +        queryObject.callbacksSetup.delete(uuid);
 +    }
 +    exports.remove = remove;
 +    /**
 +     * Returns a boolean value if a media query expression currently matches.
 +     */
 +    function is(query) {
 +        return _getQueryObject(query).mql.matches;
 +    }
 +    exports.is = is;
 +    /**
 +     * Disables scrolling of body element.
 +     */
 +    function scrollDisable() {
 +        if (_scrollDisableCounter === 0) {
 +            _scrollTop = document.body.scrollTop;
 +            _scrollOffsetFrom = "body";
 +            if (!_scrollTop) {
 +                _scrollTop = document.documentElement.scrollTop;
 +                _scrollOffsetFrom = "documentElement";
 +            }
 +            const pageContainer = document.getElementById("pageContainer");
 +            // setting translateY causes Mobile Safari to snap
 +            if (Environment.platform() === "ios") {
 +                pageContainer.style.setProperty("position", "relative", "");
 +                pageContainer.style.setProperty("top", `-${_scrollTop}px`, "");
 +            }
 +            else {
 +                pageContainer.style.setProperty("margin-top", `-${_scrollTop}px`, "");
 +            }
 +            document.documentElement.classList.add("disableScrolling");
 +        }
 +        _scrollDisableCounter++;
 +    }
 +    exports.scrollDisable = scrollDisable;
 +    /**
 +     * Re-enables scrolling of body element.
 +     */
 +    function scrollEnable() {
 +        if (_scrollDisableCounter) {
 +            _scrollDisableCounter--;
 +            if (_scrollDisableCounter === 0) {
 +                document.documentElement.classList.remove("disableScrolling");
 +                const pageContainer = document.getElementById("pageContainer");
 +                if (Environment.platform() === "ios") {
 +                    pageContainer.style.removeProperty("position");
 +                    pageContainer.style.removeProperty("top");
 +                }
 +                else {
 +                    pageContainer.style.removeProperty("margin-top");
 +                }
 +                if (_scrollTop) {
 +                    document[_scrollOffsetFrom].scrollTop = ~~_scrollTop;
 +                }
 +            }
 +        }
 +    }
 +    exports.scrollEnable = scrollEnable;
 +    /**
 +     * Indicates that at least one page overlay is currently open.
 +     */
 +    function pageOverlayOpen() {
 +        if (_pageOverlayCounter === 0) {
 +            document.documentElement.classList.add("pageOverlayActive");
 +        }
 +        _pageOverlayCounter++;
 +    }
 +    exports.pageOverlayOpen = pageOverlayOpen;
 +    /**
 +     * Marks one page overlay as closed.
 +     */
 +    function pageOverlayClose() {
 +        if (_pageOverlayCounter) {
 +            _pageOverlayCounter--;
 +            if (_pageOverlayCounter === 0) {
 +                document.documentElement.classList.remove("pageOverlayActive");
 +            }
 +        }
 +    }
 +    exports.pageOverlayClose = pageOverlayClose;
 +    /**
 +     * Returns true if at least one page overlay is currently open.
 +     *
 +     * @returns {boolean}
 +     */
 +    function pageOverlayIsActive() {
 +        return _pageOverlayCounter > 0;
 +    }
 +    exports.pageOverlayIsActive = pageOverlayIsActive;
 +    /**
 +     * @deprecated 5.4 - This method is a noop.
 +     */
 +    function setDialogContainer(_container) {
 +        // Do nothing.
 +    }
 +    exports.setDialogContainer = setDialogContainer;
 +    function _getQueryObject(query) {
 +        if (typeof query !== "string" || query.trim() === "") {
 +            throw new TypeError("Expected a non-empty string for parameter 'query'.");
 +        }
 +        // Microsoft Edge rewrites the media queries to whatever it
 +        // pleases, causing the input and output query to mismatch
 +        if (_mqMapEdge.has(query))
 +            query = _mqMapEdge.get(query);
 +        if (_mqMap.has(query))
 +            query = _mqMap.get(query);
 +        let queryObject = _mql.get(query);
 +        if (!queryObject) {
 +            queryObject = {
 +                callbacksMatch: new Map(),
 +                callbacksUnmatch: new Map(),
 +                callbacksSetup: new Map(),
 +                mql: window.matchMedia(query),
 +            };
 +            //noinspection JSDeprecatedSymbols
 +            queryObject.mql.addListener(_mqlChange);
 +            _mql.set(query, queryObject);
 +            if (query !== queryObject.mql.media) {
 +                _mqMapEdge.set(queryObject.mql.media, query);
 +            }
 +        }
 +        return queryObject;
 +    }
 +    /**
 +     * Triggered whenever a registered media query now matches or no longer matches.
 +     */
 +    function _mqlChange(event) {
 +        const queryObject = _getQueryObject(event.media);
 +        if (event.matches) {
 +            if (queryObject.callbacksSetup.size) {
 +                queryObject.callbacksSetup.forEach((callback) => {
 +                    callback();
 +                });
 +                // discard all setup callbacks after execution
 +                queryObject.callbacksSetup = new Map();
 +            }
 +            else {
 +                queryObject.callbacksMatch.forEach((callback) => {
 +                    callback();
 +                });
 +            }
 +        }
 +        else {
++            // Chromium based browsers running on Windows suffer from a bug when
++            // used with the responsive mode of the DevTools. Enabling and
++            // disabling it will trigger some media queries to report a change
++            // even when there isn't really one. This cause errors when invoking
++            // "unmatch" handlers that rely on the setup being executed before.
++            if (queryObject.callbacksSetup.size) {
++                return;
++            }
 +            queryObject.callbacksUnmatch.forEach((callback) => {
 +                callback();
 +            });
 +        }
 +    }
  });
index fc887ebc1abdd22bfb3fe52a2a0163f2738b3eed,0000000000000000000000000000000000000000..cc59cfcd8cfe7e3afcdb1a7eb8e0addeb73a9b6f
mode 100644,000000..100644
--- /dev/null
@@@ -1,117 -1,0 +1,125 @@@
++/**
++ * Simple SMTP connection testing.
++ * 
++ * @author    Alexander Ebert
++ * @copyright 2001-2018 WoltLab GmbH
++ * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
++ * @module    WoltLabSuite/Core/Acp/Ui/Option/EmailSmtpTest
++ */
 +define(['Ajax', 'Core', 'Language'], function(Ajax, Core, Language) {
 +      "use strict";
 +      
 +      var _buttonRunTest = null;
 +      var _container = null;
 +      
 +      return {
 +              init: function () {
 +                      var smtpCheckbox = null;
 +                      var methods = elBySelAll('input[name="values[mail_send_method]"]', undefined, (function (radioCheckbox) {
 +                              radioCheckbox.addEventListener('change', this._onChange.bind(this));
 +                              
 +                              if (radioCheckbox.value === 'smtp') smtpCheckbox = radioCheckbox;
 +                      }).bind(this));
 +                      
 +                      // This configuration part is unavailable when running in enterprise mode.
 +                      if (methods.length === 0) {
 +                              return;
 +                      }
 +                      
 +                      Core.triggerEvent(smtpCheckbox, 'change');
 +              },
 +              
 +              _onChange: function (event) {
 +                      var checkbox = event.currentTarget;
 +                      
 +                      if (checkbox.value === 'smtp' && checkbox.checked) {
 +                              if (_container === null) this._initUi(checkbox);
 +                              
 +                              elShow(_container);
 +                      }
 +                      else if (_container !== null) {
 +                              elHide(_container);
 +                      }
 +              },
 +              
 +              _initUi: function (checkbox) {
 +                      var html = '<dt>' + Language.get('wcf.acp.email.smtp.test') + '</dt>';
 +                      html += '<dd>';
 +                      html += '<a href="#" class="button">' + Language.get('wcf.acp.email.smtp.test.run') + '</a>';
 +                      html += '<small>' + Language.get('wcf.acp.email.smtp.test.description') + '</small>';
 +                      html += '</dd>';
 +                      
 +                      _container = elCreate('dl');
 +                      _container.innerHTML = html;
 +                      
 +                      _buttonRunTest = elBySel('a', _container);
 +                      _buttonRunTest.addEventListener('click', this._onClick.bind(this));
 +                      
 +                      var insertAfter = checkbox.closest('dl');
 +                      insertAfter.parentNode.insertBefore(_container, insertAfter.nextSibling);
 +              },
 +              
 +              _onClick: function (event) {
 +                      event.preventDefault();
 +                      
 +                      _buttonRunTest.disabled = true;
 +                      _buttonRunTest.innerHTML = '<span class="icon icon16 fa-spinner"></span> ' + Language.get('wcf.global.loading');
 +                      
 +                      elInnerError(_buttonRunTest, false);
 +                      
 +                      window.setTimeout((function () {
 +                              var startTls = elBySel('input[name="values[mail_smtp_starttls]"]:checked');
 +                              
 +                              Ajax.api(this, {
 +                                      parameters: {
 +                                              host: elById('mail_smtp_host').value,
 +                                              port: elById('mail_smtp_port').value,
 +                                              startTls: (startTls) ? startTls.value : '',
 +                                              user: elById('mail_smtp_user').value,
 +                                              password: elById('mail_smtp_password').value
 +                                      }
 +                              });
 +                      }).bind(this), 100);
 +              },
 +              
 +              _ajaxSuccess: function (data) {
 +                      var result = data.returnValues.validationResult;
 +                      if (result === '') {
 +                              this._resetButton(true);
 +                      }
 +                      else {
 +                              this._resetButton(false, result);
 +                      }
 +              },
 +              
 +              _ajaxFailure: function (data) {
 +                      var result = '';
 +                      if (data && data.returnValues && data.returnValues.fieldName) {
 +                              result = Language.get('wcf.acp.email.smtp.test.error.empty.' + data.returnValues.fieldName);
 +                      }
 +                      
 +                      this._resetButton(false, result);
 +                      
 +                      return (result === '');
 +              },
 +              
 +              _resetButton: function (success, errorMessage) {
 +                      _buttonRunTest.disabled = false;
 +                      
 +                      if (success) _buttonRunTest.innerHTML = '<span class="icon icon16 fa-check green"></span> ' + Language.get('wcf.acp.email.smtp.test.run.success');
 +                      else _buttonRunTest.innerHTML = Language.get('wcf.acp.email.smtp.test.run');
 +                      
 +                      if (errorMessage) elInnerError(_buttonRunTest, errorMessage);
 +              },
 +              
 +              _ajaxSetup: function () {
 +                      return {
 +                              data: {
 +                                      actionName: 'emailSmtpTest',
 +                                      className: 'wcf\\data\\option\\OptionAction'
 +                              },
 +                              silent: true
 +                      };
 +              }
 +      };
 +});
index 0adedde0bdd76971232813aae2da0ea355c3607e,0000000000000000000000000000000000000000..f2051a15bb1b9c92f8aac8afd56cfe395b45741f
mode 100644,000000..100644
--- /dev/null
@@@ -1,257 -1,0 +1,266 @@@
 +/**
 + * Provides consistent support for media queries and body scrolling.
 + *
 + * @author  Alexander Ebert
 + * @copyright  2001-2019 WoltLab GmbH
 + * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  Ui/Screen (alias)
 + * @module  WoltLabSuite/Core/Ui/Screen
 + */
 +
 +import * as Core from "../Core";
 +import * as Environment from "../Environment";
 +
 +const _mql = new Map<string, MediaQueryData>();
 +
 +let _scrollDisableCounter = 0;
 +let _scrollOffsetFrom: string;
 +let _scrollTop = 0;
 +let _pageOverlayCounter = 0;
 +
 +const _mqMap = new Map<string, string>(
 +  Object.entries({
 +    "screen-xs": "(max-width: 544px)" /* smartphone */,
 +    "screen-sm": "(min-width: 545px) and (max-width: 768px)" /* tablet (portrait) */,
 +    "screen-sm-down": "(max-width: 768px)" /* smartphone + tablet (portrait) */,
 +    "screen-sm-up": "(min-width: 545px)" /* tablet (portrait) + tablet (landscape) + desktop */,
 +    "screen-sm-md": "(min-width: 545px) and (max-width: 1024px)" /* tablet (portrait) + tablet (landscape) */,
 +    "screen-md": "(min-width: 769px) and (max-width: 1024px)" /* tablet (landscape) */,
 +    "screen-md-down": "(max-width: 1024px)" /* smartphone + tablet (portrait) + tablet (landscape) */,
 +    "screen-md-up": "(min-width: 769px)" /* tablet (landscape) + desktop */,
 +    "screen-lg": "(min-width: 1025px)" /* desktop */,
 +    "screen-lg-only": "(min-width: 1025px) and (max-width: 1280px)",
 +    "screen-lg-down": "(max-width: 1280px)",
 +    "screen-xl": "(min-width: 1281px)",
 +  }),
 +);
 +
 +// Microsoft Edge rewrites the media queries to whatever it
 +// pleases, causing the input and output query to mismatch
 +const _mqMapEdge = new Map<string, string>();
 +
 +/**
 + * Registers event listeners for media query match/unmatch.
 + *
 + * The `callbacks` object may contain the following keys:
 + *  - `match`, triggered when media query matches
 + *  - `unmatch`, triggered when media query no longer matches
 + *  - `setup`, invoked when media query first matches
 + *
 + * Returns a UUID that is used to internal identify the callbacks, can be used
 + * to remove binding by calling the `remove` method.
 + */
 +export function on(query: string, callbacks: Callbacks): string {
 +  const uuid = Core.getUuid(),
 +    queryObject = _getQueryObject(query);
 +
 +  if (typeof callbacks.match === "function") {
 +    queryObject.callbacksMatch.set(uuid, callbacks.match);
 +  }
 +
 +  if (typeof callbacks.unmatch === "function") {
 +    queryObject.callbacksUnmatch.set(uuid, callbacks.unmatch);
 +  }
 +
 +  if (typeof callbacks.setup === "function") {
 +    if (queryObject.mql.matches) {
 +      callbacks.setup();
 +    } else {
 +      queryObject.callbacksSetup.set(uuid, callbacks.setup);
 +    }
 +  }
 +
 +  return uuid;
 +}
 +
 +/**
 + * Removes all listeners identified by their common UUID.
 + */
 +export function remove(query: string, uuid: string): void {
 +  const queryObject = _getQueryObject(query);
 +
 +  queryObject.callbacksMatch.delete(uuid);
 +  queryObject.callbacksUnmatch.delete(uuid);
 +  queryObject.callbacksSetup.delete(uuid);
 +}
 +
 +/**
 + * Returns a boolean value if a media query expression currently matches.
 + */
 +export function is(query: string): boolean {
 +  return _getQueryObject(query).mql.matches;
 +}
 +
 +/**
 + * Disables scrolling of body element.
 + */
 +export function scrollDisable(): void {
 +  if (_scrollDisableCounter === 0) {
 +    _scrollTop = document.body.scrollTop;
 +    _scrollOffsetFrom = "body";
 +    if (!_scrollTop) {
 +      _scrollTop = document.documentElement.scrollTop;
 +      _scrollOffsetFrom = "documentElement";
 +    }
 +
 +    const pageContainer = document.getElementById("pageContainer")!;
 +
 +    // setting translateY causes Mobile Safari to snap
 +    if (Environment.platform() === "ios") {
 +      pageContainer.style.setProperty("position", "relative", "");
 +      pageContainer.style.setProperty("top", `-${_scrollTop}px`, "");
 +    } else {
 +      pageContainer.style.setProperty("margin-top", `-${_scrollTop}px`, "");
 +    }
 +
 +    document.documentElement.classList.add("disableScrolling");
 +  }
 +
 +  _scrollDisableCounter++;
 +}
 +
 +/**
 + * Re-enables scrolling of body element.
 + */
 +export function scrollEnable(): void {
 +  if (_scrollDisableCounter) {
 +    _scrollDisableCounter--;
 +
 +    if (_scrollDisableCounter === 0) {
 +      document.documentElement.classList.remove("disableScrolling");
 +
 +      const pageContainer = document.getElementById("pageContainer")!;
 +      if (Environment.platform() === "ios") {
 +        pageContainer.style.removeProperty("position");
 +        pageContainer.style.removeProperty("top");
 +      } else {
 +        pageContainer.style.removeProperty("margin-top");
 +      }
 +
 +      if (_scrollTop) {
 +        document[_scrollOffsetFrom].scrollTop = ~~_scrollTop;
 +      }
 +    }
 +  }
 +}
 +
 +/**
 + * Indicates that at least one page overlay is currently open.
 + */
 +export function pageOverlayOpen(): void {
 +  if (_pageOverlayCounter === 0) {
 +    document.documentElement.classList.add("pageOverlayActive");
 +  }
 +
 +  _pageOverlayCounter++;
 +}
 +
 +/**
 + * Marks one page overlay as closed.
 + */
 +export function pageOverlayClose(): void {
 +  if (_pageOverlayCounter) {
 +    _pageOverlayCounter--;
 +
 +    if (_pageOverlayCounter === 0) {
 +      document.documentElement.classList.remove("pageOverlayActive");
 +    }
 +  }
 +}
 +
 +/**
 + * Returns true if at least one page overlay is currently open.
 + *
 + * @returns {boolean}
 + */
 +export function pageOverlayIsActive(): boolean {
 +  return _pageOverlayCounter > 0;
 +}
 +
 +/**
 + * @deprecated 5.4 - This method is a noop.
 + */
 +export function setDialogContainer(_container: Element): void {
 +  // Do nothing.
 +}
 +
 +function _getQueryObject(query: string): MediaQueryData {
 +  if (typeof (query as any) !== "string" || query.trim() === "") {
 +    throw new TypeError("Expected a non-empty string for parameter 'query'.");
 +  }
 +
 +  // Microsoft Edge rewrites the media queries to whatever it
 +  // pleases, causing the input and output query to mismatch
 +  if (_mqMapEdge.has(query)) query = _mqMapEdge.get(query)!;
 +
 +  if (_mqMap.has(query)) query = _mqMap.get(query) as string;
 +
 +  let queryObject = _mql.get(query);
 +  if (!queryObject) {
 +    queryObject = {
 +      callbacksMatch: new Map<string, Callback>(),
 +      callbacksUnmatch: new Map<string, Callback>(),
 +      callbacksSetup: new Map<string, Callback>(),
 +      mql: window.matchMedia(query),
 +    };
 +    //noinspection JSDeprecatedSymbols
 +    queryObject.mql.addListener(_mqlChange);
 +
 +    _mql.set(query, queryObject);
 +
 +    if (query !== queryObject.mql.media) {
 +      _mqMapEdge.set(queryObject.mql.media, query);
 +    }
 +  }
 +
 +  return queryObject;
 +}
 +
 +/**
 + * Triggered whenever a registered media query now matches or no longer matches.
 + */
 +function _mqlChange(event: MediaQueryListEvent): void {
 +  const queryObject = _getQueryObject(event.media);
 +  if (event.matches) {
 +    if (queryObject.callbacksSetup.size) {
 +      queryObject.callbacksSetup.forEach((callback) => {
 +        callback();
 +      });
 +
 +      // discard all setup callbacks after execution
 +      queryObject.callbacksSetup = new Map<string, Callback>();
 +    } else {
 +      queryObject.callbacksMatch.forEach((callback) => {
 +        callback();
 +      });
 +    }
 +  } else {
++    // Chromium based browsers running on Windows suffer from a bug when
++    // used with the responsive mode of the DevTools. Enabling and
++    // disabling it will trigger some media queries to report a change
++    // even when there isn't really one. This cause errors when invoking
++    // "unmatch" handlers that rely on the setup being executed before.
++    if (queryObject.callbacksSetup.size) {
++      return;
++    }
++
 +    queryObject.callbacksUnmatch.forEach((callback) => {
 +      callback();
 +    });
 +  }
 +}
 +
 +type Callback = () => void;
 +
 +interface Callbacks {
 +  match: Callback;
 +  setup: Callback;
 +  unmatch: Callback;
 +}
 +
 +interface MediaQueryData {
 +  callbacksMatch: Map<string, Callback>;
 +  callbacksSetup: Map<string, Callback>;
 +  callbacksUnmatch: Map<string, Callback>;
 +  mql: MediaQueryList;
 +}