Rename JavaScript modules into proper camel case
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 10 Jul 2015 21:56:28 +0000 (23:56 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 10 Jul 2015 21:56:28 +0000 (23:56 +0200)
42 files changed:
wcfsetup/install/files/js/WoltLab/WCF/ACP/Bootstrap.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Bootstrap.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Bbcode/FromHtml.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Bbcode/Parser.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Bbcode/ToHtml.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/DOM/Change/Listener.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/DOM/Traverse.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Dom/Change/Listener.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Dom/Traverse.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/CloseOverlay.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/Collapsible/Sidebar.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/Confirmation.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList/User.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/Mobile.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/Suggestion.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Alignment.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/CloseOverlay.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Collapsible/Sidebar.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Confirmation.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/FlexibleMenu.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/User.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Mobile.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Suggestion.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/TabMenu.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/TabMenu/Simple.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Tooltip.js [new file with mode: 0644]

diff --git a/wcfsetup/install/files/js/WoltLab/WCF/ACP/Bootstrap.js b/wcfsetup/install/files/js/WoltLab/WCF/ACP/Bootstrap.js
deleted file mode 100644 (file)
index 1243c44..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Bootstraps WCF's JavaScript with additions for the ACP usage.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/ACP/Bootstrap
- */
-define(['WoltLab/WCF/Bootstrap'], function(Bootstrap) {
-       "use strict";
-       
-       /**
-        * ACP Boostrapper.
-        * 
-        * @exports     WoltLab/WCF/ACP/Bootstrap
-        */
-       var ACPBootstrap = {
-               /**
-                * Bootstraps general modules and frontend exclusive ones.
-                * 
-                * @param       {object<string, *>}     options         bootstrap options
-                */
-               setup: function(options) {
-                       Bootstrap.setup();
-               }
-       };
-       
-       return ACPBootstrap;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Bootstrap.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Bootstrap.js
new file mode 100644 (file)
index 0000000..1243c44
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Bootstraps WCF's JavaScript with additions for the ACP usage.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/ACP/Bootstrap
+ */
+define(['WoltLab/WCF/Bootstrap'], function(Bootstrap) {
+       "use strict";
+       
+       /**
+        * ACP Boostrapper.
+        * 
+        * @exports     WoltLab/WCF/ACP/Bootstrap
+        */
+       var ACPBootstrap = {
+               /**
+                * Bootstraps general modules and frontend exclusive ones.
+                * 
+                * @param       {object<string, *>}     options         bootstrap options
+                */
+               setup: function(options) {
+                       Bootstrap.setup();
+               }
+       };
+       
+       return ACPBootstrap;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/FromHtml.js
deleted file mode 100644 (file)
index 9745f18..0000000
+++ /dev/null
@@ -1,547 +0,0 @@
-/**
- * Converts a message containing HTML tags into BBCodes.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/BBCode/FromHtml
- */
-define(['EventHandler', 'StringUtil', 'DOM/Traverse'], function(EventHandler, StringUtil, DOMTraverse) {
-       "use strict";
-       
-       var _converter = [];
-       var _inlineConverter = {};
-       var _sourceConverter = [];
-       
-       /**
-        * Returns true if a whitespace should be inserted before or after the smiley.
-        * 
-        * @param       {Element}       element         image element
-        * @param       {boolean}       before          evaluate previous node
-        * @return      {boolean}       true if a whitespace should be inserted
-        */
-       function addSmileyPadding(element, before) {
-               var target = element[(before ? 'previousSibling' : 'nextSibling')];
-               if (target === null || target.nodeType !== Node.TEXT_NODE || !/\s$/.test(target.textContent)) {
-                       return true;
-               }
-               
-               return false;
-       }
-       
-       /**
-        * @module      WoltLab/WCF/BBCode/FromHtml
-        */
-       var BBCodeFromHtml = {
-               /**
-                * Converts a message containing HTML elements into BBCodes.
-                * 
-                * @param       {string}        message         message containing HTML elements
-                * @return      {string}        message containing BBCodes
-                */
-               convert: function(message) {
-                       if (message.length) this._setup();
-                       
-                       var container = document.createElement('div');
-                       container.innerHTML = message;
-                       
-                       // convert line breaks
-                       var elements = container.getElementsByTagName('P');
-                       while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
-                       
-                       elements = container.getElementsByTagName('BR');
-                       while (elements.length) elements[0].outerHTML = "\n";
-                       
-                       // prevent conversion taking place inside source bbcodes
-                       var sourceElements = this._preserveSourceElements(container);
-                       
-                       EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'beforeConvert', { container: container });
-                       
-                       for (var i = 0, length = _converter.length; i < length; i++) {
-                               this._convert(container, _converter[i]);
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'afterConvert', { container: container });
-                       
-                       this._restoreSourceElements(container, sourceElements);
-                       
-                       // remove remaining HTML elements
-                       elements = container.getElementsByTagName('*');
-                       while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
-                       
-                       message = this._convertSpecials(container.innerHTML);
-                       
-                       return message;
-               },
-               
-               /**
-                * Replaces HTML elements mapping to source BBCodes to avoid
-                * them being handled by other converters.
-                * 
-                * @param       {Element}       container       container element
-                * @return      {array<object>} list of source elements and their placeholder
-                */
-               _preserveSourceElements: function(container) {
-                       var elements, sourceElements = [], tmp;
-                       
-                       for (var i = 0, length = _sourceConverter.length; i < length; i++) {
-                               elements = container.querySelectorAll(_sourceConverter[i].selector);
-                               
-                               tmp = [];
-                               for (var j = 0, innerLength = elements.length; j < innerLength; j++) {
-                                       this._preserveSourceElement(elements[j], tmp);
-                               }
-                               
-                               sourceElements.push(tmp);
-                       }
-                       
-                       return sourceElements;
-               },
-               
-               /**
-                * Replaces an element with a placeholder.
-                * 
-                * @param       {Element}       element         target element
-                * @param       {array<object>} list of removed elements and their placeholders
-                */
-               _preserveSourceElement: function(element, sourceElements) {
-                       var placeholder = document.createElement('var');
-                       placeholder.setAttribute('data-source', 'wcf');
-                       element.parentNode.insertBefore(placeholder, element);
-                       
-                       var fragment = document.createDocumentFragment();
-                       fragment.appendChild(element);
-                       
-                       sourceElements.push({
-                               fragment: fragment,
-                               placeholder: placeholder
-                       });
-               },
-               
-               /**
-                * Reinserts source elements for parsing.
-                * 
-                * @param       {Element}       container       container element
-                * @param       {array<object>} sourceElements  list of removed elements and their placeholders
-                */
-               _restoreSourceElements: function(container, sourceElements) {
-                       var element, elements, placeholder;
-                       for (var i = 0, length = sourceElements.length; i < length; i++) {
-                               elements = sourceElements[i];
-                               
-                               if (elements.length === 0) {
-                                       continue;
-                               }
-                               
-                               for (var j = 0, innerLength = elements.length; j < innerLength; j++) {
-                                       element = elements[j];
-                                       placeholder = element.placeholder;
-                                       
-                                       placeholder.parentNode.insertBefore(element.fragment, placeholder);
-                                       
-                                       _sourceConverter[i].callback(placeholder.previousElementSibling);
-                                       
-                                       placeholder.parentNode.removeChild(placeholder);
-                               }
-                       }
-               },
-               
-               /**
-                * Converts special entities.
-                * 
-                * @param       {string}        message         HTML message
-                * @return      {string}        HTML message
-                */
-               _convertSpecials: function(message) {
-                       message = message.replace(/&amp;/g, '&');
-                       message = message.replace(/&lt;/g, '<');
-                       message = message.replace(/&gt;/g, '>');
-                       
-                       return message;
-               },
-               
-               /**
-                * Sets up converters applied to elements in linear order.
-                */
-               _setup: function() {
-                       if (_converter.length) {
-                               return;
-                       }
-                       
-                       _converter = [
-                               // simple replacement
-                               { tagName: 'STRONG', bbcode: 'b' },
-                               { tagName: 'DEL', bbcode: 's' },
-                               { tagName: 'EM', bbcode: 'i' },
-                               { tagName: 'SUB', bbcode: 'sub' },
-                               { tagName: 'SUP', bbcode: 'sup' },
-                               { tagName: 'U', bbcode: 'u' },
-                               { tagName: 'KBD', bbcode: 'tt' },
-                               
-                               // callback replacement
-                               { tagName: 'A', callback: this._convertUrl.bind(this) },
-                               { tagName: 'IMG', callback: this._convertImage.bind(this) },
-                               { tagName: 'LI', callback: this._convertListItem.bind(this) },
-                               { tagName: 'OL', callback: this._convertList.bind(this) },
-                               { tagName: 'TABLE', callback: this._convertTable.bind(this) },
-                               { tagName: 'UL', callback: this._convertList.bind(this) },
-                               { tagName: 'BLOCKQUOTE', callback: this._convertBlockquote.bind(this) },
-                               
-                               // convert these last
-                               { tagName: 'SPAN', callback: this._convertSpan.bind(this) },
-                               { tagName: 'DIV', callback: this._convertDiv.bind(this) }
-                       ];
-                       
-                       _inlineConverter = {
-                               span: [
-                                       { style: 'color', callback: this._convertInlineColor.bind(this) },
-                                       { style: 'font-size', callback: this._convertInlineFontSize.bind(this) },
-                                       { style: 'font-family', callback: this._convertInlineFontFamily.bind(this) }
-                               ],
-                               div: [
-                                       { style: 'text-align', callback: this._convertInlineTextAlign.bind(this) }
-                               ]
-                       };
-                       
-                       _sourceConverter = [
-                               { selector: 'div.codeBox', callback: this._convertSourceCodeBox.bind(this) }
-                       ];
-                       
-                       EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'init', {
-                               converter: _converter,
-                               inlineConverter: _inlineConverter,
-                               sourceConverter: _sourceConverter
-                       });
-               },
-               
-               /**
-                * Converts an element into a raw string.
-                * 
-                * @param       {Element}       container       container element
-                * @param       {object}        converter       converter object
-                */
-               _convert: function(container, converter) {
-                       if (typeof converter === 'function') {
-                               converter(container);
-                               return;
-                       }
-                       
-                       var element, elements = container.getElementsByTagName(converter.tagName);
-                       while (elements.length) {
-                               element = elements[0];
-                               
-                               if (converter.bbcode) {
-                                       element.outerHTML = '[' + converter.bbcode + ']' + element.innerHTML + '[/' + converter.bbcode + ']';
-                               }
-                               else {
-                                       converter.callback(element);
-                               }
-                       }
-               },
-               
-               /**
-                * Converts <blockquote> into [quote].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertBlockquote: function(element) {
-                       var author = element.getAttribute('data-author') || '';
-                       var link = element.getAttribute('cite') || '';
-                       
-                       var open = '[quote]';
-                       if (author) {
-                               author = StringUtil.escapeHTML(author).replace(/(\\)?'/g, function(match, isEscaped) { return isEscaped ? match : "\\'"; });
-                               if (link) {
-                                       open = "[quote='" + author + "','" + StringUtil.escapeHTML(link) + "']";
-                               }
-                               else {
-                                       open = "[quote='" + author + "']";
-                               }
-                       }
-                       
-                       var header = DOMTraverse.childByTag(element, 'HEADER');
-                       if (header !== null) element.removeChild(header);
-                       
-                       var divs = DOMTraverse.childrenByTag(element, 'DIV');
-                       for (var i = 0, length = divs.length; i < length; i++) {
-                               divs[i].outerHTML = divs[i].innerHTML + '\n';
-                       }
-                       
-                       element.outerHTML = open + element.innerHTML.replace(/^\n*/, '').replace(/\n*$/, '') + '[/quote]\n';
-               },
-               
-               /**
-                * Converts <img> into smilies, [attach] or [img].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertImage: function(element) {
-                       if (element.classList.contains('smiley')) {
-                               // smiley
-                               element.outerHTML = (addSmileyPadding(element, true) ? ' ' : '') + element.getAttribute('alt') + (addSmileyPadding(element, false) ? ' ' : '');
-                               return;
-                       }
-                       
-                       var float = element.style.getPropertyValue('float') || 'none';
-                       var width = element.style.getPropertyValue('width');
-                       width = (typeof width === 'string') ? ~~width.replace(/px$/, '') : 0;
-                       
-                       if (element.classList.contains('redactorEmbeddedAttachment')) {
-                               var attachmentId = element.getAttribute('data-attachment-id');
-                               
-                               if (width > 0) {
-                                       element.outerHTML = "[attach=" + attachmentId + "," + float + "," + width + "][/attach]";
-                               }
-                               else if (float !== 'none') {
-                                       element.outerHTML = "[attach=" + attachmentId + "," + float + "][/attach]";
-                               }
-                               else {
-                                       element.outerHTML = "[attach=" + attachmentId + "][/attach]";
-                               }
-                       }
-                       else {
-                               // regular image
-                               var source = element.src.trim();
-                               
-                               if (width > 0) {
-                                       element.outerHTML = "[img='" + source + "'," + float + "," + width + "][/img]";
-                               }
-                               else if (float !== 'none') {
-                                       element.outerHTML = "[img='" + source + "'," + float + "][/img]";
-                               }
-                               else {
-                                       element.outerHTML = "[img]" + source + "[/img]";
-                               }
-                       }
-               },
-               
-               /**
-                * Converts <ol> and <ul> into [list].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertList: function(element) {
-                       var open;
-                       
-                       if (element.nodeName === 'OL') {
-                               open = '[list=1]';
-                       }
-                       else {
-                               var type = element.style.getPropertyValue('list-style-type') || '';
-                               if (type === '') {
-                                       open = '[list]';
-                               }
-                               else {
-                                       open = '[list=' + (type === 'lower-latin' ? 'a' : type) + ']';
-                               }
-                       }
-                       
-                       element.outerHTML = open + element.innerHTML + '[/list]';
-               },
-               
-               /**
-                * Converts <li> into [*] unless it is not encapsulated in <ol> or <ul>.
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertListItem: function(element) {
-                       if (element.parentNode.nodeName !== 'UL' && element.parentNode.nodeName !== 'OL') {
-                               element.outerHTML = element.innerHTML;
-                       }
-                       else {
-                               element.outerHTML = '[*]' + element.innerHTML;
-                       }
-               },
-               
-               /**
-                * Converts <span> into a series of BBCodes including [color], [font] and [size].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertSpan: function(element) {
-                       if (element.style.length || element.className) {
-                               var converter, value;
-                               for (var i = 0, length = _inlineConverter.span.length; i < length; i++) {
-                                       converter = _inlineConverter.span[i];
-                                       
-                                       if (converter.style) {
-                                               value = element.style.getPropertyValue(converter.style) || '';
-                                               if (value) {
-                                                       converter.callback(element, value);
-                                               }
-                                       }
-                                       else {
-                                               if (element.classList.contains(converter.className)) {
-                                                       converter.callback(element);
-                                               }
-                                       }
-                               }
-                       }
-                       
-                       element.outerHTML = element.innerHTML;
-               },
-               
-               /**
-                * Converts <div> into a series of BBCodes including [align].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertDiv: function(element) {
-                       if (element.className.length || element.style.length) {
-                               var converter, value;
-                               for (var i = 0, length = _inlineConverter.div.length; i < length; i++) {
-                                       converter = _inlineConverter.div[i];
-                                       
-                                       if (converter.className && element.classList.contains(converter.className)) {
-                                               converter.callback(element);
-                                       }
-                                       else if (converter.style) {
-                                               value = element.style.getPropertyValue(converter.style) || '';
-                                               if (value) {
-                                                       converter.callback(element, value);
-                                               }
-                                       }
-                               }
-                       }
-                       
-                       element.outerHTML = element.innerHTML;
-               },
-               
-               /**
-                * Converts the CSS style `color` into [color].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertInlineColor: function(element, value) {
-                       if (value.match(/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i)) {
-                               var r = RegExp.$1;
-                               var g = RegExp.$2;
-                               var b = RegExp.$3;
-                               
-                               var chars = '0123456789ABCDEF';
-                               value = '#' + (chars.charAt((r - r % 16) / 16) + '' + chars.charAt(r % 16)) + '' + (chars.charAt((g - g % 16) / 16) + '' + chars.charAt(g % 16)) + '' + (chars.charAt((b - b % 16) / 16) + '' + chars.charAt(b % 16));
-                       }
-                       
-                       element.innerHTML = '[color=' + value + ']' + element.innerHTML + '[/color]';
-               },
-               
-               /**
-                * Converts the CSS style `font-size` into [size].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertInlineFontSize: function(element, value) {
-                       if (value.match(/^(\d+)pt$/)) {
-                               value = RegExp.$1;
-                       }
-                       else if (value.match(/^(\d+)(px|em|rem|%)$/)) {
-                               value = window.getComputedStyle(value).fontSize.replace(/^(\d+).*$/, '$1');
-                               value = Math.round(value);
-                       }
-                       else {
-                               // unknown or unsupported value, ignore
-                               value = '';
-                       }
-                       
-                       if (value) {
-                               // min size is 8 and maximum is 36
-                               value = Math.min(Math.max(value, 8), 36);
-                               
-                               element.innerHTML = '[size=' + value + ']' + element.innerHTML + '[/size]';
-                       }
-               },
-               
-               /**
-                * Converts the CSS style `font-family` into [font].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertInlineFontFamily: function(element, value) {
-                       element.innerHTML = '[font=' + value.replace(/'/g, '') + ']' + element.innerHTML + '[/font]';
-               },
-               
-               /**
-                * Converts the CSS style `text-align` into [align].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertInlineTextAlign: function(element, value) {
-                       if (['center', 'justify', 'left', 'right'].indexOf(value) !== -1) {
-                               element.innerHTML = '[align=' + value + ']' + element.innerHTML + '[/align]';
-                       }
-               },
-               
-               /**
-                * Converts tables and their children into BBCodes.
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertTable: function(element) {
-                       var elements = element.getElementsByTagName('TD');
-                       while (elements.length) {
-                               elements[0].outerHTML = '[td]' + elements[0].innerHTML + '[/td]\n';
-                       }
-                       
-                       elements = element.getElementsByTagName('TR');
-                       while (elements.length) {
-                               elements[0].outerHTML = '\n[tr]\n' + elements[0].innerHTML + '[/tr]';
-                       }
-                       
-                       var tbody = DOMTraverse.childByTag(element, 'TBODY');
-                       var innerHtml = (tbody === null) ? element.innerHTML : tbody.innerHTML;
-                       element.outerHTML = '\n[table]' + innerHtml + '\n[/table]\n';
-               },
-               
-               /**
-                * Converts <a> into [email] or [url].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertUrl: function(element) {
-                       var content = element.textContent.trim(), href = element.href.trim(), tagName = 'url';
-                       
-                       if (href === '' || content === '') {
-                               // empty href or content
-                               element.outerHTML = element.innerHTML;
-                               return;
-                       }
-                       
-                       if (href.indexOf('mailto:') === 0) {
-                               href = href.substr(7);
-                               tagName = 'email';
-                       }
-                       
-                       if (href === content) {
-                               element.outerHTML = '[' + tagName + ']' + href + '[/' + tagName + ']';
-                       }
-                       else {
-                               element.outerHTML = "[" + tagName + "='" + href + "']" + element.innerHTML + "[/" + tagName + "]";
-                       }
-               },
-               
-               /**
-                * Converts <div class="codeBox"> into [code].
-                * 
-                * @param       {Element}       element         target element
-                */
-               _convertSourceCodeBox: function(element) {
-                       var filename = element.getAttribute('data-filename').trim() || '';
-                       var highlighter = element.getAttribute('data-highlighter') || '';
-                       window.dtdesign = element;
-                       var list = DOMTraverse.childByTag(element.children[0], 'OL');
-                       var lineNumber = ~~list.getAttribute('start') || 1;
-                       
-                       var content = '';
-                       for (var i = 0, length = list.childElementCount; i < length; i++) {
-                               if (content) content += "\n";
-                               content += list.children[i].textContent;
-                       }
-                       
-                       var open = "[code='" + highlighter + "'," + lineNumber + ",'" + filename + "']";
-                       
-                       element.outerHTML = open + content + '[/code]';
-               }
-       };
-       
-       return BBCodeFromHtml;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/Parser.js
deleted file mode 100644 (file)
index 3fbb4a1..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- * Versatile BBCode parser based upon the PHP implementation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/BBCode/Parser
- */
-define([], function() {
-       "use strict";
-       
-       /**
-        * @module      WoltLab/WCF/BBCode/Parser
-        */
-       var BBCodeParser = {
-               /**
-                * Parses a message and returns an XML-conform linear tree.
-                * 
-                * @param       {string}        message         message containing BBCodes
-                * @return      {array<mixed>}  linear tree
-                */
-               parse: function(message) {
-                       var stack = this._splitTags(message);
-                       this._buildLinearTree(stack);
-                       
-                       return stack;
-               },
-               
-               /**
-                * Splits message into strings and BBCode objects.
-                * 
-                * @param       {string}        message         message containing BBCodes
-                * @returns     {array<mixed>}  linear tree
-                */
-               _splitTags: function(message) {
-                       var validTags = __REDACTOR_BBCODES.join('|');
-                       var pattern = '(\\\[(?:/(?:' + validTags + ')|(?:' + validTags + ')'
-                               + '(?:='
-                                       + '(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\\\'|[^,\\\]]*)'
-                                       + '(?:,(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\'|[^,\\\]]*))*'
-                               + ')?)\\\])';
-                       
-                       var isBBCode = new RegExp('^' + pattern + '$', 'i');
-                       var part, parts = message.split(new RegExp(pattern, 'i')), stack = [], tag;
-                       for (var i = 0, length = parts.length; i < length; i++) {
-                               part = parts[i];
-                               
-                               if (part === '') {
-                                       continue;
-                               }
-                               else if (part.match(isBBCode)) {
-                                       tag = { name: '', closing: false, attributes: [], source: part };
-                                       
-                                       if (part[1] === '/') {
-                                               tag.name = part.substring(2, part.length - 1);
-                                               tag.closing = true;
-                                       }
-                                       else if (part.match(/^\[([a-z0-9]+)=?(.*)\]$/i)) {
-                                               tag.name = RegExp.$1;
-                                               
-                                               if (RegExp.$2) {
-                                                       tag.attributes = this._parseAttributes(RegExp.$2);
-                                               }
-                                       }
-                                       
-                                       stack.push(tag);
-                               }
-                               else {
-                                       stack.push(part);
-                               }
-                       }
-                       
-                       return stack;
-               },
-               
-               /**
-                * Finds pairs and enforces XML-conformity in terms of pairing and proper nesting.
-                * 
-                * @param       {array<mixed>}  stack   linear tree
-                */
-               _buildLinearTree: function(stack) {
-                       var item, openTags = [], reopenTags, sourceBBCode = '';
-                       for (var i = 0; i < stack.length; i++) { // do not cache stack.length, its size is dynamic
-                               item = stack[i];
-                               
-                               if (typeof item === 'object') {
-                                       if (sourceBBCode.length && (item.name !== sourceBBCode || !item.closing)) {
-                                               stack[i] = item.source;
-                                               continue;
-                                       }
-                                       
-                                       if (item.closing) {
-                                               if (this._hasOpenTag(openTags, item.name)) {
-                                                       reopenTags = this._closeUnclosedTags(stack, openTags, item.name);
-                                                       for (var j = 0, innerLength = reopenTags.length; j < innerLength; j++) {
-                                                               stack.splice(i, reopenTags[j]);
-                                                               i++;
-                                                       }
-                                                       
-                                                       openTags.pop().pair = i;
-                                               }
-                                               else {
-                                                       // tag was never opened, treat as plain text
-                                                       stack[i] = item.source;
-                                               }
-                                               
-                                               if (sourceBBCode === item.name) {
-                                                       sourceBBCode = '';
-                                               }
-                                       }
-                                       else {  
-                                               openTags.push(item);
-                                               
-                                               if (__REDACTOR_SOURCE_BBCODES.indexOf(item.name) !== -1) {
-                                                       sourceBBCode = item.name;
-                                               }
-                                       }
-                               }
-                       }
-                       
-                       // close unclosed tags
-                       this._closeUnclosedTags(stack, openTags, '');
-               },
-               
-               /**
-                * Closes unclosed BBCodes and returns a list of BBCodes in order of appearance that should be
-                * opened again to enforce proper nesting.
-                * 
-                * @param       {array<mixed>}  stack           linear tree
-                * @param       {array<object>} openTags        list of unclosed elements
-                * @param       {string}        until           tag name to stop at
-                * @return      {array<mixed>}  list of tags to open in order of appearance
-                */
-               _closeUnclosedTags: function(stack, openTags, until) {
-                       var item, reopenTags = [], tag;
-                       
-                       for (var i = openTags.length - 1; i >= 0; i--) {
-                               item = openTags[i];
-                               
-                               if (item.name === until) {
-                                       break;
-                               }
-                               
-                               tag = { name: item.name, closing: true, attributes: item.attributes.slice(), source: '[/' + item.name + ']' };
-                               item.pair = stack.length;
-                               
-                               stack.push(tag);
-                               
-                               openTags.pop();
-                               reopenTags.push({ name: item.name, closing: false, attributes: item.attributes.slice(), source: item.source });
-                       }
-                       
-                       return reopenTags.reverse();
-               },
-               
-               /**
-                * Returns true if given BBCode was opened before.
-                * 
-                * @param       {array<object>} openTags        list of unclosed elements
-                * @param       {string}        name            BBCode to search for
-                * @returns     {boolean}       false if tag was not opened before
-                */
-               _hasOpenTag: function(openTags, name) {
-                       for (var i = openTags.length - 1; i >= 0; i--) {
-                               if (openTags[i].name === name) {
-                                       return true;
-                               }
-                       }
-                       
-                       return false;
-               },
-               
-               /**
-                * Parses the attribute list and returns a list of attributes without enclosing quotes.
-                * 
-                * @param       {string}        attrString      comma separated string with optional quotes per attribute
-                * @returns     {array<string>} list of attributes
-                */
-               _parseAttributes: function(attrString) {
-                       var tmp = attrString.split(/(?:^|,)('[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^,]*)/g);
-                       
-                       var attribute, attributes = [];
-                       for (var i = 0, length = tmp.length; i < length; i++) {
-                               attribute = tmp[i];
-                               
-                               if (attribute !== '') {
-                                       if (attribute.charAt(0) === "'" && attribute.substr(-1) === "'") {
-                                               attributes.push(attribute.substring(1, attribute.length - 1).trim());
-                                       }
-                                       else {
-                                               attributes.push(attribute.trim());
-                                       }
-                               }
-                       }
-                       
-                       return attributes;
-               }
-       };
-       
-       return BBCodeParser;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/BBCode/ToHtml.js
deleted file mode 100644 (file)
index 399d2eb..0000000
+++ /dev/null
@@ -1,623 +0,0 @@
-/**
- * Converts a message containing BBCodes into HTML.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/BBCode/ToHtml
- */
-define(['Core', 'EventHandler', 'Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Core, EventHandler, Language, StringUtil, BBCodeParser) {
-       "use strict";
-       
-       var _bbcodes = null;
-       var _options = {};
-       var _removeNewlineAfter = [];
-       var _removeNewlineBefore = [];
-       
-       /**
-        * Returns true if given value is a non-zero integer.
-        * 
-        * @param       {string}        value           target value
-        * @return      {boolean}       true if `value` is a non-zero integer
-        */
-       function isNumber(value) {
-               return value && value == ~~value;
-       }
-       
-       /**
-        * Returns true if given value appears to be a filename, which means that it contains a dot
-        * or is neither numeric nor a known highlighter.
-        * 
-        * @param       {string}        value           target value
-        * @return      {boolean}       true if `value` appears to be a filename
-        */
-       function isFilename(value) {
-               return (value.indexOf('.') !== -1) || (!isNumber(value) && !isHighlighter(value));
-       }
-       
-       /**
-        * Returns true if given value is a known highlighter.
-        * 
-        * @param       {string}        value           target value
-        * @return      {boolean}       true if `value` is a known highlighter
-        */
-       function isHighlighter(value) {
-               return __REDACTOR_CODE_HIGHLIGHTERS.hasOwnProperty(value);
-       }
-       
-       /**
-        * @module      WoltLab/WCF/BBCode/ToHtml
-        */
-       var BBCodeToHtml = {
-               /**
-                * Converts a message containing BBCodes to HTML.
-                * 
-                * @param       {string}        message         message containing BBCodes
-                * @return      {string}        HTML message
-                */
-               convert: function(message, options) {
-                       _options = Core.extend({
-                               attachments: {
-                                       images: {},
-                                       thumbnailUrl: '',
-                                       url: ''
-                               }
-                       }, options);
-                       
-                       this._convertSpecials(message);
-                       
-                       var stack = BBCodeParser.parse(message);
-                       
-                       if (stack.length) {
-                               this._initBBCodes();
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'beforeConvert', { stack: stack });
-                       
-                       var item, value;
-                       for (var i = 0, length = stack.length; i < length; i++) {
-                               item = stack[i];
-                               
-                               if (typeof item === 'object') {
-                                       value = this._convert(stack, item, i);
-                                       if (Array.isArray(value)) {
-                                               stack[i] = (value[0] === null ? item.source : value[0]);
-                                               stack[item.pair] = (value[1] === null ? stack[item.pair].source : value[1]);
-                                       }
-                                       else {
-                                               stack[i] = value;
-                                       }
-                               }
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'afterConvert', { stack: stack });
-                       
-                       message = stack.join('');
-                       
-                       message = message.replace(/\n/g, '<br>');
-                       
-                       return message;
-               },
-               
-               /**
-                * Converts special characters to their entities.
-                * 
-                * @param       {string}        message         message containing BBCodes
-                * @return      {string}        message with replaced special characters
-                */
-               _convertSpecials: function(message) {
-                       message = message.replace(/&/g, '&amp;');
-                       message = message.replace(/</g, '&lt;');
-                       message = message.replace(/>/g, '&gt;');
-                       
-                       return message;
-               },
-               
-               /**
-                * Sets up converters applied to HTML elements.
-                */
-               _initBBCodes: function() {
-                       if (_bbcodes !== null) {
-                               return;
-                       }
-                       
-                       _bbcodes = {
-                               // simple replacements
-                               b: 'strong',
-                               i: 'em',
-                               u: 'u',
-                               s: 'del',
-                               sub: 'sub',
-                               sup: 'sup',
-                               table: 'table',
-                               td: 'td',
-                               tr: 'tr',
-                               tt: 'kbd',
-                               
-                               // callback replacement
-                               align: this._convertAlignment.bind(this),
-                               attach: this._convertAttachment.bind(this),
-                               color: this._convertColor.bind(this),
-                               code: this._convertCode.bind(this),
-                               email: this._convertEmail.bind(this),
-                               list: this._convertList.bind(this),
-                               quote: this._convertQuote.bind(this),
-                               size: this._convertSize.bind(this),
-                               url: this._convertUrl.bind(this),
-                               img: this._convertImage.bind(this)
-                       };
-                       
-                       _removeNewlineAfter = ['quote', 'table', 'td', 'tr'];
-                       _removeNewlineBefore = ['table', 'td', 'tr'];
-                       
-                       EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'init', {
-                               bbcodes: _bbcodes,
-                               removeNewlineAfter: _removeNewlineAfter,
-                               removeNewlineBefore: _removeNewlineBefore
-                       });
-               },
-               
-               /**
-                * Converts an item from the stack.
-                * 
-                * @param       {array<mixed>}          stack           linear list of BBCode tags and regular strings
-                * @param       {object}                item            current BBCode tag object
-                * @param       {integer}               index           current stack index representing `item`
-                * @return      {(string|array)}        string if only the current item should be replaced or an array with
-                *                                      the first item used for the opening tag and the second item for the closing tag
-                */
-               _convert: function(stack, item, index) {
-                       var replace = _bbcodes[item.name], tmp;
-                       
-                       if (replace === undefined) {
-                               // treat as plain text
-                               return [null, null];
-                       }
-                       
-                       if (_removeNewlineAfter.indexOf(item.name) !== -1) {
-                               tmp = stack[index + 1];
-                               if (typeof tmp === 'string') {
-                                       stack[index + 1] = tmp.replace(/^\n/, '');
-                               }
-                               
-                               if (stack.length > item.pair + 1) {
-                                       tmp = stack[item.pair + 1];
-                                       if (typeof tmp === 'string') {
-                                               stack[item.pair + 1] = tmp.replace(/^\n/, '');
-                                       }
-                               }
-                       }
-                       
-                       if (_removeNewlineBefore.indexOf(item.name) !== -1) {
-                               if (index - 1 >= 0) {
-                                       tmp = stack[index - 1];
-                                       if (typeof tmp === 'string') {
-                                               stack[index - 1] = tmp.replace(/\n$/, '');
-                                       }
-                               }
-                               
-                               tmp = stack[item.pair - 1];
-                               if (typeof tmp === 'string') {
-                                       stack[item.pair - 1] = tmp.replace(/\n$/, '');
-                               }
-                       }
-                       
-                       // replace smilies
-                       this._convertSmilies(stack);
-                       
-                       if (typeof replace === 'string') {
-                               return ['<' + replace + '>', '</' + replace + '>'];
-                       }
-                       else {
-                               return replace(stack, item, index);
-                       }
-               },
-               
-               /**
-                * Converts [align] into <div style="text-align: ...">.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertAlignment: function(stack, item, index) {
-                       var align = (item.attributes.length) ? item.attributes[0] : '';
-                       if (['center', 'justify', 'left', 'right'].indexOf(align) === -1) {
-                               return [null, null];
-                       }
-                       
-                       return ['<div style="text-align: ' + align + '">', '</div>'];
-               },
-               
-               /**
-                * Converts [attach] into an <img> or to plain text if attachment is a non-image.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertAttachment: function(stack, item, index) {
-                       var attachmentId = 0, attributes = item.attributes, length = attributes.length;
-                       if (!_options.attachments.url) {
-                               length = 0;
-                       }
-                       else if (length > 0) {
-                               attachmentId = ~~attributes[0];
-                               if (!_options.attachments.images.hasOwnProperty(attachmentId)) {
-                                       length = 0;
-                               }
-                       }
-                       
-                       if (length === 0) {
-                               return [null, null];
-                       }
-                       
-                       var maxHeight = ~~_options.attachments.images[attachmentId].height;
-                       var maxWidth = ~~_options.attachments.images[attachmentId].width;
-                       var styles = ['max-height: ' + maxHeight + 'px', 'max-width: ' + maxWidth + 'px'];
-                       
-                       if (length > 1) {
-                               if (item.attributes[1] === 'left' || attributes[1] === 'right') {
-                                       styles.push('float: ' + attributes[1]);
-                                       styles.push('margin: ' + (attributes[1] === 'left' ? '0 15px 7px 0' : '0 0 7px 15px'));
-                               }
-                       }
-                       
-                       var baseUrl = _options.attachments.thumbnailUrl;
-                       if (length > 2) {
-                               width = ~~attributes[2] || 0;
-                               if (width) {
-                                       if (width > maxWidth) width = maxWidth;
-                                       
-                                       styles.push('width: ' + width + 'px');
-                                       baseUrl = _options.attachments.url;
-                               }
-                       }
-                       
-                       return [
-                               '<img src="' + baseUrl.replace(/987654321/, attachmentId) + '" class="redactorEmbeddedAttachment redactorDisableResize" data-attachment-id="' + attachmentId + '"' + (styles.length ? ' style="' + styles.join(';') + '"' : '') + '>',
-                               ''
-                       ];
-               },
-               
-               /**
-                * Converts [code] to <div class="codeBox">.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertCode: function(stack, item, index) {
-                       var attributes = item.attributes, filename = '', highlighter = 'auto', lineNumber = 0;
-                       
-                       // parse arguments
-                       switch (attributes.length) {
-                               case 1:
-                                       if (isNumber(attributes[0])) {
-                                               lineNumber = ~~attributes[0];
-                                       }
-                                       else if (isFilename(attributes[0])) {
-                                               filename = attributes[0];
-                                       }
-                                       else if (isHighlighter(attributes[0])) {
-                                               highlighter = attributes[0];
-                                       }
-                                       break;
-                               case 2:
-                                       if (isNumber(attributes[0])) {
-                                               lineNumber = ~~attributes[0];
-                                               
-                                               if (isHighlighter(attributes[1])) {
-                                                       highlighter = attributes[1];
-                                               }
-                                               else if (isFilename(attributes[1])) {
-                                                       filename = attributes[1];
-                                               }
-                                       }
-                                       else {
-                                               if (isHighlighter(attributes[0])) highlighter = attributes[0];
-                                               if (isFilename(attributes[1])) filename = attributes[1];
-                                       }
-                                       break;
-                               case 3:
-                                       if (isHighlighter(attributes[0])) highlighter = attributes[0];
-                                       if (isNumber(attributes[1])) lineNumber = ~~attributes[1];
-                                       if (isFilename(attributes[2])) filename = attributes[2];
-                                       break;
-                       }
-                       
-                       // transform content
-                       var before = true, content, line, empty = -1;
-                       for (var i = index + 1; i < item.pair; i++) {
-                               line = stack[i];
-                               
-                               if (line.trim() === '') {
-                                       if (before) {
-                                               stack[i] = '';
-                                               continue;
-                                       }
-                                       else if (empty === -1) {
-                                               empty = i;
-                                       }
-                               }
-                               else {
-                                       before = false;
-                                       empty = -1;
-                               }
-                               
-                               content = line.split('\n');
-                               for (var j = 0, innerLength = content.length; j < innerLength; j++) {
-                                       content[j] = '<li>' + (content[j] ? StringUtil.escapeHTML(content[j]) : '\u200b') + '</li>';
-                               }
-                               
-                               stack[i] = content.join('');
-                       }
-                       
-                       if (!before && empty !== -1) {
-                               for (var i = item.pair - 1; i >= empty; i--) {
-                                       stack[i] = '';
-                               }
-                       }
-                       
-                       return [
-                               '<div class="codeBox container" contenteditable="false" data-highlighter="' + highlighter + '" data-filename="' + (filename ? StringUtil.escapeHTML(filename) : '') + '">'
-                                       + '<div>'
-                                       + '<div>'
-                                               + '<h3>' + __REDACTOR_CODE_HIGHLIGHTERS[highlighter] + (filename ? ': ' + StringUtil.escapeHTML(filename) : '') + '</h3>'
-                                       + '</div>'
-                                       + '<ol start="' + (lineNumber > 1 ? lineNumber : 1) + '">',
-                               '</ol></div></div>'
-                       ];
-               },
-               
-               /**
-                * Converts [color] to <span style="color: ...">.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertColor: function(stack, item, index) {
-                       if (!item.attributes.length || !item.attributes[0].match(/^[a-z0-9#]+$/i)) {
-                               return [null, null];
-                       }
-                       
-                       return ['<span style="color: ' + StringUtil.escapeHTML(item.attributes[0]) + '">', '</span>'];
-               },
-               
-               /**
-                * Converts [email] to <a href="mailto: ...">.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertEmail: function(stack, item, index) {
-                       var email = '';
-                       if (item.attributes.length) {
-                               email = item.attributes[0];
-                       }
-                       else {
-                               var element;
-                               for (var i = index + 1; i < item.pair; i++) {
-                                       element = stack[i];
-                                       
-                                       if (typeof element === 'object') {
-                                               email = '';
-                                               break;
-                                       }
-                                       else {
-                                               email += element;
-                                       }
-                               }
-                               
-                               // no attribute present and element is empty, handle as plain text
-                               if (email.trim() === '') {
-                                       return [null, null];
-                               }
-                       }
-                       
-                       return ['<a href="mailto:' + StringUtil.escapeHTML(email) + '">', '</a>'];
-               },
-               
-               /**
-                * Converts [img] to <img>.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertImage: function(stack, item, index) {
-                       var float = 'none', source = '', width = 0;
-                       
-                       switch (item.attributes.length) {
-                               case 0:
-                                       if (index + 1 < item.pair && typeof stack[index + 1] === 'string') {
-                                               source = stack[index + 1];
-                                               stack[index + 1] = '';
-                                       }
-                                       else {
-                                               // [img] without attributes and content, discard
-                                               return '';
-                                       }
-                               break;
-                               
-                               case 1:
-                                       source = item.attributes[0];
-                               break;
-                               
-                               case 2:
-                                       source = item.attributes[0];
-                                       float = item.attributes[1];
-                               break;
-                               
-                               case 3:
-                                       source = item.attributes[0];
-                                       float = item.attributes[1];
-                                       width = ~~item.attributes[2];
-                               break;
-                       }
-                       
-                       if (float !== 'left' && float !== 'right') float = 'none';
-                       
-                       var styles = [];
-                       if (width > 0) {
-                               styles.push('width: ' + width + 'px');
-                       }
-                       
-                       if (float !== 'none') {
-                               styles.push('float: ' + float);
-                               styles.push('margin: ' + (float === 'left' ? '0 15px 7px 0' : '0 0 7px 15px'));
-                       }
-                       
-                       return ['<img src="' + StringUtil.escapeHTML(source) + '"' + (styles.length ? ' style="' + styles.join(';') + '"' : '') + '>', ''];
-               },
-               
-               /**
-                * Converts [list] to <ol> or <ul>.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertList: function(stack, item, index) {
-                       var type = (items.attributes.length) ? item.attributes[0] : '';
-                       
-                       // replace list items
-                       for (var i = index + 1; i < item.pair; i++) {
-                               if (typeof stack[i] === 'string') {
-                                       stack[i] = stack[i].replace(/\[\*\]/g, '<li>');
-                               }
-                       }
-                       
-                       if (type == '1' || type === 'decimal') {
-                               return ['<ol>', '</ol>'];
-                       }
-                       
-                       if (type.length && type.match(/^(?:none|circle|square|disc|decimal|lower-roman|upper-roman|decimal-leading-zero|lower-greek|lower-latin|upper-latin|armenian|georgian)$/)) {
-                               return ['<ul style="list-style-type: ' + type + '">', '</ul>'];
-                       }
-                       
-                       return ['<ul>', '</ul>'];
-               },
-               
-               /**
-                * Converts [quote] to <blockquote>.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertQuote: function(stack, item, index) {
-                       var author = '', link = '';
-                       if (item.attributes.length > 1) {
-                               author = item.attributes[0];
-                               link = item.attributes[1];
-                       }
-                       else if (item.attributes.length === 1) {
-                               author = item.attributes[0];
-                       }
-                       
-                       // get rid of the trailing newline for quote content
-                       for (var i = item.pair - 1; i > index; i--) {
-                               if (typeof stack[i] === 'string') {
-                                       stack[i] = stack[i].replace(/\n$/, '');
-                                       break;
-                               }
-                       }
-                       
-                       var header = '';
-                       if (author) {
-                               if (link) header = '<a href="' + StringUtil.escapeHTML(link) + '" tabindex="-1">';
-                               header += Language.get('wcf.bbcode.quote.title.javascript', { quoteAuthor: author.replace(/\\'/g, "'") });
-                               if (link) header += '</a>';
-                       }
-                       else {
-                               header = '<small>' + Language.get('wcf.bbcode.quote.title.clickToSet') + '</small>';
-                       }
-                       
-                       return [
-                               '<blockquote class="quoteBox container containerPadding quoteBoxSimple" cite="' + StringUtil.escapeHTML(link) + '" data-author="' + StringUtil.escapeHTML(author) + '">'
-                                       + '<header contenteditable="false">'
-                                               + '<h3>'
-                                                       + header
-                                               + '</h3>'
-                                               + '<a class="redactorQuoteEdit"></a>'
-                                       + '</header>'
-                                       + '<div>\u200b',
-                               '</div></blockquote>'
-                       ];
-               },
-               
-               /**
-                * Converts smiley codes into <img>.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                */
-               _convertSmilies: function(stack) {
-                       var altValue, item, regexp;
-                       for (var i = 0, length = stack.length; i < length; i++) {
-                               item = stack[i];
-                               
-                               if (typeof item === 'string') {
-                                       for (var smileyCode in __REDACTOR_SMILIES) {
-                                               if (__REDACTOR_SMILIES.hasOwnProperty(smileyCode)) {
-                                                       altValue = smileyCode.replace(/</g, '&lt;').replace(/>/g, '&gt;');
-                                                       regexp = new RegExp('(\\s|^)' + StringUtil.escapeRegExp(smileyCode) + '(?=\\s|$)', 'gi');
-                                                       item = item.replace(regexp, '$1<img src="' + __REDACTOR_SMILIES[smileyCode] + '" class="smiley" alt="' + altValue + '">');
-                                               }
-                                       }
-                                       
-                                       stack[i] = item;
-                               }
-                               else if (__REDACTOR_SOURCE_BBCODES.indexOf(item.name) !== -1) {
-                                       // skip processing content
-                                       i = item.pair;
-                               }
-                       }
-               },
-               
-               /**
-                * Converts [size] to <span style="font-size: ...">.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertSize: function(stack, item, index) {
-                       if (!item.attributes.length || ~~item.attributes[0] === 0) {
-                               return [null, null];
-                       }
-                       
-                       return ['<span style="font-size: ' + ~~item.attributes[0] + 'pt">', '</span>'];
-               },
-               
-               /**
-                * Converts [url] to <a>.
-                * 
-                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
-                * @param       {object}        item    current BBCode tag object
-                * @param       {integer}       index   current stack index representing `item`
-                * @returns     {array}         first item represents the opening tag, the second the closing one
-                */
-               _convertUrl: function(stack, item, index) {
-                       // ignore url bbcode without arguments
-                       if (!item.attributes.length) {
-                               return [null, null];
-                       }
-                       
-                       return ['<a href="' + StringUtil.escapeHTML(item.attributes[0]) + '">', '</a>'];
-               }
-       };
-       
-       return BBCodeToHtml;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/FromHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/FromHtml.js
new file mode 100644 (file)
index 0000000..9745f18
--- /dev/null
@@ -0,0 +1,547 @@
+/**
+ * Converts a message containing HTML tags into BBCodes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/BBCode/FromHtml
+ */
+define(['EventHandler', 'StringUtil', 'DOM/Traverse'], function(EventHandler, StringUtil, DOMTraverse) {
+       "use strict";
+       
+       var _converter = [];
+       var _inlineConverter = {};
+       var _sourceConverter = [];
+       
+       /**
+        * Returns true if a whitespace should be inserted before or after the smiley.
+        * 
+        * @param       {Element}       element         image element
+        * @param       {boolean}       before          evaluate previous node
+        * @return      {boolean}       true if a whitespace should be inserted
+        */
+       function addSmileyPadding(element, before) {
+               var target = element[(before ? 'previousSibling' : 'nextSibling')];
+               if (target === null || target.nodeType !== Node.TEXT_NODE || !/\s$/.test(target.textContent)) {
+                       return true;
+               }
+               
+               return false;
+       }
+       
+       /**
+        * @module      WoltLab/WCF/BBCode/FromHtml
+        */
+       var BBCodeFromHtml = {
+               /**
+                * Converts a message containing HTML elements into BBCodes.
+                * 
+                * @param       {string}        message         message containing HTML elements
+                * @return      {string}        message containing BBCodes
+                */
+               convert: function(message) {
+                       if (message.length) this._setup();
+                       
+                       var container = document.createElement('div');
+                       container.innerHTML = message;
+                       
+                       // convert line breaks
+                       var elements = container.getElementsByTagName('P');
+                       while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
+                       
+                       elements = container.getElementsByTagName('BR');
+                       while (elements.length) elements[0].outerHTML = "\n";
+                       
+                       // prevent conversion taking place inside source bbcodes
+                       var sourceElements = this._preserveSourceElements(container);
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'beforeConvert', { container: container });
+                       
+                       for (var i = 0, length = _converter.length; i < length; i++) {
+                               this._convert(container, _converter[i]);
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'afterConvert', { container: container });
+                       
+                       this._restoreSourceElements(container, sourceElements);
+                       
+                       // remove remaining HTML elements
+                       elements = container.getElementsByTagName('*');
+                       while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
+                       
+                       message = this._convertSpecials(container.innerHTML);
+                       
+                       return message;
+               },
+               
+               /**
+                * Replaces HTML elements mapping to source BBCodes to avoid
+                * them being handled by other converters.
+                * 
+                * @param       {Element}       container       container element
+                * @return      {array<object>} list of source elements and their placeholder
+                */
+               _preserveSourceElements: function(container) {
+                       var elements, sourceElements = [], tmp;
+                       
+                       for (var i = 0, length = _sourceConverter.length; i < length; i++) {
+                               elements = container.querySelectorAll(_sourceConverter[i].selector);
+                               
+                               tmp = [];
+                               for (var j = 0, innerLength = elements.length; j < innerLength; j++) {
+                                       this._preserveSourceElement(elements[j], tmp);
+                               }
+                               
+                               sourceElements.push(tmp);
+                       }
+                       
+                       return sourceElements;
+               },
+               
+               /**
+                * Replaces an element with a placeholder.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {array<object>} list of removed elements and their placeholders
+                */
+               _preserveSourceElement: function(element, sourceElements) {
+                       var placeholder = document.createElement('var');
+                       placeholder.setAttribute('data-source', 'wcf');
+                       element.parentNode.insertBefore(placeholder, element);
+                       
+                       var fragment = document.createDocumentFragment();
+                       fragment.appendChild(element);
+                       
+                       sourceElements.push({
+                               fragment: fragment,
+                               placeholder: placeholder
+                       });
+               },
+               
+               /**
+                * Reinserts source elements for parsing.
+                * 
+                * @param       {Element}       container       container element
+                * @param       {array<object>} sourceElements  list of removed elements and their placeholders
+                */
+               _restoreSourceElements: function(container, sourceElements) {
+                       var element, elements, placeholder;
+                       for (var i = 0, length = sourceElements.length; i < length; i++) {
+                               elements = sourceElements[i];
+                               
+                               if (elements.length === 0) {
+                                       continue;
+                               }
+                               
+                               for (var j = 0, innerLength = elements.length; j < innerLength; j++) {
+                                       element = elements[j];
+                                       placeholder = element.placeholder;
+                                       
+                                       placeholder.parentNode.insertBefore(element.fragment, placeholder);
+                                       
+                                       _sourceConverter[i].callback(placeholder.previousElementSibling);
+                                       
+                                       placeholder.parentNode.removeChild(placeholder);
+                               }
+                       }
+               },
+               
+               /**
+                * Converts special entities.
+                * 
+                * @param       {string}        message         HTML message
+                * @return      {string}        HTML message
+                */
+               _convertSpecials: function(message) {
+                       message = message.replace(/&amp;/g, '&');
+                       message = message.replace(/&lt;/g, '<');
+                       message = message.replace(/&gt;/g, '>');
+                       
+                       return message;
+               },
+               
+               /**
+                * Sets up converters applied to elements in linear order.
+                */
+               _setup: function() {
+                       if (_converter.length) {
+                               return;
+                       }
+                       
+                       _converter = [
+                               // simple replacement
+                               { tagName: 'STRONG', bbcode: 'b' },
+                               { tagName: 'DEL', bbcode: 's' },
+                               { tagName: 'EM', bbcode: 'i' },
+                               { tagName: 'SUB', bbcode: 'sub' },
+                               { tagName: 'SUP', bbcode: 'sup' },
+                               { tagName: 'U', bbcode: 'u' },
+                               { tagName: 'KBD', bbcode: 'tt' },
+                               
+                               // callback replacement
+                               { tagName: 'A', callback: this._convertUrl.bind(this) },
+                               { tagName: 'IMG', callback: this._convertImage.bind(this) },
+                               { tagName: 'LI', callback: this._convertListItem.bind(this) },
+                               { tagName: 'OL', callback: this._convertList.bind(this) },
+                               { tagName: 'TABLE', callback: this._convertTable.bind(this) },
+                               { tagName: 'UL', callback: this._convertList.bind(this) },
+                               { tagName: 'BLOCKQUOTE', callback: this._convertBlockquote.bind(this) },
+                               
+                               // convert these last
+                               { tagName: 'SPAN', callback: this._convertSpan.bind(this) },
+                               { tagName: 'DIV', callback: this._convertDiv.bind(this) }
+                       ];
+                       
+                       _inlineConverter = {
+                               span: [
+                                       { style: 'color', callback: this._convertInlineColor.bind(this) },
+                                       { style: 'font-size', callback: this._convertInlineFontSize.bind(this) },
+                                       { style: 'font-family', callback: this._convertInlineFontFamily.bind(this) }
+                               ],
+                               div: [
+                                       { style: 'text-align', callback: this._convertInlineTextAlign.bind(this) }
+                               ]
+                       };
+                       
+                       _sourceConverter = [
+                               { selector: 'div.codeBox', callback: this._convertSourceCodeBox.bind(this) }
+                       ];
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.fromHtml', 'init', {
+                               converter: _converter,
+                               inlineConverter: _inlineConverter,
+                               sourceConverter: _sourceConverter
+                       });
+               },
+               
+               /**
+                * Converts an element into a raw string.
+                * 
+                * @param       {Element}       container       container element
+                * @param       {object}        converter       converter object
+                */
+               _convert: function(container, converter) {
+                       if (typeof converter === 'function') {
+                               converter(container);
+                               return;
+                       }
+                       
+                       var element, elements = container.getElementsByTagName(converter.tagName);
+                       while (elements.length) {
+                               element = elements[0];
+                               
+                               if (converter.bbcode) {
+                                       element.outerHTML = '[' + converter.bbcode + ']' + element.innerHTML + '[/' + converter.bbcode + ']';
+                               }
+                               else {
+                                       converter.callback(element);
+                               }
+                       }
+               },
+               
+               /**
+                * Converts <blockquote> into [quote].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertBlockquote: function(element) {
+                       var author = element.getAttribute('data-author') || '';
+                       var link = element.getAttribute('cite') || '';
+                       
+                       var open = '[quote]';
+                       if (author) {
+                               author = StringUtil.escapeHTML(author).replace(/(\\)?'/g, function(match, isEscaped) { return isEscaped ? match : "\\'"; });
+                               if (link) {
+                                       open = "[quote='" + author + "','" + StringUtil.escapeHTML(link) + "']";
+                               }
+                               else {
+                                       open = "[quote='" + author + "']";
+                               }
+                       }
+                       
+                       var header = DOMTraverse.childByTag(element, 'HEADER');
+                       if (header !== null) element.removeChild(header);
+                       
+                       var divs = DOMTraverse.childrenByTag(element, 'DIV');
+                       for (var i = 0, length = divs.length; i < length; i++) {
+                               divs[i].outerHTML = divs[i].innerHTML + '\n';
+                       }
+                       
+                       element.outerHTML = open + element.innerHTML.replace(/^\n*/, '').replace(/\n*$/, '') + '[/quote]\n';
+               },
+               
+               /**
+                * Converts <img> into smilies, [attach] or [img].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertImage: function(element) {
+                       if (element.classList.contains('smiley')) {
+                               // smiley
+                               element.outerHTML = (addSmileyPadding(element, true) ? ' ' : '') + element.getAttribute('alt') + (addSmileyPadding(element, false) ? ' ' : '');
+                               return;
+                       }
+                       
+                       var float = element.style.getPropertyValue('float') || 'none';
+                       var width = element.style.getPropertyValue('width');
+                       width = (typeof width === 'string') ? ~~width.replace(/px$/, '') : 0;
+                       
+                       if (element.classList.contains('redactorEmbeddedAttachment')) {
+                               var attachmentId = element.getAttribute('data-attachment-id');
+                               
+                               if (width > 0) {
+                                       element.outerHTML = "[attach=" + attachmentId + "," + float + "," + width + "][/attach]";
+                               }
+                               else if (float !== 'none') {
+                                       element.outerHTML = "[attach=" + attachmentId + "," + float + "][/attach]";
+                               }
+                               else {
+                                       element.outerHTML = "[attach=" + attachmentId + "][/attach]";
+                               }
+                       }
+                       else {
+                               // regular image
+                               var source = element.src.trim();
+                               
+                               if (width > 0) {
+                                       element.outerHTML = "[img='" + source + "'," + float + "," + width + "][/img]";
+                               }
+                               else if (float !== 'none') {
+                                       element.outerHTML = "[img='" + source + "'," + float + "][/img]";
+                               }
+                               else {
+                                       element.outerHTML = "[img]" + source + "[/img]";
+                               }
+                       }
+               },
+               
+               /**
+                * Converts <ol> and <ul> into [list].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertList: function(element) {
+                       var open;
+                       
+                       if (element.nodeName === 'OL') {
+                               open = '[list=1]';
+                       }
+                       else {
+                               var type = element.style.getPropertyValue('list-style-type') || '';
+                               if (type === '') {
+                                       open = '[list]';
+                               }
+                               else {
+                                       open = '[list=' + (type === 'lower-latin' ? 'a' : type) + ']';
+                               }
+                       }
+                       
+                       element.outerHTML = open + element.innerHTML + '[/list]';
+               },
+               
+               /**
+                * Converts <li> into [*] unless it is not encapsulated in <ol> or <ul>.
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertListItem: function(element) {
+                       if (element.parentNode.nodeName !== 'UL' && element.parentNode.nodeName !== 'OL') {
+                               element.outerHTML = element.innerHTML;
+                       }
+                       else {
+                               element.outerHTML = '[*]' + element.innerHTML;
+                       }
+               },
+               
+               /**
+                * Converts <span> into a series of BBCodes including [color], [font] and [size].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertSpan: function(element) {
+                       if (element.style.length || element.className) {
+                               var converter, value;
+                               for (var i = 0, length = _inlineConverter.span.length; i < length; i++) {
+                                       converter = _inlineConverter.span[i];
+                                       
+                                       if (converter.style) {
+                                               value = element.style.getPropertyValue(converter.style) || '';
+                                               if (value) {
+                                                       converter.callback(element, value);
+                                               }
+                                       }
+                                       else {
+                                               if (element.classList.contains(converter.className)) {
+                                                       converter.callback(element);
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       element.outerHTML = element.innerHTML;
+               },
+               
+               /**
+                * Converts <div> into a series of BBCodes including [align].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertDiv: function(element) {
+                       if (element.className.length || element.style.length) {
+                               var converter, value;
+                               for (var i = 0, length = _inlineConverter.div.length; i < length; i++) {
+                                       converter = _inlineConverter.div[i];
+                                       
+                                       if (converter.className && element.classList.contains(converter.className)) {
+                                               converter.callback(element);
+                                       }
+                                       else if (converter.style) {
+                                               value = element.style.getPropertyValue(converter.style) || '';
+                                               if (value) {
+                                                       converter.callback(element, value);
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       element.outerHTML = element.innerHTML;
+               },
+               
+               /**
+                * Converts the CSS style `color` into [color].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertInlineColor: function(element, value) {
+                       if (value.match(/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i)) {
+                               var r = RegExp.$1;
+                               var g = RegExp.$2;
+                               var b = RegExp.$3;
+                               
+                               var chars = '0123456789ABCDEF';
+                               value = '#' + (chars.charAt((r - r % 16) / 16) + '' + chars.charAt(r % 16)) + '' + (chars.charAt((g - g % 16) / 16) + '' + chars.charAt(g % 16)) + '' + (chars.charAt((b - b % 16) / 16) + '' + chars.charAt(b % 16));
+                       }
+                       
+                       element.innerHTML = '[color=' + value + ']' + element.innerHTML + '[/color]';
+               },
+               
+               /**
+                * Converts the CSS style `font-size` into [size].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertInlineFontSize: function(element, value) {
+                       if (value.match(/^(\d+)pt$/)) {
+                               value = RegExp.$1;
+                       }
+                       else if (value.match(/^(\d+)(px|em|rem|%)$/)) {
+                               value = window.getComputedStyle(value).fontSize.replace(/^(\d+).*$/, '$1');
+                               value = Math.round(value);
+                       }
+                       else {
+                               // unknown or unsupported value, ignore
+                               value = '';
+                       }
+                       
+                       if (value) {
+                               // min size is 8 and maximum is 36
+                               value = Math.min(Math.max(value, 8), 36);
+                               
+                               element.innerHTML = '[size=' + value + ']' + element.innerHTML + '[/size]';
+                       }
+               },
+               
+               /**
+                * Converts the CSS style `font-family` into [font].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertInlineFontFamily: function(element, value) {
+                       element.innerHTML = '[font=' + value.replace(/'/g, '') + ']' + element.innerHTML + '[/font]';
+               },
+               
+               /**
+                * Converts the CSS style `text-align` into [align].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertInlineTextAlign: function(element, value) {
+                       if (['center', 'justify', 'left', 'right'].indexOf(value) !== -1) {
+                               element.innerHTML = '[align=' + value + ']' + element.innerHTML + '[/align]';
+                       }
+               },
+               
+               /**
+                * Converts tables and their children into BBCodes.
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertTable: function(element) {
+                       var elements = element.getElementsByTagName('TD');
+                       while (elements.length) {
+                               elements[0].outerHTML = '[td]' + elements[0].innerHTML + '[/td]\n';
+                       }
+                       
+                       elements = element.getElementsByTagName('TR');
+                       while (elements.length) {
+                               elements[0].outerHTML = '\n[tr]\n' + elements[0].innerHTML + '[/tr]';
+                       }
+                       
+                       var tbody = DOMTraverse.childByTag(element, 'TBODY');
+                       var innerHtml = (tbody === null) ? element.innerHTML : tbody.innerHTML;
+                       element.outerHTML = '\n[table]' + innerHtml + '\n[/table]\n';
+               },
+               
+               /**
+                * Converts <a> into [email] or [url].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertUrl: function(element) {
+                       var content = element.textContent.trim(), href = element.href.trim(), tagName = 'url';
+                       
+                       if (href === '' || content === '') {
+                               // empty href or content
+                               element.outerHTML = element.innerHTML;
+                               return;
+                       }
+                       
+                       if (href.indexOf('mailto:') === 0) {
+                               href = href.substr(7);
+                               tagName = 'email';
+                       }
+                       
+                       if (href === content) {
+                               element.outerHTML = '[' + tagName + ']' + href + '[/' + tagName + ']';
+                       }
+                       else {
+                               element.outerHTML = "[" + tagName + "='" + href + "']" + element.innerHTML + "[/" + tagName + "]";
+                       }
+               },
+               
+               /**
+                * Converts <div class="codeBox"> into [code].
+                * 
+                * @param       {Element}       element         target element
+                */
+               _convertSourceCodeBox: function(element) {
+                       var filename = element.getAttribute('data-filename').trim() || '';
+                       var highlighter = element.getAttribute('data-highlighter') || '';
+                       window.dtdesign = element;
+                       var list = DOMTraverse.childByTag(element.children[0], 'OL');
+                       var lineNumber = ~~list.getAttribute('start') || 1;
+                       
+                       var content = '';
+                       for (var i = 0, length = list.childElementCount; i < length; i++) {
+                               if (content) content += "\n";
+                               content += list.children[i].textContent;
+                       }
+                       
+                       var open = "[code='" + highlighter + "'," + lineNumber + ",'" + filename + "']";
+                       
+                       element.outerHTML = open + content + '[/code]';
+               }
+       };
+       
+       return BBCodeFromHtml;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/Parser.js b/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/Parser.js
new file mode 100644 (file)
index 0000000..3fbb4a1
--- /dev/null
@@ -0,0 +1,201 @@
+/**
+ * Versatile BBCode parser based upon the PHP implementation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/BBCode/Parser
+ */
+define([], function() {
+       "use strict";
+       
+       /**
+        * @module      WoltLab/WCF/BBCode/Parser
+        */
+       var BBCodeParser = {
+               /**
+                * Parses a message and returns an XML-conform linear tree.
+                * 
+                * @param       {string}        message         message containing BBCodes
+                * @return      {array<mixed>}  linear tree
+                */
+               parse: function(message) {
+                       var stack = this._splitTags(message);
+                       this._buildLinearTree(stack);
+                       
+                       return stack;
+               },
+               
+               /**
+                * Splits message into strings and BBCode objects.
+                * 
+                * @param       {string}        message         message containing BBCodes
+                * @returns     {array<mixed>}  linear tree
+                */
+               _splitTags: function(message) {
+                       var validTags = __REDACTOR_BBCODES.join('|');
+                       var pattern = '(\\\[(?:/(?:' + validTags + ')|(?:' + validTags + ')'
+                               + '(?:='
+                                       + '(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\\\'|[^,\\\]]*)'
+                                       + '(?:,(?:\\\'[^\\\'\\\\]*(?:\\\\.[^\\\'\\\\]*)*\'|[^,\\\]]*))*'
+                               + ')?)\\\])';
+                       
+                       var isBBCode = new RegExp('^' + pattern + '$', 'i');
+                       var part, parts = message.split(new RegExp(pattern, 'i')), stack = [], tag;
+                       for (var i = 0, length = parts.length; i < length; i++) {
+                               part = parts[i];
+                               
+                               if (part === '') {
+                                       continue;
+                               }
+                               else if (part.match(isBBCode)) {
+                                       tag = { name: '', closing: false, attributes: [], source: part };
+                                       
+                                       if (part[1] === '/') {
+                                               tag.name = part.substring(2, part.length - 1);
+                                               tag.closing = true;
+                                       }
+                                       else if (part.match(/^\[([a-z0-9]+)=?(.*)\]$/i)) {
+                                               tag.name = RegExp.$1;
+                                               
+                                               if (RegExp.$2) {
+                                                       tag.attributes = this._parseAttributes(RegExp.$2);
+                                               }
+                                       }
+                                       
+                                       stack.push(tag);
+                               }
+                               else {
+                                       stack.push(part);
+                               }
+                       }
+                       
+                       return stack;
+               },
+               
+               /**
+                * Finds pairs and enforces XML-conformity in terms of pairing and proper nesting.
+                * 
+                * @param       {array<mixed>}  stack   linear tree
+                */
+               _buildLinearTree: function(stack) {
+                       var item, openTags = [], reopenTags, sourceBBCode = '';
+                       for (var i = 0; i < stack.length; i++) { // do not cache stack.length, its size is dynamic
+                               item = stack[i];
+                               
+                               if (typeof item === 'object') {
+                                       if (sourceBBCode.length && (item.name !== sourceBBCode || !item.closing)) {
+                                               stack[i] = item.source;
+                                               continue;
+                                       }
+                                       
+                                       if (item.closing) {
+                                               if (this._hasOpenTag(openTags, item.name)) {
+                                                       reopenTags = this._closeUnclosedTags(stack, openTags, item.name);
+                                                       for (var j = 0, innerLength = reopenTags.length; j < innerLength; j++) {
+                                                               stack.splice(i, reopenTags[j]);
+                                                               i++;
+                                                       }
+                                                       
+                                                       openTags.pop().pair = i;
+                                               }
+                                               else {
+                                                       // tag was never opened, treat as plain text
+                                                       stack[i] = item.source;
+                                               }
+                                               
+                                               if (sourceBBCode === item.name) {
+                                                       sourceBBCode = '';
+                                               }
+                                       }
+                                       else {  
+                                               openTags.push(item);
+                                               
+                                               if (__REDACTOR_SOURCE_BBCODES.indexOf(item.name) !== -1) {
+                                                       sourceBBCode = item.name;
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       // close unclosed tags
+                       this._closeUnclosedTags(stack, openTags, '');
+               },
+               
+               /**
+                * Closes unclosed BBCodes and returns a list of BBCodes in order of appearance that should be
+                * opened again to enforce proper nesting.
+                * 
+                * @param       {array<mixed>}  stack           linear tree
+                * @param       {array<object>} openTags        list of unclosed elements
+                * @param       {string}        until           tag name to stop at
+                * @return      {array<mixed>}  list of tags to open in order of appearance
+                */
+               _closeUnclosedTags: function(stack, openTags, until) {
+                       var item, reopenTags = [], tag;
+                       
+                       for (var i = openTags.length - 1; i >= 0; i--) {
+                               item = openTags[i];
+                               
+                               if (item.name === until) {
+                                       break;
+                               }
+                               
+                               tag = { name: item.name, closing: true, attributes: item.attributes.slice(), source: '[/' + item.name + ']' };
+                               item.pair = stack.length;
+                               
+                               stack.push(tag);
+                               
+                               openTags.pop();
+                               reopenTags.push({ name: item.name, closing: false, attributes: item.attributes.slice(), source: item.source });
+                       }
+                       
+                       return reopenTags.reverse();
+               },
+               
+               /**
+                * Returns true if given BBCode was opened before.
+                * 
+                * @param       {array<object>} openTags        list of unclosed elements
+                * @param       {string}        name            BBCode to search for
+                * @returns     {boolean}       false if tag was not opened before
+                */
+               _hasOpenTag: function(openTags, name) {
+                       for (var i = openTags.length - 1; i >= 0; i--) {
+                               if (openTags[i].name === name) {
+                                       return true;
+                               }
+                       }
+                       
+                       return false;
+               },
+               
+               /**
+                * Parses the attribute list and returns a list of attributes without enclosing quotes.
+                * 
+                * @param       {string}        attrString      comma separated string with optional quotes per attribute
+                * @returns     {array<string>} list of attributes
+                */
+               _parseAttributes: function(attrString) {
+                       var tmp = attrString.split(/(?:^|,)('[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|[^,]*)/g);
+                       
+                       var attribute, attributes = [];
+                       for (var i = 0, length = tmp.length; i < length; i++) {
+                               attribute = tmp[i];
+                               
+                               if (attribute !== '') {
+                                       if (attribute.charAt(0) === "'" && attribute.substr(-1) === "'") {
+                                               attributes.push(attribute.substring(1, attribute.length - 1).trim());
+                                       }
+                                       else {
+                                               attributes.push(attribute.trim());
+                                       }
+                               }
+                       }
+                       
+                       return attributes;
+               }
+       };
+       
+       return BBCodeParser;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/ToHtml.js b/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/ToHtml.js
new file mode 100644 (file)
index 0000000..399d2eb
--- /dev/null
@@ -0,0 +1,623 @@
+/**
+ * Converts a message containing BBCodes into HTML.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/BBCode/ToHtml
+ */
+define(['Core', 'EventHandler', 'Language', 'StringUtil', 'WoltLab/WCF/BBCode/Parser'], function(Core, EventHandler, Language, StringUtil, BBCodeParser) {
+       "use strict";
+       
+       var _bbcodes = null;
+       var _options = {};
+       var _removeNewlineAfter = [];
+       var _removeNewlineBefore = [];
+       
+       /**
+        * Returns true if given value is a non-zero integer.
+        * 
+        * @param       {string}        value           target value
+        * @return      {boolean}       true if `value` is a non-zero integer
+        */
+       function isNumber(value) {
+               return value && value == ~~value;
+       }
+       
+       /**
+        * Returns true if given value appears to be a filename, which means that it contains a dot
+        * or is neither numeric nor a known highlighter.
+        * 
+        * @param       {string}        value           target value
+        * @return      {boolean}       true if `value` appears to be a filename
+        */
+       function isFilename(value) {
+               return (value.indexOf('.') !== -1) || (!isNumber(value) && !isHighlighter(value));
+       }
+       
+       /**
+        * Returns true if given value is a known highlighter.
+        * 
+        * @param       {string}        value           target value
+        * @return      {boolean}       true if `value` is a known highlighter
+        */
+       function isHighlighter(value) {
+               return __REDACTOR_CODE_HIGHLIGHTERS.hasOwnProperty(value);
+       }
+       
+       /**
+        * @module      WoltLab/WCF/BBCode/ToHtml
+        */
+       var BBCodeToHtml = {
+               /**
+                * Converts a message containing BBCodes to HTML.
+                * 
+                * @param       {string}        message         message containing BBCodes
+                * @return      {string}        HTML message
+                */
+               convert: function(message, options) {
+                       _options = Core.extend({
+                               attachments: {
+                                       images: {},
+                                       thumbnailUrl: '',
+                                       url: ''
+                               }
+                       }, options);
+                       
+                       this._convertSpecials(message);
+                       
+                       var stack = BBCodeParser.parse(message);
+                       
+                       if (stack.length) {
+                               this._initBBCodes();
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'beforeConvert', { stack: stack });
+                       
+                       var item, value;
+                       for (var i = 0, length = stack.length; i < length; i++) {
+                               item = stack[i];
+                               
+                               if (typeof item === 'object') {
+                                       value = this._convert(stack, item, i);
+                                       if (Array.isArray(value)) {
+                                               stack[i] = (value[0] === null ? item.source : value[0]);
+                                               stack[item.pair] = (value[1] === null ? stack[item.pair].source : value[1]);
+                                       }
+                                       else {
+                                               stack[i] = value;
+                                       }
+                               }
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'afterConvert', { stack: stack });
+                       
+                       message = stack.join('');
+                       
+                       message = message.replace(/\n/g, '<br>');
+                       
+                       return message;
+               },
+               
+               /**
+                * Converts special characters to their entities.
+                * 
+                * @param       {string}        message         message containing BBCodes
+                * @return      {string}        message with replaced special characters
+                */
+               _convertSpecials: function(message) {
+                       message = message.replace(/&/g, '&amp;');
+                       message = message.replace(/</g, '&lt;');
+                       message = message.replace(/>/g, '&gt;');
+                       
+                       return message;
+               },
+               
+               /**
+                * Sets up converters applied to HTML elements.
+                */
+               _initBBCodes: function() {
+                       if (_bbcodes !== null) {
+                               return;
+                       }
+                       
+                       _bbcodes = {
+                               // simple replacements
+                               b: 'strong',
+                               i: 'em',
+                               u: 'u',
+                               s: 'del',
+                               sub: 'sub',
+                               sup: 'sup',
+                               table: 'table',
+                               td: 'td',
+                               tr: 'tr',
+                               tt: 'kbd',
+                               
+                               // callback replacement
+                               align: this._convertAlignment.bind(this),
+                               attach: this._convertAttachment.bind(this),
+                               color: this._convertColor.bind(this),
+                               code: this._convertCode.bind(this),
+                               email: this._convertEmail.bind(this),
+                               list: this._convertList.bind(this),
+                               quote: this._convertQuote.bind(this),
+                               size: this._convertSize.bind(this),
+                               url: this._convertUrl.bind(this),
+                               img: this._convertImage.bind(this)
+                       };
+                       
+                       _removeNewlineAfter = ['quote', 'table', 'td', 'tr'];
+                       _removeNewlineBefore = ['table', 'td', 'tr'];
+                       
+                       EventHandler.fire('com.woltlab.wcf.bbcode.toHtml', 'init', {
+                               bbcodes: _bbcodes,
+                               removeNewlineAfter: _removeNewlineAfter,
+                               removeNewlineBefore: _removeNewlineBefore
+                       });
+               },
+               
+               /**
+                * Converts an item from the stack.
+                * 
+                * @param       {array<mixed>}          stack           linear list of BBCode tags and regular strings
+                * @param       {object}                item            current BBCode tag object
+                * @param       {integer}               index           current stack index representing `item`
+                * @return      {(string|array)}        string if only the current item should be replaced or an array with
+                *                                      the first item used for the opening tag and the second item for the closing tag
+                */
+               _convert: function(stack, item, index) {
+                       var replace = _bbcodes[item.name], tmp;
+                       
+                       if (replace === undefined) {
+                               // treat as plain text
+                               return [null, null];
+                       }
+                       
+                       if (_removeNewlineAfter.indexOf(item.name) !== -1) {
+                               tmp = stack[index + 1];
+                               if (typeof tmp === 'string') {
+                                       stack[index + 1] = tmp.replace(/^\n/, '');
+                               }
+                               
+                               if (stack.length > item.pair + 1) {
+                                       tmp = stack[item.pair + 1];
+                                       if (typeof tmp === 'string') {
+                                               stack[item.pair + 1] = tmp.replace(/^\n/, '');
+                                       }
+                               }
+                       }
+                       
+                       if (_removeNewlineBefore.indexOf(item.name) !== -1) {
+                               if (index - 1 >= 0) {
+                                       tmp = stack[index - 1];
+                                       if (typeof tmp === 'string') {
+                                               stack[index - 1] = tmp.replace(/\n$/, '');
+                                       }
+                               }
+                               
+                               tmp = stack[item.pair - 1];
+                               if (typeof tmp === 'string') {
+                                       stack[item.pair - 1] = tmp.replace(/\n$/, '');
+                               }
+                       }
+                       
+                       // replace smilies
+                       this._convertSmilies(stack);
+                       
+                       if (typeof replace === 'string') {
+                               return ['<' + replace + '>', '</' + replace + '>'];
+                       }
+                       else {
+                               return replace(stack, item, index);
+                       }
+               },
+               
+               /**
+                * Converts [align] into <div style="text-align: ...">.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertAlignment: function(stack, item, index) {
+                       var align = (item.attributes.length) ? item.attributes[0] : '';
+                       if (['center', 'justify', 'left', 'right'].indexOf(align) === -1) {
+                               return [null, null];
+                       }
+                       
+                       return ['<div style="text-align: ' + align + '">', '</div>'];
+               },
+               
+               /**
+                * Converts [attach] into an <img> or to plain text if attachment is a non-image.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertAttachment: function(stack, item, index) {
+                       var attachmentId = 0, attributes = item.attributes, length = attributes.length;
+                       if (!_options.attachments.url) {
+                               length = 0;
+                       }
+                       else if (length > 0) {
+                               attachmentId = ~~attributes[0];
+                               if (!_options.attachments.images.hasOwnProperty(attachmentId)) {
+                                       length = 0;
+                               }
+                       }
+                       
+                       if (length === 0) {
+                               return [null, null];
+                       }
+                       
+                       var maxHeight = ~~_options.attachments.images[attachmentId].height;
+                       var maxWidth = ~~_options.attachments.images[attachmentId].width;
+                       var styles = ['max-height: ' + maxHeight + 'px', 'max-width: ' + maxWidth + 'px'];
+                       
+                       if (length > 1) {
+                               if (item.attributes[1] === 'left' || attributes[1] === 'right') {
+                                       styles.push('float: ' + attributes[1]);
+                                       styles.push('margin: ' + (attributes[1] === 'left' ? '0 15px 7px 0' : '0 0 7px 15px'));
+                               }
+                       }
+                       
+                       var baseUrl = _options.attachments.thumbnailUrl;
+                       if (length > 2) {
+                               width = ~~attributes[2] || 0;
+                               if (width) {
+                                       if (width > maxWidth) width = maxWidth;
+                                       
+                                       styles.push('width: ' + width + 'px');
+                                       baseUrl = _options.attachments.url;
+                               }
+                       }
+                       
+                       return [
+                               '<img src="' + baseUrl.replace(/987654321/, attachmentId) + '" class="redactorEmbeddedAttachment redactorDisableResize" data-attachment-id="' + attachmentId + '"' + (styles.length ? ' style="' + styles.join(';') + '"' : '') + '>',
+                               ''
+                       ];
+               },
+               
+               /**
+                * Converts [code] to <div class="codeBox">.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertCode: function(stack, item, index) {
+                       var attributes = item.attributes, filename = '', highlighter = 'auto', lineNumber = 0;
+                       
+                       // parse arguments
+                       switch (attributes.length) {
+                               case 1:
+                                       if (isNumber(attributes[0])) {
+                                               lineNumber = ~~attributes[0];
+                                       }
+                                       else if (isFilename(attributes[0])) {
+                                               filename = attributes[0];
+                                       }
+                                       else if (isHighlighter(attributes[0])) {
+                                               highlighter = attributes[0];
+                                       }
+                                       break;
+                               case 2:
+                                       if (isNumber(attributes[0])) {
+                                               lineNumber = ~~attributes[0];
+                                               
+                                               if (isHighlighter(attributes[1])) {
+                                                       highlighter = attributes[1];
+                                               }
+                                               else if (isFilename(attributes[1])) {
+                                                       filename = attributes[1];
+                                               }
+                                       }
+                                       else {
+                                               if (isHighlighter(attributes[0])) highlighter = attributes[0];
+                                               if (isFilename(attributes[1])) filename = attributes[1];
+                                       }
+                                       break;
+                               case 3:
+                                       if (isHighlighter(attributes[0])) highlighter = attributes[0];
+                                       if (isNumber(attributes[1])) lineNumber = ~~attributes[1];
+                                       if (isFilename(attributes[2])) filename = attributes[2];
+                                       break;
+                       }
+                       
+                       // transform content
+                       var before = true, content, line, empty = -1;
+                       for (var i = index + 1; i < item.pair; i++) {
+                               line = stack[i];
+                               
+                               if (line.trim() === '') {
+                                       if (before) {
+                                               stack[i] = '';
+                                               continue;
+                                       }
+                                       else if (empty === -1) {
+                                               empty = i;
+                                       }
+                               }
+                               else {
+                                       before = false;
+                                       empty = -1;
+                               }
+                               
+                               content = line.split('\n');
+                               for (var j = 0, innerLength = content.length; j < innerLength; j++) {
+                                       content[j] = '<li>' + (content[j] ? StringUtil.escapeHTML(content[j]) : '\u200b') + '</li>';
+                               }
+                               
+                               stack[i] = content.join('');
+                       }
+                       
+                       if (!before && empty !== -1) {
+                               for (var i = item.pair - 1; i >= empty; i--) {
+                                       stack[i] = '';
+                               }
+                       }
+                       
+                       return [
+                               '<div class="codeBox container" contenteditable="false" data-highlighter="' + highlighter + '" data-filename="' + (filename ? StringUtil.escapeHTML(filename) : '') + '">'
+                                       + '<div>'
+                                       + '<div>'
+                                               + '<h3>' + __REDACTOR_CODE_HIGHLIGHTERS[highlighter] + (filename ? ': ' + StringUtil.escapeHTML(filename) : '') + '</h3>'
+                                       + '</div>'
+                                       + '<ol start="' + (lineNumber > 1 ? lineNumber : 1) + '">',
+                               '</ol></div></div>'
+                       ];
+               },
+               
+               /**
+                * Converts [color] to <span style="color: ...">.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertColor: function(stack, item, index) {
+                       if (!item.attributes.length || !item.attributes[0].match(/^[a-z0-9#]+$/i)) {
+                               return [null, null];
+                       }
+                       
+                       return ['<span style="color: ' + StringUtil.escapeHTML(item.attributes[0]) + '">', '</span>'];
+               },
+               
+               /**
+                * Converts [email] to <a href="mailto: ...">.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertEmail: function(stack, item, index) {
+                       var email = '';
+                       if (item.attributes.length) {
+                               email = item.attributes[0];
+                       }
+                       else {
+                               var element;
+                               for (var i = index + 1; i < item.pair; i++) {
+                                       element = stack[i];
+                                       
+                                       if (typeof element === 'object') {
+                                               email = '';
+                                               break;
+                                       }
+                                       else {
+                                               email += element;
+                                       }
+                               }
+                               
+                               // no attribute present and element is empty, handle as plain text
+                               if (email.trim() === '') {
+                                       return [null, null];
+                               }
+                       }
+                       
+                       return ['<a href="mailto:' + StringUtil.escapeHTML(email) + '">', '</a>'];
+               },
+               
+               /**
+                * Converts [img] to <img>.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertImage: function(stack, item, index) {
+                       var float = 'none', source = '', width = 0;
+                       
+                       switch (item.attributes.length) {
+                               case 0:
+                                       if (index + 1 < item.pair && typeof stack[index + 1] === 'string') {
+                                               source = stack[index + 1];
+                                               stack[index + 1] = '';
+                                       }
+                                       else {
+                                               // [img] without attributes and content, discard
+                                               return '';
+                                       }
+                               break;
+                               
+                               case 1:
+                                       source = item.attributes[0];
+                               break;
+                               
+                               case 2:
+                                       source = item.attributes[0];
+                                       float = item.attributes[1];
+                               break;
+                               
+                               case 3:
+                                       source = item.attributes[0];
+                                       float = item.attributes[1];
+                                       width = ~~item.attributes[2];
+                               break;
+                       }
+                       
+                       if (float !== 'left' && float !== 'right') float = 'none';
+                       
+                       var styles = [];
+                       if (width > 0) {
+                               styles.push('width: ' + width + 'px');
+                       }
+                       
+                       if (float !== 'none') {
+                               styles.push('float: ' + float);
+                               styles.push('margin: ' + (float === 'left' ? '0 15px 7px 0' : '0 0 7px 15px'));
+                       }
+                       
+                       return ['<img src="' + StringUtil.escapeHTML(source) + '"' + (styles.length ? ' style="' + styles.join(';') + '"' : '') + '>', ''];
+               },
+               
+               /**
+                * Converts [list] to <ol> or <ul>.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertList: function(stack, item, index) {
+                       var type = (items.attributes.length) ? item.attributes[0] : '';
+                       
+                       // replace list items
+                       for (var i = index + 1; i < item.pair; i++) {
+                               if (typeof stack[i] === 'string') {
+                                       stack[i] = stack[i].replace(/\[\*\]/g, '<li>');
+                               }
+                       }
+                       
+                       if (type == '1' || type === 'decimal') {
+                               return ['<ol>', '</ol>'];
+                       }
+                       
+                       if (type.length && type.match(/^(?:none|circle|square|disc|decimal|lower-roman|upper-roman|decimal-leading-zero|lower-greek|lower-latin|upper-latin|armenian|georgian)$/)) {
+                               return ['<ul style="list-style-type: ' + type + '">', '</ul>'];
+                       }
+                       
+                       return ['<ul>', '</ul>'];
+               },
+               
+               /**
+                * Converts [quote] to <blockquote>.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertQuote: function(stack, item, index) {
+                       var author = '', link = '';
+                       if (item.attributes.length > 1) {
+                               author = item.attributes[0];
+                               link = item.attributes[1];
+                       }
+                       else if (item.attributes.length === 1) {
+                               author = item.attributes[0];
+                       }
+                       
+                       // get rid of the trailing newline for quote content
+                       for (var i = item.pair - 1; i > index; i--) {
+                               if (typeof stack[i] === 'string') {
+                                       stack[i] = stack[i].replace(/\n$/, '');
+                                       break;
+                               }
+                       }
+                       
+                       var header = '';
+                       if (author) {
+                               if (link) header = '<a href="' + StringUtil.escapeHTML(link) + '" tabindex="-1">';
+                               header += Language.get('wcf.bbcode.quote.title.javascript', { quoteAuthor: author.replace(/\\'/g, "'") });
+                               if (link) header += '</a>';
+                       }
+                       else {
+                               header = '<small>' + Language.get('wcf.bbcode.quote.title.clickToSet') + '</small>';
+                       }
+                       
+                       return [
+                               '<blockquote class="quoteBox container containerPadding quoteBoxSimple" cite="' + StringUtil.escapeHTML(link) + '" data-author="' + StringUtil.escapeHTML(author) + '">'
+                                       + '<header contenteditable="false">'
+                                               + '<h3>'
+                                                       + header
+                                               + '</h3>'
+                                               + '<a class="redactorQuoteEdit"></a>'
+                                       + '</header>'
+                                       + '<div>\u200b',
+                               '</div></blockquote>'
+                       ];
+               },
+               
+               /**
+                * Converts smiley codes into <img>.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                */
+               _convertSmilies: function(stack) {
+                       var altValue, item, regexp;
+                       for (var i = 0, length = stack.length; i < length; i++) {
+                               item = stack[i];
+                               
+                               if (typeof item === 'string') {
+                                       for (var smileyCode in __REDACTOR_SMILIES) {
+                                               if (__REDACTOR_SMILIES.hasOwnProperty(smileyCode)) {
+                                                       altValue = smileyCode.replace(/</g, '&lt;').replace(/>/g, '&gt;');
+                                                       regexp = new RegExp('(\\s|^)' + StringUtil.escapeRegExp(smileyCode) + '(?=\\s|$)', 'gi');
+                                                       item = item.replace(regexp, '$1<img src="' + __REDACTOR_SMILIES[smileyCode] + '" class="smiley" alt="' + altValue + '">');
+                                               }
+                                       }
+                                       
+                                       stack[i] = item;
+                               }
+                               else if (__REDACTOR_SOURCE_BBCODES.indexOf(item.name) !== -1) {
+                                       // skip processing content
+                                       i = item.pair;
+                               }
+                       }
+               },
+               
+               /**
+                * Converts [size] to <span style="font-size: ...">.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertSize: function(stack, item, index) {
+                       if (!item.attributes.length || ~~item.attributes[0] === 0) {
+                               return [null, null];
+                       }
+                       
+                       return ['<span style="font-size: ' + ~~item.attributes[0] + 'pt">', '</span>'];
+               },
+               
+               /**
+                * Converts [url] to <a>.
+                * 
+                * @param       {array<mixed>}  stack   linear list of BBCode tags and regular strings
+                * @param       {object}        item    current BBCode tag object
+                * @param       {integer}       index   current stack index representing `item`
+                * @returns     {array}         first item represents the opening tag, the second the closing one
+                */
+               _convertUrl: function(stack, item, index) {
+                       // ignore url bbcode without arguments
+                       if (!item.attributes.length) {
+                               return [null, null];
+                       }
+                       
+                       return ['<a href="' + StringUtil.escapeHTML(item.attributes[0]) + '">', '</a>'];
+               }
+       };
+       
+       return BBCodeToHtml;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Change/Listener.js b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Change/Listener.js
deleted file mode 100644 (file)
index e295c7d..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Allows to be informed when the DOM may have changed and
- * new elements that are relevant to you may have been added.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/DOM/Change/Listener
- */
-define(['CallbackList'], function(CallbackList) {
-       "use strict";
-       
-       var _callbackList = new CallbackList();
-       var _hot = false;
-       
-       /**
-        * @exports     WoltLab/WCF/DOM/Change/Listener
-        */
-       var Listener = {
-               /**
-                * @see WoltLab/WCF/CallbackList#add
-                */
-               add: _callbackList.add.bind(_callbackList),
-               
-               /**
-                * @see WoltLab/WCF/CallbackList#remove
-                */
-               remove: _callbackList.remove.bind(_callbackList),
-               
-               /**
-                * Triggers the execution of all the listeners.
-                * Use this function when you added new elements to the DOM that might
-                * be relevant to others.
-                * While this function is in progress further calls to it will be ignored.
-                */
-               trigger: function() {
-                       if (_hot) return;
-                       
-                       try {
-                               _hot = true;
-                               _callbackList.forEach(null, function(callback) {
-                                       callback();
-                               });
-                       }
-                       finally {
-                               _hot = false;
-                       }
-               }
-       };
-       
-       return Listener;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Traverse.js b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Traverse.js
deleted file mode 100644 (file)
index 9608b66..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-/**
- * Provides helper functions to traverse the DOM.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/DOM/Traverse
- */
-define(['DOM/Util'], function(DOMUtil) {
-       "use strict";
-       
-       /** @const */ var NONE = 0;
-       /** @const */ var SELECTOR = 1;
-       /** @const */ var CLASS_NAME = 2;
-       /** @const */ var TAG_NAME = 3;
-       
-       var _probe = [
-               function(el, none) { return true; },
-               function(el, selector) { return DOMUtil.matches(el, selector); },
-               function(el, className) { return el.classList.contains(className); },
-               function(el, tagName) { return el.nodeName === tagName; }
-       ];
-       
-       var _children = function(el, type, value) {
-               if (!(el instanceof Element)) {
-                       throw new TypeError("Expected a valid element as first argument.");
-               }
-               
-               var children = [];
-               
-               for (var i = 0; i < el.childElementCount; i++) {
-                       if (_probe[type](el.children[i], value)) {
-                               children.push(el.children[i]);
-                       }
-               }
-               
-               return children;
-       };
-       
-       var _parent = function(el, type, value, untilElement) {
-               if (!(el instanceof Element)) {
-                       throw new TypeError("Expected a valid element as first argument.");
-               }
-               
-               el = el.parentNode;
-               
-               while (el instanceof Element) {
-                       if (el === untilElement) {
-                               return null;
-                       }
-                       
-                       if (_probe[type](el, value)) {
-                               return el;
-                       }
-                       
-                       el = el.parentNode;
-               }
-               
-               return null;
-       };
-       
-       var _sibling = function(el, siblingType, type, value) {
-               if (!(el instanceof Element)) {
-                       throw new TypeError("Expected a valid element as first argument.");
-               }
-               
-               if (el instanceof Element) {
-                       if (el[siblingType] !== null && _probe[type](el[siblingType], value)) {
-                               return el[siblingType];
-                       }
-               }
-               
-               return null;
-       };
-       
-       /**
-        * @exports     WoltLab/WCF/DOM/Traverse
-        */
-       var DOMTraverse = {
-               /**
-                * Examines child elements and returns the first child matching the given selector.
-                * 
-                * @param       {Element}               el              element
-                * @param       {string}                selector        CSS selector to match child elements against
-                * @return      {(Element|null)}        null if there is no child node matching the selector
-                */
-               childBySel: function(el, selector) {
-                       return _children(el, SELECTOR, selector)[0] || null;
-               },
-               
-               /**
-                * Examines child elements and returns the first child that has the given CSS class set.
-                * 
-                * @param       {Element}               el              element
-                * @param       {string}                className       CSS class name
-                * @return      {(Element|null)}        null if there is no child node with given CSS class
-                */
-               childByClass: function(el, className) {
-                       return _children(el, CLASS_NAME, className)[0] || null;
-               },
-               
-               /**
-                * Examines child elements and returns the first child which equals the given tag.
-                * 
-                * @param       {Element}               el              element
-                * @param       {string}                tagName         element tag name
-                * @return      {(Element|null)}        null if there is no child node which equals given tag
-                */
-               childByTag: function(el, tagName) {
-                       return _children(el, TAG_NAME, tagName)[0] || null;
-               },
-               
-               /**
-                * Examines child elements and returns all children matching the given selector.
-                * 
-                * @param       {Element}               el              element
-                * @param       {string}                selector        CSS selector to match child elements against
-                * @return      {array<Element>}        list of children matching the selector
-                */
-               childrenBySel: function(el, selector) {
-                       return _children(el, SELECTOR, selector);
-               },
-               
-               /**
-                * Examines child elements and returns all children that have the given CSS class set.
-                * 
-                * @param       {Element}               el              element
-                * @param       {string}                className       CSS class name
-                * @return      {array<Element>}        list of children with the given class
-                */
-               childrenByClass: function(el, className) {
-                       return _children(el, CLASS_NAME, className);
-               },
-               
-               /**
-                * Examines child elements and returns all children which equal the given tag.
-                * 
-                * @param       {Element}               el              element
-                * @param       {string}                tagName         element tag name
-                * @return      {array<Element>}        list of children equaling the tag name
-                */
-               childrenByTag: function(el, tagName) {
-                       return _children(el, TAG_NAME, tagName);
-               },
-               
-               /**
-                * Examines parent nodes and returns the first parent that matches the given selector.
-                * 
-                * @param       {Element}       el              child element
-                * @param       {string}        selector        CSS selector to match parent nodes against
-                * @param       {Element=}      untilElement    stop when reaching this element
-                * @return      {(Element|null)}        null if no parent node matched the selector
-                */
-               parentBySel: function(el, selector, untilElement) {
-                       return _parent(el, SELECTOR, selector, untilElement);
-               },
-               
-               /**
-                * Examines parent nodes and returns the first parent that has the given CSS class set.
-                * 
-                * @param       {Element}       el              child element
-                * @param       {string}        className       CSS class name
-                * @param       {Element=}      untilElement    stop when reaching this element
-                * @return      {(Element|null)}        null if there is no parent node with given class
-                */
-               parentByClass: function(el, className, untilElement) {
-                       return _parent(el, CLASS_NAME, className, untilElement);
-               },
-               
-               /**
-                * Examines parent nodes and returns the first parent which equals the given tag.
-                * 
-                * @param       {Element}       el              child element
-                * @param       {string}        tagName         element tag name
-                * @param       {Element=}      untilElement    stop when reaching this element
-                * @return      {(Element|null)}        null if there is no parent node of given tag type
-                */
-               parentByTag: function(el, tagName, untilElement) {
-                       return _parent(el, TAG_NAME, tagName, untilElement);
-               },
-               
-               /**
-                * Returns the next element sibling.
-                * 
-                * @param       {Element}       el              element
-                * @return      {(Element|null)}        null if there is no next sibling element
-                */
-               next: function(el) {
-                       return _sibling(el, 'nextElementSibling', NONE, null);
-               },
-               
-               /**
-                * Returns the next element sibling that matches the given selector.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        selector        CSS selector to match parent nodes against
-                * @return      {(Element|null)}        null if there is no next sibling element or it does not match the selector
-                */
-               nextBySel: function(el, selector) {
-                       return _sibling(el, 'nextElementSibling', SELECTOR, selector);
-               },
-               
-               /**
-                * Returns the next element sibling with given CSS class.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        className       CSS class name
-                * @return      {(Element|null)}        null if there is no next sibling element or it does not have the class set
-                */
-               nextByClass: function(el, className) {
-                       return _sibling(el, 'nextElementSibling', CLASS_NAME, className);
-               },
-               
-               /**
-                * Returns the next element sibling with given CSS class.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        className       CSS class name
-                * @return      {(Element|null)}        null if there is no next sibling element or it does not have the class set
-                */
-               nextByTag: function(el, tagName) {
-                       return _sibling(el, 'nextElementSibling', CLASS_NAME, className);
-               },
-               
-               /**
-                * Returns the previous element sibling.
-                * 
-                * @param       {Element}       el              element
-                * @return      {(Element|null)}        null if there is no previous sibling element
-                */
-               prev: function(el) {
-                       return _sibling(el, 'previousElementSibling', NONE, null);
-               },
-               
-               /**
-                * Returns the previous element sibling that matches the given selector.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        selector        CSS selector to match parent nodes against
-                * @return      {(Element|null)}        null if there is no previous sibling element or it does not match the selector
-                */
-               prevBySel: function(el, selector) {
-                       return _sibling(el, 'previousElementSibling', SELECTOR, selector);
-               },
-               
-               /**
-                * Returns the previous element sibling with given CSS class.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        className       CSS class name
-                * @return      {(Element|null)}        null if there is no previous sibling element or it does not have the class set
-                */
-               prevByClass: function(el, className) {
-                       return _sibling(el, 'previousElementSibling', CLASS_NAME, className);
-               },
-               
-               /**
-                * Returns the previous element sibling with given CSS class.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        className       CSS class name
-                * @return      {(Element|null)}        null if there is no previous sibling element or it does not have the class set
-                */
-               prevByTag: function(el, tagName) {
-                       return _sibling(el, 'previousElementSibling', CLASS_NAME, className);
-               }
-       };
-       
-       return DOMTraverse;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js
deleted file mode 100644 (file)
index 91680c1..0000000
+++ /dev/null
@@ -1,223 +0,0 @@
-/**
- * Provides helper functions to work with DOM nodes.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/DOM/Util
- */
-define([], function() {
-       "use strict";
-       
-       var _matchesSelectorFunction = '';
-       var _possibleFunctions = ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector'];
-       for (var i = 0; i < 4; i++) {
-               if (Element.prototype.hasOwnProperty(_possibleFunctions[i])) {
-                       _matchesSelectorFunction = _possibleFunctions[i];
-                       break;
-               }
-       }
-       
-       var _idCounter = 0;
-       
-       /**
-        * @exports     WoltLab/WCF/DOM/Util
-        */
-       var DOMUtil = {
-               /**
-                * Returns a DocumentFragment containing the provided HTML string as DOM nodes.
-                * 
-                * @param       {string}        html    HTML string
-                * @return      {DocumentFragment}      fragment containing DOM nodes
-                */
-               createFragmentFromHtml: function(html) {
-                       var tmp = document.createElement('div');
-                       tmp.innerHTML = html;
-                       
-                       var fragment = document.createDocumentFragment();
-                       while (tmp.childNodes.length) {
-                               fragment.appendChild(tmp.childNodes[0]);
-                       }
-                       
-                       return fragment;
-               },
-               
-               /**
-                * Returns a unique element id.
-                * 
-                * @return      {string}        unique id
-                */
-               getUniqueId: function() {
-                       var elementId;
-                       
-                       do {
-                               elementId = 'wcf' + _idCounter++;
-                       }
-                       while (document.getElementById(elementId) !== null);
-                       
-                       return elementId;
-               },
-               
-               /**
-                * Returns the element's id. If there is no id set, a unique id will be
-                * created and assigned.
-                * 
-                * @param       {Element}       el      element
-                * @return      {string}        element id
-                */
-               identify: function(el) {
-                       if (!el || !(el instanceof Element)) {
-                               return null;
-                       }
-                       
-                       var id = el.getAttribute('id');
-                       if (!id) {
-                               id = this.getUniqueId();
-                               el.setAttribute('id', id);
-                       }
-                       
-                       return id;
-               },
-               
-               /**
-                * Returns true if element matches given CSS selector.
-                * 
-                * @param       {Element}       el              element
-                * @param       {string}        selector        CSS selector
-                * @return      {boolean}       true if element matches selector
-                */
-               matches: function(el, selector) {
-                       return el[_matchesSelectorFunction](selector);
-               },
-               
-               /**
-                * Returns the outer height of an element including margins.
-                * 
-                * @param       {Element}               el              element
-                * @param       {CSSStyleDeclaration=}  styles          result of window.getComputedStyle()
-                * @return      {integer}       outer height in px
-                */
-               outerHeight: function(el, styles) {
-                       styles = styles || window.getComputedStyle(el);
-                       
-                       var height = el.offsetHeight;
-                       height += ~~styles.marginTop + ~~styles.marginBottom;
-                       
-                       return height;
-               },
-               
-               /**
-                * Returns the outer width of an element including margins.
-                * 
-                * @param       {Element}               el              element
-                * @param       {CSSStyleDeclaration=}  styles          result of window.getComputedStyle()
-                * @return      {integer}       outer width in px
-                */
-               outerWidth: function(el, styles) {
-                       styles = styles || window.getComputedStyle(el);
-                       
-                       var width = el.offsetWidth;
-                       width += ~~styles.marginLeft + ~~styles.marginRight;
-                       
-                       return width;
-               },
-               
-               /**
-                * Returns the outer dimensions of an element including margins.
-                * 
-                * @param       {Element}               el              element
-                * @return      {{height: integer, width: integer}}     dimensions in px
-                */
-               outerDimensions: function(el) {
-                       var styles = window.getComputedStyle(el);
-                       
-                       return {
-                               height: this.outerHeight(el, styles),
-                               width: this.outerWidth(el, styles)
-                       };
-               },
-               
-               /**
-                * Returns the element's offset relative to the document's top left corner.
-                * 
-                * @param       {Element}       el      element
-                * @return      {{left: integer, top: integer}}         offset relative to top left corner
-                */
-               offset: function(el) {
-                       var rect = el.getBoundingClientRect();
-                       
-                       return {
-                               top: rect.top + document.body.scrollTop,
-                               left: rect.left + document.body.scrollLeft
-                       };
-               },
-               
-               /**
-                * 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]);
-                       }
-               },
-               
-               /**
-                * Inserts an element after an existing element.
-                * 
-                * @param       {Element}       newEl           element to insert
-                * @param       {Element}       el              reference element
-                */
-               insertAfter: function(newEl, el) {
-                       if (el.nextElementSibling !== null) {
-                               el.parentNode.insertBefore(newEl, el.nextElementSibling);
-                       }
-                       else {
-                               el.parentNode.appendChild(newEl);
-                       }
-               },
-               
-               /**
-                * Applies a list of CSS properties to an element.
-                * 
-                * @param       {Element}               el      element
-                * @param       {Object<string, mixed>} styles  list of CSS styles
-                */
-               setStyles: function(el, styles) {
-                       for (var property in styles) {
-                               if (styles.hasOwnProperty(property)) {
-                                       el.style.setProperty(property, styles[property]);
-                               }
-                       }
-               },
-               
-               /**
-                * Returns a style property value as integer.
-                * 
-                * The behavior of this method is undefined for properties that are not considered
-                * to have a "numeric" value, e.g. "background-image".
-                * 
-                * @param       {CSSStyleDeclaration}   styles          result of window.getComputedStyle()
-                * @param       {string}                propertyName    property name
-                * @return      {integer}       property value as integer
-                */
-               styleAsInt: function(styles, propertyName) {
-                       var value = styles.getPropertyValue(propertyName);
-                       if (value === null) {
-                               return 0;
-                       }
-                       
-                       return parseInt(value);
-               }
-       };
-       
-       // expose on window object for backward compatibility
-       window.bc_wcfDOMUtil = DOMUtil;
-       
-       return DOMUtil;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Dom/Change/Listener.js b/wcfsetup/install/files/js/WoltLab/WCF/Dom/Change/Listener.js
new file mode 100644 (file)
index 0000000..e295c7d
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Allows to be informed when the DOM may have changed and
+ * new elements that are relevant to you may have been added.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/DOM/Change/Listener
+ */
+define(['CallbackList'], function(CallbackList) {
+       "use strict";
+       
+       var _callbackList = new CallbackList();
+       var _hot = false;
+       
+       /**
+        * @exports     WoltLab/WCF/DOM/Change/Listener
+        */
+       var Listener = {
+               /**
+                * @see WoltLab/WCF/CallbackList#add
+                */
+               add: _callbackList.add.bind(_callbackList),
+               
+               /**
+                * @see WoltLab/WCF/CallbackList#remove
+                */
+               remove: _callbackList.remove.bind(_callbackList),
+               
+               /**
+                * Triggers the execution of all the listeners.
+                * Use this function when you added new elements to the DOM that might
+                * be relevant to others.
+                * While this function is in progress further calls to it will be ignored.
+                */
+               trigger: function() {
+                       if (_hot) return;
+                       
+                       try {
+                               _hot = true;
+                               _callbackList.forEach(null, function(callback) {
+                                       callback();
+                               });
+                       }
+                       finally {
+                               _hot = false;
+                       }
+               }
+       };
+       
+       return Listener;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Dom/Traverse.js b/wcfsetup/install/files/js/WoltLab/WCF/Dom/Traverse.js
new file mode 100644 (file)
index 0000000..9608b66
--- /dev/null
@@ -0,0 +1,270 @@
+/**
+ * Provides helper functions to traverse the DOM.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/DOM/Traverse
+ */
+define(['DOM/Util'], function(DOMUtil) {
+       "use strict";
+       
+       /** @const */ var NONE = 0;
+       /** @const */ var SELECTOR = 1;
+       /** @const */ var CLASS_NAME = 2;
+       /** @const */ var TAG_NAME = 3;
+       
+       var _probe = [
+               function(el, none) { return true; },
+               function(el, selector) { return DOMUtil.matches(el, selector); },
+               function(el, className) { return el.classList.contains(className); },
+               function(el, tagName) { return el.nodeName === tagName; }
+       ];
+       
+       var _children = function(el, type, value) {
+               if (!(el instanceof Element)) {
+                       throw new TypeError("Expected a valid element as first argument.");
+               }
+               
+               var children = [];
+               
+               for (var i = 0; i < el.childElementCount; i++) {
+                       if (_probe[type](el.children[i], value)) {
+                               children.push(el.children[i]);
+                       }
+               }
+               
+               return children;
+       };
+       
+       var _parent = function(el, type, value, untilElement) {
+               if (!(el instanceof Element)) {
+                       throw new TypeError("Expected a valid element as first argument.");
+               }
+               
+               el = el.parentNode;
+               
+               while (el instanceof Element) {
+                       if (el === untilElement) {
+                               return null;
+                       }
+                       
+                       if (_probe[type](el, value)) {
+                               return el;
+                       }
+                       
+                       el = el.parentNode;
+               }
+               
+               return null;
+       };
+       
+       var _sibling = function(el, siblingType, type, value) {
+               if (!(el instanceof Element)) {
+                       throw new TypeError("Expected a valid element as first argument.");
+               }
+               
+               if (el instanceof Element) {
+                       if (el[siblingType] !== null && _probe[type](el[siblingType], value)) {
+                               return el[siblingType];
+                       }
+               }
+               
+               return null;
+       };
+       
+       /**
+        * @exports     WoltLab/WCF/DOM/Traverse
+        */
+       var DOMTraverse = {
+               /**
+                * Examines child elements and returns the first child matching the given selector.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                selector        CSS selector to match child elements against
+                * @return      {(Element|null)}        null if there is no child node matching the selector
+                */
+               childBySel: function(el, selector) {
+                       return _children(el, SELECTOR, selector)[0] || null;
+               },
+               
+               /**
+                * Examines child elements and returns the first child that has the given CSS class set.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                className       CSS class name
+                * @return      {(Element|null)}        null if there is no child node with given CSS class
+                */
+               childByClass: function(el, className) {
+                       return _children(el, CLASS_NAME, className)[0] || null;
+               },
+               
+               /**
+                * Examines child elements and returns the first child which equals the given tag.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                tagName         element tag name
+                * @return      {(Element|null)}        null if there is no child node which equals given tag
+                */
+               childByTag: function(el, tagName) {
+                       return _children(el, TAG_NAME, tagName)[0] || null;
+               },
+               
+               /**
+                * Examines child elements and returns all children matching the given selector.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                selector        CSS selector to match child elements against
+                * @return      {array<Element>}        list of children matching the selector
+                */
+               childrenBySel: function(el, selector) {
+                       return _children(el, SELECTOR, selector);
+               },
+               
+               /**
+                * Examines child elements and returns all children that have the given CSS class set.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                className       CSS class name
+                * @return      {array<Element>}        list of children with the given class
+                */
+               childrenByClass: function(el, className) {
+                       return _children(el, CLASS_NAME, className);
+               },
+               
+               /**
+                * Examines child elements and returns all children which equal the given tag.
+                * 
+                * @param       {Element}               el              element
+                * @param       {string}                tagName         element tag name
+                * @return      {array<Element>}        list of children equaling the tag name
+                */
+               childrenByTag: function(el, tagName) {
+                       return _children(el, TAG_NAME, tagName);
+               },
+               
+               /**
+                * Examines parent nodes and returns the first parent that matches the given selector.
+                * 
+                * @param       {Element}       el              child element
+                * @param       {string}        selector        CSS selector to match parent nodes against
+                * @param       {Element=}      untilElement    stop when reaching this element
+                * @return      {(Element|null)}        null if no parent node matched the selector
+                */
+               parentBySel: function(el, selector, untilElement) {
+                       return _parent(el, SELECTOR, selector, untilElement);
+               },
+               
+               /**
+                * Examines parent nodes and returns the first parent that has the given CSS class set.
+                * 
+                * @param       {Element}       el              child element
+                * @param       {string}        className       CSS class name
+                * @param       {Element=}      untilElement    stop when reaching this element
+                * @return      {(Element|null)}        null if there is no parent node with given class
+                */
+               parentByClass: function(el, className, untilElement) {
+                       return _parent(el, CLASS_NAME, className, untilElement);
+               },
+               
+               /**
+                * Examines parent nodes and returns the first parent which equals the given tag.
+                * 
+                * @param       {Element}       el              child element
+                * @param       {string}        tagName         element tag name
+                * @param       {Element=}      untilElement    stop when reaching this element
+                * @return      {(Element|null)}        null if there is no parent node of given tag type
+                */
+               parentByTag: function(el, tagName, untilElement) {
+                       return _parent(el, TAG_NAME, tagName, untilElement);
+               },
+               
+               /**
+                * Returns the next element sibling.
+                * 
+                * @param       {Element}       el              element
+                * @return      {(Element|null)}        null if there is no next sibling element
+                */
+               next: function(el) {
+                       return _sibling(el, 'nextElementSibling', NONE, null);
+               },
+               
+               /**
+                * Returns the next element sibling that matches the given selector.
+                * 
+                * @param       {Element}       el              element
+                * @param       {string}        selector        CSS selector to match parent nodes against
+                * @return      {(Element|null)}        null if there is no next sibling element or it does not match the selector
+                */
+               nextBySel: function(el, selector) {
+                       return _sibling(el, 'nextElementSibling', SELECTOR, selector);
+               },
+               
+               /**
+                * Returns the next element sibling with given CSS class.
+                * 
+                * @param       {Element}       el              element
+                * @param       {string}        className       CSS class name
+                * @return      {(Element|null)}        null if there is no next sibling element or it does not have the class set
+                */
+               nextByClass: function(el, className) {
+                       return _sibling(el, 'nextElementSibling', CLASS_NAME, className);
+               },
+               
+               /**
+                * Returns the next element sibling with given CSS class.
+                * 
+                * @param       {Element}       el              element
+                * @param       {string}        className       CSS class name
+                * @return      {(Element|null)}        null if there is no next sibling element or it does not have the class set
+                */
+               nextByTag: function(el, tagName) {
+                       return _sibling(el, 'nextElementSibling', CLASS_NAME, className);
+               },
+               
+               /**
+                * Returns the previous element sibling.
+                * 
+                * @param       {Element}       el              element
+                * @return      {(Element|null)}        null if there is no previous sibling element
+                */
+               prev: function(el) {
+                       return _sibling(el, 'previousElementSibling', NONE, null);
+               },
+               
+               /**
+                * Returns the previous element sibling that matches the given selector.
+                * 
+                * @param       {Element}       el              element
+                * @param       {string}        selector        CSS selector to match parent nodes against
+                * @return      {(Element|null)}        null if there is no previous sibling element or it does not match the selector
+                */
+               prevBySel: function(el, selector) {
+                       return _sibling(el, 'previousElementSibling', SELECTOR, selector);
+               },
+               
+               /**
+                * Returns the previous element sibling with given CSS class.
+                * 
+                * @param       {Element}       el              element
+                * @param       {string}        className       CSS class name
+                * @return      {(Element|null)}        null if there is no previous sibling element or it does not have the class set
+                */
+               prevByClass: function(el, className) {
+                       return _sibling(el, 'previousElementSibling', CLASS_NAME, className);
+               },
+               
+               /**
+                * Returns the previous element sibling with given CSS class.
+                * 
+                * @param       {Element}       el              element
+                * @param       {string}        className       CSS class name
+                * @return      {(Element|null)}        null if there is no previous sibling element or it does not have the class set
+                */
+               prevByTag: function(el, tagName) {
+                       return _sibling(el, 'previousElementSibling', CLASS_NAME, className);
+               }
+       };
+       
+       return DOMTraverse;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/Dom/Util.js
new file mode 100644 (file)
index 0000000..91680c1
--- /dev/null
@@ -0,0 +1,223 @@
+/**
+ * Provides helper functions to work with DOM nodes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLab/WCF/DOM/Util
+ */
+define([], function() {
+       "use strict";
+       
+       var _matchesSelectorFunction = '';
+       var _possibleFunctions = ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector'];
+       for (var i = 0; i < 4; i++) {
+               if (Element.prototype.hasOwnProperty(_possibleFunctions[i])) {
+                       _matchesSelectorFunction = _possibleFunctions[i];
+                       break;
+               }
+       }
+       
+       var _idCounter = 0;
+       
+       /**
+        * @exports     WoltLab/WCF/DOM/Util
+        */
+       var DOMUtil = {
+               /**
+                * Returns a DocumentFragment containing the provided HTML string as DOM nodes.
+                * 
+                * @param       {string}        html    HTML string
+                * @return      {DocumentFragment}      fragment containing DOM nodes
+                */
+               createFragmentFromHtml: function(html) {
+                       var tmp = document.createElement('div');
+                       tmp.innerHTML = html;
+                       
+                       var fragment = document.createDocumentFragment();
+                       while (tmp.childNodes.length) {
+                               fragment.appendChild(tmp.childNodes[0]);
+                       }
+                       
+                       return fragment;
+               },
+               
+               /**
+                * Returns a unique element id.
+                * 
+                * @return      {string}        unique id
+                */
+               getUniqueId: function() {
+                       var elementId;
+                       
+                       do {
+                               elementId = 'wcf' + _idCounter++;
+                       }
+                       while (document.getElementById(elementId) !== null);
+                       
+                       return elementId;
+               },
+               
+               /**
+                * Returns the element's id. If there is no id set, a unique id will be
+                * created and assigned.
+                * 
+                * @param       {Element}       el      element
+                * @return      {string}        element id
+                */
+               identify: function(el) {
+                       if (!el || !(el instanceof Element)) {
+                               return null;
+                       }
+                       
+                       var id = el.getAttribute('id');
+                       if (!id) {
+                               id = this.getUniqueId();
+                               el.setAttribute('id', id);
+                       }
+                       
+                       return id;
+               },
+               
+               /**
+                * Returns true if element matches given CSS selector.
+                * 
+                * @param       {Element}       el              element
+                * @param       {string}        selector        CSS selector
+                * @return      {boolean}       true if element matches selector
+                */
+               matches: function(el, selector) {
+                       return el[_matchesSelectorFunction](selector);
+               },
+               
+               /**
+                * Returns the outer height of an element including margins.
+                * 
+                * @param       {Element}               el              element
+                * @param       {CSSStyleDeclaration=}  styles          result of window.getComputedStyle()
+                * @return      {integer}       outer height in px
+                */
+               outerHeight: function(el, styles) {
+                       styles = styles || window.getComputedStyle(el);
+                       
+                       var height = el.offsetHeight;
+                       height += ~~styles.marginTop + ~~styles.marginBottom;
+                       
+                       return height;
+               },
+               
+               /**
+                * Returns the outer width of an element including margins.
+                * 
+                * @param       {Element}               el              element
+                * @param       {CSSStyleDeclaration=}  styles          result of window.getComputedStyle()
+                * @return      {integer}       outer width in px
+                */
+               outerWidth: function(el, styles) {
+                       styles = styles || window.getComputedStyle(el);
+                       
+                       var width = el.offsetWidth;
+                       width += ~~styles.marginLeft + ~~styles.marginRight;
+                       
+                       return width;
+               },
+               
+               /**
+                * Returns the outer dimensions of an element including margins.
+                * 
+                * @param       {Element}               el              element
+                * @return      {{height: integer, width: integer}}     dimensions in px
+                */
+               outerDimensions: function(el) {
+                       var styles = window.getComputedStyle(el);
+                       
+                       return {
+                               height: this.outerHeight(el, styles),
+                               width: this.outerWidth(el, styles)
+                       };
+               },
+               
+               /**
+                * Returns the element's offset relative to the document's top left corner.
+                * 
+                * @param       {Element}       el      element
+                * @return      {{left: integer, top: integer}}         offset relative to top left corner
+                */
+               offset: function(el) {
+                       var rect = el.getBoundingClientRect();
+                       
+                       return {
+                               top: rect.top + document.body.scrollTop,
+                               left: rect.left + document.body.scrollLeft
+                       };
+               },
+               
+               /**
+                * 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]);
+                       }
+               },
+               
+               /**
+                * Inserts an element after an existing element.
+                * 
+                * @param       {Element}       newEl           element to insert
+                * @param       {Element}       el              reference element
+                */
+               insertAfter: function(newEl, el) {
+                       if (el.nextElementSibling !== null) {
+                               el.parentNode.insertBefore(newEl, el.nextElementSibling);
+                       }
+                       else {
+                               el.parentNode.appendChild(newEl);
+                       }
+               },
+               
+               /**
+                * Applies a list of CSS properties to an element.
+                * 
+                * @param       {Element}               el      element
+                * @param       {Object<string, mixed>} styles  list of CSS styles
+                */
+               setStyles: function(el, styles) {
+                       for (var property in styles) {
+                               if (styles.hasOwnProperty(property)) {
+                                       el.style.setProperty(property, styles[property]);
+                               }
+                       }
+               },
+               
+               /**
+                * Returns a style property value as integer.
+                * 
+                * The behavior of this method is undefined for properties that are not considered
+                * to have a "numeric" value, e.g. "background-image".
+                * 
+                * @param       {CSSStyleDeclaration}   styles          result of window.getComputedStyle()
+                * @param       {string}                propertyName    property name
+                * @return      {integer}       property value as integer
+                */
+               styleAsInt: function(styles, propertyName) {
+                       var value = styles.getPropertyValue(propertyName);
+                       if (value === null) {
+                               return 0;
+                       }
+                       
+                       return parseInt(value);
+               }
+       };
+       
+       // expose on window object for backward compatibility
+       window.bc_wcfDOMUtil = DOMUtil;
+       
+       return DOMUtil;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Alignment.js
deleted file mode 100644 (file)
index 13e79f8..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-/**
- * Utility class to align elements relatively to another.
- * 
- * @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/Alignment
- */
-define(['Core', 'Language', 'DOM/Traverse', 'DOM/Util'], function(Core, Language, DOMTraverse, DOMUtil) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/UI/Alignment
-        */
-       var UIAlignment = {
-               /**
-                * Sets the alignment for target element relatively to the reference element.
-                * 
-                * @param       {Element}               el              target element
-                * @param       {Element}               ref             reference element
-                * @param       {object<string, *>}     options         list of options to alter the behavior
-                */
-               set: function(el, ref, options) {
-                       options = Core.extend({
-                               // offset to reference element
-                               verticalOffset: 7,
-                               
-                               // align the pointer element, expects .elementPointer 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/center and top/bottom
-                               horizontal: 'left',
-                               vertical: 'bottom',
-                               
-                               // allow flipping over axis, possible values: both, horizontal, vertical and none
-                               allowFlip: 'both'
-                       }, options);
-                       
-                       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.vertical = '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 !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 = 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 (Language.get('wcf.global.pageDirection') === 'rtl') {
-                               options.horizontal = (options.horizontal === 'left') ? 'right' : 'left';
-                       }
-                       
-                       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;
-                                       }
-                               }
-                       }
-                       
-                       var left = horizontal.left;
-                       var right = horizontal.right;
-                       
-                       var vertical = this._tryAlignmentVertical(options.vertical, elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
-                       if (!vertical.result && (options.allowFlip === 'both' || options.allowFlip === 'vertical')) {
-                               var verticalFlipped = this._tryAlignmentVertical((options.vertical === 'top' ? 'bottom' : 'top'), elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
-                               // only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction
-                               if (verticalFlipped.result) {
-                                       vertical = verticalFlipped;
-                               }
-                       }
-                       
-                       var bottom = vertical.bottom;
-                       var top = vertical.top;
-                       
-                       // set pointer position
-                       if (options.pointer) {
-                               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;
-                               var pointerBottom = 1;
-                               
-                               el.classList[(top === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerBottom]);
-                               el.classList[(left === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerRight]);
-                       }
-                       
-                       DOMUtil.setStyles(el, {
-                               bottom: bottom + (bottom !== 'auto' ? 'px' : ''),
-                               left: left + (left !== 'auto' ? 'px' : ''),
-                               right: right + (right !== 'auto' ? 'px' : ''),
-                               top: top + (top !== 'auto' ? 'px' : '')
-                       });
-               },
-               
-               /**
-                * Calculates left/right position and verifys if the element would be still within the page's boundaries.
-                * 
-                * @param       {string}                        align           align to this side of the reference element
-                * @param       {object<string, integer>}       elDimensions    element dimensions
-                * @param       {object<string, integer>}       refDimensions   reference element dimensions
-                * @param       {object<string, integer>}       refOffsets      position of reference element relative to the document
-                * @param       {integer}                       windowWidth     window width
-                * @returns     {object<string, *>}     calculation results
-                */
-               _tryAlignmentHorizontal: function(align, elDimensions, refDimensions, refOffsets, windowWidth) {
-                       var left = 'auto';
-                       var right = 'auto';
-                       var result = true;
-                       
-                       if (align === 'left') {
-                               left = refOffsets.left;
-                               if (left + elDimensions.width > windowWidth) {
-                                       result = false;
-                               }
-                       }
-                       else if (align === 'right') {
-                               right = windowWidth - (refOffsets.left + refDimensions.width);
-                               if (right < 0) {
-                                       result = false;
-                               }
-                       }
-                       else {
-                               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
-                       };
-               },
-               
-               /**
-                * Calculates top/bottom position and verifys if the element would be still within the page's boundaries.
-                * 
-                * @param       {string}                        align           align to this side of the reference element
-                * @param       {object<string, integer>}       elDimensions    element dimensions
-                * @param       {object<string, integer>}       refDimensions   reference element dimensions
-                * @param       {object<string, integer>}       refOffsets      position of reference element relative to the document
-                * @param       {integer}                       windowHeight    window height
-                * @param       {integer}                       verticalOffset  desired gap between element and reference element
-                * @returns     {object<string, *>}     calculation results
-                */
-               _tryAlignmentVertical: function(align, elDimensions, refDimensions, refOffsets, windowHeight, verticalOffset) {
-                       var bottom = 'auto';
-                       var top = 'auto';
-                       var result = true;
-                       
-                       if (align === 'top') {
-                               var bodyHeight = document.body.clientHeight;
-                               bottom = (bodyHeight - refOffsets.top) + verticalOffset;
-                               if (bodyHeight - (bottom + elDimensions.height) < document.body.scrollTop) {
-                                       result = false;
-                               }
-                       }
-                       else {
-                               top = refOffsets.top + refDimensions.height + verticalOffset;
-                               if (top + elDimensions.height > windowHeight) {
-                                       result = false;
-                               }
-                       }
-                       
-                       return {
-                               align: align,
-                               bottom: bottom,
-                               top: top,
-                               result: result
-                       };
-               }
-       };
-       
-       return UIAlignment;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/CloseOverlay.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/CloseOverlay.js
deleted file mode 100644 (file)
index fe89092..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Allows to be informed when a click event bubbled up to the document's body.
- * 
- * @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/CloseOlveray
- */
-define(['CallbackList'], function(CallbackList) {
-       "use strict";
-       
-       var _callbackList = new CallbackList();
-       
-       /**
-        * @exports     WoltLab/WCF/UI/CloseOverlay
-        */
-       var UICloseOverlay = {
-               /**
-                * Sets up global event listener for bubbled clicks events.
-                */
-               setup: function() {
-                       document.body.addEventListener('click', this.execute.bind(this));
-               },
-               
-               /**
-                * @see WoltLab/WCF/CallbackList#add
-                */
-               add: _callbackList.add.bind(_callbackList),
-               
-               /**
-                * @see WoltLab/WCF/CallbackList#remove
-                */
-               remove: _callbackList.remove.bind(_callbackList),
-               
-               /**
-                * Invokes all registered callbacks.
-                */
-               execute: function() {
-                       _callbackList.forEach(null, function(callback) {
-                               callback();
-                       });
-               }
-       };
-       
-       UICloseOverlay.setup();
-       
-       return UICloseOverlay;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Collapsible/Sidebar.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Collapsible/Sidebar.js
deleted file mode 100644 (file)
index 79d8f01..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * Provides the sidebar toggle button.
- * 
- * @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/Collapsible/Sidebar
- */
-define(['Ajax', 'Language', 'DOM/Util'], function(Ajax, Language, DOMUtil) {
-       "use strict";
-       
-       var _isOpen = false;
-       var _main = null;
-       var _name = '';
-       
-       /**
-        * @module      WoltLab/WCF/UI/Collapsible/Sidebar
-        */
-       var UICollapsibleSidebar = {
-               /**
-                * Sets up the toggle button.
-                */
-               setup: function() {
-                       var sidebar = document.querySelector('.sidebar');
-                       if (sidebar === null) {
-                               return;
-                       }
-                       
-                       _isOpen = (sidebar.getAttribute('data-is-open') === 'true');
-                       _main = document.getElementById('main');
-                       _name = sidebar.getAttribute('data-sidebar-name');
-                       
-                       this._createUI(sidebar);
-                       
-                       _main.classList[(_isOpen ? 'remove' : 'add')]('sidebarCollapsed');
-               },
-               
-               /**
-                * Creates the toggle button.
-                * 
-                * @param       {Element}       sidebar         sidebar element
-                */
-               _createUI: function(sidebar) {
-                       var button = document.createElement('a');
-                       button.href = '#';
-                       button.className = 'collapsibleButton jsTooltip';
-                       button.setAttribute('title', Language.get('wcf.global.button.collapsible'));
-                       
-                       var span = document.createElement('span');
-                       span.appendChild(button);
-                       DOMUtil.prepend(span, sidebar);
-                       
-                       button.addEventListener('click', this._click.bind(this));
-               },
-               
-               /**
-                * Toggles the sidebar on click.
-                * 
-                * @param       {object}        event           event object
-                */
-               _click: function(event) {
-                       event.preventDefault();
-                       
-                       _isOpen = (_isOpen === false);
-                       
-                       Ajax.api(this, {
-                               isOpen: ~~_isOpen
-                       });
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'toggle',
-                                       className: 'wcf\\system\\user\\collapsible\\content\\UserCollapsibleSidebarHandler',
-                                       sidebarName: _name
-                               },
-                               url: 'index.php/AJAXInvoke/?t=' + SECURITY_TOKEN + SID_ARG_2ND
-                       };
-               },
-               
-               _ajaxSuccess: function(data) {
-                       _main.classList[(_isOpen ? 'remove' : 'add')]('sidebarCollapsed');
-               }
-       };
-       
-       return UICollapsibleSidebar;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Confirmation.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Confirmation.js
deleted file mode 100644 (file)
index ec32bcd..0000000
+++ /dev/null
@@ -1,169 +0,0 @@
-/**
- * Provides the confirmation dialog overlay.
- * 
- * @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/Confirmation
- */
-define(['Core', 'Language', 'UI/Dialog'], function(Core, Language, UIDialog) {
-       "use strict";
-       
-       var _active = false;
-       var _confirmButton = null;
-       var _content = null;
-       var _options = {};
-       var _text = null;
-       
-       /**
-        * Confirmation dialog overlay.
-        * 
-        * @exports     WoltLab/WCF/UI/Confirmation
-        */
-       var UIConfirmation = {
-               /**
-                * Shows the confirmation dialog.
-                * 
-                * Possible options:
-                *  - cancel: callback if user cancels the dialog
-                *  - confirm: callback if user confirm the dialog
-                *  - legacyCallback: WCF 2.0/2.1 compatible callback with string parameter
-                *  - message: displayed confirmation message
-                *  - parameters: list of parameters passed to the callback on confirm
-                *  - template: optional HTML string to be inserted below the `message`
-                * 
-                * @param       {object<string, *>}     options         confirmation options
-                */
-               show: function(options) {
-                       if (UIDialog === undefined) UIDialog = require('UI/Dialog');
-                       
-                       if (_active) {
-                               return;
-                       }
-                       
-                       _options = Core.extend({
-                               cancel: null,
-                               confirm: null,
-                               legacyCallback: null,
-                               message: '',
-                               parameters: {},
-                               template: ''
-                       }, options);
-                       
-                       _options.message = (typeof _options.message === 'string') ? _options.message.trim() : '';
-                       if (!_options.message.length) {
-                               throw new Error("Expected a non-empty string for option 'message'.");
-                       }
-                       
-                       if (typeof _options.confirm !== 'function' && typeof _options.legacyCallback !== 'function') {
-                               throw new TypeError("Expected a valid callback for option 'confirm'.");
-                       }
-                       
-                       if (_content === null) {
-                               this._createDialog();
-                       }
-                       
-                       _content.innerHTML = (typeof options.template === 'string') ? options.template.trim() : '';
-                       _text.textContent = _options.message;
-                       
-                       _active = true;
-                       
-                       UIDialog.open(this);
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'wcfSystemConfirmation',
-                               options: {
-                                       onClose: this._onClose.bind(this),
-                                       onShow: this._onShow.bind(this),
-                                       title: Language.get('wcf.global.confirmation.title')
-                               }
-                       };
-               },
-               
-               /**
-                * Returns content container element.
-                * 
-                * @return      {Element}       content container element
-                */
-               getContentElement: function() {
-                       return _content;
-               },
-               
-               /**
-                * Creates the dialog DOM elements.
-                */
-               _createDialog: function() {
-                       var dialog = document.createElement('div');
-                       dialog.setAttribute('id', 'wcfSystemConfirmation');
-                       dialog.classList.add('systemConfirmation');
-                       
-                       _text = document.createElement('p');
-                       dialog.appendChild(_text);
-                       
-                       _content = document.createElement('div');
-                       _content.setAttribute('id', 'wcfSystemConfirmationContent');
-                       dialog.appendChild(_content);
-                       
-                       var formSubmit = document.createElement('div');
-                       formSubmit.classList.add('formSubmit');
-                       dialog.appendChild(formSubmit);
-                       
-                       _confirmButton = document.createElement('button');
-                       _confirmButton.classList.add('buttonPrimary');
-                       _confirmButton.textContent = Language.get('wcf.global.confirmation.confirm');
-                       _confirmButton.addEventListener('click', this._confirm.bind(this));
-                       formSubmit.appendChild(_confirmButton);
-                       
-                       var cancelButton = document.createElement('button');
-                       cancelButton.textContent = Language.get('wcf.global.confirmation.cancel');
-                       cancelButton.addEventListener('click', function() { UIDialog.close('wcfSystemConfirmation'); });
-                       formSubmit.appendChild(cancelButton);
-                       
-                       document.body.appendChild(dialog);
-               },
-               
-               /**
-                * Invoked if the user confirms the dialog.
-                */
-               _confirm: function() {
-                       if (typeof _options.legacyCallback === 'function') {
-                               _options.legacyCallback('confirm', _options.parameters);
-                       }
-                       else {
-                               _options.confirm(_options.parameters);
-                       }
-                       
-                       _active = false;
-                       UIDialog.close('wcfSystemConfirmation');
-               },
-               
-               /**
-                * Invoked on dialog close or if user cancels the dialog.
-                */
-               _onClose: function() {
-                       if (_active) {
-                               _confirmButton.blur();
-                               _active = false;
-                               
-                               if (typeof _options.legacyCallback === 'function') {
-                                       _options.legacyCallback('cancel', _options.parameters);
-                               }
-                               else if (typeof _options.cancel === 'function') {
-                                       _options.cancel(_options.parameters);
-                               }
-                       }
-               },
-               
-               /**
-                * Sets the focus on the confirm button on dialog open for proper keyboard support.
-                */
-               _onShow: function() {
-                       _confirmButton.blur();
-                       _confirmButton.focus();
-               }
-       };
-       
-       return UIConfirmation;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dialog.js
deleted file mode 100644 (file)
index 8e445e8..0000000
+++ /dev/null
@@ -1,511 +0,0 @@
-/**
- * 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(
-       [
-               'enquire',     'Ajax',       'Core',      'Dictionary',
-               'Environment', 'Language',   'ObjectMap', 'DOM/ChangeListener',
-               'DOM/Util',    'UI/Confirmation'
-       ],
-       function(
-               enquire,        Ajax,         Core,        Dictionary,
-               Environment,    Language,     ObjectMap,   DOMChangeListener,
-               DOMUtil,        UIConfirmation
-       )
-{
-       "use strict";
-       
-       var _activeDialog = null;
-       var _container = null;
-       var _dialogs = new Dictionary();
-       var _dialogObjects = new ObjectMap();
-       var _dialogFullHeight = false;
-       var _keyupListener = null;
-       
-       /**
-        * @exports     WoltLab/WCF/UI/Dialog
-        */
-       var UIDialog = {
-               /**
-                * Sets up global container and internal variables.
-                */
-               setup: function() {
-                       // Fetch Ajax, as it cannot be provided because of a circular dependency
-                       if (Ajax === undefined) Ajax = require('Ajax');
-                       
-                       _container = document.createElement('div');
-                       _container.classList.add('dialogOverlay');
-                       _container.setAttribute('aria-hidden', 'true');
-                       _container.addEventListener('click', this._closeOnBackdrop.bind(this));
-                       
-                       document.body.appendChild(_container);
-                       
-                       _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);
-                       
-                       enquire.register('screen and (max-width: 800px)', {
-                               match: function() { _dialogFullHeight = true; },
-                               unmatch: function() { _dialogFullHeight = false; },
-                               setup: function() { _dialogFullHeight = true; },
-                               deferSetup: true
-                       });
-               },
-               
-               /**
-                * Opens the dialog and implicitly creates it on first usage.
-                * 
-                * @param       {object}                        callbackObject  used to invoke `_dialogSetup()` on first call
-                * @param       {(string|DocumentFragment=}     html            html content or document fragment to use for dialog content
-                * @returns     {object<string, *>}             dialog data
-                */
-               open: function(callbackObject, html) {
-                       var dialogData = _dialogObjects.get(callbackObject);
-                       if (Core.isPlainObject(dialogData)) {
-                               // dialog already exists
-                               return this.openStatic(dialogData.id, html);
-                       }
-                       
-                       // initialize a new dialog
-                       if (typeof callbackObject._dialogSetup !== 'function') {
-                               throw new Error("Callback object does not implement the method '_dialogSetup()'.");
-                       }
-                       
-                       var setupData = callbackObject._dialogSetup();
-                       if (!Core.isPlainObject(setupData)) {
-                               throw new Error("Expected an object literal as return value of '_dialogSetup()'.");
-                       }
-                       
-                       dialogData = { id: setupData.id };
-                       
-                       var createOnly = true;
-                       if (setupData.source === undefined) {
-                               var dialogElement = document.getElementById(setupData.id);
-                               if (dialogElement === null) {
-                                       throw new Error("Element id '" + setupData.id + "' is invalid and no source attribute was given.");
-                               }
-                               
-                               setupData.source = document.createDocumentFragment();
-                               setupData.source.appendChild(dialogElement);
-                       }
-                       else if (setupData.source === null) {
-                               // `null` means there is no static markup and `html` should be used instead
-                               setupData.source = html;
-                       }
-                       
-                       else if (typeof setupData.source === 'function') {
-                               setupData.source();
-                       }
-                       else if (Core.isPlainObject(setupData.source)) {
-                               Ajax.api(this, setupData.source.data, (function(data) {
-                                       if (data.returnValues && typeof data.returnValues.template === 'string') {
-                                               this.open(callbackObject, data.returnValues.template);
-                                               
-                                               if (typeof setupData.source.after === 'function') {
-                                                       setupData.source.after(_dialogs.get(setupData.id).content, data);
-                                               }
-                                       }
-                               }).bind(this));
-                       }
-                       else {
-                               if (typeof setupData.source === 'string') {
-                                       var dialogElement = document.createElement('div');
-                                       dialogElement.setAttribute('id', setupData.id);
-                                       dialogElement.innerHTML = setupData.source;
-                                       
-                                       setupData.source = document.createDocumentFragment();
-                                       setupData.source.appendChild(dialogElement);
-                               }
-                               
-                               if (!setupData.source.nodeType || setupData.source.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
-                                       throw new Error("Expected at least a document fragment as 'source' attribute.");
-                               }
-                               
-                               createOnly = false;
-                       }
-                       
-                       _dialogObjects.set(callbackObject, dialogData);
-                       
-                       return this.openStatic(setupData.id, setupData.source, setupData.options, createOnly);
-               },
-               
-               /**
-                * 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|DocumentFragment)}    html            content html
-                * @param       {object<string, *>}             options         list of options, is completely ignored if the dialog already exists
-                * @param       {boolean=}                      createOnly      create the dialog but do not open it
-                * @return      {object<string, *>}             dialog data
-                */
-               openStatic: function(id, html, options, createOnly) {
-                       if (_dialogs.has(id)) {
-                               this._updateDialog(id, html);
-                       }
-                       else {
-                               options = Core.extend({
-                                       backdropCloseOnClick: true,
-                                       closable: true,
-                                       closeButtonLabel: Language.get('wcf.global.button.close'),
-                                       closeConfirmMessage: '',
-                                       disableContentPadding: false,
-                                       disposeOnClose: false,
-                                       title: '',
-                                       
-                                       // callbacks
-                                       onBeforeClose: null,
-                                       onClose: null,
-                                       onShow: null
-                               }, options);
-                               
-                               if (!options.closable) options.backdropCloseOnClick = false;
-                               if (options.closeConfirmMessage) {
-                                       options.onBeforeClose = (function(id) {
-                                               UIConfirmation.show({
-                                                       confirm: this.close.bind(this, id),
-                                                       message: options.closeConfirmMessage
-                                               });
-                                       }).bind(this);
-                               }
-                               
-                               this._createDialog(id, html, options);
-                       }
-                       
-                       return _dialogs.get(id);
-               },
-               
-               /**
-                * Sets the dialog title.
-                * 
-                * @param       {string}        id              element id
-                * @param       {string}        title           dialog title
-                */
-               setTitle: function(id, title) {
-                       var data = _dialogs.get(id);
-                       if (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|DocumentFragment)}    html            content html
-                * @param       {object<string, *>}             options         list of options
-                * @param       {boolean=}                      createOnly      create the dialog but do not open it
-                */
-               _createDialog: function(id, html, options, createOnly) {
-                       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');
-                       if (options.disableContentPadding) contentContainer.classList.add('dialogContentNoPadding');
-                       dialog.appendChild(contentContainer);
-                       
-                       var content;
-                       if (element === null) {
-                               content = document.createElement('div');
-                               
-                               if (typeof html === 'string') {
-                                       content.innerHTML = html;
-                               }
-                               else if (html instanceof DocumentFragment) {
-                                       if (html.children[0].nodeName !== 'div' || html.childElementCount > 1) {
-                                               content.appendChild(html);
-                                       }
-                                       else {
-                                               content = html.children[0];
-                                       }
-                               }
-                               
-                               content.id = id;
-                       }
-                       else {
-                               content = element;
-                       }
-                       
-                       contentContainer.appendChild(content);
-                       
-                       if (content.style.getPropertyValue('display') === 'none') {
-                               content.style.removeProperty('display');
-                       }
-                       
-                       _dialogs.set(id, {
-                               backdropCloseOnClick: options.backdropCloseOnClick,
-                               content: content,
-                               dialog: dialog,
-                               header: header,
-                               onBeforeClose: options.onBeforeClose,
-                               onClose: options.onClose,
-                               onShow: options.onShow
-                       });
-                       
-                       DOMUtil.prepend(dialog, _container);
-                       
-                       if (createOnly !== true) {
-                               this._updateDialog(id, null);
-                       }
-               },
-               
-               /**
-                * 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 (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') {
-                               if (_container.getAttribute('aria-hidden') === 'true') {
-                                       window.addEventListener('keyup', _keyupListener);
-                               }
-                               
-                               data.dialog.setAttribute('aria-hidden', 'false');
-                               _container.setAttribute('aria-hidden', 'false');
-                               _container.setAttribute('data-close-on-click', (data.backdropCloseOnClick ? 'true' : 'false'));
-                               _activeDialog = id;
-                               
-                               this.rebuild(id);
-                               
-                               if (typeof data.onShow === 'function') {
-                                       data.onShow(id);
-                               }
-                       }
-                       
-                       DOMChangeListener.trigger();
-               },
-               
-               /**
-                * Rebuilds dialog identified by given id.
-                * 
-                * @param       {string}        id      element id
-                */
-               rebuild: function(id) {
-                       var data = _dialogs.get(id);
-                       if (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;
-                       }
-                       
-                       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 * (_dialogFullHeight ? 1 : 0.8)) - unavailableHeight;
-                       contentContainer.style.setProperty('max-height', ~~maximumHeight + 'px');
-                       
-                       // fix for a calculation bug in Chrome causing the scrollbar to overlap the border
-                       if (Environment.browser() === 'chrome') {
-                               if (data.content.scrollHeight > maximumHeight) {
-                                       data.content.style.setProperty('margin-right', '-1px');
-                               }
-                               else {
-                                       data.content.style.removeProperty('margin-right');
-                               }
-                       }
-               },
-               
-               /**
-                * 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|object)}       id      element id or callback object
-                */
-               close: function(id) {
-                       if (typeof id === 'object') {
-                               var dialogData = _dialogObjects.get(id);
-                               if (dialogData !== undefined) {
-                                       id = dialogData.id;
-                               }
-                       }
-                       
-                       var data = _dialogs.get(id);
-                       if (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['delete'](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);
-               },
-               
-               _ajaxSetup: function() {
-                       return {};
-               }
-       };
-       
-       return UIDialog;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js
deleted file mode 100644 (file)
index 1fc5326..0000000
+++ /dev/null
@@ -1,390 +0,0 @@
-/**
- * Simple Dropdown
- * 
- * @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/Dropdown/Simple
- */
-define(
-       [       'CallbackList', 'Core', 'Dictionary', 'UI/Alignment', 'DOM/ChangeListener', 'DOM/Traverse', 'DOM/Util', 'UI/CloseOverlay'],
-       function(CallbackList,   Core,   Dictionary,   UIAlignment,    DOMChangeListener,    DOMTraverse,    DOMUtil,    UICloseOverlay)
-{
-       "use strict";
-       
-       var _availableDropdowns = null;
-       var _callbacks = new CallbackList();
-       var _didInit = false;
-       var _dropdowns = new Dictionary();
-       var _menus = new Dictionary();
-       var _menuContainer = null;
-       
-       /**
-        * @exports     WoltLab/WCF/UI/Dropdown/Simple
-        */
-       var SimpleDropdown = {
-               /**
-                * Performs initial setup such as setting up dropdowns and binding listeners.
-                */
-               setup: function() {
-                       if (_didInit) return;
-                       _didInit = true;
-                       
-                       _menuContainer = document.createElement('div');
-                       _menuContainer.setAttribute('id', 'dropdownMenuContainer');
-                       document.body.appendChild(_menuContainer);
-                       
-                       _availableDropdowns = document.getElementsByClassName('dropdownToggle');
-                       
-                       this.initAll();
-                       
-                       UICloseOverlay.add('WoltLab/WCF/UI/Dropdown/Simple', this.closeAll.bind(this));
-                       DOMChangeListener.add('WoltLab/WCF/UI/Dropdown/Simple', this.initAll.bind(this));
-                       
-                       document.addEventListener('scroll', this._onScroll.bind(this));
-                       
-                       // expose on window object for backward compatibility
-                       window.bc_wcfSimpleDropdown = this;
-               },
-               
-               /**
-                * Loops through all possible dropdowns and registers new ones.
-                */
-               initAll: function() {
-                       for (var i = 0, length = _availableDropdowns.length; i < length; i++) {
-                               this.init(_availableDropdowns[i], false);
-                       }
-               },
-               
-               /**
-                * Initializes a dropdown.
-                * 
-                * @param       {Element}       button
-                * @param       {boolean}       isLazyInitialization
-                */
-               init: function(button, isLazyInitialization) {
-                       this.setup();
-                       
-                       if (button.classList.contains('jsDropdownEnabled') || button.getAttribute('data-target')) {
-                               return false;
-                       }
-                       
-                       var dropdown = DOMTraverse.parentByClass(button, 'dropdown');
-                       if (dropdown === null) {
-                               throw new Error("Invalid dropdown passed, button '" + DOMUtil.identify(button) + "' does not have a parent with .dropdown.");
-                       }
-                       
-                       var menu = DOMTraverse.nextByClass(button, 'dropdownMenu');
-                       if (menu === null) {
-                               throw new Error("Invalid dropdown passed, button '" + DOMUtil.identify(button) + "' does not have a menu as next sibling.");
-                       }
-                       
-                       // move menu into global container
-                       _menuContainer.appendChild(menu);
-                       
-                       var containerId = DOMUtil.identify(dropdown);
-                       if (!_dropdowns.has(containerId)) {
-                               button.classList.add('jsDropdownEnabled');
-                               button.addEventListener('click', this._toggle.bind(this));
-                               
-                               _dropdowns.set(containerId, dropdown);
-                               _menus.set(containerId, menu);
-                               
-                               if (!containerId.match(/^wcf\d+$/)) {
-                                       menu.setAttribute('data-source', containerId);
-                               }
-                       }
-                       
-                       button.setAttribute('data-target', containerId);
-                       
-                       if (isLazyInitialization) {
-                               setTimeout(function() { Core.triggerEvent(button, 'click'); }, 10);
-                       }
-               },
-               
-               /**
-                * Initializes a remote-controlled dropdown.
-                * 
-                * @param       {Element}       dropdown        dropdown wrapper element
-                * @param       {Element}       menu            menu list element
-                */
-               initFragment: function(dropdown, menu) {
-                       this.setup();
-                       
-                       if (_dropdowns.has(dropdown)) {
-                               return;
-                       }
-                       
-                       var containerId = DOMUtil.identify(dropdown);
-                       _dropdowns.set(containerId, dropdown);
-                       _menuContainer.appendChild(menu);
-                       
-                       _menus.set(containerId, menu);
-               },
-               
-               /**
-                * Registers a callback for open/close events.
-                * 
-                * @param       {string}                        containerId     dropdown wrapper id
-                * @param       {function(string, string)}      callback
-                */
-               registerCallback: function(containerId, callback) {
-                       _callbacks.add(containerId, callback);
-               },
-               
-               /**
-                * Returns the requested dropdown wrapper element.
-                * 
-                * @return      {Element}       dropdown wrapper element
-                */
-               getDropdown: function(containerId) {
-                       return _dropdowns.get(containerId);
-               },
-               
-               /**
-                * Returns the requested dropdown menu list element.
-                * 
-                * @return      {Element}       menu list element
-                */
-               getDropdownMenu: function(containerId) {
-                       return _menus.get(containerId);
-               },
-               
-               /**
-                * Toggles the requested dropdown between opened and closed.
-                * 
-                * @param       {string}        containerId     dropdown wrapper id
-                */
-               toggleDropdown: function(containerId) {
-                       this._toggle(null, containerId);
-               },
-               
-               /**
-                * Calculates and sets the alignment of given dropdown.
-                * 
-                * @param       {Element}       dropdown        dropdown wrapper element
-                * @param       {Element}       dropdownMenu    menu list element
-                */
-               setAlignment: function(dropdown, dropdownMenu) {
-                       // check if button belongs to an i18n textarea
-                       var button = dropdown.querySelector('.dropdownToggle');
-                       var refDimensionsElement = null;
-                       if (button !== null && button.classList.contains('dropdownCaptionTextarea')) {
-                               refDimensionsElement = button;
-                       }
-                       
-                       UIAlignment.set(dropdownMenu, dropdown, {
-                               pointerClassNames: ['dropdownArrowBottom', 'dropdownArrowRight'],
-                               refDimensionsElement: refDimensionsElement
-                       });
-               },
-               
-               /**
-                * Calculats and sets the alignment of the dropdown identified by given id.
-                * 
-                * @param       {string}        containerId     dropdown wrapper id
-                */
-               setAlignmentById: function(containerId) {
-                       var dropdown = _dropdowns.get(containerId);
-                       if (dropdown === undefined) {
-                               throw new Error("Unknown dropdown identifier '" + containerId + "'.");
-                       }
-                       
-                       var menu = _menus.get(containerId);
-                       
-                       this.setAlignment(dropdown, menu);
-               },
-               
-               /**
-                * Returns true if target dropdown exists and is open.
-                * 
-                * @param       {string}        containerId     dropdown wrapper id
-                * @return      {boolean}       true if dropdown exists and is open
-                */
-               isOpen: function(containerId) {
-                       var menu = _menus.get(containerId);
-                       if (menu !== undefined && menu.classList.contains('dropdownOpen')) {
-                               return true;
-                       }
-                       
-                       return false;
-               },
-               
-               /**
-                * Opens the dropdown unless it is already open.
-                * 
-                * @param       {string}        containerId     dropdown wrapper id
-                */
-               open: function(containerId) {
-                       var menu = _menus.get(containerId);
-                       if (menu !== undefined && !menu.classList.contains('dropdownOpen')) {
-                               this.toggleDropdown(containerId);
-                       }
-               },
-               
-               /**
-                * Closes the dropdown identified by given id without notifying callbacks.
-                * 
-                * @param       {string}        containerId     dropdown wrapper id
-                */
-               close: function(containerId) {
-                       var dropdown = _dropdowns.get(containerId);
-                       if (dropdown !== undefined) {
-                               dropdown.classList.remove('dropdownOpen');
-                               _menus.get(containerId).classList.remove('dropdownOpen');
-                       }
-               },
-               
-               /**
-                * Closes all dropdowns.
-                */
-               closeAll: function() {
-                       _dropdowns.forEach((function(dropdown, containerId) {
-                               if (dropdown.classList.contains('dropdownOpen')) {
-                                       dropdown.classList.remove('dropdownOpen');
-                                       _menus.get(containerId).classList.remove('dropdownOpen');
-                                       
-                                       this._notifyCallbacks(containerId, 'close');
-                               }
-                       }).bind(this));
-               },
-               
-               /**
-                * Destroys a dropdown identified by given id.
-                * 
-                * @param       {string}        containerId     dropdown wrapper id
-                * @return      {boolean}       false for unknown dropdowns
-                */
-               destroy: function(containerId) {
-                       if (!_dropdowns.has(containerId)) {
-                               return false;
-                       }
-                       
-                       this.close(containerId);
-                       
-                       var menu = _menus.get(containerId);
-                       _menus.parentNode.removeChild(menu);
-                       
-                       _menus['delete'](containerId);
-                       _dropdowns['delete'](containerId);
-                       
-                       return true;
-               },
-               
-               /**
-                * Handles dropdown positions in overlays when scrolling in the overlay.
-                * 
-                * @param       {Event}         event   event object
-                */
-               _onDialogScroll: function(event) {
-                       var dialogContent = event.currentTarget;
-                       var dropdowns = dialogContent.querySelectorAll('.dropdown.dropdownOpen');
-                       
-                       for (var i = 0, length = dropdowns.length; i < length; i++) {
-                               var dropdown = dropdowns[i];
-                               var containerId = DOMUtil.identify(dropdown);
-                               var offset = DOMUtil.offset(dropdown);
-                               var dialogOffset = DOMUtil.offset(dialogContent);
-                               
-                               // check if dropdown toggle is still (partially) visible
-                               if (offset.top + dropdown.clientHeight <= dialogOffset.top) {
-                                       // top check
-                                       this.toggleDropdown(containerId);
-                               }
-                               else if (offset.top >= dialogOffset.top + dialogContent.offsetHeight) {
-                                       // bottom check
-                                       this.toggleDropdown(containerId);
-                               }
-                               else if (offset.left <= dialogOffset.left) {
-                                       // left check
-                                       this.toggleDropdown(containerId);
-                               }
-                               else if (offset.left >= dialogOffset.left + dialogContent.offsetWidth) {
-                                       // right check
-                                       this.toggleDropdown(containerId);
-                               }
-                               else {
-                                       this.setAlignment(containerId, _menus.get(containerId));
-                               }
-                       }
-               },
-               
-               /**
-                * Recalculates dropdown positions on page scroll.
-                */
-               _onScroll: function() {
-                       _dropdowns.forEach((function(dropdown, containerId) {
-                               if (dropdown.getAttribute('data-is-overlay-dropdown-button') === true && dropdown.classList.contains('dropdownOpen')) {
-                                       this.setAlignment(dropdown, _menus.get(containerId));
-                               }
-                       }).bind(this));
-               },
-               
-               /**
-                * Notifies callbacks on status change.
-                * 
-                * @param       {string}        containerId     dropdown wrapper id
-                * @param       {string}        action          can be either 'open' or 'close'
-                */
-               _notifyCallbacks: function(containerId, action) {
-                       _callbacks.forEach(containerId, function(callback) {
-                               callback(containerId, action);
-                       });
-               },
-               
-               /**
-                * Toggles the dropdown's state between open and close.
-                * 
-                * @param       {?Event}        event           event object, should be 'null' if targetId is given
-                * @param       {string=}       targetId        dropdown wrapper id
-                * @return      {boolean}       'false' if event is not null
-                */
-               _toggle: function(event, targetId) {
-                       if (event !== null) {
-                               event.preventDefault();
-                               event.stopPropagation();
-                               
-                               targetId = event.currentTarget.getAttribute('data-target');
-                       }
-                       
-                       // check if 'isOverlayDropdownButton' is set which indicates if
-                       // the dropdown toggle is in an overlay
-                       var dropdown = _dropdowns.get(targetId);
-                       if (dropdown !== undefined && dropdown.getAttribute('data-is-overlay-dropdown-button') === null) {
-                               var dialogContent = DOMTraverse.parentByClass(dropdown, 'dialogContent');
-                               dropdown.setAttribute('data-is-overlay-dropdown-button', (dialogContent !== null));
-                               
-                               if (dialogContent !== null) {
-                                       dialogContent.addEventListener('scroll', this._onDialogScroll.bind(this));
-                               }
-                       }
-                       
-                       // close all dropdowns
-                       _dropdowns.forEach((function(dropdown, containerId) {
-                               var menu = _menus.get(containerId);
-                               
-                               if (dropdown.classList.contains('dropdownOpen')) {
-                                       dropdown.classList.remove('dropdownOpen');
-                                       menu.classList.remove('dropdownOpen');
-                                       
-                                       this._notifyCallbacks(containerId, 'close');
-                               }
-                               else if (containerId === targetId && menu.childElementCount > 0) {
-                                       dropdown.classList.add('dropdownOpen');
-                                       menu.classList.add('dropdownOpen');
-                                       
-                                       this._notifyCallbacks(containerId, 'open');
-                                       
-                                       this.setAlignment(dropdown, menu);
-                               }
-                       }).bind(this));
-                       
-                       // TODO
-                       WCF.Dropdown.Interactive.Handler.closeAll();
-                       
-                       return (event === null);
-               }
-       };
-       
-       return SimpleDropdown;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/FlexibleMenu.js
deleted file mode 100644 (file)
index b201251..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-/**
- * Dynamically transforms menu-like structures to handle items exceeding the available width
- * by moving them into a separate dropdown.  
- * 
- * @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/FlexibleMenu
- */
-define(['Core', 'Dictionary', 'DOM/ChangeListener', 'DOM/Traverse', 'DOM/Util', 'UI/SimpleDropdown'], function(Core, Dictionary, DOMChangeListener, DOMTraverse, DOMUtil, SimpleDropdown) {
-       "use strict";
-       
-       var _containers = new Dictionary();
-       var _dropdowns = new Dictionary();
-       var _dropdownMenus = new Dictionary();
-       var _itemLists = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/UI/FlexibleMenu
-        */
-       var UIFlexibleMenu = {
-               /**
-                * Register default menus and set up event listeners.
-                */
-               setup: function() {
-                       if (document.getElementById('mainMenu') !== null) this.register('mainMenu');
-                       var navigationHeader = document.querySelector('.navigationHeader');
-                       if (navigationHeader !== null) this.register(DOMUtil.identify(navigationHeader));
-                       
-                       window.addEventListener('resize', this.rebuildAll.bind(this));
-                       DOMChangeListener.add('WoltLab/WCF/UI/FlexibleMenu', this.registerTabMenus.bind(this));
-               },
-               
-               /**
-                * Registers a menu by element id.
-                * 
-                * @param       {string}        containerId     element id
-                */
-               register: function(containerId) {
-                       var container = document.getElementById(containerId);
-                       if (container === null) {
-                               throw "Expected a valid element id, '" + containerId + "' does not exist.";
-                       }
-                       
-                       if (_containers.has(containerId)) {
-                               return;
-                       }
-                       
-                       var list = DOMTraverse.childByTag(container, 'UL');
-                       if (list === null) {
-                               throw "Expected an <ul> element as child of container '" + containerId + "'.";
-                       }
-                       
-                       _containers.set(containerId, container);
-                       _itemLists.set(containerId, list);
-                       
-                       this.rebuild(containerId);
-               },
-               
-               /**
-                * Registers tab menus.
-                */
-               registerTabMenus: function() {
-                       var tabMenus = document.querySelectorAll('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)');
-                       for (var i = 0, length = tabMenus.length; i < length; i++) {
-                               var tabMenu = tabMenus[i];
-                               var nav = DOMTraverse.childByTag(tabMenu, 'NAV');
-                               if (nav !== null) {
-                                       tabMenu.classList.add('jsFlexibleMenuEnabled');
-                                       this.register(DOMUtil.identify(nav));
-                               }
-                       }
-               },
-               
-               /**
-                * Rebuilds all menus, e.g. on window resize.
-                */
-               rebuildAll: function() {
-                       _containers.forEach((function(container, containerId) {
-                               this.rebuild(containerId);
-                       }).bind(this));
-               },
-               
-               /**
-                * Rebuild the menu identified by given element id.
-                * 
-                * @param       {string}        containerId     element id
-                */
-               rebuild: function(containerId) {
-                       var container = _containers.get(containerId);
-                       if (container === undefined) {
-                               throw "Expected a valid element id, '" + containerId + "' is unknown.";
-                       }
-                       
-                       var styles = window.getComputedStyle(container);
-                       
-                       var availableWidth = container.parentNode.clientWidth;
-                       availableWidth -= DOMUtil.styleAsInt(styles, 'margin-left');
-                       availableWidth -= DOMUtil.styleAsInt(styles, 'margin-right');
-                       
-                       var list = _itemLists.get(containerId);
-                       var items = DOMTraverse.childrenByTag(list, 'LI');
-                       var dropdown = _dropdowns.get(containerId);
-                       var dropdownWidth = 0;
-                       if (dropdown !== undefined) {
-                               // show all items for calculation
-                               for (var i = 0, length = items.length; i < length; i++) {
-                                       var item = items[i];
-                                       if (item.classList.contains('dropdown')) {
-                                               continue;
-                                       }
-                                       
-                                       item.style.removeProperty('display'); 
-                               }
-                               
-                               if (dropdown.parentNode !== null) {
-                                       dropdownWidth = DOMUtil.outerWidth(dropdown);
-                               }
-                       }
-                       
-                       var currentWidth = list.scrollWidth - dropdownWidth;
-                       var hiddenItems = [];
-                       if (currentWidth > availableWidth) {
-                               // hide items starting with the last one
-                               for (var i = items.length - 1; i >= 0; i--) {
-                                       var item = items[i];
-                                       
-                                       // ignore dropdown and active item
-                                       if (item.classList.contains('dropdown') || item.classList.contains('active') || item.classList.contains('ui-state-active')) {
-                                               continue;
-                                       }
-                                       
-                                       hiddenItems.push(item);
-                                       item.style.setProperty('display', 'none');
-                                       
-                                       if (list.scrollWidth < availableWidth) {
-                                               break;
-                                       }
-                               }
-                       }
-                       
-                       if (hiddenItems.length) {
-                               var dropdownMenu;
-                               if (dropdown === undefined) {
-                                       dropdown = document.createElement('li');
-                                       dropdown.className = 'dropdown jsFlexibleMenuDropdown';
-                                       var icon = document.createElement('a');
-                                       icon.className = 'icon icon16 fa-list';
-                                       dropdown.appendChild(icon);
-                                       
-                                       dropdownMenu = document.createElement('ul');
-                                       dropdownMenu.classList.add('dropdownMenu');
-                                       dropdown.appendChild(dropdownMenu);
-                                       
-                                       _dropdowns.set(containerId, dropdown);
-                                       _dropdownMenus.set(containerId, dropdownMenu);
-                                       
-                                       SimpleDropdown.init(icon);
-                               }
-                               else {
-                                       dropdownMenu = _dropdownMenus.get(containerId);
-                               }
-                               
-                               if (dropdown.parentNode === null) {
-                                       list.appendChild(dropdown);
-                               }
-                               
-                               // build dropdown menu
-                               var fragment = document.createDocumentFragment();
-                               
-                               var self = this;
-                               hiddenItems.forEach(function(hiddenItem) {
-                                       var item = document.createElement('li');
-                                       item.innerHTML = hiddenItem.innerHTML;
-                                       
-                                       item.addEventListener('click', (function(event) {
-                                               event.preventDefault();
-                                               
-                                               Core.triggerEvent(hiddenItem.querySelector('a'), 'click');
-                                               
-                                               // force a rebuild to guarantee the active item being visible
-                                               setTimeout(function() {
-                                                       self.rebuild(containerId);
-                                               }, 59);
-                                       }).bind(this));
-                                       
-                                       fragment.appendChild(item);
-                               });
-                               
-                               dropdownMenu.innerHTML = '';
-                               dropdownMenu.appendChild(fragment);
-                       }
-                       else if (dropdown !== undefined && dropdown.parentNode !== null) {
-                               dropdown.parentNode.removeChild(dropdown);
-                       }
-               }
-       };
-       
-       return UIFlexibleMenu;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList.js
deleted file mode 100644 (file)
index 4148714..0000000
+++ /dev/null
@@ -1,413 +0,0 @@
-/**
- * Flexible UI element featuring both a list of items and an input field with suggestion support.
- * 
- * @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/ItemList
- */
-define(['Core', 'Dictionary', 'Language', 'DOM/Traverse', 'WoltLab/WCF/UI/Suggestion'], function(Core, Dictionary, Language, DOMTraverse, UISuggestion) {
-       "use strict";
-       
-       var _activeId = '';
-       var _data = new Dictionary();
-       var _didInit = false;
-       
-       var _callbackKeyDown = null;
-       var _callbackKeyPress = null;
-       var _callbackKeyUp = null;
-       var _callbackRemoveItem = null;
-       
-       /**
-        * @exports     WoltLab/WCF/UI/ItemList
-        */
-       var UIItemList = {
-               /**
-                * Initializes an item list.
-                * 
-                * The `values` argument must be empty or contain a list of strings or object, e.g.
-                * `['foo', 'bar']` or `[{ objectId: 1337, value: 'baz'}, {...}]`
-                * 
-                * @param       {string}                elementId       input element id
-                * @param       {array<mixed>}          values          list of existing values
-                * @param       {object<string>}        options         option list
-                */
-               init: function(elementId, values, options) {
-                       var element = document.getElementById(elementId);
-                       if (element === null) {
-                               throw new Error("Expected a valid element id.");
-                       }
-                       
-                       options = Core.extend({
-                               // search parameters for suggestions
-                               ajax: {
-                                       actionName: 'getSearchResultList',
-                                       className: '',
-                                       data: {}
-                               },
-                               
-                               // list of excluded string values, e.g. `['ignore', 'these strings', 'when', 'searching']`
-                               excludedSearchValues: [],
-                               // maximum number of items this list may contain, `-1` for infinite
-                               maxItems: -1,
-                               // maximum length of an item value, `-1` for infinite
-                               maxLength: -1,
-                               // disallow custom values, only values offered by the suggestion dropdown are accepted
-                               restricted: false,
-                               
-                               // initial value will be interpreted as comma separated value and submitted as such
-                               isCSV: false,
-                               
-                               // will be invoked whenever the items change, receives the element id first and list of values second
-                               callbackChange: null,
-                               // callback once the form is about to be submitted
-                               callbackSubmit: null,
-                               // value may contain the placeholder `{$objectId}`
-                               submitFieldName: ''
-                       }, options);
-                       
-                       var form = DOMTraverse.parentByTag(element, 'FORM');
-                       if (form !== null) {
-                               if (options.isCSV === false) {
-                                       if (!options.submitFieldName.length && typeof options.callbackSubmit !== 'function') {
-                                               throw new Error("Expected a valid function for option 'callbackSubmit', a non-empty value for option 'submitFieldName' or enabling the option 'submitFieldCSV'.");
-                                       }
-                                       
-                                       form.addEventListener('submit', (function() {
-                                               var values = this.getValues(elementId);
-                                               if (options.submitFieldName.length) {
-                                                       var input;
-                                                       for (var i = 0, length = values.length; i < length; i++) {
-                                                               input = document.createElement('input');
-                                                               input.type = 'hidden';
-                                                               input.name = options.submitFieldName.replace(/{$objectId}/, values[i].objectId);
-                                                               input.value = values[i].value;
-                                                               
-                                                               form.appendChild(input);
-                                                       }
-                                               }
-                                               else {
-                                                       options.callbackSubmit(form, values);
-                                               }
-                                       }).bind(this));
-                               }
-                       }
-                       
-                       this._setup();
-                       
-                       var data = this._createUI(element, options, values);
-                       var suggestion = new UISuggestion(elementId, {
-                               ajax: options.ajax,
-                               callbackSelect: this._addItem.bind(this),
-                               excludedSearchValues: options.excludedSearchValues
-                       });
-                       
-                       _data.set(elementId, {
-                               dropdownMenu: null,
-                               element: data.element,
-                               list: data.list,
-                               listItem: data.element.parentNode,
-                               options: options,
-                               shadow: data.shadow,
-                               suggestion: suggestion
-                       });
-                       
-                       values = (data.values.length) ? data.values : values;
-                       if (Array.isArray(values)) {
-                               var value;
-                               for (var i = 0, length = values.length; i < length; i++) {
-                                       value = values[i];
-                                       if (typeof value === 'string') {
-                                               value = { objectId: 0, value: value };
-                                       }
-                                       
-                                       this._addItem(elementId, value);
-                               }
-                       }
-               },
-               
-               /**
-                * Returns the list of current values.
-                * 
-                * @param       {string}                element id      input element id
-                * @return      {array<object>}         list of objects containing object id and value
-                */
-               getValues: function(elementId) {
-                       if (!_data.has(elementId)) {
-                               throw new Error("Element id '" + elementId + "' is unknown.");
-                       }
-                       
-                       var data = _data.get(elementId);
-                       var items = DOMTraverse.childrenByClass(data.list, 'item');
-                       var values = [], value, item;
-                       for (var i = 0, length = items.length; i < length; i++) {
-                               item = items[i];
-                               value = {
-                                       objectId: item.getAttribute('data-object-id'),
-                                       value: DOMTraverse.childByTag(item, 'SPAN').textContent
-                               };
-                               
-                               values.push(value);
-                       }
-                       
-                       return values;
-               },
-               
-               /**
-                * Binds static event listeners.
-                */
-               _setup: function() {
-                       if (_didInit) {
-                               return;
-                       }
-                       
-                       _didInit = true;
-                       
-                       _callbackKeyDown = this._keyDown.bind(this);
-                       _callbackKeyPress = this._keyPress.bind(this);
-                       _callbackKeyUp = this._keyUp.bind(this);
-                       _callbackRemoveItem = this._removeItem.bind(this);
-               },
-               
-               /**
-                * Creates the DOM structure for target element. If `element` is a `<textarea>`
-                * it will be automatically replaced with an `<input>` element.
-                * 
-                * @param       {Element}               element         input element
-                * @param       {object<string>}        options         option list
-                */
-               _createUI: function(element, options) {
-                       var list = document.createElement('ol');
-                       list.className = 'inputItemList';
-                       list.setAttribute('data-element-id', element.id);
-                       list.addEventListener('click', function(event) {
-                               if (event.target === list) element.focus();
-                       });
-                       
-                       var listItem = document.createElement('li');
-                       listItem.className = 'input';
-                       list.appendChild(listItem);
-                       
-                       element.addEventListener('keydown', _callbackKeyDown);
-                       element.addEventListener('keypress', _callbackKeyPress);
-                       element.addEventListener('keyup', _callbackKeyUp);
-                       
-                       element.parentNode.insertBefore(list, element);
-                       listItem.appendChild(element);
-                       
-                       if (options.maxLength !== -1) {
-                               element.setAttribute('maxLength', options.maxLength);
-                       }
-                       
-                       var shadow = null, values = [];
-                       if (options.isCSV) {
-                               shadow = document.createElement('input');
-                               shadow.className = 'itemListInputShadow';
-                               shadow.type = 'hidden';
-                               shadow.name = element.name;
-                               element.removeAttribute('name');
-                               
-                               list.parentNode.insertBefore(shadow, list);
-                               
-                               if (element.nodeName === 'TEXTAREA') {
-                                       var value, tmp = element.value.split(',');
-                                       for (var i = 0, length = tmp.length; i < length; i++) {
-                                               value = tmp[i].trim();
-                                               if (value.length) {
-                                                       values.push(value);
-                                               }
-                                       }
-                                       
-                                       var inputElement = document.createElement('input');
-                                       element.parentNode.insertBefore(inputElement, element);
-                                       inputElement.id = element.id;
-                                       
-                                       element.parentNode.removeChild(element);
-                                       element = inputElement;
-                               }
-                       }
-                       
-                       return {
-                               element: element,
-                               list: list,
-                               shadow: shadow,
-                               values: values
-                       };
-               },
-               
-               /**
-                * Enforces the maximum number of items.
-                * 
-                * @param       {string}        elementId       input element id
-                */
-               _handleLimit: function(elementId) {
-                       var data = _data.get(elementId);
-                       if (data.options.maxItems === -1) {
-                               return;
-                       }
-                       
-                       if (data.list.childElementCount - 1 < data.options.maxItems) {
-                               if (data.element.disabled) {
-                                       data.element.disabled = false;
-                                       data.element.removeAttribute('placeholder');
-                               }
-                       }
-                       else if (!data.element.disabled) {
-                               data.element.disabled = true;
-                               data.element.setAttribute('placeholder', Language.get('wcf.global.form.input.maxItems'));
-                       }
-               },
-               
-               /**
-                * Sets the active item list id and handles keyboard access to remove an existing item.
-                * 
-                * @param       {object}        event           event object
-                */
-               _keyDown: function(event) {
-                       var input = event.currentTarget;
-                       var lastItem = input.parentNode.previousElementSibling;
-                       
-                       _activeId = input.id;
-                       
-                       if (event.keyCode === 8) {
-                               // 8 = [BACKSPACE]
-                               if (input.value.length === 0) {
-                                       if (lastItem !== null) {
-                                               if (lastItem.classList.contains('active')) {
-                                                       this._removeItem(null, lastItem);
-                                               }
-                                               else {
-                                                       lastItem.classList.add('active');
-                                               }
-                                       }
-                               }
-                       }
-                       else if (event.keyCode === 27) {
-                               // 27 = [ESC]
-                               if (lastItem !== null && lastItem.classList.contains('active')) {
-                                       lastItem.classList.remove('active');
-                               }
-                       }
-               },
-               
-               /**
-                * Handles the `[ENTER]` and `[,]` key to add an item to the list unless it is restricted.
-                * 
-                * @param       {object}        event           event object
-                */
-               _keyPress: function(event) {
-                       // 13 = [ENTER], 44 = [,]
-                       if (event.charCode === 13 || event.charCode === 44) {
-                               event.preventDefault();
-                               
-                               if (_data.get(event.currentTarget.id).options.restricted) {
-                                       // restricted item lists only allow results from the dropdown to be picked
-                                       return;
-                               }
-                               
-                               var value = event.currentTarget.value.trim();
-                               if (value.length) {
-                                       this._addItem(event.currentTarget.id, { objectId: 0, value: value });
-                               }
-                       }
-               },
-               
-               /**
-                * Handles the keyup event to unmark an item for deletion.
-                * 
-                * @param       {object}        event           event object
-                */
-               _keyUp: function(event) {
-                       var input = event.currentTarget;
-                       
-                       if (input.value.length > 0) {
-                               var lastItem = input.parentNode.previousElementSibling;
-                               if (lastItem !== null) {
-                                       lastItem.classList.remove('active');
-                               }
-                       }
-               },
-               
-               /**
-                * Adds an item to the list.
-                * 
-                * @param       {string}        elementId       input element id
-                * @param       {string}        value           item value
-                */
-               _addItem: function(elementId, value) {
-                       var data = _data.get(elementId);
-                       
-                       var listItem = document.createElement('li');
-                       listItem.className = 'item';
-                       
-                       var content = document.createElement('span');
-                       content.className = 'content';
-                       content.setAttribute('data-object-id', value.objectId);
-                       content.textContent = value.value;
-                       
-                       var button = document.createElement('a');
-                       button.className = 'icon icon16 fa-times';
-                       button.addEventListener('click', _callbackRemoveItem);
-                       listItem.appendChild(content);
-                       listItem.appendChild(button);
-                       
-                       data.list.insertBefore(listItem, data.listItem);
-                       data.suggestion.addExcludedValue(value.value);
-                       data.element.value = '';
-                       
-                       this._handleLimit(elementId);
-                       var values = this._syncShadow(data);
-                       
-                       if (typeof data.options.callbackChange === 'function') {
-                               if (values === null) values = this.getValues(elementId);
-                               data.options.callbackChange(elementId, values);
-                       }
-               },
-               
-               /**
-                * Removes an item from the list.
-                * 
-                * @param       {?object}       event           event object
-                * @param       {Element=}      item            list item
-                */
-               _removeItem: function(event, item) {
-                       item = (event === null) ? item : event.currentTarget.parentNode;
-                       
-                       var parent = item.parentNode;
-                       var elementId = parent.getAttribute('data-element-id');
-                       var data = _data.get(elementId);
-                       
-                       data.suggestion.removeExcludedValue(item.children[0].textContent);
-                       parent.removeChild(item);
-                       data.element.focus();
-                       
-                       this._handleLimit(elementId);
-                       var values = this._syncShadow(data);
-                       
-                       if (typeof data.options.callbackChange === 'function') {
-                               if (values === null) values = this.getValues(elementId);
-                               data.options.callbackChange(elementId, values);
-                       }
-               },
-               
-               /**
-                * Synchronizes the shadow input field with the current list item values.
-                * 
-                * @param       {object}        data            element data
-                */
-               _syncShadow: function(data) {
-                       if (!data.options.isCSV) return null;
-                       
-                       var value = '', values = this.getValues(data.element.id);
-                       for (var i = 0, length = values.length; i < length; i++) {
-                               value += (value.length ? ',' : '') + values[i].value;
-                       }
-                       
-                       data.shadow.value = value;
-                       
-                       return values;
-               }
-       };
-       
-       return UIItemList;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList/User.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/ItemList/User.js
deleted file mode 100644 (file)
index 739ddc2..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-/**
- * Provides an item list for users and groups.
- * 
- * @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/ItemList/User
- */
-define(['WoltLab/WCF/UI/ItemList'], function(UIItemList) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/UI/ItemList/User
-        */
-       var UIItemListUser = {
-               /**
-                * Initializes user suggestion support for an element.
-                * 
-                * @param       {string}        elementId       input element id
-                * @param       {object}        options         option list
-                */
-               init: function(elementId, options) {
-                       UIItemList.init(elementId, [], {
-                               ajax: {
-                                       className: 'wcf\\data\\user\\UserAction',
-                                       parameters: {
-                                               data: {
-                                                       includeUserGroups: ~~options.includeUserGroups
-                                               }
-                                       }
-                               },
-                               callbackChange: (typeof options.callbackChange === 'function' ? options.callbackChange : null),
-                               excludedSearchValues: (Array.isArray(options.excludedSearchValues) ? options.excludedSearchValues : []),
-                               isCSV: true,
-                               maxItems: ~~options.maxItems || -1,
-                               restricted: true
-                       });
-               },
-               
-               /**
-                * @see WoltLab/WCF/UI/ItemList::getValues()
-                */
-               getValues: function(elementId) {
-                       return UIItemList.getValues(elementId);
-               }
-       };
-       
-       return UIItemListUser;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Mobile.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Mobile.js
deleted file mode 100644 (file)
index 288a827..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-/**
- * Modifies the interface to provide a better usability for mobile devices.
- * 
- * @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/Mobile
- */
-define(
-       [       'enquire', 'Environment', 'Language', 'DOM/ChangeListener', 'DOM/Traverse', 'UI/CloseOverlay'],
-       function(enquire,   Environment,   Language,   DOMChangeListener,    DOMTraverse,    UICloseOverlay)
-{
-       "use strict";
-       
-       var _buttonGroupNavigations = null;
-       var _enabled = false;
-       var _main = null;
-       var _sidebar = null;
-       
-       /**
-        * @exports     WoltLab/WCF/UI/Mobile
-        */
-       var UIMobile = {
-               /**
-                * Initializes the mobile UI using enquire.js.
-                */
-               setup: function() {
-                       _buttonGroupNavigations = document.getElementsByClassName('buttonGroupNavigation');
-                       _main = document.getElementById('main');
-                       _sidebar = _main.querySelector('#main > div > div > .sidebar');
-                       
-                       if (Environment.touch()) {
-                               document.documentElement.classList.add('touch');
-                       }
-                       
-                       if (Environment.platform() !== 'desktop') {
-                               document.documentElement.classList.add('mobile');
-                       }
-                       
-                       enquire.register('screen and (max-width: 800px)', {
-                               match: this.enable.bind(this),
-                               unmatch: this.disable.bind(this),
-                               setup: this._init.bind(this),
-                               deferSetup: true
-                       });
-                       
-                       if (Environment.browser() === 'microsoft' && _sidebar !== null && _sidebar.clientWidth > 305) {
-                               this._fixSidebarIE();
-                       }
-               },
-               
-               /**
-                * Enables the mobile UI.
-                */
-               enable: function() {
-                       _enabled = true;
-                       
-                       if (Environment.browser() === 'microsoft') this._fixSidebarIE();
-               },
-               
-               /**
-                * Disables the mobile UI.
-                */
-               disable: function() {
-                       _enabled = false;
-                       
-                       if (Environment.browser() === 'microsoft') this._fixSidebarIE();
-               },
-               
-               _fixSidebarIE: function() {
-                       if (_sidebar === null) return;
-                       
-                       // sidebar is rarely broken on IE9/IE10
-                       _sidebar.style.setProperty('display', 'none');
-                       _sidebar.style.removeProperty('display');
-               },
-               
-               _init: function() {
-                       this._initSidebarToggleButtons();
-                       this._initSearchBar();
-                       this._initButtonGroupNavigation();
-                       
-                       UICloseOverlay.add('WoltLab/WCF/UI/Mobile', this._closeAllMenus.bind(this));
-                       DOMChangeListener.add('WoltLab/WCF/UI/Mobile', this._initButtonGroupNavigation.bind(this));
-               },
-               
-               _initSidebarToggleButtons: function() {
-                       if (_sidebar === null) return;
-                       
-                       var sidebarPosition = (_main.classList.contains('sidebarOrientationLeft')) ? 'Left' : '';
-                       sidebarPosition = (sidebarPosition) ? sidebarPosition : (_main.classList.contains('sidebarOrientationRight') ? 'Right' : '');
-                       
-                       if (!sidebarPosition) {
-                               return;
-                       }
-                       
-                       // use icons if language item is empty/non-existant
-                       var languageShowSidebar = 'wcf.global.sidebar.show' + sidebarPosition + 'Sidebar';
-                       if (languageShowSidebar === Language.get(languageShowSidebar) || Language.get(languageShowSidebar) === '') {
-                               languageShowSidebar = document.createElement('span');
-                               languageShowSidebar.className = 'icon icon16 fa-angle-double-' + sidebarPosition.toLowerCase();
-                       }
-                       
-                       var languageHideSidebar = 'wcf.global.sidebar.hide' + sidebarPosition + 'Sidebar';
-                       if (languageHideSidebar === Language.get(languageHideSidebar) || Language.get(languageHideSidebar) === '') {
-                               languageHideSidebar = document.createElement('span');
-                               languageHideSidebar.className = 'icon icon16 fa-angle-double-' + (sidebarPosition === 'Left' ? 'right' : 'left');
-                       }
-                       
-                       // add toggle buttons
-                       var showSidebar = document.createElement('span');
-                       showSidebar.className = 'button small mobileSidebarToggleButton';
-                       showSidebar.addEventListener('click', function() { _main.classList.add('mobileShowSidebar'); });
-                       if (languageShowSidebar instanceof Element) showSidebar.appendChild(languageShowSidebar);
-                       else showSidebar.textContent = languageShowSidebar;
-                       
-                       var hideSidebar = document.createElement('span');
-                       hideSidebar.className = 'button small mobileSidebarToggleButton';
-                       hideSidebar.addEventListener('click', function() { _main.classList.remove('mobileShowSidebar'); });
-                       if (languageHideSidebar instanceof Element) hideSidebar.appendChild(languageHideSidebar);
-                       else hideSidebar.textContent = languageHideSidebar;
-                       
-                       document.querySelector('.content').appendChild(showSidebar);
-                       _sidebar.appendChild(hideSidebar);
-               },
-               
-               _initSearchBar: function() {
-                       var _searchBar = document.querySelector('.searchBar');
-                       
-                       _searchBar.addEventListener('click', function() {
-                               if (_enabled) {
-                                       _searchBar.classList.add('searchBarOpen');
-                                       
-                                       return false;
-                               }
-                               
-                               return false;
-                       });
-                       
-                       _main.addEventListener('click', function() { _searchBar.classList.remove('searchBarOpen'); });
-               },
-               
-               _initButtonGroupNavigation: function() {
-                       for (var i = 0, length = _buttonGroupNavigations.length; i < length; i++) {
-                               var navigation = _buttonGroupNavigations[i];
-                               
-                               if (navigation.classList.contains('jsMobileButtonGroupNavigation')) continue;
-                               else navigation.classList.add('jsMobileButtonGroupNavigation');
-                               
-                               var button = document.createElement('a');
-                               button.classList.add('dropdownLabel');
-                               
-                               var span = document.createElement('span');
-                               span.className = 'icon icon24 fa-list';
-                               button.appendChild(span);
-                               
-                               button.addEventListener('click', function(ev) {
-                                       var next = DOMTraverse.next(button);
-                                       if (next !== null) {
-                                               next.classList.toggle('open');
-                                               
-                                               ev.stopPropagation();
-                                               return false;
-                                       }
-                                       
-                                       return true;
-                               });
-                               
-                               navigation.insertBefore(button, navigation.firstChild);
-                       }
-               },
-               
-               _closeAllMenus: function() {
-                       var openMenus = document.querySelectorAll('.jsMobileButtonGroupNavigation > ul.open');
-                       for (var i = 0, length = openMenus.length; i < length; i++) {
-                               openMenus[i].classList.remove('open');
-                       }
-               }
-       };
-       
-       return UIMobile;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Suggestion.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Suggestion.js
deleted file mode 100644 (file)
index 8a04097..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/**
- * Flexible UI element featuring both a list of items and an input field with suggestion support.
- * 
- * @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/Suggestion
- */
-define(['Ajax', 'Core', 'UI/SimpleDropdown'], function(Ajax, Core, UISimpleDropdown) {
-       "use strict";
-       
-       /**
-        * @constructor
-        * @param       {string}                elementId       input element id
-        * @param       {object<mixed>}         options         option list
-        */
-       function UISuggestion(elementId, options) { this.init(elementId, options); };
-       UISuggestion.prototype = {
-               /**
-                * Initializes a new suggestion input.
-                * 
-                * @param       {string}                element id      input element id
-                * @param       {object<mixed>}         options         option list
-                */
-               init: function(elementId, options) {
-                       this._dropdownMenu = null;
-                       this._value = '';
-                       
-                       this._element = document.getElementById(elementId);
-                       if (this._element === null) {
-                               throw new Error("Expected a valid element id.");
-                       }
-                       
-                       this._options = Core.extend({
-                               ajax: {
-                                       actionName: 'getSearchResultList',
-                                       className: '',
-                                       interfaceName: 'wcf\\data\\ISearchAction',
-                                       parameters: {
-                                               data: {}
-                                       }
-                               },
-                               
-                               // will be executed once a value from the dropdown has been selected
-                               callbackSelect: null,
-                               // list of excluded search values
-                               excludedSearchValues: [],
-                               // minimum number of characters required to trigger a search request
-                               treshold: 3
-                       }, options);
-                       
-                       if (typeof this._options.callbackSelect !== 'function') {
-                               throw new Error("Expected a valid callback for option 'callbackSelect'.");
-                       }
-                       
-                       this._element.addEventListener('click', function(event) { event.stopPropagation(); });
-                       this._element.addEventListener('keydown', this._keyDown.bind(this));
-                       this._element.addEventListener('keyup', this._keyUp.bind(this));
-               },
-               
-               /**
-                * Adds an excluded search value.
-                * 
-                * @param       {string}        value           excluded value
-                */
-               addExcludedValue: function(value) {
-                       if (this._options.excludedSearchValues.indexOf(value) === -1) {
-                               this._options.excludedSearchValues.push(value);
-                       }
-               },
-               
-               /**
-                * Removes an excluded search value.
-                * 
-                * @param       {string}        value           excluded value
-                */
-               removeExcludedValue: function(value) {
-                       var index = this._options.excludedSearchValues.indexOf(value);
-                       if (index !== -1) {
-                               this._options.excludedSearchValues.splice(index, 1);
-                       }
-               },
-               
-               /**
-                * Handles the keyboard navigation for interaction with the suggestion list.
-                * 
-                * @param       {object}        event           event object
-                */
-               _keyDown: function(event) {
-                       if (this._dropdownMenu === null || !UISimpleDropdown.isOpen(this._element.id)) {
-                               return true;
-                       }
-                       
-                       if (event.keyCode !== 13 && event.keyCode !== 27 && event.keyCode !== 38 && event.keyCode !== 40) {
-                               return true;
-                       }
-                       
-                       var active, i = 0, length = this._dropdownMenu.childElementCount;
-                       while (i < length) {
-                               active = this._dropdownMenu.children[i];
-                               if (active.classList.contains('active')) {
-                                       break;
-                               }
-                               
-                               i++;
-                       }
-                       
-                       if (event.keyCode === 13) {
-                               // Enter
-                               UISimpleDropdown.close(this._element.id);
-                               
-                               this._select(active);
-                       }
-                       else if (event.keyCode === 27) {
-                               if (UISimpleDropdown.isOpen(this._element.id)) {
-                                       UISimpleDropdown.close(this._element.id);
-                               }
-                               else {
-                                       // let the event pass through
-                                       return true;
-                               }
-                       }
-                       else {
-                               var index = 0;
-                               
-                               if (event.keyCode === 38) {
-                                       // ArrowUp
-                                       index = ((i === 0) ? length : i) - 1;
-                               }
-                               else if (event.keyCode === 40) {
-                                       // ArrowDown
-                                       index = i + 1;
-                                       if (index === length) index = 0;
-                               }
-                               
-                               if (index !== i) {
-                                       active.classList.remove('active');
-                                       this._dropdownMenu.children[index].classList.add('active');
-                               }
-                       }
-                       
-                       event.preventDefault();
-                       return false;
-               },
-               
-               /**
-                * Selects an item from the list.
-                * 
-                * @param       {(Element|Event)}       item    list item or event object
-                */
-               _select: function(item) {
-                       var isEvent = (item instanceof Event);
-                       if (isEvent) {
-                               item = item.currentTarget.parentNode;
-                       }
-                       
-                       this._options.callbackSelect(this._element.id, { objectId: item.children[0].getAttribute('data-object-id'), value: item.textContent });
-                       
-                       if (isEvent) {
-                               this._element.focus();
-                       }
-               },
-               
-               /**
-                * Performs a search for the input value unless it is below the treshold.
-                * 
-                * @param       {object}                event           event object
-                */
-               _keyUp: function(event) {
-                       var value = event.currentTarget.value.trim();
-                       
-                       if (this._value === value) {
-                               return;
-                       }
-                       else if (value.length < this._options.treshold) {
-                               if (this._dropdownMenu !== null) {
-                                       UISimpleDropdown.close(this._element.id);
-                               }
-                               
-                               this._value = value;
-                               
-                               return;
-                       }
-                       
-                       this._value = value;
-                       
-                       Ajax.api(this, {
-                               parameters: {
-                                       data: {
-                                               excludedSearchValues: this._options.excludedSearchValues,
-                                               searchString: value
-                                       }
-                               }
-                       });
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: this._options.ajax
-                       };
-               },
-               
-               /**
-                * Handles successful Ajax requests.
-                * 
-                * @param       {object}        data            response values
-                */
-               _ajaxSuccess: function(data) {
-                       if (this._dropdownMenu === null) {
-                               this._dropdownMenu = document.createElement('div');
-                               this._dropdownMenu.className = 'dropdownMenu';
-                               
-                               UISimpleDropdown.initFragment(this._element, this._dropdownMenu);
-                       }
-                       else {
-                               this._dropdownMenu.innerHTML = '';
-                       }
-                       
-                       if (data.returnValues.length) {
-                               var anchor, item, listItem;
-                               for (var i = 0, length = data.returnValues.length; i < length; i++) {
-                                       item = data.returnValues[i];
-                                       
-                                       anchor = document.createElement('a');
-                                       anchor.textContent = item.label;
-                                       anchor.setAttribute('data-object-id', item.objectID);
-                                       anchor.addEventListener('click', this._select.bind(this));
-                                       
-                                       listItem = document.createElement('li');
-                                       if (i === 0) listItem.className = 'active';
-                                       listItem.appendChild(anchor);
-                                       
-                                       this._dropdownMenu.appendChild(listItem);
-                               }
-                               
-                               UISimpleDropdown.open(this._element.id);
-                       }
-                       else {
-                               UISimpleDropdown.close(this._element.id);
-                       }
-               }
-       };
-       
-       return UISuggestion;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu.js
deleted file mode 100644 (file)
index b3f49a7..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Common interface for tab menu access.
- * 
- * @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/TabMenu
- */
-define(['Dictionary', 'DOM/ChangeListener', 'DOM/Util', './TabMenu/Simple'], function(Dictionary, DOMChangeListener, DOMUtil, SimpleTabMenu) {
-       "use strict";
-       
-       var _tabMenus = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/UI/TabMenu
-        */
-       var UITabMenu = {
-               /**
-                * Sets up tab menus and binds listeners.
-                */
-               setup: function() {
-                       this._init();
-                       this._selectErroneousTabs();
-                       
-                       DOMChangeListener.add('WoltLab/WCF/UI/TabMenu', this._init.bind(this));
-               },
-               
-               /**
-                * Initializes available tab menus.
-                */
-               _init: function() {
-                       var tabMenus = document.querySelectorAll('.tabMenuContainer:not(.staticTabMenuContainer)');
-                       for (var i = 0, length = tabMenus.length; i < length; i++) {
-                               var container = tabMenus[i];
-                               var containerId = DOMUtil.identify(container);
-                               
-                               if (_tabMenus.has(containerId)) {
-                                       continue;
-                               }
-                               
-                               var tabMenu = new SimpleTabMenu(containerId, container);
-                               if (tabMenu.validate()) {
-                                       tabMenu.init();
-                                       
-                                       _tabMenus.set(containerId, tabMenu);
-                               }
-                       }
-               },
-               
-               /**
-                * Selects the first tab containing an element with class `formError`.
-                */
-               _selectErroneousTabs: function() {
-                       _tabMenus.forEach(function(tabMenu) {
-                               var foundError = false;
-                               tabMenu.getContainers().forEach(function(container) {
-                                       if (!foundError && container.getElementsByClassName('formError').length) {
-                                               foundError = true;
-                                               
-                                               tabMenu.select(container.id);
-                                       }
-                               });
-                       });
-               },
-               
-               /**
-                * Returns a SimpleTabMenu instance for given container id.
-                * 
-                * @param       {string}        containerId     tab menu container id
-                * @return      {(SimpleTabMenu|undefined)}     tab menu object
-                */
-               getTabMenu: function(containerId) {
-                       return _tabMenus.get(containerId);
-               }
-       };
-       
-       return UITabMenu;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/TabMenu/Simple.js
deleted file mode 100644 (file)
index 027e86b..0000000
+++ /dev/null
@@ -1,316 +0,0 @@
-/**
- * Simple tab menu implementation with a straight-forward logic.
- * 
- * @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/TabMenu/Simple
- */
-define(['Dictionary', 'DOM/Traverse', 'DOM/Util', 'EventHandler'], function(Dictionary, DOMTraverse, DOMUtil, EventHandler) {
-       "use strict";
-       
-       /**
-        * @param       {string}        containerId     container id
-        * @param       {Element}       container       container element
-        * @constructor
-        */
-       function TabMenuSimple(containerId, container) {
-               this._container = container;
-               this._containers = new Dictionary();
-               this._containerId = containerId;
-               this._isLegacy = null;
-               this._isParent = false;
-               this._parent = null;
-               this._tabs = new Dictionary();
-       };
-       
-       TabMenuSimple.prototype = {
-               /**
-                * Validates the properties and DOM structure of this container.
-                * 
-                * Expected DOM:
-                * <div class="tabMenuContainer">
-                *      <nav>
-                *              <ul>
-                *                      <li data-name="foo"><a>bar</a></li>
-                *              </ul>
-                *      </nav>
-                *      
-                *      <div id="foo">baz</div>
-                * </div>
-                * 
-                * @return      {boolean}       false if any properties are invalid or the DOM does not match the expectations
-                */
-               validate: function() {
-                       if (!this._container.classList.contains('tabMenuContainer')) {
-                               return false;
-                       }
-                       
-                       var nav = DOMTraverse.childByTag(this._container, 'NAV');
-                       if (nav === null) {
-                               return false;
-                       }
-                       
-                       // get children
-                       var tabs = nav.getElementsByTagName('li');
-                       if (tabs.length === null) {
-                               return false;
-                       }
-                       
-                       var containers = DOMTraverse.childrenByTag(this._container, 'DIV');
-                       for (var i = 0, length = containers.length; i < length; i++) {
-                               var container = containers[i];
-                               var name = container.getAttribute('data-name');
-                               
-                               if (!name) {
-                                       name = DOMUtil.identify(container);
-                               }
-                               
-                               container.setAttribute('data-name', name);
-                               this._containers.set(name, container);
-                       }
-                       
-                       for (var i = 0, length = tabs.length; i < length; i++) {
-                               var tab = tabs[i];
-                               var name = this._getTabName(tab);
-                               
-                               if (!name) {
-                                       continue;
-                               }
-                               
-                               if (this._tabs.has(name)) {
-                                       throw new Error("Tab names must be unique, li[data-name='" + name + "'] (tab menu id: '" + this._containerId + "') exists more than once.");
-                               }
-                               
-                               var container = this._containers.get(name);
-                               if (container === undefined) {
-                                       throw new Error("Expected content element for li[data-name='" + name + "'] (tab menu id: '" + this._containerId + "').");
-                               }
-                               else if (container.parentNode !== this._container) {
-                                       throw new Error("Expected content element '" + name + "' (tab menu id: '" + this._containerId + "') to be a direct children.");
-                               }
-                               
-                               // check if tab holds exactly one children which is an anchor element
-                               if (tab.childElementCount !== 1 || tab.children[0].nodeName !== 'A') {
-                                       throw new Error("Expected exactly one <a> as children for li[data-name='" + name + "'] (tab menu id: '" + this._containerId + "').");
-                               }
-                               
-                               this._tabs.set(name, tab);
-                       }
-                       
-                       if (!this._tabs.size) {
-                               throw new Error("Expected at least one tab (tab menu id: '" + this._containerId + "').");
-                       }
-                       
-                       if (this._isLegacy) {
-                               this._container.setAttribute('data-is-legacy', true);
-                               
-                               this._tabs.forEach(function(tab, name) {
-                                       tab.setAttribute('aria-controls', name);
-                               });
-                       }
-                       
-                       return true;
-               },
-               
-               /**
-                * Initializes this tab menu.
-                * 
-                * @param       {Dictionary=}   oldTabs         previous list of tabs
-                */
-               init: function(oldTabs) {
-                       oldTabs = oldTabs || null;
-                       
-                       // bind listeners
-                       this._tabs.forEach((function(tab) {
-                               if (oldTabs === null || oldTabs.get(tab.getAttribute('data-name')) !== tab) {
-                                       tab.children[0].addEventListener('click', this._onClick.bind(this));
-                               }
-                       }).bind(this));
-                       
-                       if (oldTabs === null) {
-                               var preselect = this._container.getAttribute('data-preselect');
-                               if (preselect === "true" || preselect === null || preselect === "") preselect = true;
-                               if (preselect === "false") preselect = false;
-                               
-                               this._containers.forEach(function(container) {
-                                       container.classList.add('hidden');
-                               });
-                               
-                               if (preselect !== false) {
-                                       if (preselect !== true) {
-                                               var tab = this._tabs.get(preselect);
-                                               if (tab !== undefined) {
-                                                       this.select(null, tab, true);
-                                               }
-                                       }
-                                       else {
-                                               var selectTab = null;
-                                               this._tabs.forEach(function(tab) {
-                                                       if (selectTab === null && tab.previousElementSibling === null) {
-                                                               selectTab = tab; 
-                                                       }
-                                               });
-                                               
-                                               if (selectTab !== null) {
-                                                       this.select(null, selectTab, true);
-                                               }
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Selects a tab.
-                * 
-                * @param       {?(string|integer)}     name            tab name or sequence no
-                * @param       {Element=}              tab             tab element
-                * @param       {boolean=}              disableEvent    suppress event handling
-                */
-               select: function(name, tab, disableEvent) {
-                       tab = tab || this._tabs.get(name) || null;
-                       
-                       if (tab === null) {
-                               // check if name is an integer
-                               if (~~name == name) {
-                                       name = ~~name;
-                                       
-                                       var i = 0;
-                                       this._tabs.forEach(function(item) {
-                                               if (i === name) {
-                                                       tab = item;
-                                               }
-                                               
-                                               i++;
-                                       });
-                               }
-                               
-                               if (tab === null) {
-                                       throw new Error("Expected a valid tab name, '" + name + "' given (tab menu id: '" + this._containerId + "').");
-                               }
-                       }
-                       
-                       if (!name) name = tab.getAttribute('data-name');
-                       
-                       // unmark active tab
-                       var oldTab = document.querySelector('#' + this._containerId + ' > nav > ul > li.active');
-                       var oldContent = null;
-                       if (oldTab !== null) {
-                               oldTab.classList.remove('active');
-                               oldContent = this._containers.get(oldTab.getAttribute('data-name'));
-                               oldContent.classList.remove('active');
-                               oldContent.classList.add('hidden');
-                               
-                               if (this._isLegacy) {
-                                       oldTab.classList.remove('ui-state-active');
-                                       oldContent.classList.remove('ui-state-active');
-                               }
-                       }
-                       
-                       tab.classList.add('active');
-                       var newContent = this._containers.get(name);
-                       newContent.classList.add('active');
-                       
-                       if (this._isLegacy) {
-                               tab.classList.add('ui-state-active');
-                               newContent.classList.add('ui-state-active');
-                               newContent.classList.remove('hidden');
-                       }
-                       
-                       if (disableEvent !== true) {
-                               EventHandler.fire('com.woltlab.wcf.simpleTabMenu_' + this._containerId, 'select', {
-                                       active: tab,
-                                       activeName: name,
-                                       previous: oldTab,
-                                       previousName: oldTab.getAttribute('data-name')
-                               });
-                               
-                               if (this._isLegacy && typeof window.jQuery === 'function') {
-                                       // simulate jQuery UI Tabs event
-                                       window.jQuery(this._container).trigger('wcftabsbeforeactivate', {
-                                               newTab: window.jQuery(tab),
-                                               oldTab: window.jQuery(oldTab),
-                                               newPanel: window.jQuery(newContent),
-                                               oldPanel: window.jQuery(oldContent)
-                                       });
-                               }
-                       }
-               },
-               
-               /**
-                * Rebuilds all tabs, must be invoked after adding or removing of tabs.
-                * 
-                * Warning: Do not remove tabs if you plan to add these later again or at least clone the nodes
-                *          to prevent issues with already bound event listeners. Consider hiding them via CSS.
-                */
-               rebuild: function() {
-                       var oldTabs = this._tabs;
-                       
-                       this.validate();
-                       this.init(oldTabs);
-               },
-               
-               /**
-                * Handles clicks on a tab.
-                * 
-                * @param       {object}        event   event object
-                */
-               _onClick: function(event) {
-                       event.preventDefault();
-                       
-                       var tab = event.currentTarget.parentNode;
-                       
-                       this.select(null, tab);
-               },
-               
-               /**
-                * Returns the tab name.
-                * 
-                * @param       {Element}       tab     tab element
-                * @return      {string}        tab name
-                */
-               _getTabName: function(tab) {
-                       var name = tab.getAttribute('data-name');
-                       
-                       // handle legacy tab menus
-                       if (!name) {
-                               if (tab.childElementCount === 1 && tab.children[0].nodeName === 'A') {
-                                       var href = tab.children[0].getAttribute('href');
-                                       if (href.match(/#([^#]+)$/)) {
-                                               name = RegExp.$1;
-                                               
-                                               if (document.getElementById(name) === null) {
-                                                       name = null;
-                                               }
-                                               else {
-                                                       this._isLegacy = true;
-                                                       tab.setAttribute('data-name', name);
-                                               }
-                                       }
-                               }
-                       }
-                       
-                       return name;
-               },
-               
-               /**
-                * Returns the list of registered content containers.
-                * 
-                * @returns     {Dictionary}    content containers
-                */
-               getContainers: function() {
-                       return this._containers;
-               },
-               
-               /**
-                * Returns the list of registered tabs.
-                * 
-                * @returns     {Dictionary}    tab items
-                */
-               getTabs: function() {
-                       return this._tabs;
-               }
-       };
-       
-       return TabMenuSimple;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Tooltip.js
deleted file mode 100644 (file)
index 5ba1439..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-/**
- * 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(['Environment', 'DOM/ChangeListener', 'UI/Alignment'], function(Environment, DOMChangeListener, UIAlignment) {
-       "use strict";
-       
-       var _elements = null;
-       var _pointer = null;
-       var _text = null;
-       var _tooltip = null;
-       
-       /**
-        * @exports     WoltLab/WCF/UI/Tooltip
-        */
-       var UITooltip = {
-               /**
-                * Initializes the tooltip element and binds event listener.
-                */
-               setup: function() {
-                       if (Environment.platform() !== 'desktop') 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();
-                       
-                       DOMChangeListener.add('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');
-                               title = (typeof title === 'string') ? title.trim() : '';
-                               
-                               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');
-                       title = (typeof title === 'string') ? title.trim() : '';
-                       
-                       if (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'],
-                               vertical: 'top'
-                       });
-               },
-               
-               /**
-                * Hides the tooltip once the mouse leaves the element.
-                * 
-                * @param       {object}        event   event object
-                */
-               _mouseLeave: function(event) {
-                       _tooltip.classList.remove('active');
-               }
-       };
-       
-       return UITooltip;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Alignment.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Alignment.js
new file mode 100644 (file)
index 0000000..13e79f8
--- /dev/null
@@ -0,0 +1,246 @@
+/**
+ * Utility class to align elements relatively to another.
+ * 
+ * @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/Alignment
+ */
+define(['Core', 'Language', 'DOM/Traverse', 'DOM/Util'], function(Core, Language, DOMTraverse, DOMUtil) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLab/WCF/UI/Alignment
+        */
+       var UIAlignment = {
+               /**
+                * Sets the alignment for target element relatively to the reference element.
+                * 
+                * @param       {Element}               el              target element
+                * @param       {Element}               ref             reference element
+                * @param       {object<string, *>}     options         list of options to alter the behavior
+                */
+               set: function(el, ref, options) {
+                       options = Core.extend({
+                               // offset to reference element
+                               verticalOffset: 7,
+                               
+                               // align the pointer element, expects .elementPointer 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/center and top/bottom
+                               horizontal: 'left',
+                               vertical: 'bottom',
+                               
+                               // allow flipping over axis, possible values: both, horizontal, vertical and none
+                               allowFlip: 'both'
+                       }, options);
+                       
+                       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.vertical = '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 !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 = 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 (Language.get('wcf.global.pageDirection') === 'rtl') {
+                               options.horizontal = (options.horizontal === 'left') ? 'right' : 'left';
+                       }
+                       
+                       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;
+                                       }
+                               }
+                       }
+                       
+                       var left = horizontal.left;
+                       var right = horizontal.right;
+                       
+                       var vertical = this._tryAlignmentVertical(options.vertical, elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
+                       if (!vertical.result && (options.allowFlip === 'both' || options.allowFlip === 'vertical')) {
+                               var verticalFlipped = this._tryAlignmentVertical((options.vertical === 'top' ? 'bottom' : 'top'), elDimensions, refDimensions, refOffsets, windowHeight, options.verticalOffset);
+                               // only use these results if it fits into the boundaries, otherwise both directions exceed and we honor the demanded direction
+                               if (verticalFlipped.result) {
+                                       vertical = verticalFlipped;
+                               }
+                       }
+                       
+                       var bottom = vertical.bottom;
+                       var top = vertical.top;
+                       
+                       // set pointer position
+                       if (options.pointer) {
+                               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;
+                               var pointerBottom = 1;
+                               
+                               el.classList[(top === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerBottom]);
+                               el.classList[(left === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerRight]);
+                       }
+                       
+                       DOMUtil.setStyles(el, {
+                               bottom: bottom + (bottom !== 'auto' ? 'px' : ''),
+                               left: left + (left !== 'auto' ? 'px' : ''),
+                               right: right + (right !== 'auto' ? 'px' : ''),
+                               top: top + (top !== 'auto' ? 'px' : '')
+                       });
+               },
+               
+               /**
+                * Calculates left/right position and verifys if the element would be still within the page's boundaries.
+                * 
+                * @param       {string}                        align           align to this side of the reference element
+                * @param       {object<string, integer>}       elDimensions    element dimensions
+                * @param       {object<string, integer>}       refDimensions   reference element dimensions
+                * @param       {object<string, integer>}       refOffsets      position of reference element relative to the document
+                * @param       {integer}                       windowWidth     window width
+                * @returns     {object<string, *>}     calculation results
+                */
+               _tryAlignmentHorizontal: function(align, elDimensions, refDimensions, refOffsets, windowWidth) {
+                       var left = 'auto';
+                       var right = 'auto';
+                       var result = true;
+                       
+                       if (align === 'left') {
+                               left = refOffsets.left;
+                               if (left + elDimensions.width > windowWidth) {
+                                       result = false;
+                               }
+                       }
+                       else if (align === 'right') {
+                               right = windowWidth - (refOffsets.left + refDimensions.width);
+                               if (right < 0) {
+                                       result = false;
+                               }
+                       }
+                       else {
+                               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
+                       };
+               },
+               
+               /**
+                * Calculates top/bottom position and verifys if the element would be still within the page's boundaries.
+                * 
+                * @param       {string}                        align           align to this side of the reference element
+                * @param       {object<string, integer>}       elDimensions    element dimensions
+                * @param       {object<string, integer>}       refDimensions   reference element dimensions
+                * @param       {object<string, integer>}       refOffsets      position of reference element relative to the document
+                * @param       {integer}                       windowHeight    window height
+                * @param       {integer}                       verticalOffset  desired gap between element and reference element
+                * @returns     {object<string, *>}     calculation results
+                */
+               _tryAlignmentVertical: function(align, elDimensions, refDimensions, refOffsets, windowHeight, verticalOffset) {
+                       var bottom = 'auto';
+                       var top = 'auto';
+                       var result = true;
+                       
+                       if (align === 'top') {
+                               var bodyHeight = document.body.clientHeight;
+                               bottom = (bodyHeight - refOffsets.top) + verticalOffset;
+                               if (bodyHeight - (bottom + elDimensions.height) < document.body.scrollTop) {
+                                       result = false;
+                               }
+                       }
+                       else {
+                               top = refOffsets.top + refDimensions.height + verticalOffset;
+                               if (top + elDimensions.height > windowHeight) {
+                                       result = false;
+                               }
+                       }
+                       
+                       return {
+                               align: align,
+                               bottom: bottom,
+                               top: top,
+                               result: result
+                       };
+               }
+       };
+       
+       return UIAlignment;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/CloseOverlay.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/CloseOverlay.js
new file mode 100644 (file)
index 0000000..fe89092
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Allows to be informed when a click event bubbled up to the document's body.
+ * 
+ * @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/CloseOlveray
+ */
+define(['CallbackList'], function(CallbackList) {
+       "use strict";
+       
+       var _callbackList = new CallbackList();
+       
+       /**
+        * @exports     WoltLab/WCF/UI/CloseOverlay
+        */
+       var UICloseOverlay = {
+               /**
+                * Sets up global event listener for bubbled clicks events.
+                */
+               setup: function() {
+                       document.body.addEventListener('click', this.execute.bind(this));
+               },
+               
+               /**
+                * @see WoltLab/WCF/CallbackList#add
+                */
+               add: _callbackList.add.bind(_callbackList),
+               
+               /**
+                * @see WoltLab/WCF/CallbackList#remove
+                */
+               remove: _callbackList.remove.bind(_callbackList),
+               
+               /**
+                * Invokes all registered callbacks.
+                */
+               execute: function() {
+                       _callbackList.forEach(null, function(callback) {
+                               callback();
+                       });
+               }
+       };
+       
+       UICloseOverlay.setup();
+       
+       return UICloseOverlay;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Collapsible/Sidebar.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Collapsible/Sidebar.js
new file mode 100644 (file)
index 0000000..79d8f01
--- /dev/null
@@ -0,0 +1,88 @@
+/**
+ * Provides the sidebar toggle button.
+ * 
+ * @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/Collapsible/Sidebar
+ */
+define(['Ajax', 'Language', 'DOM/Util'], function(Ajax, Language, DOMUtil) {
+       "use strict";
+       
+       var _isOpen = false;
+       var _main = null;
+       var _name = '';
+       
+       /**
+        * @module      WoltLab/WCF/UI/Collapsible/Sidebar
+        */
+       var UICollapsibleSidebar = {
+               /**
+                * Sets up the toggle button.
+                */
+               setup: function() {
+                       var sidebar = document.querySelector('.sidebar');
+                       if (sidebar === null) {
+                               return;
+                       }
+                       
+                       _isOpen = (sidebar.getAttribute('data-is-open') === 'true');
+                       _main = document.getElementById('main');
+                       _name = sidebar.getAttribute('data-sidebar-name');
+                       
+                       this._createUI(sidebar);
+                       
+                       _main.classList[(_isOpen ? 'remove' : 'add')]('sidebarCollapsed');
+               },
+               
+               /**
+                * Creates the toggle button.
+                * 
+                * @param       {Element}       sidebar         sidebar element
+                */
+               _createUI: function(sidebar) {
+                       var button = document.createElement('a');
+                       button.href = '#';
+                       button.className = 'collapsibleButton jsTooltip';
+                       button.setAttribute('title', Language.get('wcf.global.button.collapsible'));
+                       
+                       var span = document.createElement('span');
+                       span.appendChild(button);
+                       DOMUtil.prepend(span, sidebar);
+                       
+                       button.addEventListener('click', this._click.bind(this));
+               },
+               
+               /**
+                * Toggles the sidebar on click.
+                * 
+                * @param       {object}        event           event object
+                */
+               _click: function(event) {
+                       event.preventDefault();
+                       
+                       _isOpen = (_isOpen === false);
+                       
+                       Ajax.api(this, {
+                               isOpen: ~~_isOpen
+                       });
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'toggle',
+                                       className: 'wcf\\system\\user\\collapsible\\content\\UserCollapsibleSidebarHandler',
+                                       sidebarName: _name
+                               },
+                               url: 'index.php/AJAXInvoke/?t=' + SECURITY_TOKEN + SID_ARG_2ND
+                       };
+               },
+               
+               _ajaxSuccess: function(data) {
+                       _main.classList[(_isOpen ? 'remove' : 'add')]('sidebarCollapsed');
+               }
+       };
+       
+       return UICollapsibleSidebar;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Confirmation.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Confirmation.js
new file mode 100644 (file)
index 0000000..ec32bcd
--- /dev/null
@@ -0,0 +1,169 @@
+/**
+ * Provides the confirmation dialog overlay.
+ * 
+ * @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/Confirmation
+ */
+define(['Core', 'Language', 'UI/Dialog'], function(Core, Language, UIDialog) {
+       "use strict";
+       
+       var _active = false;
+       var _confirmButton = null;
+       var _content = null;
+       var _options = {};
+       var _text = null;
+       
+       /**
+        * Confirmation dialog overlay.
+        * 
+        * @exports     WoltLab/WCF/UI/Confirmation
+        */
+       var UIConfirmation = {
+               /**
+                * Shows the confirmation dialog.
+                * 
+                * Possible options:
+                *  - cancel: callback if user cancels the dialog
+                *  - confirm: callback if user confirm the dialog
+                *  - legacyCallback: WCF 2.0/2.1 compatible callback with string parameter
+                *  - message: displayed confirmation message
+                *  - parameters: list of parameters passed to the callback on confirm
+                *  - template: optional HTML string to be inserted below the `message`
+                * 
+                * @param       {object<string, *>}     options         confirmation options
+                */
+               show: function(options) {
+                       if (UIDialog === undefined) UIDialog = require('UI/Dialog');
+                       
+                       if (_active) {
+                               return;
+                       }
+                       
+                       _options = Core.extend({
+                               cancel: null,
+                               confirm: null,
+                               legacyCallback: null,
+                               message: '',
+                               parameters: {},
+                               template: ''
+                       }, options);
+                       
+                       _options.message = (typeof _options.message === 'string') ? _options.message.trim() : '';
+                       if (!_options.message.length) {
+                               throw new Error("Expected a non-empty string for option 'message'.");
+                       }
+                       
+                       if (typeof _options.confirm !== 'function' && typeof _options.legacyCallback !== 'function') {
+                               throw new TypeError("Expected a valid callback for option 'confirm'.");
+                       }
+                       
+                       if (_content === null) {
+                               this._createDialog();
+                       }
+                       
+                       _content.innerHTML = (typeof options.template === 'string') ? options.template.trim() : '';
+                       _text.textContent = _options.message;
+                       
+                       _active = true;
+                       
+                       UIDialog.open(this);
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'wcfSystemConfirmation',
+                               options: {
+                                       onClose: this._onClose.bind(this),
+                                       onShow: this._onShow.bind(this),
+                                       title: Language.get('wcf.global.confirmation.title')
+                               }
+                       };
+               },
+               
+               /**
+                * Returns content container element.
+                * 
+                * @return      {Element}       content container element
+                */
+               getContentElement: function() {
+                       return _content;
+               },
+               
+               /**
+                * Creates the dialog DOM elements.
+                */
+               _createDialog: function() {
+                       var dialog = document.createElement('div');
+                       dialog.setAttribute('id', 'wcfSystemConfirmation');
+                       dialog.classList.add('systemConfirmation');
+                       
+                       _text = document.createElement('p');
+                       dialog.appendChild(_text);
+                       
+                       _content = document.createElement('div');
+                       _content.setAttribute('id', 'wcfSystemConfirmationContent');
+                       dialog.appendChild(_content);
+                       
+                       var formSubmit = document.createElement('div');
+                       formSubmit.classList.add('formSubmit');
+                       dialog.appendChild(formSubmit);
+                       
+                       _confirmButton = document.createElement('button');
+                       _confirmButton.classList.add('buttonPrimary');
+                       _confirmButton.textContent = Language.get('wcf.global.confirmation.confirm');
+                       _confirmButton.addEventListener('click', this._confirm.bind(this));
+                       formSubmit.appendChild(_confirmButton);
+                       
+                       var cancelButton = document.createElement('button');
+                       cancelButton.textContent = Language.get('wcf.global.confirmation.cancel');
+                       cancelButton.addEventListener('click', function() { UIDialog.close('wcfSystemConfirmation'); });
+                       formSubmit.appendChild(cancelButton);
+                       
+                       document.body.appendChild(dialog);
+               },
+               
+               /**
+                * Invoked if the user confirms the dialog.
+                */
+               _confirm: function() {
+                       if (typeof _options.legacyCallback === 'function') {
+                               _options.legacyCallback('confirm', _options.parameters);
+                       }
+                       else {
+                               _options.confirm(_options.parameters);
+                       }
+                       
+                       _active = false;
+                       UIDialog.close('wcfSystemConfirmation');
+               },
+               
+               /**
+                * Invoked on dialog close or if user cancels the dialog.
+                */
+               _onClose: function() {
+                       if (_active) {
+                               _confirmButton.blur();
+                               _active = false;
+                               
+                               if (typeof _options.legacyCallback === 'function') {
+                                       _options.legacyCallback('cancel', _options.parameters);
+                               }
+                               else if (typeof _options.cancel === 'function') {
+                                       _options.cancel(_options.parameters);
+                               }
+                       }
+               },
+               
+               /**
+                * Sets the focus on the confirm button on dialog open for proper keyboard support.
+                */
+               _onShow: function() {
+                       _confirmButton.blur();
+                       _confirmButton.focus();
+               }
+       };
+       
+       return UIConfirmation;
+});
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..8e445e8
--- /dev/null
@@ -0,0 +1,511 @@
+/**
+ * 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(
+       [
+               'enquire',     'Ajax',       'Core',      'Dictionary',
+               'Environment', 'Language',   'ObjectMap', 'DOM/ChangeListener',
+               'DOM/Util',    'UI/Confirmation'
+       ],
+       function(
+               enquire,        Ajax,         Core,        Dictionary,
+               Environment,    Language,     ObjectMap,   DOMChangeListener,
+               DOMUtil,        UIConfirmation
+       )
+{
+       "use strict";
+       
+       var _activeDialog = null;
+       var _container = null;
+       var _dialogs = new Dictionary();
+       var _dialogObjects = new ObjectMap();
+       var _dialogFullHeight = false;
+       var _keyupListener = null;
+       
+       /**
+        * @exports     WoltLab/WCF/UI/Dialog
+        */
+       var UIDialog = {
+               /**
+                * Sets up global container and internal variables.
+                */
+               setup: function() {
+                       // Fetch Ajax, as it cannot be provided because of a circular dependency
+                       if (Ajax === undefined) Ajax = require('Ajax');
+                       
+                       _container = document.createElement('div');
+                       _container.classList.add('dialogOverlay');
+                       _container.setAttribute('aria-hidden', 'true');
+                       _container.addEventListener('click', this._closeOnBackdrop.bind(this));
+                       
+                       document.body.appendChild(_container);
+                       
+                       _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);
+                       
+                       enquire.register('screen and (max-width: 800px)', {
+                               match: function() { _dialogFullHeight = true; },
+                               unmatch: function() { _dialogFullHeight = false; },
+                               setup: function() { _dialogFullHeight = true; },
+                               deferSetup: true
+                       });
+               },
+               
+               /**
+                * Opens the dialog and implicitly creates it on first usage.
+                * 
+                * @param       {object}                        callbackObject  used to invoke `_dialogSetup()` on first call
+                * @param       {(string|DocumentFragment=}     html            html content or document fragment to use for dialog content
+                * @returns     {object<string, *>}             dialog data
+                */
+               open: function(callbackObject, html) {
+                       var dialogData = _dialogObjects.get(callbackObject);
+                       if (Core.isPlainObject(dialogData)) {
+                               // dialog already exists
+                               return this.openStatic(dialogData.id, html);
+                       }
+                       
+                       // initialize a new dialog
+                       if (typeof callbackObject._dialogSetup !== 'function') {
+                               throw new Error("Callback object does not implement the method '_dialogSetup()'.");
+                       }
+                       
+                       var setupData = callbackObject._dialogSetup();
+                       if (!Core.isPlainObject(setupData)) {
+                               throw new Error("Expected an object literal as return value of '_dialogSetup()'.");
+                       }
+                       
+                       dialogData = { id: setupData.id };
+                       
+                       var createOnly = true;
+                       if (setupData.source === undefined) {
+                               var dialogElement = document.getElementById(setupData.id);
+                               if (dialogElement === null) {
+                                       throw new Error("Element id '" + setupData.id + "' is invalid and no source attribute was given.");
+                               }
+                               
+                               setupData.source = document.createDocumentFragment();
+                               setupData.source.appendChild(dialogElement);
+                       }
+                       else if (setupData.source === null) {
+                               // `null` means there is no static markup and `html` should be used instead
+                               setupData.source = html;
+                       }
+                       
+                       else if (typeof setupData.source === 'function') {
+                               setupData.source();
+                       }
+                       else if (Core.isPlainObject(setupData.source)) {
+                               Ajax.api(this, setupData.source.data, (function(data) {
+                                       if (data.returnValues && typeof data.returnValues.template === 'string') {
+                                               this.open(callbackObject, data.returnValues.template);
+                                               
+                                               if (typeof setupData.source.after === 'function') {
+                                                       setupData.source.after(_dialogs.get(setupData.id).content, data);
+                                               }
+                                       }
+                               }).bind(this));
+                       }
+                       else {
+                               if (typeof setupData.source === 'string') {
+                                       var dialogElement = document.createElement('div');
+                                       dialogElement.setAttribute('id', setupData.id);
+                                       dialogElement.innerHTML = setupData.source;
+                                       
+                                       setupData.source = document.createDocumentFragment();
+                                       setupData.source.appendChild(dialogElement);
+                               }
+                               
+                               if (!setupData.source.nodeType || setupData.source.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
+                                       throw new Error("Expected at least a document fragment as 'source' attribute.");
+                               }
+                               
+                               createOnly = false;
+                       }
+                       
+                       _dialogObjects.set(callbackObject, dialogData);
+                       
+                       return this.openStatic(setupData.id, setupData.source, setupData.options, createOnly);
+               },
+               
+               /**
+                * 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|DocumentFragment)}    html            content html
+                * @param       {object<string, *>}             options         list of options, is completely ignored if the dialog already exists
+                * @param       {boolean=}                      createOnly      create the dialog but do not open it
+                * @return      {object<string, *>}             dialog data
+                */
+               openStatic: function(id, html, options, createOnly) {
+                       if (_dialogs.has(id)) {
+                               this._updateDialog(id, html);
+                       }
+                       else {
+                               options = Core.extend({
+                                       backdropCloseOnClick: true,
+                                       closable: true,
+                                       closeButtonLabel: Language.get('wcf.global.button.close'),
+                                       closeConfirmMessage: '',
+                                       disableContentPadding: false,
+                                       disposeOnClose: false,
+                                       title: '',
+                                       
+                                       // callbacks
+                                       onBeforeClose: null,
+                                       onClose: null,
+                                       onShow: null
+                               }, options);
+                               
+                               if (!options.closable) options.backdropCloseOnClick = false;
+                               if (options.closeConfirmMessage) {
+                                       options.onBeforeClose = (function(id) {
+                                               UIConfirmation.show({
+                                                       confirm: this.close.bind(this, id),
+                                                       message: options.closeConfirmMessage
+                                               });
+                                       }).bind(this);
+                               }
+                               
+                               this._createDialog(id, html, options);
+                       }
+                       
+                       return _dialogs.get(id);
+               },
+               
+               /**
+                * Sets the dialog title.
+                * 
+                * @param       {string}        id              element id
+                * @param       {string}        title           dialog title
+                */
+               setTitle: function(id, title) {
+                       var data = _dialogs.get(id);
+                       if (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|DocumentFragment)}    html            content html
+                * @param       {object<string, *>}             options         list of options
+                * @param       {boolean=}                      createOnly      create the dialog but do not open it
+                */
+               _createDialog: function(id, html, options, createOnly) {
+                       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');
+                       if (options.disableContentPadding) contentContainer.classList.add('dialogContentNoPadding');
+                       dialog.appendChild(contentContainer);
+                       
+                       var content;
+                       if (element === null) {
+                               content = document.createElement('div');
+                               
+                               if (typeof html === 'string') {
+                                       content.innerHTML = html;
+                               }
+                               else if (html instanceof DocumentFragment) {
+                                       if (html.children[0].nodeName !== 'div' || html.childElementCount > 1) {
+                                               content.appendChild(html);
+                                       }
+                                       else {
+                                               content = html.children[0];
+                                       }
+                               }
+                               
+                               content.id = id;
+                       }
+                       else {
+                               content = element;
+                       }
+                       
+                       contentContainer.appendChild(content);
+                       
+                       if (content.style.getPropertyValue('display') === 'none') {
+                               content.style.removeProperty('display');
+                       }
+                       
+                       _dialogs.set(id, {
+                               backdropCloseOnClick: options.backdropCloseOnClick,
+                               content: content,
+                               dialog: dialog,
+                               header: header,
+                               onBeforeClose: options.onBeforeClose,
+                               onClose: options.onClose,
+                               onShow: options.onShow
+                       });
+                       
+                       DOMUtil.prepend(dialog, _container);
+                       
+                       if (createOnly !== true) {
+                               this._updateDialog(id, null);
+                       }
+               },
+               
+               /**
+                * 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 (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') {
+                               if (_container.getAttribute('aria-hidden') === 'true') {
+                                       window.addEventListener('keyup', _keyupListener);
+                               }
+                               
+                               data.dialog.setAttribute('aria-hidden', 'false');
+                               _container.setAttribute('aria-hidden', 'false');
+                               _container.setAttribute('data-close-on-click', (data.backdropCloseOnClick ? 'true' : 'false'));
+                               _activeDialog = id;
+                               
+                               this.rebuild(id);
+                               
+                               if (typeof data.onShow === 'function') {
+                                       data.onShow(id);
+                               }
+                       }
+                       
+                       DOMChangeListener.trigger();
+               },
+               
+               /**
+                * Rebuilds dialog identified by given id.
+                * 
+                * @param       {string}        id      element id
+                */
+               rebuild: function(id) {
+                       var data = _dialogs.get(id);
+                       if (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;
+                       }
+                       
+                       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 * (_dialogFullHeight ? 1 : 0.8)) - unavailableHeight;
+                       contentContainer.style.setProperty('max-height', ~~maximumHeight + 'px');
+                       
+                       // fix for a calculation bug in Chrome causing the scrollbar to overlap the border
+                       if (Environment.browser() === 'chrome') {
+                               if (data.content.scrollHeight > maximumHeight) {
+                                       data.content.style.setProperty('margin-right', '-1px');
+                               }
+                               else {
+                                       data.content.style.removeProperty('margin-right');
+                               }
+                       }
+               },
+               
+               /**
+                * 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|object)}       id      element id or callback object
+                */
+               close: function(id) {
+                       if (typeof id === 'object') {
+                               var dialogData = _dialogObjects.get(id);
+                               if (dialogData !== undefined) {
+                                       id = dialogData.id;
+                               }
+                       }
+                       
+                       var data = _dialogs.get(id);
+                       if (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['delete'](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);
+               },
+               
+               _ajaxSetup: function() {
+                       return {};
+               }
+       };
+       
+       return UIDialog;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Simple.js
new file mode 100644 (file)
index 0000000..1fc5326
--- /dev/null
@@ -0,0 +1,390 @@
+/**
+ * Simple Dropdown
+ * 
+ * @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/Dropdown/Simple
+ */
+define(
+       [       'CallbackList', 'Core', 'Dictionary', 'UI/Alignment', 'DOM/ChangeListener', 'DOM/Traverse', 'DOM/Util', 'UI/CloseOverlay'],
+       function(CallbackList,   Core,   Dictionary,   UIAlignment,    DOMChangeListener,    DOMTraverse,    DOMUtil,    UICloseOverlay)
+{
+       "use strict";
+       
+       var _availableDropdowns = null;
+       var _callbacks = new CallbackList();
+       var _didInit = false;
+       var _dropdowns = new Dictionary();
+       var _menus = new Dictionary();
+       var _menuContainer = null;
+       
+       /**
+        * @exports     WoltLab/WCF/UI/Dropdown/Simple
+        */
+       var SimpleDropdown = {
+               /**
+                * Performs initial setup such as setting up dropdowns and binding listeners.
+                */
+               setup: function() {
+                       if (_didInit) return;
+                       _didInit = true;
+                       
+                       _menuContainer = document.createElement('div');
+                       _menuContainer.setAttribute('id', 'dropdownMenuContainer');
+                       document.body.appendChild(_menuContainer);
+                       
+                       _availableDropdowns = document.getElementsByClassName('dropdownToggle');
+                       
+                       this.initAll();
+                       
+                       UICloseOverlay.add('WoltLab/WCF/UI/Dropdown/Simple', this.closeAll.bind(this));
+                       DOMChangeListener.add('WoltLab/WCF/UI/Dropdown/Simple', this.initAll.bind(this));
+                       
+                       document.addEventListener('scroll', this._onScroll.bind(this));
+                       
+                       // expose on window object for backward compatibility
+                       window.bc_wcfSimpleDropdown = this;
+               },
+               
+               /**
+                * Loops through all possible dropdowns and registers new ones.
+                */
+               initAll: function() {
+                       for (var i = 0, length = _availableDropdowns.length; i < length; i++) {
+                               this.init(_availableDropdowns[i], false);
+                       }
+               },
+               
+               /**
+                * Initializes a dropdown.
+                * 
+                * @param       {Element}       button
+                * @param       {boolean}       isLazyInitialization
+                */
+               init: function(button, isLazyInitialization) {
+                       this.setup();
+                       
+                       if (button.classList.contains('jsDropdownEnabled') || button.getAttribute('data-target')) {
+                               return false;
+                       }
+                       
+                       var dropdown = DOMTraverse.parentByClass(button, 'dropdown');
+                       if (dropdown === null) {
+                               throw new Error("Invalid dropdown passed, button '" + DOMUtil.identify(button) + "' does not have a parent with .dropdown.");
+                       }
+                       
+                       var menu = DOMTraverse.nextByClass(button, 'dropdownMenu');
+                       if (menu === null) {
+                               throw new Error("Invalid dropdown passed, button '" + DOMUtil.identify(button) + "' does not have a menu as next sibling.");
+                       }
+                       
+                       // move menu into global container
+                       _menuContainer.appendChild(menu);
+                       
+                       var containerId = DOMUtil.identify(dropdown);
+                       if (!_dropdowns.has(containerId)) {
+                               button.classList.add('jsDropdownEnabled');
+                               button.addEventListener('click', this._toggle.bind(this));
+                               
+                               _dropdowns.set(containerId, dropdown);
+                               _menus.set(containerId, menu);
+                               
+                               if (!containerId.match(/^wcf\d+$/)) {
+                                       menu.setAttribute('data-source', containerId);
+                               }
+                       }
+                       
+                       button.setAttribute('data-target', containerId);
+                       
+                       if (isLazyInitialization) {
+                               setTimeout(function() { Core.triggerEvent(button, 'click'); }, 10);
+                       }
+               },
+               
+               /**
+                * Initializes a remote-controlled dropdown.
+                * 
+                * @param       {Element}       dropdown        dropdown wrapper element
+                * @param       {Element}       menu            menu list element
+                */
+               initFragment: function(dropdown, menu) {
+                       this.setup();
+                       
+                       if (_dropdowns.has(dropdown)) {
+                               return;
+                       }
+                       
+                       var containerId = DOMUtil.identify(dropdown);
+                       _dropdowns.set(containerId, dropdown);
+                       _menuContainer.appendChild(menu);
+                       
+                       _menus.set(containerId, menu);
+               },
+               
+               /**
+                * Registers a callback for open/close events.
+                * 
+                * @param       {string}                        containerId     dropdown wrapper id
+                * @param       {function(string, string)}      callback
+                */
+               registerCallback: function(containerId, callback) {
+                       _callbacks.add(containerId, callback);
+               },
+               
+               /**
+                * Returns the requested dropdown wrapper element.
+                * 
+                * @return      {Element}       dropdown wrapper element
+                */
+               getDropdown: function(containerId) {
+                       return _dropdowns.get(containerId);
+               },
+               
+               /**
+                * Returns the requested dropdown menu list element.
+                * 
+                * @return      {Element}       menu list element
+                */
+               getDropdownMenu: function(containerId) {
+                       return _menus.get(containerId);
+               },
+               
+               /**
+                * Toggles the requested dropdown between opened and closed.
+                * 
+                * @param       {string}        containerId     dropdown wrapper id
+                */
+               toggleDropdown: function(containerId) {
+                       this._toggle(null, containerId);
+               },
+               
+               /**
+                * Calculates and sets the alignment of given dropdown.
+                * 
+                * @param       {Element}       dropdown        dropdown wrapper element
+                * @param       {Element}       dropdownMenu    menu list element
+                */
+               setAlignment: function(dropdown, dropdownMenu) {
+                       // check if button belongs to an i18n textarea
+                       var button = dropdown.querySelector('.dropdownToggle');
+                       var refDimensionsElement = null;
+                       if (button !== null && button.classList.contains('dropdownCaptionTextarea')) {
+                               refDimensionsElement = button;
+                       }
+                       
+                       UIAlignment.set(dropdownMenu, dropdown, {
+                               pointerClassNames: ['dropdownArrowBottom', 'dropdownArrowRight'],
+                               refDimensionsElement: refDimensionsElement
+                       });
+               },
+               
+               /**
+                * Calculats and sets the alignment of the dropdown identified by given id.
+                * 
+                * @param       {string}        containerId     dropdown wrapper id
+                */
+               setAlignmentById: function(containerId) {
+                       var dropdown = _dropdowns.get(containerId);
+                       if (dropdown === undefined) {
+                               throw new Error("Unknown dropdown identifier '" + containerId + "'.");
+                       }
+                       
+                       var menu = _menus.get(containerId);
+                       
+                       this.setAlignment(dropdown, menu);
+               },
+               
+               /**
+                * Returns true if target dropdown exists and is open.
+                * 
+                * @param       {string}        containerId     dropdown wrapper id
+                * @return      {boolean}       true if dropdown exists and is open
+                */
+               isOpen: function(containerId) {
+                       var menu = _menus.get(containerId);
+                       if (menu !== undefined && menu.classList.contains('dropdownOpen')) {
+                               return true;
+                       }
+                       
+                       return false;
+               },
+               
+               /**
+                * Opens the dropdown unless it is already open.
+                * 
+                * @param       {string}        containerId     dropdown wrapper id
+                */
+               open: function(containerId) {
+                       var menu = _menus.get(containerId);
+                       if (menu !== undefined && !menu.classList.contains('dropdownOpen')) {
+                               this.toggleDropdown(containerId);
+                       }
+               },
+               
+               /**
+                * Closes the dropdown identified by given id without notifying callbacks.
+                * 
+                * @param       {string}        containerId     dropdown wrapper id
+                */
+               close: function(containerId) {
+                       var dropdown = _dropdowns.get(containerId);
+                       if (dropdown !== undefined) {
+                               dropdown.classList.remove('dropdownOpen');
+                               _menus.get(containerId).classList.remove('dropdownOpen');
+                       }
+               },
+               
+               /**
+                * Closes all dropdowns.
+                */
+               closeAll: function() {
+                       _dropdowns.forEach((function(dropdown, containerId) {
+                               if (dropdown.classList.contains('dropdownOpen')) {
+                                       dropdown.classList.remove('dropdownOpen');
+                                       _menus.get(containerId).classList.remove('dropdownOpen');
+                                       
+                                       this._notifyCallbacks(containerId, 'close');
+                               }
+                       }).bind(this));
+               },
+               
+               /**
+                * Destroys a dropdown identified by given id.
+                * 
+                * @param       {string}        containerId     dropdown wrapper id
+                * @return      {boolean}       false for unknown dropdowns
+                */
+               destroy: function(containerId) {
+                       if (!_dropdowns.has(containerId)) {
+                               return false;
+                       }
+                       
+                       this.close(containerId);
+                       
+                       var menu = _menus.get(containerId);
+                       _menus.parentNode.removeChild(menu);
+                       
+                       _menus['delete'](containerId);
+                       _dropdowns['delete'](containerId);
+                       
+                       return true;
+               },
+               
+               /**
+                * Handles dropdown positions in overlays when scrolling in the overlay.
+                * 
+                * @param       {Event}         event   event object
+                */
+               _onDialogScroll: function(event) {
+                       var dialogContent = event.currentTarget;
+                       var dropdowns = dialogContent.querySelectorAll('.dropdown.dropdownOpen');
+                       
+                       for (var i = 0, length = dropdowns.length; i < length; i++) {
+                               var dropdown = dropdowns[i];
+                               var containerId = DOMUtil.identify(dropdown);
+                               var offset = DOMUtil.offset(dropdown);
+                               var dialogOffset = DOMUtil.offset(dialogContent);
+                               
+                               // check if dropdown toggle is still (partially) visible
+                               if (offset.top + dropdown.clientHeight <= dialogOffset.top) {
+                                       // top check
+                                       this.toggleDropdown(containerId);
+                               }
+                               else if (offset.top >= dialogOffset.top + dialogContent.offsetHeight) {
+                                       // bottom check
+                                       this.toggleDropdown(containerId);
+                               }
+                               else if (offset.left <= dialogOffset.left) {
+                                       // left check
+                                       this.toggleDropdown(containerId);
+                               }
+                               else if (offset.left >= dialogOffset.left + dialogContent.offsetWidth) {
+                                       // right check
+                                       this.toggleDropdown(containerId);
+                               }
+                               else {
+                                       this.setAlignment(containerId, _menus.get(containerId));
+                               }
+                       }
+               },
+               
+               /**
+                * Recalculates dropdown positions on page scroll.
+                */
+               _onScroll: function() {
+                       _dropdowns.forEach((function(dropdown, containerId) {
+                               if (dropdown.getAttribute('data-is-overlay-dropdown-button') === true && dropdown.classList.contains('dropdownOpen')) {
+                                       this.setAlignment(dropdown, _menus.get(containerId));
+                               }
+                       }).bind(this));
+               },
+               
+               /**
+                * Notifies callbacks on status change.
+                * 
+                * @param       {string}        containerId     dropdown wrapper id
+                * @param       {string}        action          can be either 'open' or 'close'
+                */
+               _notifyCallbacks: function(containerId, action) {
+                       _callbacks.forEach(containerId, function(callback) {
+                               callback(containerId, action);
+                       });
+               },
+               
+               /**
+                * Toggles the dropdown's state between open and close.
+                * 
+                * @param       {?Event}        event           event object, should be 'null' if targetId is given
+                * @param       {string=}       targetId        dropdown wrapper id
+                * @return      {boolean}       'false' if event is not null
+                */
+               _toggle: function(event, targetId) {
+                       if (event !== null) {
+                               event.preventDefault();
+                               event.stopPropagation();
+                               
+                               targetId = event.currentTarget.getAttribute('data-target');
+                       }
+                       
+                       // check if 'isOverlayDropdownButton' is set which indicates if
+                       // the dropdown toggle is in an overlay
+                       var dropdown = _dropdowns.get(targetId);
+                       if (dropdown !== undefined && dropdown.getAttribute('data-is-overlay-dropdown-button') === null) {
+                               var dialogContent = DOMTraverse.parentByClass(dropdown, 'dialogContent');
+                               dropdown.setAttribute('data-is-overlay-dropdown-button', (dialogContent !== null));
+                               
+                               if (dialogContent !== null) {
+                                       dialogContent.addEventListener('scroll', this._onDialogScroll.bind(this));
+                               }
+                       }
+                       
+                       // close all dropdowns
+                       _dropdowns.forEach((function(dropdown, containerId) {
+                               var menu = _menus.get(containerId);
+                               
+                               if (dropdown.classList.contains('dropdownOpen')) {
+                                       dropdown.classList.remove('dropdownOpen');
+                                       menu.classList.remove('dropdownOpen');
+                                       
+                                       this._notifyCallbacks(containerId, 'close');
+                               }
+                               else if (containerId === targetId && menu.childElementCount > 0) {
+                                       dropdown.classList.add('dropdownOpen');
+                                       menu.classList.add('dropdownOpen');
+                                       
+                                       this._notifyCallbacks(containerId, 'open');
+                                       
+                                       this.setAlignment(dropdown, menu);
+                               }
+                       }).bind(this));
+                       
+                       // TODO
+                       WCF.Dropdown.Interactive.Handler.closeAll();
+                       
+                       return (event === null);
+               }
+       };
+       
+       return SimpleDropdown;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/FlexibleMenu.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/FlexibleMenu.js
new file mode 100644 (file)
index 0000000..b201251
--- /dev/null
@@ -0,0 +1,200 @@
+/**
+ * Dynamically transforms menu-like structures to handle items exceeding the available width
+ * by moving them into a separate dropdown.  
+ * 
+ * @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/FlexibleMenu
+ */
+define(['Core', 'Dictionary', 'DOM/ChangeListener', 'DOM/Traverse', 'DOM/Util', 'UI/SimpleDropdown'], function(Core, Dictionary, DOMChangeListener, DOMTraverse, DOMUtil, SimpleDropdown) {
+       "use strict";
+       
+       var _containers = new Dictionary();
+       var _dropdowns = new Dictionary();
+       var _dropdownMenus = new Dictionary();
+       var _itemLists = new Dictionary();
+       
+       /**
+        * @exports     WoltLab/WCF/UI/FlexibleMenu
+        */
+       var UIFlexibleMenu = {
+               /**
+                * Register default menus and set up event listeners.
+                */
+               setup: function() {
+                       if (document.getElementById('mainMenu') !== null) this.register('mainMenu');
+                       var navigationHeader = document.querySelector('.navigationHeader');
+                       if (navigationHeader !== null) this.register(DOMUtil.identify(navigationHeader));
+                       
+                       window.addEventListener('resize', this.rebuildAll.bind(this));
+                       DOMChangeListener.add('WoltLab/WCF/UI/FlexibleMenu', this.registerTabMenus.bind(this));
+               },
+               
+               /**
+                * Registers a menu by element id.
+                * 
+                * @param       {string}        containerId     element id
+                */
+               register: function(containerId) {
+                       var container = document.getElementById(containerId);
+                       if (container === null) {
+                               throw "Expected a valid element id, '" + containerId + "' does not exist.";
+                       }
+                       
+                       if (_containers.has(containerId)) {
+                               return;
+                       }
+                       
+                       var list = DOMTraverse.childByTag(container, 'UL');
+                       if (list === null) {
+                               throw "Expected an <ul> element as child of container '" + containerId + "'.";
+                       }
+                       
+                       _containers.set(containerId, container);
+                       _itemLists.set(containerId, list);
+                       
+                       this.rebuild(containerId);
+               },
+               
+               /**
+                * Registers tab menus.
+                */
+               registerTabMenus: function() {
+                       var tabMenus = document.querySelectorAll('.tabMenuContainer:not(.jsFlexibleMenuEnabled), .messageTabMenu:not(.jsFlexibleMenuEnabled)');
+                       for (var i = 0, length = tabMenus.length; i < length; i++) {
+                               var tabMenu = tabMenus[i];
+                               var nav = DOMTraverse.childByTag(tabMenu, 'NAV');
+                               if (nav !== null) {
+                                       tabMenu.classList.add('jsFlexibleMenuEnabled');
+                                       this.register(DOMUtil.identify(nav));
+                               }
+                       }
+               },
+               
+               /**
+                * Rebuilds all menus, e.g. on window resize.
+                */
+               rebuildAll: function() {
+                       _containers.forEach((function(container, containerId) {
+                               this.rebuild(containerId);
+                       }).bind(this));
+               },
+               
+               /**
+                * Rebuild the menu identified by given element id.
+                * 
+                * @param       {string}        containerId     element id
+                */
+               rebuild: function(containerId) {
+                       var container = _containers.get(containerId);
+                       if (container === undefined) {
+                               throw "Expected a valid element id, '" + containerId + "' is unknown.";
+                       }
+                       
+                       var styles = window.getComputedStyle(container);
+                       
+                       var availableWidth = container.parentNode.clientWidth;
+                       availableWidth -= DOMUtil.styleAsInt(styles, 'margin-left');
+                       availableWidth -= DOMUtil.styleAsInt(styles, 'margin-right');
+                       
+                       var list = _itemLists.get(containerId);
+                       var items = DOMTraverse.childrenByTag(list, 'LI');
+                       var dropdown = _dropdowns.get(containerId);
+                       var dropdownWidth = 0;
+                       if (dropdown !== undefined) {
+                               // show all items for calculation
+                               for (var i = 0, length = items.length; i < length; i++) {
+                                       var item = items[i];
+                                       if (item.classList.contains('dropdown')) {
+                                               continue;
+                                       }
+                                       
+                                       item.style.removeProperty('display'); 
+                               }
+                               
+                               if (dropdown.parentNode !== null) {
+                                       dropdownWidth = DOMUtil.outerWidth(dropdown);
+                               }
+                       }
+                       
+                       var currentWidth = list.scrollWidth - dropdownWidth;
+                       var hiddenItems = [];
+                       if (currentWidth > availableWidth) {
+                               // hide items starting with the last one
+                               for (var i = items.length - 1; i >= 0; i--) {
+                                       var item = items[i];
+                                       
+                                       // ignore dropdown and active item
+                                       if (item.classList.contains('dropdown') || item.classList.contains('active') || item.classList.contains('ui-state-active')) {
+                                               continue;
+                                       }
+                                       
+                                       hiddenItems.push(item);
+                                       item.style.setProperty('display', 'none');
+                                       
+                                       if (list.scrollWidth < availableWidth) {
+                                               break;
+                                       }
+                               }
+                       }
+                       
+                       if (hiddenItems.length) {
+                               var dropdownMenu;
+                               if (dropdown === undefined) {
+                                       dropdown = document.createElement('li');
+                                       dropdown.className = 'dropdown jsFlexibleMenuDropdown';
+                                       var icon = document.createElement('a');
+                                       icon.className = 'icon icon16 fa-list';
+                                       dropdown.appendChild(icon);
+                                       
+                                       dropdownMenu = document.createElement('ul');
+                                       dropdownMenu.classList.add('dropdownMenu');
+                                       dropdown.appendChild(dropdownMenu);
+                                       
+                                       _dropdowns.set(containerId, dropdown);
+                                       _dropdownMenus.set(containerId, dropdownMenu);
+                                       
+                                       SimpleDropdown.init(icon);
+                               }
+                               else {
+                                       dropdownMenu = _dropdownMenus.get(containerId);
+                               }
+                               
+                               if (dropdown.parentNode === null) {
+                                       list.appendChild(dropdown);
+                               }
+                               
+                               // build dropdown menu
+                               var fragment = document.createDocumentFragment();
+                               
+                               var self = this;
+                               hiddenItems.forEach(function(hiddenItem) {
+                                       var item = document.createElement('li');
+                                       item.innerHTML = hiddenItem.innerHTML;
+                                       
+                                       item.addEventListener('click', (function(event) {
+                                               event.preventDefault();
+                                               
+                                               Core.triggerEvent(hiddenItem.querySelector('a'), 'click');
+                                               
+                                               // force a rebuild to guarantee the active item being visible
+                                               setTimeout(function() {
+                                                       self.rebuild(containerId);
+                                               }, 59);
+                                       }).bind(this));
+                                       
+                                       fragment.appendChild(item);
+                               });
+                               
+                               dropdownMenu.innerHTML = '';
+                               dropdownMenu.appendChild(fragment);
+                       }
+                       else if (dropdown !== undefined && dropdown.parentNode !== null) {
+                               dropdown.parentNode.removeChild(dropdown);
+                       }
+               }
+       };
+       
+       return UIFlexibleMenu;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList.js
new file mode 100644 (file)
index 0000000..4148714
--- /dev/null
@@ -0,0 +1,413 @@
+/**
+ * Flexible UI element featuring both a list of items and an input field with suggestion support.
+ * 
+ * @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/ItemList
+ */
+define(['Core', 'Dictionary', 'Language', 'DOM/Traverse', 'WoltLab/WCF/UI/Suggestion'], function(Core, Dictionary, Language, DOMTraverse, UISuggestion) {
+       "use strict";
+       
+       var _activeId = '';
+       var _data = new Dictionary();
+       var _didInit = false;
+       
+       var _callbackKeyDown = null;
+       var _callbackKeyPress = null;
+       var _callbackKeyUp = null;
+       var _callbackRemoveItem = null;
+       
+       /**
+        * @exports     WoltLab/WCF/UI/ItemList
+        */
+       var UIItemList = {
+               /**
+                * Initializes an item list.
+                * 
+                * The `values` argument must be empty or contain a list of strings or object, e.g.
+                * `['foo', 'bar']` or `[{ objectId: 1337, value: 'baz'}, {...}]`
+                * 
+                * @param       {string}                elementId       input element id
+                * @param       {array<mixed>}          values          list of existing values
+                * @param       {object<string>}        options         option list
+                */
+               init: function(elementId, values, options) {
+                       var element = document.getElementById(elementId);
+                       if (element === null) {
+                               throw new Error("Expected a valid element id.");
+                       }
+                       
+                       options = Core.extend({
+                               // search parameters for suggestions
+                               ajax: {
+                                       actionName: 'getSearchResultList',
+                                       className: '',
+                                       data: {}
+                               },
+                               
+                               // list of excluded string values, e.g. `['ignore', 'these strings', 'when', 'searching']`
+                               excludedSearchValues: [],
+                               // maximum number of items this list may contain, `-1` for infinite
+                               maxItems: -1,
+                               // maximum length of an item value, `-1` for infinite
+                               maxLength: -1,
+                               // disallow custom values, only values offered by the suggestion dropdown are accepted
+                               restricted: false,
+                               
+                               // initial value will be interpreted as comma separated value and submitted as such
+                               isCSV: false,
+                               
+                               // will be invoked whenever the items change, receives the element id first and list of values second
+                               callbackChange: null,
+                               // callback once the form is about to be submitted
+                               callbackSubmit: null,
+                               // value may contain the placeholder `{$objectId}`
+                               submitFieldName: ''
+                       }, options);
+                       
+                       var form = DOMTraverse.parentByTag(element, 'FORM');
+                       if (form !== null) {
+                               if (options.isCSV === false) {
+                                       if (!options.submitFieldName.length && typeof options.callbackSubmit !== 'function') {
+                                               throw new Error("Expected a valid function for option 'callbackSubmit', a non-empty value for option 'submitFieldName' or enabling the option 'submitFieldCSV'.");
+                                       }
+                                       
+                                       form.addEventListener('submit', (function() {
+                                               var values = this.getValues(elementId);
+                                               if (options.submitFieldName.length) {
+                                                       var input;
+                                                       for (var i = 0, length = values.length; i < length; i++) {
+                                                               input = document.createElement('input');
+                                                               input.type = 'hidden';
+                                                               input.name = options.submitFieldName.replace(/{$objectId}/, values[i].objectId);
+                                                               input.value = values[i].value;
+                                                               
+                                                               form.appendChild(input);
+                                                       }
+                                               }
+                                               else {
+                                                       options.callbackSubmit(form, values);
+                                               }
+                                       }).bind(this));
+                               }
+                       }
+                       
+                       this._setup();
+                       
+                       var data = this._createUI(element, options, values);
+                       var suggestion = new UISuggestion(elementId, {
+                               ajax: options.ajax,
+                               callbackSelect: this._addItem.bind(this),
+                               excludedSearchValues: options.excludedSearchValues
+                       });
+                       
+                       _data.set(elementId, {
+                               dropdownMenu: null,
+                               element: data.element,
+                               list: data.list,
+                               listItem: data.element.parentNode,
+                               options: options,
+                               shadow: data.shadow,
+                               suggestion: suggestion
+                       });
+                       
+                       values = (data.values.length) ? data.values : values;
+                       if (Array.isArray(values)) {
+                               var value;
+                               for (var i = 0, length = values.length; i < length; i++) {
+                                       value = values[i];
+                                       if (typeof value === 'string') {
+                                               value = { objectId: 0, value: value };
+                                       }
+                                       
+                                       this._addItem(elementId, value);
+                               }
+                       }
+               },
+               
+               /**
+                * Returns the list of current values.
+                * 
+                * @param       {string}                element id      input element id
+                * @return      {array<object>}         list of objects containing object id and value
+                */
+               getValues: function(elementId) {
+                       if (!_data.has(elementId)) {
+                               throw new Error("Element id '" + elementId + "' is unknown.");
+                       }
+                       
+                       var data = _data.get(elementId);
+                       var items = DOMTraverse.childrenByClass(data.list, 'item');
+                       var values = [], value, item;
+                       for (var i = 0, length = items.length; i < length; i++) {
+                               item = items[i];
+                               value = {
+                                       objectId: item.getAttribute('data-object-id'),
+                                       value: DOMTraverse.childByTag(item, 'SPAN').textContent
+                               };
+                               
+                               values.push(value);
+                       }
+                       
+                       return values;
+               },
+               
+               /**
+                * Binds static event listeners.
+                */
+               _setup: function() {
+                       if (_didInit) {
+                               return;
+                       }
+                       
+                       _didInit = true;
+                       
+                       _callbackKeyDown = this._keyDown.bind(this);
+                       _callbackKeyPress = this._keyPress.bind(this);
+                       _callbackKeyUp = this._keyUp.bind(this);
+                       _callbackRemoveItem = this._removeItem.bind(this);
+               },
+               
+               /**
+                * Creates the DOM structure for target element. If `element` is a `<textarea>`
+                * it will be automatically replaced with an `<input>` element.
+                * 
+                * @param       {Element}               element         input element
+                * @param       {object<string>}        options         option list
+                */
+               _createUI: function(element, options) {
+                       var list = document.createElement('ol');
+                       list.className = 'inputItemList';
+                       list.setAttribute('data-element-id', element.id);
+                       list.addEventListener('click', function(event) {
+                               if (event.target === list) element.focus();
+                       });
+                       
+                       var listItem = document.createElement('li');
+                       listItem.className = 'input';
+                       list.appendChild(listItem);
+                       
+                       element.addEventListener('keydown', _callbackKeyDown);
+                       element.addEventListener('keypress', _callbackKeyPress);
+                       element.addEventListener('keyup', _callbackKeyUp);
+                       
+                       element.parentNode.insertBefore(list, element);
+                       listItem.appendChild(element);
+                       
+                       if (options.maxLength !== -1) {
+                               element.setAttribute('maxLength', options.maxLength);
+                       }
+                       
+                       var shadow = null, values = [];
+                       if (options.isCSV) {
+                               shadow = document.createElement('input');
+                               shadow.className = 'itemListInputShadow';
+                               shadow.type = 'hidden';
+                               shadow.name = element.name;
+                               element.removeAttribute('name');
+                               
+                               list.parentNode.insertBefore(shadow, list);
+                               
+                               if (element.nodeName === 'TEXTAREA') {
+                                       var value, tmp = element.value.split(',');
+                                       for (var i = 0, length = tmp.length; i < length; i++) {
+                                               value = tmp[i].trim();
+                                               if (value.length) {
+                                                       values.push(value);
+                                               }
+                                       }
+                                       
+                                       var inputElement = document.createElement('input');
+                                       element.parentNode.insertBefore(inputElement, element);
+                                       inputElement.id = element.id;
+                                       
+                                       element.parentNode.removeChild(element);
+                                       element = inputElement;
+                               }
+                       }
+                       
+                       return {
+                               element: element,
+                               list: list,
+                               shadow: shadow,
+                               values: values
+                       };
+               },
+               
+               /**
+                * Enforces the maximum number of items.
+                * 
+                * @param       {string}        elementId       input element id
+                */
+               _handleLimit: function(elementId) {
+                       var data = _data.get(elementId);
+                       if (data.options.maxItems === -1) {
+                               return;
+                       }
+                       
+                       if (data.list.childElementCount - 1 < data.options.maxItems) {
+                               if (data.element.disabled) {
+                                       data.element.disabled = false;
+                                       data.element.removeAttribute('placeholder');
+                               }
+                       }
+                       else if (!data.element.disabled) {
+                               data.element.disabled = true;
+                               data.element.setAttribute('placeholder', Language.get('wcf.global.form.input.maxItems'));
+                       }
+               },
+               
+               /**
+                * Sets the active item list id and handles keyboard access to remove an existing item.
+                * 
+                * @param       {object}        event           event object
+                */
+               _keyDown: function(event) {
+                       var input = event.currentTarget;
+                       var lastItem = input.parentNode.previousElementSibling;
+                       
+                       _activeId = input.id;
+                       
+                       if (event.keyCode === 8) {
+                               // 8 = [BACKSPACE]
+                               if (input.value.length === 0) {
+                                       if (lastItem !== null) {
+                                               if (lastItem.classList.contains('active')) {
+                                                       this._removeItem(null, lastItem);
+                                               }
+                                               else {
+                                                       lastItem.classList.add('active');
+                                               }
+                                       }
+                               }
+                       }
+                       else if (event.keyCode === 27) {
+                               // 27 = [ESC]
+                               if (lastItem !== null && lastItem.classList.contains('active')) {
+                                       lastItem.classList.remove('active');
+                               }
+                       }
+               },
+               
+               /**
+                * Handles the `[ENTER]` and `[,]` key to add an item to the list unless it is restricted.
+                * 
+                * @param       {object}        event           event object
+                */
+               _keyPress: function(event) {
+                       // 13 = [ENTER], 44 = [,]
+                       if (event.charCode === 13 || event.charCode === 44) {
+                               event.preventDefault();
+                               
+                               if (_data.get(event.currentTarget.id).options.restricted) {
+                                       // restricted item lists only allow results from the dropdown to be picked
+                                       return;
+                               }
+                               
+                               var value = event.currentTarget.value.trim();
+                               if (value.length) {
+                                       this._addItem(event.currentTarget.id, { objectId: 0, value: value });
+                               }
+                       }
+               },
+               
+               /**
+                * Handles the keyup event to unmark an item for deletion.
+                * 
+                * @param       {object}        event           event object
+                */
+               _keyUp: function(event) {
+                       var input = event.currentTarget;
+                       
+                       if (input.value.length > 0) {
+                               var lastItem = input.parentNode.previousElementSibling;
+                               if (lastItem !== null) {
+                                       lastItem.classList.remove('active');
+                               }
+                       }
+               },
+               
+               /**
+                * Adds an item to the list.
+                * 
+                * @param       {string}        elementId       input element id
+                * @param       {string}        value           item value
+                */
+               _addItem: function(elementId, value) {
+                       var data = _data.get(elementId);
+                       
+                       var listItem = document.createElement('li');
+                       listItem.className = 'item';
+                       
+                       var content = document.createElement('span');
+                       content.className = 'content';
+                       content.setAttribute('data-object-id', value.objectId);
+                       content.textContent = value.value;
+                       
+                       var button = document.createElement('a');
+                       button.className = 'icon icon16 fa-times';
+                       button.addEventListener('click', _callbackRemoveItem);
+                       listItem.appendChild(content);
+                       listItem.appendChild(button);
+                       
+                       data.list.insertBefore(listItem, data.listItem);
+                       data.suggestion.addExcludedValue(value.value);
+                       data.element.value = '';
+                       
+                       this._handleLimit(elementId);
+                       var values = this._syncShadow(data);
+                       
+                       if (typeof data.options.callbackChange === 'function') {
+                               if (values === null) values = this.getValues(elementId);
+                               data.options.callbackChange(elementId, values);
+                       }
+               },
+               
+               /**
+                * Removes an item from the list.
+                * 
+                * @param       {?object}       event           event object
+                * @param       {Element=}      item            list item
+                */
+               _removeItem: function(event, item) {
+                       item = (event === null) ? item : event.currentTarget.parentNode;
+                       
+                       var parent = item.parentNode;
+                       var elementId = parent.getAttribute('data-element-id');
+                       var data = _data.get(elementId);
+                       
+                       data.suggestion.removeExcludedValue(item.children[0].textContent);
+                       parent.removeChild(item);
+                       data.element.focus();
+                       
+                       this._handleLimit(elementId);
+                       var values = this._syncShadow(data);
+                       
+                       if (typeof data.options.callbackChange === 'function') {
+                               if (values === null) values = this.getValues(elementId);
+                               data.options.callbackChange(elementId, values);
+                       }
+               },
+               
+               /**
+                * Synchronizes the shadow input field with the current list item values.
+                * 
+                * @param       {object}        data            element data
+                */
+               _syncShadow: function(data) {
+                       if (!data.options.isCSV) return null;
+                       
+                       var value = '', values = this.getValues(data.element.id);
+                       for (var i = 0, length = values.length; i < length; i++) {
+                               value += (value.length ? ',' : '') + values[i].value;
+                       }
+                       
+                       data.shadow.value = value;
+                       
+                       return values;
+               }
+       };
+       
+       return UIItemList;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/User.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/User.js
new file mode 100644 (file)
index 0000000..739ddc2
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Provides an item list for users and groups.
+ * 
+ * @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/ItemList/User
+ */
+define(['WoltLab/WCF/UI/ItemList'], function(UIItemList) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLab/WCF/UI/ItemList/User
+        */
+       var UIItemListUser = {
+               /**
+                * Initializes user suggestion support for an element.
+                * 
+                * @param       {string}        elementId       input element id
+                * @param       {object}        options         option list
+                */
+               init: function(elementId, options) {
+                       UIItemList.init(elementId, [], {
+                               ajax: {
+                                       className: 'wcf\\data\\user\\UserAction',
+                                       parameters: {
+                                               data: {
+                                                       includeUserGroups: ~~options.includeUserGroups
+                                               }
+                                       }
+                               },
+                               callbackChange: (typeof options.callbackChange === 'function' ? options.callbackChange : null),
+                               excludedSearchValues: (Array.isArray(options.excludedSearchValues) ? options.excludedSearchValues : []),
+                               isCSV: true,
+                               maxItems: ~~options.maxItems || -1,
+                               restricted: true
+                       });
+               },
+               
+               /**
+                * @see WoltLab/WCF/UI/ItemList::getValues()
+                */
+               getValues: function(elementId) {
+                       return UIItemList.getValues(elementId);
+               }
+       };
+       
+       return UIItemListUser;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Mobile.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Mobile.js
new file mode 100644 (file)
index 0000000..288a827
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * Modifies the interface to provide a better usability for mobile devices.
+ * 
+ * @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/Mobile
+ */
+define(
+       [       'enquire', 'Environment', 'Language', 'DOM/ChangeListener', 'DOM/Traverse', 'UI/CloseOverlay'],
+       function(enquire,   Environment,   Language,   DOMChangeListener,    DOMTraverse,    UICloseOverlay)
+{
+       "use strict";
+       
+       var _buttonGroupNavigations = null;
+       var _enabled = false;
+       var _main = null;
+       var _sidebar = null;
+       
+       /**
+        * @exports     WoltLab/WCF/UI/Mobile
+        */
+       var UIMobile = {
+               /**
+                * Initializes the mobile UI using enquire.js.
+                */
+               setup: function() {
+                       _buttonGroupNavigations = document.getElementsByClassName('buttonGroupNavigation');
+                       _main = document.getElementById('main');
+                       _sidebar = _main.querySelector('#main > div > div > .sidebar');
+                       
+                       if (Environment.touch()) {
+                               document.documentElement.classList.add('touch');
+                       }
+                       
+                       if (Environment.platform() !== 'desktop') {
+                               document.documentElement.classList.add('mobile');
+                       }
+                       
+                       enquire.register('screen and (max-width: 800px)', {
+                               match: this.enable.bind(this),
+                               unmatch: this.disable.bind(this),
+                               setup: this._init.bind(this),
+                               deferSetup: true
+                       });
+                       
+                       if (Environment.browser() === 'microsoft' && _sidebar !== null && _sidebar.clientWidth > 305) {
+                               this._fixSidebarIE();
+                       }
+               },
+               
+               /**
+                * Enables the mobile UI.
+                */
+               enable: function() {
+                       _enabled = true;
+                       
+                       if (Environment.browser() === 'microsoft') this._fixSidebarIE();
+               },
+               
+               /**
+                * Disables the mobile UI.
+                */
+               disable: function() {
+                       _enabled = false;
+                       
+                       if (Environment.browser() === 'microsoft') this._fixSidebarIE();
+               },
+               
+               _fixSidebarIE: function() {
+                       if (_sidebar === null) return;
+                       
+                       // sidebar is rarely broken on IE9/IE10
+                       _sidebar.style.setProperty('display', 'none');
+                       _sidebar.style.removeProperty('display');
+               },
+               
+               _init: function() {
+                       this._initSidebarToggleButtons();
+                       this._initSearchBar();
+                       this._initButtonGroupNavigation();
+                       
+                       UICloseOverlay.add('WoltLab/WCF/UI/Mobile', this._closeAllMenus.bind(this));
+                       DOMChangeListener.add('WoltLab/WCF/UI/Mobile', this._initButtonGroupNavigation.bind(this));
+               },
+               
+               _initSidebarToggleButtons: function() {
+                       if (_sidebar === null) return;
+                       
+                       var sidebarPosition = (_main.classList.contains('sidebarOrientationLeft')) ? 'Left' : '';
+                       sidebarPosition = (sidebarPosition) ? sidebarPosition : (_main.classList.contains('sidebarOrientationRight') ? 'Right' : '');
+                       
+                       if (!sidebarPosition) {
+                               return;
+                       }
+                       
+                       // use icons if language item is empty/non-existant
+                       var languageShowSidebar = 'wcf.global.sidebar.show' + sidebarPosition + 'Sidebar';
+                       if (languageShowSidebar === Language.get(languageShowSidebar) || Language.get(languageShowSidebar) === '') {
+                               languageShowSidebar = document.createElement('span');
+                               languageShowSidebar.className = 'icon icon16 fa-angle-double-' + sidebarPosition.toLowerCase();
+                       }
+                       
+                       var languageHideSidebar = 'wcf.global.sidebar.hide' + sidebarPosition + 'Sidebar';
+                       if (languageHideSidebar === Language.get(languageHideSidebar) || Language.get(languageHideSidebar) === '') {
+                               languageHideSidebar = document.createElement('span');
+                               languageHideSidebar.className = 'icon icon16 fa-angle-double-' + (sidebarPosition === 'Left' ? 'right' : 'left');
+                       }
+                       
+                       // add toggle buttons
+                       var showSidebar = document.createElement('span');
+                       showSidebar.className = 'button small mobileSidebarToggleButton';
+                       showSidebar.addEventListener('click', function() { _main.classList.add('mobileShowSidebar'); });
+                       if (languageShowSidebar instanceof Element) showSidebar.appendChild(languageShowSidebar);
+                       else showSidebar.textContent = languageShowSidebar;
+                       
+                       var hideSidebar = document.createElement('span');
+                       hideSidebar.className = 'button small mobileSidebarToggleButton';
+                       hideSidebar.addEventListener('click', function() { _main.classList.remove('mobileShowSidebar'); });
+                       if (languageHideSidebar instanceof Element) hideSidebar.appendChild(languageHideSidebar);
+                       else hideSidebar.textContent = languageHideSidebar;
+                       
+                       document.querySelector('.content').appendChild(showSidebar);
+                       _sidebar.appendChild(hideSidebar);
+               },
+               
+               _initSearchBar: function() {
+                       var _searchBar = document.querySelector('.searchBar');
+                       
+                       _searchBar.addEventListener('click', function() {
+                               if (_enabled) {
+                                       _searchBar.classList.add('searchBarOpen');
+                                       
+                                       return false;
+                               }
+                               
+                               return false;
+                       });
+                       
+                       _main.addEventListener('click', function() { _searchBar.classList.remove('searchBarOpen'); });
+               },
+               
+               _initButtonGroupNavigation: function() {
+                       for (var i = 0, length = _buttonGroupNavigations.length; i < length; i++) {
+                               var navigation = _buttonGroupNavigations[i];
+                               
+                               if (navigation.classList.contains('jsMobileButtonGroupNavigation')) continue;
+                               else navigation.classList.add('jsMobileButtonGroupNavigation');
+                               
+                               var button = document.createElement('a');
+                               button.classList.add('dropdownLabel');
+                               
+                               var span = document.createElement('span');
+                               span.className = 'icon icon24 fa-list';
+                               button.appendChild(span);
+                               
+                               button.addEventListener('click', function(ev) {
+                                       var next = DOMTraverse.next(button);
+                                       if (next !== null) {
+                                               next.classList.toggle('open');
+                                               
+                                               ev.stopPropagation();
+                                               return false;
+                                       }
+                                       
+                                       return true;
+                               });
+                               
+                               navigation.insertBefore(button, navigation.firstChild);
+                       }
+               },
+               
+               _closeAllMenus: function() {
+                       var openMenus = document.querySelectorAll('.jsMobileButtonGroupNavigation > ul.open');
+                       for (var i = 0, length = openMenus.length; i < length; i++) {
+                               openMenus[i].classList.remove('open');
+                       }
+               }
+       };
+       
+       return UIMobile;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Suggestion.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Suggestion.js
new file mode 100644 (file)
index 0000000..8a04097
--- /dev/null
@@ -0,0 +1,245 @@
+/**
+ * Flexible UI element featuring both a list of items and an input field with suggestion support.
+ * 
+ * @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/Suggestion
+ */
+define(['Ajax', 'Core', 'UI/SimpleDropdown'], function(Ajax, Core, UISimpleDropdown) {
+       "use strict";
+       
+       /**
+        * @constructor
+        * @param       {string}                elementId       input element id
+        * @param       {object<mixed>}         options         option list
+        */
+       function UISuggestion(elementId, options) { this.init(elementId, options); };
+       UISuggestion.prototype = {
+               /**
+                * Initializes a new suggestion input.
+                * 
+                * @param       {string}                element id      input element id
+                * @param       {object<mixed>}         options         option list
+                */
+               init: function(elementId, options) {
+                       this._dropdownMenu = null;
+                       this._value = '';
+                       
+                       this._element = document.getElementById(elementId);
+                       if (this._element === null) {
+                               throw new Error("Expected a valid element id.");
+                       }
+                       
+                       this._options = Core.extend({
+                               ajax: {
+                                       actionName: 'getSearchResultList',
+                                       className: '',
+                                       interfaceName: 'wcf\\data\\ISearchAction',
+                                       parameters: {
+                                               data: {}
+                                       }
+                               },
+                               
+                               // will be executed once a value from the dropdown has been selected
+                               callbackSelect: null,
+                               // list of excluded search values
+                               excludedSearchValues: [],
+                               // minimum number of characters required to trigger a search request
+                               treshold: 3
+                       }, options);
+                       
+                       if (typeof this._options.callbackSelect !== 'function') {
+                               throw new Error("Expected a valid callback for option 'callbackSelect'.");
+                       }
+                       
+                       this._element.addEventListener('click', function(event) { event.stopPropagation(); });
+                       this._element.addEventListener('keydown', this._keyDown.bind(this));
+                       this._element.addEventListener('keyup', this._keyUp.bind(this));
+               },
+               
+               /**
+                * Adds an excluded search value.
+                * 
+                * @param       {string}        value           excluded value
+                */
+               addExcludedValue: function(value) {
+                       if (this._options.excludedSearchValues.indexOf(value) === -1) {
+                               this._options.excludedSearchValues.push(value);
+                       }
+               },
+               
+               /**
+                * Removes an excluded search value.
+                * 
+                * @param       {string}        value           excluded value
+                */
+               removeExcludedValue: function(value) {
+                       var index = this._options.excludedSearchValues.indexOf(value);
+                       if (index !== -1) {
+                               this._options.excludedSearchValues.splice(index, 1);
+                       }
+               },
+               
+               /**
+                * Handles the keyboard navigation for interaction with the suggestion list.
+                * 
+                * @param       {object}        event           event object
+                */
+               _keyDown: function(event) {
+                       if (this._dropdownMenu === null || !UISimpleDropdown.isOpen(this._element.id)) {
+                               return true;
+                       }
+                       
+                       if (event.keyCode !== 13 && event.keyCode !== 27 && event.keyCode !== 38 && event.keyCode !== 40) {
+                               return true;
+                       }
+                       
+                       var active, i = 0, length = this._dropdownMenu.childElementCount;
+                       while (i < length) {
+                               active = this._dropdownMenu.children[i];
+                               if (active.classList.contains('active')) {
+                                       break;
+                               }
+                               
+                               i++;
+                       }
+                       
+                       if (event.keyCode === 13) {
+                               // Enter
+                               UISimpleDropdown.close(this._element.id);
+                               
+                               this._select(active);
+                       }
+                       else if (event.keyCode === 27) {
+                               if (UISimpleDropdown.isOpen(this._element.id)) {
+                                       UISimpleDropdown.close(this._element.id);
+                               }
+                               else {
+                                       // let the event pass through
+                                       return true;
+                               }
+                       }
+                       else {
+                               var index = 0;
+                               
+                               if (event.keyCode === 38) {
+                                       // ArrowUp
+                                       index = ((i === 0) ? length : i) - 1;
+                               }
+                               else if (event.keyCode === 40) {
+                                       // ArrowDown
+                                       index = i + 1;
+                                       if (index === length) index = 0;
+                               }
+                               
+                               if (index !== i) {
+                                       active.classList.remove('active');
+                                       this._dropdownMenu.children[index].classList.add('active');
+                               }
+                       }
+                       
+                       event.preventDefault();
+                       return false;
+               },
+               
+               /**
+                * Selects an item from the list.
+                * 
+                * @param       {(Element|Event)}       item    list item or event object
+                */
+               _select: function(item) {
+                       var isEvent = (item instanceof Event);
+                       if (isEvent) {
+                               item = item.currentTarget.parentNode;
+                       }
+                       
+                       this._options.callbackSelect(this._element.id, { objectId: item.children[0].getAttribute('data-object-id'), value: item.textContent });
+                       
+                       if (isEvent) {
+                               this._element.focus();
+                       }
+               },
+               
+               /**
+                * Performs a search for the input value unless it is below the treshold.
+                * 
+                * @param       {object}                event           event object
+                */
+               _keyUp: function(event) {
+                       var value = event.currentTarget.value.trim();
+                       
+                       if (this._value === value) {
+                               return;
+                       }
+                       else if (value.length < this._options.treshold) {
+                               if (this._dropdownMenu !== null) {
+                                       UISimpleDropdown.close(this._element.id);
+                               }
+                               
+                               this._value = value;
+                               
+                               return;
+                       }
+                       
+                       this._value = value;
+                       
+                       Ajax.api(this, {
+                               parameters: {
+                                       data: {
+                                               excludedSearchValues: this._options.excludedSearchValues,
+                                               searchString: value
+                                       }
+                               }
+                       });
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: this._options.ajax
+                       };
+               },
+               
+               /**
+                * Handles successful Ajax requests.
+                * 
+                * @param       {object}        data            response values
+                */
+               _ajaxSuccess: function(data) {
+                       if (this._dropdownMenu === null) {
+                               this._dropdownMenu = document.createElement('div');
+                               this._dropdownMenu.className = 'dropdownMenu';
+                               
+                               UISimpleDropdown.initFragment(this._element, this._dropdownMenu);
+                       }
+                       else {
+                               this._dropdownMenu.innerHTML = '';
+                       }
+                       
+                       if (data.returnValues.length) {
+                               var anchor, item, listItem;
+                               for (var i = 0, length = data.returnValues.length; i < length; i++) {
+                                       item = data.returnValues[i];
+                                       
+                                       anchor = document.createElement('a');
+                                       anchor.textContent = item.label;
+                                       anchor.setAttribute('data-object-id', item.objectID);
+                                       anchor.addEventListener('click', this._select.bind(this));
+                                       
+                                       listItem = document.createElement('li');
+                                       if (i === 0) listItem.className = 'active';
+                                       listItem.appendChild(anchor);
+                                       
+                                       this._dropdownMenu.appendChild(listItem);
+                               }
+                               
+                               UISimpleDropdown.open(this._element.id);
+                       }
+                       else {
+                               UISimpleDropdown.close(this._element.id);
+                       }
+               }
+       };
+       
+       return UISuggestion;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/TabMenu.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/TabMenu.js
new file mode 100644 (file)
index 0000000..b3f49a7
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Common interface for tab menu access.
+ * 
+ * @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/TabMenu
+ */
+define(['Dictionary', 'DOM/ChangeListener', 'DOM/Util', './TabMenu/Simple'], function(Dictionary, DOMChangeListener, DOMUtil, SimpleTabMenu) {
+       "use strict";
+       
+       var _tabMenus = new Dictionary();
+       
+       /**
+        * @exports     WoltLab/WCF/UI/TabMenu
+        */
+       var UITabMenu = {
+               /**
+                * Sets up tab menus and binds listeners.
+                */
+               setup: function() {
+                       this._init();
+                       this._selectErroneousTabs();
+                       
+                       DOMChangeListener.add('WoltLab/WCF/UI/TabMenu', this._init.bind(this));
+               },
+               
+               /**
+                * Initializes available tab menus.
+                */
+               _init: function() {
+                       var tabMenus = document.querySelectorAll('.tabMenuContainer:not(.staticTabMenuContainer)');
+                       for (var i = 0, length = tabMenus.length; i < length; i++) {
+                               var container = tabMenus[i];
+                               var containerId = DOMUtil.identify(container);
+                               
+                               if (_tabMenus.has(containerId)) {
+                                       continue;
+                               }
+                               
+                               var tabMenu = new SimpleTabMenu(containerId, container);
+                               if (tabMenu.validate()) {
+                                       tabMenu.init();
+                                       
+                                       _tabMenus.set(containerId, tabMenu);
+                               }
+                       }
+               },
+               
+               /**
+                * Selects the first tab containing an element with class `formError`.
+                */
+               _selectErroneousTabs: function() {
+                       _tabMenus.forEach(function(tabMenu) {
+                               var foundError = false;
+                               tabMenu.getContainers().forEach(function(container) {
+                                       if (!foundError && container.getElementsByClassName('formError').length) {
+                                               foundError = true;
+                                               
+                                               tabMenu.select(container.id);
+                                       }
+                               });
+                       });
+               },
+               
+               /**
+                * Returns a SimpleTabMenu instance for given container id.
+                * 
+                * @param       {string}        containerId     tab menu container id
+                * @return      {(SimpleTabMenu|undefined)}     tab menu object
+                */
+               getTabMenu: function(containerId) {
+                       return _tabMenus.get(containerId);
+               }
+       };
+       
+       return UITabMenu;
+});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/TabMenu/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/TabMenu/Simple.js
new file mode 100644 (file)
index 0000000..027e86b
--- /dev/null
@@ -0,0 +1,316 @@
+/**
+ * Simple tab menu implementation with a straight-forward logic.
+ * 
+ * @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/TabMenu/Simple
+ */
+define(['Dictionary', 'DOM/Traverse', 'DOM/Util', 'EventHandler'], function(Dictionary, DOMTraverse, DOMUtil, EventHandler) {
+       "use strict";
+       
+       /**
+        * @param       {string}        containerId     container id
+        * @param       {Element}       container       container element
+        * @constructor
+        */
+       function TabMenuSimple(containerId, container) {
+               this._container = container;
+               this._containers = new Dictionary();
+               this._containerId = containerId;
+               this._isLegacy = null;
+               this._isParent = false;
+               this._parent = null;
+               this._tabs = new Dictionary();
+       };
+       
+       TabMenuSimple.prototype = {
+               /**
+                * Validates the properties and DOM structure of this container.
+                * 
+                * Expected DOM:
+                * <div class="tabMenuContainer">
+                *      <nav>
+                *              <ul>
+                *                      <li data-name="foo"><a>bar</a></li>
+                *              </ul>
+                *      </nav>
+                *      
+                *      <div id="foo">baz</div>
+                * </div>
+                * 
+                * @return      {boolean}       false if any properties are invalid or the DOM does not match the expectations
+                */
+               validate: function() {
+                       if (!this._container.classList.contains('tabMenuContainer')) {
+                               return false;
+                       }
+                       
+                       var nav = DOMTraverse.childByTag(this._container, 'NAV');
+                       if (nav === null) {
+                               return false;
+                       }
+                       
+                       // get children
+                       var tabs = nav.getElementsByTagName('li');
+                       if (tabs.length === null) {
+                               return false;
+                       }
+                       
+                       var containers = DOMTraverse.childrenByTag(this._container, 'DIV');
+                       for (var i = 0, length = containers.length; i < length; i++) {
+                               var container = containers[i];
+                               var name = container.getAttribute('data-name');
+                               
+                               if (!name) {
+                                       name = DOMUtil.identify(container);
+                               }
+                               
+                               container.setAttribute('data-name', name);
+                               this._containers.set(name, container);
+                       }
+                       
+                       for (var i = 0, length = tabs.length; i < length; i++) {
+                               var tab = tabs[i];
+                               var name = this._getTabName(tab);
+                               
+                               if (!name) {
+                                       continue;
+                               }
+                               
+                               if (this._tabs.has(name)) {
+                                       throw new Error("Tab names must be unique, li[data-name='" + name + "'] (tab menu id: '" + this._containerId + "') exists more than once.");
+                               }
+                               
+                               var container = this._containers.get(name);
+                               if (container === undefined) {
+                                       throw new Error("Expected content element for li[data-name='" + name + "'] (tab menu id: '" + this._containerId + "').");
+                               }
+                               else if (container.parentNode !== this._container) {
+                                       throw new Error("Expected content element '" + name + "' (tab menu id: '" + this._containerId + "') to be a direct children.");
+                               }
+                               
+                               // check if tab holds exactly one children which is an anchor element
+                               if (tab.childElementCount !== 1 || tab.children[0].nodeName !== 'A') {
+                                       throw new Error("Expected exactly one <a> as children for li[data-name='" + name + "'] (tab menu id: '" + this._containerId + "').");
+                               }
+                               
+                               this._tabs.set(name, tab);
+                       }
+                       
+                       if (!this._tabs.size) {
+                               throw new Error("Expected at least one tab (tab menu id: '" + this._containerId + "').");
+                       }
+                       
+                       if (this._isLegacy) {
+                               this._container.setAttribute('data-is-legacy', true);
+                               
+                               this._tabs.forEach(function(tab, name) {
+                                       tab.setAttribute('aria-controls', name);
+                               });
+                       }
+                       
+                       return true;
+               },
+               
+               /**
+                * Initializes this tab menu.
+                * 
+                * @param       {Dictionary=}   oldTabs         previous list of tabs
+                */
+               init: function(oldTabs) {
+                       oldTabs = oldTabs || null;
+                       
+                       // bind listeners
+                       this._tabs.forEach((function(tab) {
+                               if (oldTabs === null || oldTabs.get(tab.getAttribute('data-name')) !== tab) {
+                                       tab.children[0].addEventListener('click', this._onClick.bind(this));
+                               }
+                       }).bind(this));
+                       
+                       if (oldTabs === null) {
+                               var preselect = this._container.getAttribute('data-preselect');
+                               if (preselect === "true" || preselect === null || preselect === "") preselect = true;
+                               if (preselect === "false") preselect = false;
+                               
+                               this._containers.forEach(function(container) {
+                                       container.classList.add('hidden');
+                               });
+                               
+                               if (preselect !== false) {
+                                       if (preselect !== true) {
+                                               var tab = this._tabs.get(preselect);
+                                               if (tab !== undefined) {
+                                                       this.select(null, tab, true);
+                                               }
+                                       }
+                                       else {
+                                               var selectTab = null;
+                                               this._tabs.forEach(function(tab) {
+                                                       if (selectTab === null && tab.previousElementSibling === null) {
+                                                               selectTab = tab; 
+                                                       }
+                                               });
+                                               
+                                               if (selectTab !== null) {
+                                                       this.select(null, selectTab, true);
+                                               }
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Selects a tab.
+                * 
+                * @param       {?(string|integer)}     name            tab name or sequence no
+                * @param       {Element=}              tab             tab element
+                * @param       {boolean=}              disableEvent    suppress event handling
+                */
+               select: function(name, tab, disableEvent) {
+                       tab = tab || this._tabs.get(name) || null;
+                       
+                       if (tab === null) {
+                               // check if name is an integer
+                               if (~~name == name) {
+                                       name = ~~name;
+                                       
+                                       var i = 0;
+                                       this._tabs.forEach(function(item) {
+                                               if (i === name) {
+                                                       tab = item;
+                                               }
+                                               
+                                               i++;
+                                       });
+                               }
+                               
+                               if (tab === null) {
+                                       throw new Error("Expected a valid tab name, '" + name + "' given (tab menu id: '" + this._containerId + "').");
+                               }
+                       }
+                       
+                       if (!name) name = tab.getAttribute('data-name');
+                       
+                       // unmark active tab
+                       var oldTab = document.querySelector('#' + this._containerId + ' > nav > ul > li.active');
+                       var oldContent = null;
+                       if (oldTab !== null) {
+                               oldTab.classList.remove('active');
+                               oldContent = this._containers.get(oldTab.getAttribute('data-name'));
+                               oldContent.classList.remove('active');
+                               oldContent.classList.add('hidden');
+                               
+                               if (this._isLegacy) {
+                                       oldTab.classList.remove('ui-state-active');
+                                       oldContent.classList.remove('ui-state-active');
+                               }
+                       }
+                       
+                       tab.classList.add('active');
+                       var newContent = this._containers.get(name);
+                       newContent.classList.add('active');
+                       
+                       if (this._isLegacy) {
+                               tab.classList.add('ui-state-active');
+                               newContent.classList.add('ui-state-active');
+                               newContent.classList.remove('hidden');
+                       }
+                       
+                       if (disableEvent !== true) {
+                               EventHandler.fire('com.woltlab.wcf.simpleTabMenu_' + this._containerId, 'select', {
+                                       active: tab,
+                                       activeName: name,
+                                       previous: oldTab,
+                                       previousName: oldTab.getAttribute('data-name')
+                               });
+                               
+                               if (this._isLegacy && typeof window.jQuery === 'function') {
+                                       // simulate jQuery UI Tabs event
+                                       window.jQuery(this._container).trigger('wcftabsbeforeactivate', {
+                                               newTab: window.jQuery(tab),
+                                               oldTab: window.jQuery(oldTab),
+                                               newPanel: window.jQuery(newContent),
+                                               oldPanel: window.jQuery(oldContent)
+                                       });
+                               }
+                       }
+               },
+               
+               /**
+                * Rebuilds all tabs, must be invoked after adding or removing of tabs.
+                * 
+                * Warning: Do not remove tabs if you plan to add these later again or at least clone the nodes
+                *          to prevent issues with already bound event listeners. Consider hiding them via CSS.
+                */
+               rebuild: function() {
+                       var oldTabs = this._tabs;
+                       
+                       this.validate();
+                       this.init(oldTabs);
+               },
+               
+               /**
+                * Handles clicks on a tab.
+                * 
+                * @param       {object}        event   event object
+                */
+               _onClick: function(event) {
+                       event.preventDefault();
+                       
+                       var tab = event.currentTarget.parentNode;
+                       
+                       this.select(null, tab);
+               },
+               
+               /**
+                * Returns the tab name.
+                * 
+                * @param       {Element}       tab     tab element
+                * @return      {string}        tab name
+                */
+               _getTabName: function(tab) {
+                       var name = tab.getAttribute('data-name');
+                       
+                       // handle legacy tab menus
+                       if (!name) {
+                               if (tab.childElementCount === 1 && tab.children[0].nodeName === 'A') {
+                                       var href = tab.children[0].getAttribute('href');
+                                       if (href.match(/#([^#]+)$/)) {
+                                               name = RegExp.$1;
+                                               
+                                               if (document.getElementById(name) === null) {
+                                                       name = null;
+                                               }
+                                               else {
+                                                       this._isLegacy = true;
+                                                       tab.setAttribute('data-name', name);
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       return name;
+               },
+               
+               /**
+                * Returns the list of registered content containers.
+                * 
+                * @returns     {Dictionary}    content containers
+                */
+               getContainers: function() {
+                       return this._containers;
+               },
+               
+               /**
+                * Returns the list of registered tabs.
+                * 
+                * @returns     {Dictionary}    tab items
+                */
+               getTabs: function() {
+                       return this._tabs;
+               }
+       };
+       
+       return TabMenuSimple;
+});
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..5ba1439
--- /dev/null
@@ -0,0 +1,122 @@
+/**
+ * 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(['Environment', 'DOM/ChangeListener', 'UI/Alignment'], function(Environment, DOMChangeListener, UIAlignment) {
+       "use strict";
+       
+       var _elements = null;
+       var _pointer = null;
+       var _text = null;
+       var _tooltip = null;
+       
+       /**
+        * @exports     WoltLab/WCF/UI/Tooltip
+        */
+       var UITooltip = {
+               /**
+                * Initializes the tooltip element and binds event listener.
+                */
+               setup: function() {
+                       if (Environment.platform() !== 'desktop') 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();
+                       
+                       DOMChangeListener.add('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');
+                               title = (typeof title === 'string') ? title.trim() : '';
+                               
+                               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');
+                       title = (typeof title === 'string') ? title.trim() : '';
+                       
+                       if (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'],
+                               vertical: 'top'
+                       });
+               },
+               
+               /**
+                * Hides the tooltip once the mouse leaves the element.
+                * 
+                * @param       {object}        event   event object
+                */
+               _mouseLeave: function(event) {
+                       _tooltip.classList.remove('active');
+               }
+       };
+       
+       return UITooltip;
+});