Move JavaScript modules into WoltLabSuite/Core namespace
authorMatthias Schmidt <gravatronics@live.com>
Thu, 4 Aug 2016 19:30:56 +0000 (21:30 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Thu, 4 Aug 2016 19:30:56 +0000 (21:30 +0200)
293 files changed:
com.woltlab.wcf/templates/__commentJavaScript.tpl
com.woltlab.wcf/templates/aclSimple.tpl
com.woltlab.wcf/templates/article.tpl
com.woltlab.wcf/templates/codeBBCodeTag.tpl
com.woltlab.wcf/templates/codeMetaCode.tpl
com.woltlab.wcf/templates/headIncludeJavaScript.tpl
com.woltlab.wcf/templates/languageChooser.tpl
com.woltlab.wcf/templates/messageFormMultilingualism.tpl
com.woltlab.wcf/templates/multipleLanguageInputJavascript.tpl
com.woltlab.wcf/templates/notificationSettings.tpl
com.woltlab.wcf/templates/pageHeader.tpl
com.woltlab.wcf/templates/pageHeaderSearch.tpl
com.woltlab.wcf/templates/pageHeaderUser.tpl
com.woltlab.wcf/templates/shareButtons.tpl
com.woltlab.wcf/templates/tagInput.tpl
com.woltlab.wcf/templates/user.tpl
com.woltlab.wcf/templates/userNotice.tpl
com.woltlab.wcf/templates/wysiwyg.tpl
wcfsetup/install/files/acp/js/WCF.ACP.Style.js
wcfsetup/install/files/acp/templates/__pageAddContent.tpl
wcfsetup/install/files/acp/templates/aclSimple.tpl
wcfsetup/install/files/acp/templates/articleAdd.tpl
wcfsetup/install/files/acp/templates/articleAddDialog.tpl
wcfsetup/install/files/acp/templates/articleList.tpl
wcfsetup/install/files/acp/templates/boxAdd.tpl
wcfsetup/install/files/acp/templates/boxAddDialog.tpl
wcfsetup/install/files/acp/templates/bulkProcessing.tpl
wcfsetup/install/files/acp/templates/header.tpl
wcfsetup/install/files/acp/templates/index.tpl
wcfsetup/install/files/acp/templates/languageChooser.tpl
wcfsetup/install/files/acp/templates/mediaAdd.tpl
wcfsetup/install/files/acp/templates/mediaList.tpl
wcfsetup/install/files/acp/templates/menuItemAdd.tpl
wcfsetup/install/files/acp/templates/multipleLanguageInputJavascript.tpl
wcfsetup/install/files/acp/templates/notificationPresetSettings.tpl
wcfsetup/install/files/acp/templates/pageAdd.tpl
wcfsetup/install/files/acp/templates/pageAddDialog.tpl
wcfsetup/install/files/acp/templates/styleAdd.tpl
wcfsetup/install/files/acp/templates/tagAdd.tpl
wcfsetup/install/files/acp/templates/userSearch.tpl
wcfsetup/install/files/acp/templates/wysiwyg.tpl
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabCode.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabColor.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabLink.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabMedia.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabMention.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabPage.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabQuote.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabSize.js
wcfsetup/install/files/js/3rdParty/redactor2/plugins/WoltLabSpoiler.js
wcfsetup/install/files/js/WCF.Like.js
wcfsetup/install/files/js/WCF.Message.js
wcfsetup/install/files/js/WCF.Poll.js
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/js/WoltLab/WCF/Acp/Bootstrap.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Article/Add.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Add.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Controller/Handler.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Handler.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/CodeMirror/Media.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/CodeMirror/Page.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Menu/Item/Handler.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Page/Add.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Page/Menu.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Editor.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Image/Upload.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ajax.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ajax/Jsonp.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ajax/Status.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Bbcode/Collapsible.js [deleted file]
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/Bootstrap.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/BootstrapFrontend.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/ColorUtil.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Captcha.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Clipboard.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Condition/Page/Dependence.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Notice/Dismiss.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Controller/Style/Changer.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Controller/User/Notification/Settings.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Core.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Date/Picker.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Date/Time/Relative.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Date/Util.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js [deleted file]
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/Environment.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Event/Key.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/File/Util.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Language.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Language/Chooser.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/List.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Media/Editor.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Base.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Editor.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Search.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Select.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Media/Search.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Media/Upload.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/NumberUtil.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/ObjectMap.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Permission.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/StringUtil.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Template.grammar.jison [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Template.grammar.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Template.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Timer/Repeating.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Acl/Simple.js [deleted file]
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/Confirmation.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.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/Filter.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/User.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Like/Handler.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Manager.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Share.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Mobile.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Notification.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Action.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Header/Fixed.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/JumpTo.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/JumpToTop.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Abstract.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Main.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/User.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search/Handler.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search/Input.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Pagination.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Autosave.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Code.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Format.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Link.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Mention.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Metacode.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Page.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Spoiler.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Screen.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Search/Input.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Search/Page.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/Toggle/Input.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/Tooltip.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Editor.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Ignore.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/User/List.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Abstract.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Follow.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Ignore.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Search/Input.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/Upload.js [deleted file]
wcfsetup/install/files/js/WoltLab/WCF/User.js [deleted file]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Bootstrap.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/Add.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Add.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Controller/Handler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Handler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Media.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Page.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Menu/Item/Handler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Page/Add.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Page/Menu.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Editor.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Image/Upload.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Jsonp.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Status.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/Collapsible.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/FromHtml.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/Parser.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/ToHtml.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/CallbackList.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/ColorUtil.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Captcha.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Condition/Page/Dependence.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Notice/Dismiss.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Popover.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Style/Changer.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/User/Notification/Settings.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Core.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Date/Picker.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Date/Time/Relative.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Date/Util.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Dictionary.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Change/Listener.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Traverse.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Environment.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Event/Handler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/File/Util.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Language.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Language/Chooser.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Language/Input.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/List.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Editor.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Search.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Select.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Search.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Upload.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/NumberUtil.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/ObjectMap.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Permission.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/StringUtil.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Template.grammar.jison [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Template.grammar.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Template.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Timer/Repeating.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Acl/Simple.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Alignment.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/CloseOverlay.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Confirmation.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Reusable.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/FlexibleMenu.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/Filter.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/User.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Like/Handler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Share.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Notification.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Action.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Header/Fixed.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/JumpTo.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/JumpToTop.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Abstract.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Main.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/User.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search/Handler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search/Input.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Pagination.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Autosave.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Format.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Link.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Metacode.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Page.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Quote.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Spoiler.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Screen.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Scroll.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Search/Input.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Search/Page.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Suggestion.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Toggle/Input.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Tooltip.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Editor.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Ignore.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/List.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Abstract.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Follow.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Ignore.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Search/Input.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/User.js [new file with mode: 0644]
wcfsetup/install/files/js/require.build-acp.js
wcfsetup/install/files/js/require.build.js
wcfsetup/install/files/js/require.config.js

index 4c64c0daac5b1c74627ecac3cb1192aabc4b93d8..1253cb6a6c51f15abe94dc5773bebe7170e1da1d 100644 (file)
@@ -16,7 +16,7 @@
                
                new {if $commentHandlerClass|isset}{@$commentHandlerClass}{else}WCF.Comment.Handler{/if}('{$commentContainerID}', '{@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(48)}', '{@$__wcf->getUserProfileHandler()->getAvatar()->getImageTag(32)}');
                {if MODULE_LIKE && $commentList->getCommentManager()->supportsLike() && $__wcf->getSession()->getPermission('user.like.canViewLike')}
-                       require(['WoltLab/WCF/Ui/Like/Handler'], function(UiLikeHandler) {
+                       require(['WoltLabSuite/Core/Ui/Like/Handler'], function(UiLikeHandler) {
                                var canDislike = {if LIKE_ENABLE_DISLIKE}true{else}false{/if};
                                var canLike = {if $__wcf->getUser()->userID && $__wcf->getSession()->getPermission('user.like.canLike')}true{else}false{/if};
                                var canLikeOwnContent = {if LIKE_ALLOW_FOR_OWN_CONTENT}true{else}false{/if};
index 3fa5a97ceab96007e4e5444615859f7063e435ca..7e556d8b09e7d1d51e5d1e020064ea508baa0c7e 100644 (file)
@@ -53,7 +53,7 @@
 </section>
 
 <script data-relocate="true">
-       require(['WoltLab/WCF/Ui/Acl/Simple'], function(UiAclSimple) {
+       require(['WoltLabSuite/Core/Ui/Acl/Simple'], function(UiAclSimple) {
                new UiAclSimple('{@$__aclSimplePrefix}');
        });
 </script>
index 4a3352a1529fba238fc2998cbbb38b3e798b9c76..0fbba1c249f2f8a3a715daeeb83b41e12021c2ca 100644 (file)
 
 {if MODULE_LIKE && ARTICLE_ENABLE_LIKE}
        <script data-relocate="true">
-               require(['WoltLab/WCF/Ui/Like/Handler'], function(UiLikeHandler) {
+               require(['WoltLabSuite/Core/Ui/Like/Handler'], function(UiLikeHandler) {
                        new UiLikeHandler('com.woltlab.wcf.likeableArticle', {
                                // settings
                                isSingleItem: true,
index 849d80899468275f40386ed25bcb52e0ba7b22e4..37743a4dc94f758b4f817c5b565894ae6115ec38 100644 (file)
@@ -24,7 +24,7 @@
                {if !$__overlongCodeBoxSeen|isset}
                        {assign var='__overlongCodeBoxSeen' value=true}
                        <script data-relocate="true">
-                               require(['WoltLab/WCF/Bbcode/Collapsible'], function(BbcodeCollapsible) {
+                               require(['WoltLabSuite/Core/Bbcode/Collapsible'], function(BbcodeCollapsible) {
                                        BbcodeCollapsible.observe();
                                });
                        </script>
index 8fcf0abc705accbf8bee7e45bd72f866d64e351f..659cb0d27fcf66fe4c8a81b2c8b32740fe5be8e5 100644 (file)
@@ -24,7 +24,7 @@
                {if !$__overlongCodeBoxSeen|isset}
                        {assign var='__overlongCodeBoxSeen' value=true}
                        <script data-relocate="true">
-                               require(['WoltLab/WCF/Bbcode/Collapsible'], function(BbcodeCollapsible) {
+                               require(['WoltLabSuite/Core/Bbcode/Collapsible'], function(BbcodeCollapsible) {
                                        BbcodeCollapsible.observe();
                                });
                        </script>
index 30b36cbcfd317eb96d6330e1acd126865373d13a..dd6ed89c1aad866f1728337b0eb55d71b7167033 100644 (file)
@@ -22,7 +22,7 @@ requirejs.config({
 });
 </script>
 <script data-relocate="true">
-       require(['Language', 'WoltLab/WCF/BootstrapFrontend', 'User'], function(Language, BootstrapFrontend, User) {
+       require(['Language', 'WoltLabSuite/Core/BootstrapFrontend', 'User'], function(Language, BootstrapFrontend, User) {
                Language.addObject({
                        '__days': [ '{lang}wcf.date.day.sunday{/lang}', '{lang}wcf.date.day.monday{/lang}', '{lang}wcf.date.day.tuesday{/lang}', '{lang}wcf.date.day.wednesday{/lang}', '{lang}wcf.date.day.thursday{/lang}', '{lang}wcf.date.day.friday{/lang}', '{lang}wcf.date.day.saturday{/lang}' ],
                        '__daysShort': [ '{lang}wcf.date.day.sun{/lang}', '{lang}wcf.date.day.mon{/lang}', '{lang}wcf.date.day.tue{/lang}', '{lang}wcf.date.day.wed{/lang}', '{lang}wcf.date.day.thu{/lang}', '{lang}wcf.date.day.fri{/lang}', '{lang}wcf.date.day.sat{/lang}' ],
index d537396a518a82c3642cc503521e9691ba2b735f..61b78e6b23c9d9d2da5c00b1951b1b89cf2ea85a 100644 (file)
@@ -16,7 +16,7 @@
        </dl>
        
        <script data-relocate="true">
-               require(['WoltLab/WCF/Language/Chooser'], function(LanguageChooser) {
+               require(['WoltLabSuite/Core/Language/Chooser'], function(LanguageChooser) {
                        var languages = {
                                {implode from=$languages item=__language}
                                        '{@$__language->languageID}': {
index 61adf5b8453ffc233a23b85bfcb05d0e5cd8f750..c237efaab30f76b993d94f388f14c4864b10e4e5 100644 (file)
@@ -13,7 +13,7 @@
        </dl>
        
        <script data-relocate="true">
-               require(['WoltLab/WCF/Language/Chooser'], function(LanguageChooser) {
+               require(['WoltLabSuite/Core/Language/Chooser'], function(LanguageChooser) {
                        var languages = {
                                {implode from=$availableContentLanguages item=__language}
                                        '{@$__language->languageID}': {
index cdacde670434be4b8043eda63035190d7fa773f1..50830c088c57548a97fabf91aaac8383667f5ec0 100644 (file)
@@ -1,6 +1,6 @@
 {if $availableLanguages|count > 1}
        <script data-relocate="true">
-               require(['Language', 'WoltLab/WCF/Language/Input'], function(Language, LanguageInput) {
+               require(['Language', 'WoltLabSuite/Core/Language/Input'], function(Language, LanguageInput) {
                        Language.addObject({
                                'wcf.global.button.disabledI18n': '{lang}wcf.global.button.disabledI18n{/lang}'
                        });
index a6b475f82ab9922cc672493c30a7aa4e4c7e590e..4d9ce41dbc51505d20112f66270c93f7702aa845 100644 (file)
@@ -66,7 +66,7 @@
 </form>
 
 <script data-relocate="true">
-       require(['Language', 'WoltLab/WCF/Controller/User/Notification/Settings'], function(Language, ControllerUserNotificationSettings) {
+       require(['Language', 'WoltLabSuite/Core/Controller/User/Notification/Settings'], function(Language, ControllerUserNotificationSettings) {
                Language.addObject({
                        'wcf.user.notification.mailNotificationType.daily': '{lang}wcf.user.notification.mailNotificationType.daily{/lang}',
                        'wcf.user.notification.mailNotificationType.instant': '{lang}wcf.user.notification.mailNotificationType.instant{/lang}',
index 9dd0c625264b1f77525bb3ad507802b3f3a8d867..5c3deff7c3db0a27b3c634322ebc548203c4010e 100644 (file)
@@ -14,7 +14,7 @@
                
                {* TODO: this should be moved somewhere else and turned into an option *}
                <script data-relocate="true">
-                       require(['WoltLab/WCF/Ui/Page/Header/Fixed'], function(UiPageHeaderFixed) {
+                       require(['WoltLabSuite/Core/Ui/Page/Header/Fixed'], function(UiPageHeaderFixed) {
                                UiPageHeaderFixed.init();
                        });
                </script>
index 1eccf74a445b6ffefabfb054af93c9214e82da46..79037cf859badc4ffe57e203ed4bab1ef18df694 100644 (file)
@@ -71,7 +71,7 @@
 
 {if !OFFLINE || $__wcf->session->getPermission('admin.general.canViewPageDuringOfflineMode')}
        <script data-relocate="true">
-               require(['WoltLab/WCF/Ui/Search/Page'], function(UiSearchPage) {
+               require(['WoltLabSuite/Core/Ui/Search/Page'], function(UiSearchPage) {
                        UiSearchPage.init('{if !$__searchObjectTypeName|empty}{@$__searchObjectTypeName}{else}everywhere{/if}');
                });
        </script>
index 9a865522c4b1f514d0a5fe6f175f180ca5700186..41a1b3abf7f2c81eefc4b822bd9172b10847526f 100644 (file)
                        {if $__wcf->getLanguage()->getLanguages()|count > 1}
                                <li id="pageLanguageContainer">
                                        <script data-relocate="true">
-                                               require(['EventHandler', 'WoltLab/WCF/Language/Chooser'], function(EventHandler, LanguageChooser) {
+                                               require(['EventHandler', 'WoltLabSuite/Core/Language/Chooser'], function(EventHandler, LanguageChooser) {
                                                        var languages = {
                                                                {implode from=$__wcf->getLanguage()->getLanguages() item=__language}
                                                                        '{@$__language->languageID}': {
index 55943a19a2810c565c2f2b7db1c265fc2d85d852..5459276cb330ebdc7c68026711745a7823b7a235 100644 (file)
@@ -70,7 +70,7 @@
        </ul>
        
        <script data-relocate="true">
-               require(['WoltLab/WCF/Ui/Message/Share'], function(UiMessageShare) {
+               require(['WoltLabSuite/Core/Ui/Message/Share'], function(UiMessageShare) {
                        UiMessageShare.init();
                });
        </script>
index 23daa44c04ec79f10b00871bed0375f43313fbb4..998b4ad157072d4db70a6d74e051da28035a05a6 100644 (file)
@@ -8,7 +8,7 @@
        </dl>
        
        <script data-relocate="true">
-               require(['WoltLab/WCF/Ui/ItemList'], function(UiItemList) {
+               require(['WoltLabSuite/Core/Ui/ItemList'], function(UiItemList) {
                        UiItemList.init(
                                'tagSearchInput{if $tagInputSuffix|isset}{@$tagInputSuffix}{/if}',
                                [{if $tags|isset && $tags|count}{implode from=$tags item=tag}'{$tag|encodeJS}'{/implode}{/if}],
index 3ba1c173a1701e8e6f36757358ba02d4ba5c9f3f..ca751b4381bee088ca281d4d6874489d2a309c46 100644 (file)
@@ -4,7 +4,7 @@
        {event name='javascriptInclude'}
        <script data-relocate="true">
                {if $__wcf->getUser()->userID && $__wcf->getUser()->userID != $user->userID}
-                       require(['Language', 'WoltLab/WCF/Ui/User/Editor', 'WoltLab/WCF/Ui/User/Profile/Menu/Item/Ignore', 'WoltLab/WCF/Ui/User/Profile/Menu/Item/Follow'], function(Language, UiUserEditor, UiUserProfileMenuItemIgnore, UiUserProfileMenuItemFollow) {
+                       require(['Language', 'WoltLabSuite/Core/Ui/User/Editor', 'WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Ignore', 'WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Follow'], function(Language, UiUserEditor, UiUserProfileMenuItemIgnore, UiUserProfileMenuItemFollow) {
                                Language.addObject({
                                        'wcf.acp.user.disable': '{lang}wcf.acp.user.disable{/lang}',
                                        'wcf.acp.user.enable': '{lang}wcf.acp.user.enable{/lang}',
index b06d1aa15aec38f383ae034064bbdcfe128b9c3e..53c011e81aeb63196862d9cd49bacbe3cd094205 100644 (file)
@@ -28,7 +28,7 @@
                {/content}
                
                <script data-relocate="true">
-                       require(['WoltLab/WCF/Controller/Notice/Dismiss'], function(ControllerNoticeDismiss) {
+                       require(['WoltLabSuite/Core/Controller/Notice/Dismiss'], function(ControllerNoticeDismiss) {
                                ControllerNoticeDismiss.setup();
                        });
                </script>
index 5a3e8a03ef4580ae53828a96b3c2fd9682b99954..77bf603f94cf224131aaafbabcbf980026f185a3 100644 (file)
@@ -49,7 +49,7 @@
                
                {event name='redactorJavaScript'}
        ], function () {
-               require(['Language', 'WoltLab/WCF/Ui/Redactor/Autosave', 'WoltLab/WCF/Ui/Redactor/Metacode'], function(Language, UiRedactorAutosave, UiRedactorMetacode) {
+               require(['Language', 'WoltLabSuite/Core/Ui/Redactor/Autosave', 'WoltLabSuite/Core/Ui/Redactor/Metacode'], function(Language, UiRedactorAutosave, UiRedactorMetacode) {
                        Language.addObject({
                                'wcf.editor.code.edit': '{lang}wcf.editor.code.edit{/lang}',
                                'wcf.editor.code.file': '{lang}wcf.editor.code.file{/lang}',
index 06243a2d552a671cdb8ef8da4e17741cbe117074..1821bc8505fa41eab1c406b34dea04ed77f808a8 100644 (file)
@@ -69,7 +69,7 @@ WCF.ACP.Style.CopyStyle = Class.extend({
  * 
  * @param      integer         styleID
  * @param      string          tmpHash
- * @deprecated use WoltLab/WCF/Acp/Ui/Style/Image/Upload
+ * @deprecated use WoltLabSuite/Core/Acp/Ui/Style/Image/Upload
  */
 WCF.ACP.Style.ImageUpload = WCF.Upload.extend({
        /**
index 015ba9b205c348ef5961376232c19854347269c2..b16b81271ef727ccc24d4231d07ab99daf7f84b5 100644 (file)
@@ -19,7 +19,7 @@
                <li><a href="#" id="codemirror-{@$__pageContentID}-page" class="jsTooltip" title="{lang}wcf.editor.button.page{/lang}"><span class="icon icon16 fa-file-text-o"></span></a></li>
        </ul>
        <script data-relocate="true">
-               require(['WoltLab/WCF/Acp/Ui/CodeMirror/Media', 'WoltLab/WCF/Acp/Ui/CodeMirror/Page'], function(AcpUiCodeMirrorMedia, AcpUiCodeMirrorPage) {
+               require(['WoltLabSuite/Core/Acp/Ui/CodeMirror/Media', 'WoltLabSuite/Core/Acp/Ui/CodeMirror/Page'], function(AcpUiCodeMirrorMedia, AcpUiCodeMirrorPage) {
                        new AcpUiCodeMirrorMedia('{@$__pageContentID}');
                        new AcpUiCodeMirrorPage('{@$__pageContentID}');
                });
index 0ddd8d3a2c465b0ebca59ad2671f0251f96c879f..2df87fbadfd1abb0820dc19c05af4b9dd2421317 100644 (file)
@@ -53,7 +53,7 @@
 </section>
 
 <script data-relocate="true">
-       require(['WoltLab/WCF/Ui/Acl/Simple'], function(UiAclSimple) {
+       require(['WoltLabSuite/Core/Ui/Acl/Simple'], function(UiAclSimple) {
                new UiAclSimple('{@$__aclSimplePrefix}');
        });
 </script>
index 2d4b8ba513ebde930841cfd1c4c37e050ead5fb0..8408efbb0350e160f542b0aebfebb35d10fddbb5 100644 (file)
@@ -17,7 +17,7 @@
 </script>
 
 <script data-relocate="true">
-       require(['WoltLab/WCF/Ui/User/Search/Input'], function(UiUserSearchInput) {
+       require(['WoltLabSuite/Core/Ui/User/Search/Input'], function(UiUserSearchInput) {
                new UiUserSearchInput(elBySel('input[name="username"]'));
        });
 </script>
@@ -26,7 +26,7 @@
        <script data-relocate="true">
                {include file='mediaJavaScript'}
                
-               require(['WoltLab/WCF/Media/Manager/Select'], function(MediaManagerSelect) {
+               require(['WoltLabSuite/Core/Media/Manager/Select'], function(MediaManagerSelect) {
                        new MediaManagerSelect({
                                dialogTitle: '{lang}wcf.acp.media.chooseImage{/lang}',
                                fileTypeFilters: {
                                </dl>
                                
                                <script data-relocate="true">
-                                       require(['WoltLab/WCF/Ui/ItemList'], function(UiItemList) {
+                                       require(['WoltLabSuite/Core/Ui/ItemList'], function(UiItemList) {
                                                UiItemList.init(
                                                        'tagSearchInput',
                                                        [{if !$tags[0]|empty}{implode from=$tags[0] item=tag}'{$tag|encodeJS}'{/implode}{/if}],
                                                        </dl>
                                                        
                                                        <script data-relocate="true">
-                                                               require(['WoltLab/WCF/Ui/ItemList'], function(UiItemList) {
+                                                               require(['WoltLabSuite/Core/Ui/ItemList'], function(UiItemList) {
                                                                        UiItemList.init(
                                                                                'tagSearchInput{@$availableLanguage->languageID}',
                                                                                [{if !$tags[$availableLanguage->languageID]|empty}{implode from=$tags[$availableLanguage->languageID] item=tag}'{$tag|encodeJS}'{/implode}{/if}],
index d819455c19f6ace8f0c9e1d87c4b3dff87f41c9e..35cb520d5655e7b61bc9ce4311b2b382877dbcf1 100644 (file)
@@ -16,7 +16,7 @@
        </div>
 </div>
 <script data-relocate="true">
-       require(['Language', 'WoltLab/WCF/Acp/Ui/Article/Add'], function(Language, AcpUiArticleAdd) {
+       require(['Language', 'WoltLabSuite/Core/Acp/Ui/Article/Add'], function(Language, AcpUiArticleAdd) {
                Language.addObject({
                        'wcf.acp.article.add': '{lang}wcf.acp.article.add{/lang}'
                });
index 16f29efab873d9b52383d0cc8300dcebb96e07f6..194de2195847c214f96dce30f9e3bdf152667e6e 100644 (file)
@@ -1,7 +1,7 @@
 {include file='header' pageTitle='wcf.acp.article.list'}
 
 <script data-relocate="true">
-       require(['WoltLab/WCF/Ui/User/Search/Input'], function(UiUserSearchInput) {
+       require(['WoltLabSuite/Core/Ui/User/Search/Input'], function(UiUserSearchInput) {
                new UiUserSearchInput(elBySel('input[name="username"]'));
        });
 </script>
index aff4cbb3f7f9161de2e4e6ab24cfb32a870e9b47..6a259f760d0f93503b0fde31e811194548697791 100644 (file)
@@ -5,12 +5,12 @@
                {include file='mediaJavaScript'}
                
                {if $boxType == 'system'}
-                       require(['WoltLab/WCF/Acp/Ui/Box/Controller/Handler'], function(AcpUiBoxControllerHandler) {
+                       require(['WoltLabSuite/Core/Acp/Ui/Box/Controller/Handler'], function(AcpUiBoxControllerHandler) {
                                AcpUiBoxControllerHandler.init({if $boxController}{@$boxController->objectTypeID}{/if});
                        });
                {/if}
                
-               require(['Dictionary', 'Language', 'WoltLab/WCF/Acp/Ui/Box/Handler', 'WoltLab/WCF/Media/Manager/Select'], function(Dictionary, Language, AcpUiBoxHandler, MediaManagerSelect) {
+               require(['Dictionary', 'Language', 'WoltLabSuite/Core/Acp/Ui/Box/Handler', 'WoltLabSuite/Core/Media/Manager/Select'], function(Dictionary, Language, AcpUiBoxHandler, MediaManagerSelect) {
                        Language.addObject({
                                'wcf.page.pageObjectID.search.noResults': '{lang}wcf.page.pageObjectID.search.noResults{/lang}',
                                'wcf.page.pageObjectID.search.results': '{lang}wcf.page.pageObjectID.search.results{/lang}',
                                        <dd>
                                                <label><input type="checkbox" id="visibleEverywhere" name="visibleEverywhere" value="1"{if $visibleEverywhere} checked{/if}> {lang}wcf.acp.box.visibleEverywhere{/lang}</label>
                                                <script data-relocate="true">
-                                                       require(['Language', 'WoltLab/WCF/Ui/ItemList/Filter'], function(Language, UiItemListFilter) {
+                                                       require(['Language', 'WoltLabSuite/Core/Ui/ItemList/Filter'], function(Language, UiItemListFilter) {
                                                                Language.addObject({
                                                                        'wcf.global.filter.button.clear': '{lang}wcf.global.filter.button.clear{/lang}',
                                                                        'wcf.global.filter.error.noMatches': '{lang}wcf.global.filter.error.noMatches{/lang}',
index 11dc7243f5bad644da7bd7fafd3e15e3b7e40606..9e4334ffd67d04dce0831e999190881f1d0f0d70 100644 (file)
@@ -30,7 +30,7 @@
        </div>
 </div>
 <script data-relocate="true">
-       require(['Language', 'WoltLab/WCF/Acp/Ui/Box/Add'], function(Language, AcpUiBoxAdd) {
+       require(['Language', 'WoltLabSuite/Core/Acp/Ui/Box/Add'], function(Language, AcpUiBoxAdd) {
                Language.addObject({
                        'wcf.acp.box.add': '{lang}wcf.acp.box.add{/lang}'
                });
index 6d5ac8a8ff2fe1f2db30e86390e4faebd68fd5b3..19ff0505d3ac11b02aa5a44cc0517b36c595c257 100644 (file)
@@ -1,7 +1,7 @@
 {include file='header' pageTitle=$objectType->getProcessor()->getLanguageItemPrefix()}
 
 <script data-relocate="true">
-       require(['WoltLab/WCF/Ui/TabMenu'], function(UiTabMenu) {
+       require(['WoltLabSuite/Core/Ui/TabMenu'], function(UiTabMenu) {
                UiTabMenu.setup();
                
                function toggleActionOptions(event) {
index cd96f73825dc0c3ad25384965eceb4f4143e0d4b..3102e78f91cb9221b983ed60df156e4fd38a7f47 100644 (file)
@@ -36,7 +36,7 @@
        <script>
                // this caused some timing issues, check if it is still required
                //document.addEventListener('DOMContentLoaded', function() {
-                       require(['Language', 'WoltLab/WCF/Acp/Bootstrap', 'User'], function(Language, AcpBootstrap, User) {
+                       require(['Language', 'WoltLabSuite/Core/Acp/Bootstrap', 'User'], function(Language, AcpBootstrap, User) {
                                Language.addObject({
                                        '__days': [ '{lang}wcf.date.day.sunday{/lang}', '{lang}wcf.date.day.monday{/lang}', '{lang}wcf.date.day.tuesday{/lang}', '{lang}wcf.date.day.wednesday{/lang}', '{lang}wcf.date.day.thursday{/lang}', '{lang}wcf.date.day.friday{/lang}', '{lang}wcf.date.day.saturday{/lang}' ],
                                        '__daysShort': [ '{lang}wcf.date.day.sun{/lang}', '{lang}wcf.date.day.mon{/lang}', '{lang}wcf.date.day.tue{/lang}', '{lang}wcf.date.day.wed{/lang}', '{lang}wcf.date.day.thu{/lang}', '{lang}wcf.date.day.fri{/lang}', '{lang}wcf.date.day.sat{/lang}' ],
index 365df56dbe4b8fe8620d79d41b6231dfdad8c9a6..3df6451715bf9f6f4615abacdf2ca76e76da0ea8 100644 (file)
                                                <li>Andrea Berg</li>
                                                <li>Thorsten Buitkamp</li>
                                                <li>
-                                                       <a href="{@$__wcf->getPath()}acp/dereferrer.php?url={"https://github.com/WoltLab/WCF/contributors"|rawurlencode}" class="externalURL">{lang}wcf.acp.index.credits.contributor.more{/lang}</a>
+                                                       <a href="{@$__wcf->getPath()}acp/dereferrer.php?url={"https://github.com/WoltLabSuite/Core/contributors"|rawurlencode}" class="externalURL">{lang}wcf.acp.index.credits.contributor.more{/lang}</a>
                                                </li>
                                        </ul>
                                </dd>
index d537396a518a82c3642cc503521e9691ba2b735f..61b78e6b23c9d9d2da5c00b1951b1b89cf2ea85a 100644 (file)
@@ -16,7 +16,7 @@
        </dl>
        
        <script data-relocate="true">
-               require(['WoltLab/WCF/Language/Chooser'], function(LanguageChooser) {
+               require(['WoltLabSuite/Core/Language/Chooser'], function(LanguageChooser) {
                        var languages = {
                                {implode from=$languages item=__language}
                                        '{@$__language->languageID}': {
index d8c51c2db4d8281f3fa22da75b37db893dadebf5..1279f72ef25dce5e8fac417a7ae0910177c16a8f 100644 (file)
@@ -2,7 +2,7 @@
 
 {if $action == 'add'}
        <script data-relocate="true">
-               require(['EventHandler', 'WoltLab/WCF/Media/Upload'], function(EventHandler, MediaUpload) {
+               require(['EventHandler', 'WoltLabSuite/Core/Media/Upload'], function(EventHandler, MediaUpload) {
                        new MediaUpload('uploadButton', 'mediaFile');
                        
                        // redirect the user to the edit form after uploading the file
 {if $action == 'edit'}
        {* this code needs to be put after all multipleLanguageInputJavascript template have been included *}
        <script data-relocate="true">
-               require(['WoltLab/WCF/Language/Input'], function(LanguageInput) {
+               require(['WoltLabSuite/Core/Language/Input'], function(LanguageInput) {
                        function updateLanguageFields() {
                                var languageIdContainer = elById('languageIDContainer').parentNode;
                                
index 739b16c762ce760396c5b88e2d67896bfc6176da..25c6bb2dd385d874dddc29ba2f079ddb56d01fc9 100644 (file)
@@ -2,7 +2,7 @@
 
 <script data-relocate="true">
        document.addEventListener('DOMContentLoaded', function() {
-               require(['EventHandler', 'Language', 'Ui/SimpleDropdown', 'WoltLab/WCF/Controller/Clipboard', 'WoltLab/WCF/Media/Search'], function (EventHandler, Language, UiSimpleDropdown, Clipboard, MediaSearch) {
+               require(['EventHandler', 'Language', 'Ui/SimpleDropdown', 'WoltLabSuite/Core/Controller/Clipboard', 'WoltLabSuite/Core/Media/Search'], function (EventHandler, Language, UiSimpleDropdown, Clipboard, MediaSearch) {
                        Language.add('wcf.media.search.filetype', '{lang}wcf.media.search.filetype{/lang}');
                        
                        Clipboard.setup({
index 33e1eed008f2b7b3ee3891be96991e50f8f48844..0c5221ecce28345e31d5028cc6cca3a6dd5e14d8 100644 (file)
@@ -1,7 +1,7 @@
 {include file='header' pageTitle='wcf.acp.menu.item.'|concat:$action}
 
 <script data-relocate="true">
-       require(['Dictionary', 'Language', 'WoltLab/WCF/Acp/Ui/Menu/Item/Handler'], function(Dictionary, Language, AcpUiMenuItemHandler) {
+       require(['Dictionary', 'Language', 'WoltLabSuite/Core/Acp/Ui/Menu/Item/Handler'], function(Dictionary, Language, AcpUiMenuItemHandler) {
                Language.addObject({
                        'wcf.page.pageObjectID.search.noResults': '{lang}wcf.page.pageObjectID.search.noResults{/lang}',
                        'wcf.page.pageObjectID.search.results': '{lang}wcf.page.pageObjectID.search.results{/lang}',
index cdacde670434be4b8043eda63035190d7fa773f1..50830c088c57548a97fabf91aaac8383667f5ec0 100644 (file)
@@ -1,6 +1,6 @@
 {if $availableLanguages|count > 1}
        <script data-relocate="true">
-               require(['Language', 'WoltLab/WCF/Language/Input'], function(Language, LanguageInput) {
+               require(['Language', 'WoltLabSuite/Core/Language/Input'], function(Language, LanguageInput) {
                        Language.addObject({
                                'wcf.global.button.disabledI18n': '{lang}wcf.global.button.disabledI18n{/lang}'
                        });
index 70192dd2498f21bf44ce34f27e1242ea1493d0b2..0c1036d1e30e51d537a4993c6b9d8741ebebe428 100644 (file)
@@ -1,7 +1,7 @@
 {include file='header' pageTitle='wcf.acp.user.notificationPresetSettings'}
 
 <script data-relocate="true">
-       require(['Language', 'WoltLab/WCF/Controller/User/Notification/Settings'], function(Language, ControllerUserNotificationSettings) {
+       require(['Language', 'WoltLabSuite/Core/Controller/User/Notification/Settings'], function(Language, ControllerUserNotificationSettings) {
                Language.addObject({
                        'wcf.user.notification.mailNotificationType.daily': '{lang}wcf.user.notification.mailNotificationType.daily{/lang}',
                        'wcf.user.notification.mailNotificationType.instant': '{lang}wcf.user.notification.mailNotificationType.instant{/lang}',
index 2ec202c90b055923c0d87c508b533133511165d2..b96f57d4cc58e4ae96290bf5936ea2def2e675f4 100644 (file)
                                                        </small>
                                                {/if}
                                                <script data-relocate="true">
-                                                       require(['Language', 'WoltLab/WCF/Ui/ItemList/Filter'], function(Language, UiItemListFilter) {
+                                                       require(['Language', 'WoltLabSuite/Core/Ui/ItemList/Filter'], function(Language, UiItemListFilter) {
                                                                Language.addObject({
                                                                        'wcf.global.filter.button.clear': '{lang}wcf.global.filter.button.clear{/lang}',
                                                                        'wcf.global.filter.error.noMatches': '{lang}wcf.global.filter.error.noMatches{/lang}',
index 2d5647eac9f99299e5587170a8160e3484dab749..a437a9dbb03b5ac22d030bf00348c59ba8b48a0e 100644 (file)
@@ -30,7 +30,7 @@
        </div>
 </div>
 <script data-relocate="true">
-       require(['Language', 'WoltLab/WCF/Acp/Ui/Page/Add'], function(Language, AcpUiPageAdd) {
+       require(['Language', 'WoltLabSuite/Core/Acp/Ui/Page/Add'], function(Language, AcpUiPageAdd) {
                Language.addObject({
                        'wcf.acp.page.add': '{lang}wcf.acp.page.add{/lang}'
                });
index 8d9710d91c40ac776958498bd4e0c179da84d4c5..c3fa87b549dd0845b7b5c5205beba828cf6cdd63 100644 (file)
@@ -5,7 +5,7 @@
 {js application='wcf' acp='true' file='WCF.ACP.Style'}
 {js application='wcf' file='WCF.ColorPicker' bundle='WCF.Combined'}
 <script data-relocate="true">
-       require(['WoltLab/WCF/Acp/Ui/Style/Image/Upload', 'WoltLab/WCF/Acp/Ui/Style/Editor'], function(AcpUiStyleImageUpload, AcpUiStyleEditor) {
+       require(['WoltLabSuite/Core/Acp/Ui/Style/Image/Upload', 'WoltLabSuite/Core/Acp/Ui/Style/Editor'], function(AcpUiStyleImageUpload, AcpUiStyleEditor) {
                AcpUiStyleEditor.setup({
                        isTainted: {if $isTainted}true{else}false{/if},
                        styleId: {if $action === 'edit'}{@$style->styleID}{else}0{/if},
index b19cb7b7f337947b6657ea87c264a39524a0259e..2f1b2c97bdb1281e560bdc418ec4a76bf5853eb9 100644 (file)
@@ -67,7 +67,7 @@
                        </dl>
                        
                        <script data-relocate="true">
-                               require(['WoltLab/WCF/Ui/ItemList'], function(UiItemList) {
+                               require(['WoltLabSuite/Core/Ui/ItemList'], function(UiItemList) {
                                        UiItemList.init(
                                                'synonyms',
                                                [{if !$synonyms|empty}{implode from=$synonyms item=synonym}'{$synonym|encodeJS}'{/implode}{/if}],
index f61ed4399e1177e075021484a639d79dff0aa4f4..016491c968d21d750be1c4ffc443b8dfc25f6374 100644 (file)
@@ -1,7 +1,7 @@
 {include file='header' pageTitle='wcf.acp.user.search'}
 
 <script data-relocate="true">
-       require(['WoltLab/WCF/Ui/User/Search/Input'], function(UiUserSearchInput) {
+       require(['WoltLabSuite/Core/Ui/User/Search/Input'], function(UiUserSearchInput) {
                new UiUserSearchInput(elBySel('input[name="username"]'));
        });
 </script>
index 5a3e8a03ef4580ae53828a96b3c2fd9682b99954..77bf603f94cf224131aaafbabcbf980026f185a3 100644 (file)
@@ -49,7 +49,7 @@
                
                {event name='redactorJavaScript'}
        ], function () {
-               require(['Language', 'WoltLab/WCF/Ui/Redactor/Autosave', 'WoltLab/WCF/Ui/Redactor/Metacode'], function(Language, UiRedactorAutosave, UiRedactorMetacode) {
+               require(['Language', 'WoltLabSuite/Core/Ui/Redactor/Autosave', 'WoltLabSuite/Core/Ui/Redactor/Metacode'], function(Language, UiRedactorAutosave, UiRedactorMetacode) {
                        Language.addObject({
                                'wcf.editor.code.edit': '{lang}wcf.editor.code.edit{/lang}',
                                'wcf.editor.code.file': '{lang}wcf.editor.code.file{/lang}',
index 63d1a2e37f251990a59920a8de366244df4d4ecd..2a96ea44911977795c5ace290af4e430c7fb49bb 100644 (file)
@@ -3,7 +3,7 @@ $.Redactor.prototype.WoltLabCode = function() {
        
        return {
                init: function() {
-                       require(['WoltLab/WCF/Ui/Redactor/Code'], (function (UiRedactorCode) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Code'], (function (UiRedactorCode) {
                                new UiRedactorCode(this);
                        }).bind(this));
                }
index 68ae83580f1b3eadde5b7e6137caa53e886719c8..d86c334c9f8ca5f75b03e28ac79e890ccf658bf5 100644 (file)
@@ -44,7 +44,7 @@ $.Redactor.prototype.WoltLabColor = function() {
                setColor: function(key) {
                        key = key.replace(/^color_/, '');
                        
-                       require(['WoltLab/WCF/Ui/Redactor/Format'], (function(UiRedactorFormat) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Format'], (function(UiRedactorFormat) {
                                this.buffer.set();
                                
                                UiRedactorFormat.format(this.$editor[0], 'woltlab-color', 'woltlab-color-' + key);
@@ -52,7 +52,7 @@ $.Redactor.prototype.WoltLabColor = function() {
                },
                
                removeColor: function() {
-                       require(['WoltLab/WCF/Ui/Redactor/Format'], (function(UiRedactorFormat) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Format'], (function(UiRedactorFormat) {
                                this.buffer.set();
                                
                                UiRedactorFormat.removeFormat(this.$editor[0], 'woltlab-color');
index c7a5f7da1ea7d11013798d36cc498bf74b3ddbc6..1f742ce74156c54c04d9070449c4d10494ad1219 100644 (file)
@@ -7,7 +7,7 @@ $.Redactor.prototype.WoltLabLink = function() {
                init: function() {
                        this.link.show = this.WoltLabLink.show.bind(this);
                        
-                       require(['WoltLab/WCF/Ui/Redactor/Link'], function(UiRedactorLink) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Link'], function(UiRedactorLink) {
                                _dialogApi = UiRedactorLink;
                        });
                },
index 7a359f064c4291763cb47122153c324a2163a539..bac860718fcfd1103d6392bf1d2d857a4cf36760 100644 (file)
@@ -6,7 +6,7 @@ $.Redactor.prototype.WoltLabMedia = function() {
                        var button = this.button.add('woltlabMedia', '');
                        $(button).addClass('jsMediaEditorButton');
                        
-                       require(['WoltLab/WCF/Media/Manager/Editor'], function(MediaManagerEditor) {
+                       require(['WoltLabSuite/Core/Media/Manager/Editor'], function(MediaManagerEditor) {
                                new MediaManagerEditor({
                                        editor: this
                                });
index 4b64ca544e121ab5b8a507a204bd8cf9599e69e0..6bb45fbcc223f53a1f38c14c02f370e4e1aacdac 100644 (file)
@@ -5,7 +5,7 @@ $.Redactor.prototype.WoltLabMention = function() {
                init: function() {
                        //var WoltLabMention = document.registerElement('woltlab-mention');
                        
-                       require(['WoltLab/WCF/Ui/Redactor/Mention'], (function(UiRedactorMention) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Mention'], (function(UiRedactorMention) {
                                new UiRedactorMention(this);
                        }).bind(this));
                }
index 52bbf917f85380c625888c7037d69fc7d886f3a6..ccf7a1b5e5fa995135c09fee18fc8c66bf6a45cb 100644 (file)
@@ -5,7 +5,7 @@ $.Redactor.prototype.WoltLabPage = function() {
                init: function() {
                        var button = this.button.add('woltlabPage', '');
                        
-                       require(['WoltLab/WCF/Ui/Redactor/Page'], (function (UiRedactorPage) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Page'], (function (UiRedactorPage) {
                                new UiRedactorPage(this, button[0]);
                        }).bind(this));
                }
index 79b71c469ddbc596105e0baaed4828b5d9b01778..05668ce02bda06d88aaddd722be2b25773c9293e 100644 (file)
@@ -5,7 +5,7 @@ $.Redactor.prototype.WoltLabQuote = function() {
                init: function() {
                        var button = this.button.add('woltlabQuote', '');
                        
-                       require(['WoltLab/WCF/Ui/Redactor/Quote'], (function (UiRedactorQuote) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Quote'], (function (UiRedactorQuote) {
                                new UiRedactorQuote(this, button);
                        }).bind(this));
                }
index 64bea9706fa96fb53ee9882a04d91316c8e389fe..8a0cdd6837f3bb1b79a9e7505331a2a91a9d0068 100644 (file)
@@ -35,7 +35,7 @@ $.Redactor.prototype.WoltLabSize = function() {
                },
                
                setSize: function(key) {
-                       require(['WoltLab/WCF/Ui/Redactor/Format'], (function(UiRedactorFormat) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Format'], (function(UiRedactorFormat) {
                                this.buffer.set();
                                
                                UiRedactorFormat.format(this.$editor[0], 'woltlab-size', 'woltlab-size-' + key.replace(/^size_/, ''));
@@ -43,7 +43,7 @@ $.Redactor.prototype.WoltLabSize = function() {
                },
                
                removeSize: function() {
-                       require(['WoltLab/WCF/Ui/Redactor/Format'], (function(UiRedactorFormat) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Format'], (function(UiRedactorFormat) {
                                this.buffer.set();
                                
                                UiRedactorFormat.removeFormat(this.$editor[0], 'woltlab-size');
index a9295967f3b3d7421113fdcd446ddbf5dc927a11..658156572e22a5e68aa69d362d7114fa33e3eaf4 100644 (file)
@@ -3,7 +3,7 @@ $.Redactor.prototype.WoltLabSpoiler = function() {
        
        return {
                init: function() {
-                       require(['WoltLab/WCF/Ui/Redactor/Spoiler'], (function (UiRedactorSpoiler) {
+                       require(['WoltLabSuite/Core/Ui/Redactor/Spoiler'], (function (UiRedactorSpoiler) {
                                new UiRedactorSpoiler(this);
                        }).bind(this));
                }
index 50688cc15c5e5dbe241dcb4530d567097894bdd5..728d6cf6911e6c2ef901a49127b1e4ab3d93cef4 100644 (file)
@@ -7,7 +7,7 @@
  * @copyright  2001-2015 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * 
- * @deprecated 3.0 - please use `WoltLab/WCF/Ui/Like/Handler` instead
+ * @deprecated 3.0 - please use `WoltLabSuite/Core/Ui/Like/Handler` instead
  */
 WCF.Like = Class.extend({
        /**
index 2cd7b3d8d860c4d5f4b2f6a699d5e98ec51493df..5131bb812151050e3dac6c20fdac8d96db33fc78 100644 (file)
@@ -479,7 +479,7 @@ WCF.Message.DefaultPreview = WCF.Message.Preview.extend({
         * @see WCF.Message.Preview._handleResponse()
         */
        _handleResponse: function(data) {
-               require(['WoltLab/WCF/Ui/Dialog'], (function(UiDialog) {
+               require(['WoltLabSuite/Core/Ui/Dialog'], (function(UiDialog) {
                        UiDialog.open(this, '<div class="htmlContent">' + data.returnValues.message + '</div>');
                }).bind(this));
        },
@@ -729,7 +729,7 @@ WCF.Message.Smilies = Class.extend({
 /**
  * Provides an inline message editor.
  * 
- * @deprecated 3.0 - please use `WoltLab/WCF/Ui/Message/InlineEditor` instead
+ * @deprecated 3.0 - please use `WoltLabSuite/Core/Ui/Message/InlineEditor` instead
  * 
  * @param      integer         containerID
  */
@@ -772,7 +772,7 @@ WCF.Message.InlineEditor = Class.extend({
         * @param       WCF.Message.Quote.Manager       quoteManager
         */
        init: function(containerID, supportExtendedForm, quoteManager) {
-               require(['WoltLab/WCF/Ui/Message/InlineEditor'], (function(UiMessageInlineEditor) {
+               require(['WoltLabSuite/Core/Ui/Message/InlineEditor'], (function(UiMessageInlineEditor) {
                        new UiMessageInlineEditor({
                                className: this._getClassName(),
                                containerId: containerID,
@@ -795,7 +795,7 @@ WCF.Message.InlineEditor = Class.extend({
        _click: function(event, containerID) {
                containerID = (event === null) ? ~~containerID : ~~elData(event.currentTarget, 'container-id');
                
-               require(['WoltLab/WCF/Ui/Message/InlineEditor'], (function(UiMessageInlineEditor) {
+               require(['WoltLabSuite/Core/Ui/Message/InlineEditor'], (function(UiMessageInlineEditor) {
                        UiMessageInlineEditor.legacyEdit(containerID);
                }).bind(this));
                
@@ -1690,7 +1690,7 @@ WCF.Message.Quote.Manager = Class.extend({
         * Toggles the display of the 'Show quotes' button
         */
        _toggleShowQuotes: function() {
-               require(['WoltLab/WCF/Ui/Page/Action'], (function(UiPageAction) {
+               require(['WoltLabSuite/Core/Ui/Page/Action'], (function(UiPageAction) {
                        var buttonName = 'showQuotes';
                        
                        if (this._count) {
@@ -2118,11 +2118,11 @@ WCF.Message.Share.Content = Class.extend({
 /**
  * Provides buttons to share a page through multiple social community sites.
  * 
- * @deprecated  3.0 - please use `WoltLab/WCF/Ui/Message/Share` instead
+ * @deprecated  3.0 - please use `WoltLabSuite/Core/Ui/Message/Share` instead
  */
 WCF.Message.Share.Page = Class.extend({
        init: function() {
-               require(['WoltLab/WCF/Ui/Message/Share'], function(UiMessageShare) {
+               require(['WoltLabSuite/Core/Ui/Message/Share'], function(UiMessageShare) {
                        UiMessageShare.init();
                });
        }
index 9dc3d2ba98982abeb3032aca566548126b9ecbf5..62d3568414adaffc3ad949b138a65141ebce5faa 100644 (file)
@@ -263,7 +263,7 @@ WCF.Poll.Management = Class.extend({
                });
                
                // reset date picker
-               require(['WoltLab/WCF/Date/Picker'], (function(UiDatePicker) {
+               require(['WoltLabSuite/Core/Date/Picker'], (function(UiDatePicker) {
                        UiDatePicker.clear('pollEndTime_' + this._editorId);
                }).bind(this));
        },
index 71b9b5b6ae746b96a12aa32183875078648dceef..25e89a166b1e9a575d5ce000537a5bbbabc1777b 100755 (executable)
@@ -742,7 +742,7 @@ $.extend(WCF, {
        },
        
        /**
-        * @deprecated Use WoltLab/WCF/Core.getUuid().
+        * @deprecated Use WoltLabSuite/Core/Core.getUuid().
         */
        getUUID: function() {
                return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
@@ -1277,7 +1277,7 @@ WCF.Dropdown.Interactive.Instance = Class.extend({
 /**
  * Clipboard API
  * 
- * @deprecated 3.0 - please use `WoltLab/WCF/Controller/Clipboard` instead
+ * @deprecated 3.0 - please use `WoltLabSuite/Core/Controller/Clipboard` instead
  */
 WCF.Clipboard = {
        /**
@@ -1289,7 +1289,7 @@ WCF.Clipboard = {
         * @param       integer         pageObjectID
         */
        init: function(page, hasMarkedItems, actionObjects, pageObjectID) {
-               require(['WoltLab/WCF/Controller/Clipboard'], function(ControllerClipboard) {
+               require(['WoltLabSuite/Core/Controller/Clipboard'], function(ControllerClipboard) {
                        ControllerClipboard.setup({
                                hasMarkedItems: (hasMarkedItems > 0),
                                pageClassName: page,
@@ -1302,14 +1302,14 @@ WCF.Clipboard = {
         * Reloads the list of marked items.
         */
        reload: function() {
-               require(['WoltLab/WCF/Controller/Clipboard'], function(ControllerClipboard) {
+               require(['WoltLabSuite/Core/Controller/Clipboard'], function(ControllerClipboard) {
                        ControllerClipboard.reload();
                });
        }
 };
 
 /**
- * @deprecated Use WoltLab/WCF/Timer/Repeating
+ * @deprecated Use WoltLabSuite/Core/Timer/Repeating
  */
 WCF.PeriodicalExecuter = Class.extend({
        /**
@@ -1416,14 +1416,14 @@ WCF.PeriodicalExecuter = Class.extend({
 /**
  * Handler for loading overlays
  * 
- * @deprecated 3.0 - Please use WoltLab/WCF/Ajax/Status
+ * @deprecated 3.0 - Please use WoltLabSuite/Core/Ajax/Status
  */
 WCF.LoadingOverlayHandler = {
        /**
         * Adds one loading-request and shows the loading overlay if nessercery
         */
        show: function() {
-               require(['WoltLab/WCF/Ajax/Status'], function(AjaxStatus) {
+               require(['WoltLabSuite/Core/Ajax/Status'], function(AjaxStatus) {
                        AjaxStatus.show();
                });
        },
@@ -1432,7 +1432,7 @@ WCF.LoadingOverlayHandler = {
         * Removes one loading-request and hides loading overlay if there're no more pending requests
         */
        hide: function() {
-               require(['WoltLab/WCF/Ajax/Status'], function(AjaxStatus) {
+               require(['WoltLabSuite/Core/Ajax/Status'], function(AjaxStatus) {
                        AjaxStatus.hide();
                });
        },
@@ -1461,7 +1461,7 @@ WCF.Action = {};
 /**
  * Basic implementation for AJAX-based proxyies
  * 
- * @deprecated 3.0 - please use `WoltLab/WCF/Ajax.api()` instead
+ * @deprecated 3.0 - please use `WoltLabSuite/Core/Ajax.api()` instead
  * 
  * @param      object          options
  */
@@ -2302,7 +2302,7 @@ WCF.Dictionary = Class.extend({
 // non strict equals by intent
 if (window.WCF.Language == null) {
        /**
-        * @deprecated Use WoltLab/WCF/Language
+        * @deprecated Use WoltLabSuite/Core/Language
         */
        WCF.Language = {
                add: function(key, value) {
@@ -2649,7 +2649,7 @@ WCF.MultipleLanguageInput = Class.extend({
 
 /**
  * Number utilities.
- * @deprecated Use WoltLab/WCF/NumberUtil
+ * @deprecated Use WoltLabSuite/Core/NumberUtil
  */
 WCF.Number = {
        /**
@@ -2668,7 +2668,7 @@ WCF.Number = {
 
 /**
  * String utilities.
- * @deprecated Use WoltLab/WCF/StringUtil
+ * @deprecated Use WoltLabSuite/Core/StringUtil
  */
 WCF.String = {
        /**
@@ -2762,7 +2762,7 @@ WCF.TabMenu = {
         * Initializes all TabMenus
         */
        init: function() {
-               require(['WoltLab/WCF/Ui/TabMenu'], function(UiTabMenu) {
+               require(['WoltLabSuite/Core/Ui/TabMenu'], function(UiTabMenu) {
                        UiTabMenu.setup();
                });
        },
@@ -3636,16 +3636,16 @@ WCF.CloseOverlayHandler = {
 };
 
 /**
- * @deprecated Use WoltLab/WCF/Dom/Change/Listener
+ * @deprecated Use WoltLabSuite/Core/Dom/Change/Listener
  */
 WCF.DOMNodeInsertedHandler = {
        addCallback: function(identifier, callback) {
-               require(['WoltLab/WCF/Dom/Change/Listener'], function (ChangeListener) {
+               require(['WoltLabSuite/Core/Dom/Change/Listener'], function (ChangeListener) {
                        ChangeListener.add('__legacy__', callback);
                });
        },
        _executeCallbacks: function() {
-               require(['WoltLab/WCF/Dom/Change/Listener'], function (ChangeListener) {
+               require(['WoltLabSuite/Core/Dom/Change/Listener'], function (ChangeListener) {
                        ChangeListener.trigger();
                });
        },
@@ -4172,7 +4172,7 @@ WCF.Search = {};
 /**
  * Performs a quick search.
  * 
- * @deprecated  3.0 - please use `WoltLab/WCF/Ui/Search/Input` instead
+ * @deprecated  3.0 - please use `WoltLabSuite/Core/Ui/Search/Input` instead
  */
 WCF.Search.Base = Class.extend({
        /**
@@ -4701,7 +4701,7 @@ WCF.Search.Base = Class.extend({
  * Provides quick search for users and user groups.
  * 
  * @see        WCF.Search.Base
- * @deprecated  3.0 - please use `WoltLab/WCF/Ui/User/Search/Input` instead
+ * @deprecated  3.0 - please use `WoltLabSuite/Core/Ui/User/Search/Input` instead
  */
 WCF.Search.User = WCF.Search.Base.extend({
        /**
@@ -4901,7 +4901,7 @@ WCF.System.FlexibleMenu = {
         * @param       string          containerID
         */
        registerMenu: function(containerID) {
-               require(['WoltLab/WCF/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
+               require(['WoltLabSuite/Core/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
                        UiFlexibleMenu.register(containerID);
                });
        },
@@ -4912,7 +4912,7 @@ WCF.System.FlexibleMenu = {
         * @param       string          containerID
         */
        rebuild: function(containerID) {
-               require(['WoltLab/WCF/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
+               require(['WoltLabSuite/Core/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
                        UiFlexibleMenu.rebuild(containerID);
                });
        }
@@ -4973,7 +4973,7 @@ WCF.System.Captcha = {
         * @param       function        callback
         */
        addCallback: function(captchaID, callback) {
-               require(['WoltLab/WCF/Controller/Captcha'], function(ControllerCaptcha) {
+               require(['WoltLabSuite/Core/Controller/Captcha'], function(ControllerCaptcha) {
                        try {
                                ControllerCaptcha.add(captchaID, callback);
                        }
@@ -4995,7 +4995,7 @@ WCF.System.Captcha = {
         */
        getData: function(captchaID) {
                var returnValue;
-               require(['WoltLab/WCF/Controller/Captcha'], function(ControllerCaptcha) {
+               require(['WoltLabSuite/Core/Controller/Captcha'], function(ControllerCaptcha) {
                        try {
                                returnValue = ControllerCaptcha.getData(captchaID);
                        }
@@ -5011,7 +5011,7 @@ WCF.System.Captcha = {
         * Removes the callback with the given captcha id.
         */
        removeCallback: function(captchaID) {
-               require(['WoltLab/WCF/Controller/Captcha'], function(ControllerCaptcha) {
+               require(['WoltLabSuite/Core/Controller/Captcha'], function(ControllerCaptcha) {
                        try {
                                ControllerCaptcha.delete(captchaID);
                        }
@@ -5287,11 +5287,11 @@ WCF.System.Fullscreen = {
 /**
  * Provides the 'jump to page' overlay.
  * 
- * @deprecated 3.0 - use `WoltLab/WCF/Ui/Page/JumpTo` instead
+ * @deprecated 3.0 - use `WoltLabSuite/Core/Ui/Page/JumpTo` instead
  */
 WCF.System.PageNavigation = {
        init: function(selector, callback) {
-               require(['WoltLab/WCF/Ui/Page/JumpTo'], function(UiPageJumpTo) {
+               require(['WoltLabSuite/Core/Ui/Page/JumpTo'], function(UiPageJumpTo) {
                        var elements = elBySelAll(selector);
                        for (var i = 0, length = elements.length; i < length; i++) {
                                UiPageJumpTo.init(elements[i], callback);
@@ -5919,7 +5919,7 @@ WCF.InlineEditor = Class.extend({
 /**
  * Default implementation for ajax file uploads.
  * 
- * @deprecated Use WoltLab/WCF/Upload
+ * @deprecated Use WoltLabSuite/Core/Upload
  * 
  * @param      jquery          buttonSelector
  * @param      jquery          fileListSelector
@@ -6297,7 +6297,7 @@ WCF.Upload = Class.extend({
 /**
  * Default implementation for parallel AJAX file uploads.
  * 
- * @deprecated Use WoltLab/WCF/Upload
+ * @deprecated Use WoltLabSuite/Core/Upload
  */
 WCF.Upload.Parallel = WCF.Upload.extend({
        /**
@@ -6705,7 +6705,7 @@ WCF.Popover = Class.extend({
                this._activeElementID = '';
                this._identifier = selector;
                
-               require(['WoltLab/WCF/Controller/Popover'], (function(popover) {
+               require(['WoltLabSuite/Core/Controller/Popover'], (function(popover) {
                        popover.init({
                                attributeName: 'legacy',
                                className: selector,
@@ -7052,7 +7052,7 @@ WCF.EditableItemList = Class.extend({
  * @param       {function}                              callback                function called after a language is selected
  * @param       {boolean}                               allowEmptyValue         true if no language may be selected
  * 
- * @deprecated  3.0 - please use `WoltLab/WCF/Language/Chooser` instead
+ * @deprecated  3.0 - please use `WoltLabSuite/Core/Language/Chooser` instead
  */
 WCF.Language.Chooser = Class.extend({
        /**
@@ -7066,7 +7066,7 @@ WCF.Language.Chooser = Class.extend({
         * @param       {boolean}                               allowEmptyValue         true if no language may be selected
         */
        init: function(containerId, chooserId, languageId, languages, callback, allowEmptyValue) {
-               require(['WoltLab/WCF/Language/Chooser'], function(LanguageChooser) {
+               require(['WoltLabSuite/Core/Language/Chooser'], function(LanguageChooser) {
                        LanguageChooser.init(containerId, chooserId, languageId, languages, callback, allowEmptyValue);
                });
        }
@@ -7548,7 +7548,7 @@ jQuery.fn.extend({
        wcfTabs: function(method) {
                var element = this[0], parameters = Array.prototype.slice.call(arguments, 1);
                
-               require(['Dom/Util', 'WoltLab/WCF/Ui/TabMenu'], function(DomUtil, TabMenu) {
+               require(['Dom/Util', 'WoltLabSuite/Core/Ui/TabMenu'], function(DomUtil, TabMenu) {
                        var container = TabMenu.getTabMenu(DomUtil.identify(element));
                        if (container !== null) {
                                container[method].apply(container, parameters);
@@ -7560,7 +7560,7 @@ jQuery.fn.extend({
 /**
  * jQuery widget implementation of the wcf pagination.
  * 
- * @deprecated 3.0 - use `WoltLab/WCF/Ui/Pagination` instead
+ * @deprecated 3.0 - use `WoltLabSuite/Core/Ui/Pagination` instead
  */
 $.widget('ui.wcfPages', {
        _api: null,
@@ -7578,7 +7578,7 @@ $.widget('ui.wcfPages', {
         * Creates the pages widget.
         */
        _create: function() {
-               require(['WoltLab/WCF/Ui/Pagination'], (function(UiPagination) {
+               require(['WoltLabSuite/Core/Ui/Pagination'], (function(UiPagination) {
                        this._api = new UiPagination(this.element[0], {
                                activePage: this.options.activePage,
                                maxPage: this.options.maxPage,
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 7cf435c..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Bootstraps WCF's JavaScript with additions for the ACP usage.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Bootstrap
- */
-define(['Core', 'WoltLab/WCF/Bootstrap', './Ui/Page/Menu'], function(Core, Bootstrap, UiPageMenu) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Bootstrap
-        */
-       return {
-               /**
-                * Bootstraps general modules and frontend exclusive ones.
-                * 
-                * @param       {Object=}       options         bootstrap options
-                */
-               setup: function(options) {
-                       options = Core.extend({
-                               bootstrap: {
-                                       enableMobileMenu: true
-                               }
-                       }, options);
-                       
-                       Bootstrap.setup(options.bootstrap);
-                       UiPageMenu.init();
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Article/Add.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Article/Add.js
deleted file mode 100644 (file)
index fda2a2e..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * Provides the dialog overlay to add a new article.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Article/Add
- */
-define(['Core', 'Language', 'Ui/Dialog'], function(Core, Language, UiDialog) {
-       "use strict";
-       
-       var _link;
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Ui/Article/Add
-        */
-       return {
-               /**
-                * Initializes the article add handler.
-                * 
-                * @param       {string}        link    redirect URL
-                */
-               init: function(link) {
-                       _link = link;
-                       
-                       var buttons = elBySelAll('.jsButtonArticleAdd');
-                       for (var i = 0, length = buttons.length; i < length; i++) {
-                               buttons[i].addEventListener(WCF_CLICK_EVENT, this.openDialog.bind(this));
-                       }
-               },
-               
-               /**
-                * Opens the 'Add Article' dialog.
-                * 
-                * @param       {Event=}        event   event object
-                */
-               openDialog: function(event) {
-                       if (event instanceof Event) {
-                               event.preventDefault();
-                       }
-                       
-                       UiDialog.open(this);
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'articleAddDialog',
-                               options: {
-                                       onSetup: function(content) {
-                                               elBySel('button', content).addEventListener(WCF_CLICK_EVENT, function(event) {
-                                                       event.preventDefault();
-                                                       
-                                                       var isMultilingual = elBySel('input[name="isMultilingual"]:checked', content).value;
-                                                       
-                                                       window.location = _link.replace(/{\$isMultilingual}/, isMultilingual);
-                                               });
-                                       },
-                                       title: Language.get('wcf.acp.article.add')
-                               }
-                       };
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Add.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Add.js
deleted file mode 100644 (file)
index a949633..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Provides the dialog overlay to add a new box.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Box/Add
- */
-define(['Core', 'Language', 'Ui/Dialog'], function(Core, Language, UiDialog) {
-       "use strict";
-       
-       var _link;
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Ui/Box/Add
-        */
-       return {
-               /**
-                * Initializes the box add handler.
-                * 
-                * @param       {string}        link    redirect URL
-                */
-               init: function(link) {
-                       _link = link;
-                       
-                       var buttons = elBySelAll('.jsButtonBoxAdd');
-                       for (var i = 0, length = buttons.length; i < length; i++) {
-                               buttons[i].addEventListener(WCF_CLICK_EVENT, this.openDialog.bind(this));
-                       }
-               },
-               
-               /**
-                * Opens the 'Add Box' dialog.
-                * 
-                * @param       {Event=}        event   event object
-                */
-               openDialog: function(event) {
-                       if (event instanceof Event) {
-                               event.preventDefault();
-                       }
-                       
-                       UiDialog.open(this);
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'boxAddDialog',
-                               options: {
-                                       onSetup: function(content) {
-                                               elBySel('button', content).addEventListener(WCF_CLICK_EVENT, function(event) {
-                                                       event.preventDefault();
-                                                       
-                                                       var boxType = elBySel('input[name="boxType"]:checked', content).value;
-                                                       var isMultilingual = 0;
-                                                       if (boxType !== 'system') isMultilingual = elBySel('input[name="isMultilingual"]:checked', content).value;
-                                                       
-                                                       window.location = _link.replace(/{\$boxType}/, boxType).replace(/{\$isMultilingual}/, isMultilingual);
-                                               });
-                                               
-                                               elBySelAll('input[type="radio"][name="boxType"]', content, function(element) {
-                                                       element.addEventListener('change', function(event) {
-                                                               elBySelAll('input[type="radio"][name="isMultilingual"]', content, function(element) {
-                                                                       element.disabled = (event.currentTarget.value === 'system');
-                                                               });
-                                                       });
-                                               });
-                                       },
-                                       title: Language.get('wcf.acp.box.add')
-                               }
-                       };
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Controller/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Controller/Handler.js
deleted file mode 100644 (file)
index d62bbc6..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Provides the interface logic to add and edit menu items.
- *
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Box/Controller/Handler
- */
-define(['Ajax', 'Dictionary'], function(Ajax, Dictionary) {
-       "use strict";
-       
-       var _boxControllerContainer = elById('boxControllerContainer');
-       var _boxController = elById('boxControllerID');
-       var _boxConditions = elById('boxConditions');
-       var _templates = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Ui/Box/Controller/Handler
-        */
-       return {
-               init: function(initialObjectTypeId) {
-                       _boxController.addEventListener('change', this._updateConditions.bind(this));
-                       
-                       if (initialObjectTypeId) {
-                               _templates.set(~~initialObjectTypeId, _boxConditions.innerHTML);
-                       }
-                       
-                       elShow(_boxControllerContainer);
-                       
-                       this._updateConditions();
-               },
-               
-               /**
-                * Sets up ajax request object.
-                *
-                * @return      {object}        request options
-                */
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'getBoxConditionsTemplate',
-                                       className: 'wcf\\data\\box\\BoxAction'
-                               }
-                       };
-               },
-               
-               /**
-                * Handles successful AJAX requests.
-                *
-                * @param       {object}        data    response data
-                */
-               _ajaxSuccess: function(data) {
-                       _templates.set(~~data.returnValues.objectTypeID, data.returnValues.template);
-                       
-                       _boxConditions.innerHTML = data.returnValues.template;
-               },
-               
-               /**
-                * Updates the displayed box conditions based on the selected dynamic box controller.
-                * 
-                * @protected
-                */
-               _updateConditions: function() {
-                       var objectTypeId = ~~_boxController.value;
-                       
-                       if (_templates.has(objectTypeId)) {
-                               if (_templates.get(objectTypeId) !== null) {
-                                       _boxConditions.innerHTML = _templates.get(objectTypeId);
-                               }
-                       }
-                       else {
-                               _templates.set(objectTypeId, null);
-                               
-                               Ajax.api(this, {
-                                       parameters: {
-                                               objectTypeID: objectTypeId
-                                       }
-                               });
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Box/Handler.js
deleted file mode 100644 (file)
index 14297e5..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-/**
- * Provides the interface logic to add and edit boxes.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Box/Handler
- */
-define(['Dictionary', 'WoltLab/Wcf/Ui/Page/Search/Handler'], function(Dictionary, UiPageSearchHandler) {
-       "use strict";
-       
-       var _activePageId = 0;
-       var _boxController;
-       var _cache;
-       var _containerExternalLink;
-       var _containerPageID;
-       var _containerPageObjectId = null;
-       var _handlers;
-       var _pageId;
-       var _pageObjectId;
-       var _position;
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Ui/Box/Handler
-        */
-       return {
-               /**
-                * Initializes the interface logic.
-                * 
-                * @param       {Dictionary}    handlers        list of handlers by page id supporting page object ids
-                */
-               init: function(handlers) {
-                       _handlers = handlers;
-                       
-                       _boxController = elById('boxControllerID');
-                       
-                       _containerPageID = elById('linkPageIDContainer');
-                       _containerExternalLink = elById('externalURLContainer');
-                       _containerPageObjectId = elById('linkPageObjectIDContainer');
-                       
-                       if (_handlers.size) {
-                               _pageId = elById('linkPageID');
-                               _pageId.addEventListener('change', this._togglePageId.bind(this));
-                               
-                               _pageObjectId = elById('linkPageObjectID');
-                               
-                               _cache = new Dictionary();
-                               _activePageId = ~~_pageId.value;
-                               if (_activePageId && _handlers.has(_activePageId)) {
-                                       _cache.set(_activePageId, ~~_pageObjectId.value);
-                               }
-                               
-                               elById('searchLinkPageObjectID').addEventListener(WCF_CLICK_EVENT, this._openSearch.bind(this));
-                               
-                               // toggle page object id container on init
-                               if (_handlers.has(~~_pageId.value)) {
-                                       elShow(_containerPageObjectId);
-                               }
-                       }
-                       
-                       elBySelAll('input[name="linkType"]', null, (function(input) {
-                               input.addEventListener('change', this._toggleLinkType.bind(this, input.value));
-                               
-                               if (input.checked) {
-                                       this._toggleLinkType(input.value);
-                               }
-                       }).bind(this));
-                       
-                       if (_boxController !== null) {
-                               _position = elById('position');
-                               _boxController.addEventListener('change', this._setAvailableBoxPositions.bind(this));
-                               
-                               // update positions on init
-                               this._setAvailableBoxPositions();
-                       }
-               },
-               
-               /**
-                * Toggles between the interface for internal and external links.
-                * 
-                * @param       {string}        value   selected option value
-                * @protected
-                */
-               _toggleLinkType: function(value) {
-                       if (value == 'none') {
-                               elHide(_containerPageID);
-                               elHide(_containerPageObjectId);
-                               elHide(_containerExternalLink);
-                       }
-                       if (value == 'internal') {
-                               elShow(_containerPageID);
-                               elHide(_containerExternalLink);
-                               if (_handlers.size) this._togglePageId();
-                       }
-                       if (value == 'external') {
-                               elHide(_containerPageID);
-                               elHide(_containerPageObjectId);
-                               elShow(_containerExternalLink);
-                       }
-               },
-               
-               /**
-                * Handles the changed page selection.
-                * 
-                * @protected
-                */
-               _togglePageId: function() {
-                       if (_handlers.has(_activePageId)) {
-                               _cache.set(_activePageId, ~~_pageObjectId.value);
-                       }
-                       
-                       _activePageId = ~~_pageId.value;
-                       
-                       // page w/o pageObjectID support, discard value
-                       if (!_handlers.has(_activePageId)) {
-                               _pageObjectId.value = '';
-                               
-                               elHide(_containerPageObjectId);
-                               
-                               return;
-                       }
-                               
-                       var newValue = ~~_cache.get(_activePageId);
-                       _pageObjectId.value = (newValue) ? newValue : '';
-                       
-                       elShow(_containerPageObjectId);
-               },
-               
-               /**
-                * Opens the handler lookup dialog.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _openSearch: function(event) {
-                       event.preventDefault();
-                       
-                       UiPageSearchHandler.open(_activePageId, _pageId.options[_pageId.selectedIndex].textContent.trim(), function(objectId) {
-                               _pageObjectId.value = objectId;
-                               _cache.set(_activePageId, objectId);
-                       });
-               },
-               
-               /**
-                * Updates the available box positions per box controller.
-                * 
-                * @protected
-                */
-               _setAvailableBoxPositions: function() {
-                       var supportedPositions = JSON.parse(elData(_boxController.options[_boxController.selectedIndex], 'supported-positions'));
-                       
-                       var option;
-                       for (var i = 0, length = _position.childElementCount; i < length; i++) {
-                               option = _position.children[i];
-                               
-                               option.disabled = (supportedPositions.indexOf(option.value) === -1);
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/CodeMirror/Media.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/CodeMirror/Media.js
deleted file mode 100644 (file)
index 4aa3759..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-define(['WoltLab/WCF/Media/Manager/Editor'], function(MediaManagerEditor) {
-       "use strict";
-       
-       function AcpUiCodeMirrorMedia(elementId) { this.init(elementId); }
-       AcpUiCodeMirrorMedia.prototype = {
-               init: function(elementId) {
-                       this._element = elById(elementId);
-                       
-                       var button = elById('codemirror-' + elementId + '-media');
-                       button.classList.add(button.id);
-                       
-                       new MediaManagerEditor({
-                               buttonClass: button.id,
-                               callbackInsert: this._insert.bind(this),
-                               editor: null
-                       });
-               },
-               
-               _insert: function (mediaList, insertType, thumbnailSize) {
-                       var content = '';
-                       
-                       if (insertType === 'gallery') {
-                               var mediaIds = [];
-                               mediaList.forEach(function(item) {
-                                       mediaIds.push(item.mediaID);
-                               });
-                               
-                               content = '{{ mediaGallery="' + mediaIds.join(',') + '" }}';
-                       }
-                       else {
-                               mediaList.forEach(function(item) {
-                                       content += '{{ media="' + item.mediaID + '" size="' + thumbnailSize + '" }}';
-                               });
-                       }
-                       
-                       this._element.codemirror.replaceSelection(content);
-               }
-       };
-       
-       return AcpUiCodeMirrorMedia;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/CodeMirror/Page.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/CodeMirror/Page.js
deleted file mode 100644 (file)
index 2c6f4f5..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-define(['WoltLab/WCF/Ui/Page/Search'], function(UiPageSearch) {
-       "use strict";
-       
-       function AcpUiCodeMirrorPage(elementId) { this.init(elementId); }
-       AcpUiCodeMirrorPage.prototype = {
-               init: function(elementId) {
-                       this._element = elById(elementId);
-                       
-                       elById('codemirror-' + elementId + '-page').addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-               },
-               
-               _click: function (event) {
-                       event.preventDefault();
-                       
-                       UiPageSearch.open(this._insert.bind(this));
-               },
-               
-               _insert: function (pageID) {
-                       this._element.codemirror.replaceSelection('{{ page="' + pageID + '" }}');
-               }
-       };
-       
-       return AcpUiCodeMirrorPage;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Menu/Item/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Menu/Item/Handler.js
deleted file mode 100644 (file)
index eccf34d..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-/**
- * Provides the interface logic to add and edit menu items.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Menu/Item/Handler
- */
-define(['Dictionary', 'WoltLab/Wcf/Ui/Page/Search/Handler'], function(Dictionary, UiPageSearchHandler) {
-       "use strict";
-       
-       var _activePageId = 0;
-       var _cache;
-       var _containerExternalLink;
-       var _containerInternalLink;
-       var _containerPageObjectId = null;
-       var _handlers;
-       var _pageId;
-       var _pageObjectId;
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Ui/Menu/Item/Handler
-        */
-       return {
-               /**
-                * Initializes the interface logic.
-                * 
-                * @param       {Dictionary}    handlers        list of handlers by page id supporting page object ids
-                */
-               init: function(handlers) {
-                       _handlers = handlers;
-                       
-                       _containerInternalLink = elById('pageIDContainer');
-                       _containerExternalLink = elById('externalURLContainer');
-                       _containerPageObjectId = elById('pageObjectIDContainer');
-                       
-                       if (_handlers.size) {
-                               _pageId = elById('pageID');
-                               _pageId.addEventListener('change', this._togglePageId.bind(this));
-                               
-                               _pageObjectId = elById('pageObjectID');
-                               
-                               _cache = new Dictionary();
-                               _activePageId = ~~_pageId.value;
-                               if (_activePageId && _handlers.has(_activePageId)) {
-                                       _cache.set(_activePageId, ~~_pageObjectId.value);
-                               }
-                               
-                               elById('searchPageObjectID').addEventListener(WCF_CLICK_EVENT, this._openSearch.bind(this));
-                               
-                               // toggle page object id container on init
-                               if (_handlers.has(~~_pageId.value)) {
-                                       elShow(_containerPageObjectId);
-                               }
-                       }
-                       
-                       elBySelAll('input[name="isInternalLink"]', null, (function(input) {
-                               input.addEventListener('change', this._toggleIsInternalLink.bind(this, input.value));
-                               
-                               if (input.checked) {
-                                       this._toggleIsInternalLink(input.value);
-                               }
-                       }).bind(this));
-               },
-               
-               /**
-                * Toggles between the interface for internal and external links.
-                * 
-                * @param       {string}        value   selected option value
-                * @protected
-                */
-               _toggleIsInternalLink: function(value) {
-                       if (~~value) {
-                               elShow(_containerInternalLink);
-                               elHide(_containerExternalLink);
-                               if (_handlers.size) this._togglePageId();
-                       }
-                       else {
-                               elHide(_containerInternalLink);
-                               elHide(_containerPageObjectId);
-                               elShow(_containerExternalLink);
-                       }
-               },
-               
-               /**
-                * Handles the changed page selection.
-                * 
-                * @protected
-                */
-               _togglePageId: function() {
-                       if (_handlers.has(_activePageId)) {
-                               _cache.set(_activePageId, ~~_pageObjectId.value);
-                       }
-                       
-                       _activePageId = ~~_pageId.value;
-                       
-                       // page w/o pageObjectID support, discard value
-                       if (!_handlers.has(_activePageId)) {
-                               _pageObjectId.value = '';
-                               
-                               elHide(_containerPageObjectId);
-                               
-                               return;
-                       }
-                               
-                       var newValue = ~~_cache.get(_activePageId);
-                       _pageObjectId.value = (newValue) ? newValue : '';
-                       
-                       elShow(_containerPageObjectId);
-               },
-               
-               /**
-                * Opens the handler lookup dialog.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _openSearch: function(event) {
-                       event.preventDefault();
-                       
-                       UiPageSearchHandler.open(_activePageId, _pageId.options[_pageId.selectedIndex].textContent.trim(), function(objectId) {
-                               _pageObjectId.value = objectId;
-                               _cache.set(_activePageId, objectId);
-                       });
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Page/Add.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Page/Add.js
deleted file mode 100644 (file)
index cbdc471..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Provides the dialog overlay to add a new page.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Page/Add
- */
-define(['Core', 'Language', 'Ui/Dialog'], function(Core, Language, UiDialog) {
-       "use strict";
-       
-       var _languages, _link;
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Ui/Page/Add
-        */
-       return {
-               /**
-                * Initializes the page add handler.
-                * 
-                * @param       {string}        link            redirect URL
-                * @param       {int}           languages       number of available languages
-                */
-               init: function(link, languages) {
-                       _languages = languages;
-                       _link = link;
-                       
-                       var buttons = elBySelAll('.jsButtonPageAdd');
-                       for (var i = 0, length = buttons.length; i < length; i++) {
-                               buttons[i].addEventListener(WCF_CLICK_EVENT, this.openDialog.bind(this));
-                       }
-               },
-               
-               /**
-                * Opens the 'Add Page' dialog.
-                * 
-                * @param       {Event=}        event   event object
-                */
-               openDialog: function(event) {
-                       if (event instanceof Event) {
-                               event.preventDefault();
-                       }
-                       
-                       UiDialog.open(this);
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'pageAddDialog',
-                               options: {
-                                       onSetup: function(content) {
-                                               elBySel('button', content).addEventListener(WCF_CLICK_EVENT, function(event) {
-                                                       event.preventDefault();
-                                                       
-                                                       var pageType = elBySel('input[name="pageType"]:checked', content).value;
-                                                       var isMultilingual = (_languages > 1) ? elBySel('input[name="isMultilingual"]:checked', content).value : 0;
-                                                       
-                                                       window.location = _link.replace(/{\$pageType}/, pageType).replace(/{\$isMultilingual}/, isMultilingual);
-                                               });
-                                       },
-                                       title: Language.get('wcf.acp.page.add')
-                               }
-                       };
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Page/Menu.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Page/Menu.js
deleted file mode 100644 (file)
index 9811aeb..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Provides the ACP menu navigation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Page/Menu
- */
-define(['Dictionary'], function(Dictionary) {
-       "use strict";
-       
-       var _activeMenuItem = '';
-       var _menuItems = new Dictionary();
-       var _menuItemContainers = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/Acp/Ui/Page/Menu
-        */
-       return {
-               /**
-                * Initializes the ACP menu navigation.
-                */
-               init: function() {
-                       elBySelAll('.acpPageMenuLink', null, (function(link) {
-                               var menuItem = elData(link, 'menu-item');
-                               if (link.classList.contains('active')) {
-                                       _activeMenuItem = menuItem;
-                               }
-                               
-                               link.addEventListener(WCF_CLICK_EVENT, this._toggle.bind(this));
-                               
-                               _menuItems.set(menuItem, link);
-                       }).bind(this));
-                       
-                       elBySelAll('.acpPageSubMenuCategoryList', null, function(container) {
-                               _menuItemContainers.set(elData(container, 'menu-item'), container);
-                       });
-               },
-               
-               /**
-                * Toggles a menu item.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _toggle: function(event) {
-                       event.preventDefault();
-                       event.stopPropagation();
-                       
-                       var link = event.currentTarget;
-                       var menuItem = elData(link, 'menu-item');
-                       
-                       // remove active marking from currently active menu
-                       if (_activeMenuItem) {
-                               _menuItems.get(_activeMenuItem).classList.remove('active');
-                               _menuItemContainers.get(_activeMenuItem).classList.remove('active');
-                       }
-                       
-                       if (_activeMenuItem === menuItem) {
-                               // current item was active before
-                               _activeMenuItem = '';
-                       }
-                       else {
-                               link.classList.add('active');
-                               _menuItemContainers.get(menuItem).classList.add('active');
-                               
-                               _activeMenuItem = menuItem;
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Editor.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Editor.js
deleted file mode 100644 (file)
index a78f4a2..0000000
+++ /dev/null
@@ -1,269 +0,0 @@
-/**
- * Provides the style editor.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Style/Editor
- */
-define(['Ajax', 'Dictionary', 'Dom/Util', 'EventHandler'], function(Ajax, Dictionary, DomUtil, EventHandler) {
-       "use strict";
-       
-       var _stylePreviewRegions = new Dictionary();
-       var _stylePreviewRegionMarker = null;
-       
-       /**
-        * @module      WoltLab/WCF/Acp/Ui/Style/Editor
-        */
-       var AcpUiStyleEditor = {
-               /**
-                * Sets up dynamic style options.
-                */
-               setup: function(options) {
-                       this._handleLayoutWidth();
-                       this._handleScss(options.isTainted);
-                       
-                       if (!options.isTainted) {
-                               this._handleProtection(options.styleId);
-                       }
-                       
-                       this._initVisualEditor(options.styleRuleMap);
-               },
-               
-               /**
-                * Handles the switch between static and fluid layout.
-                */
-               _handleLayoutWidth: function() {
-                       var useFluidLayout = elById('useFluidLayout');
-                       var fluidLayoutMinWidth = elById('fluidLayoutMinWidth');
-                       var fluidLayoutMaxWidth = elById('fluidLayoutMaxWidth');
-                       var fixedLayoutVariables = elById('fixedLayoutVariables');
-                       
-                       function change() {
-                               var checked = useFluidLayout.checked;
-                               
-                               fluidLayoutMinWidth.style[(checked ? 'remove' : 'set') + 'Property']('display', 'none');
-                               fluidLayoutMaxWidth.style[(checked ? 'remove' : 'set') + 'Property']('display', 'none');
-                               fixedLayoutVariables.style[(checked ? 'set' : 'remove') + 'Property']('display', 'none');
-                       }
-                       
-                       useFluidLayout.addEventListener('change', change);
-                       
-                       change();
-               },
-               
-               /**
-                * Handles SCSS input fields.
-                * 
-                * @param       {boolean}       isTainted       false if style is in protected mode
-                */
-               _handleScss: function(isTainted) {
-                       var individualScss = elById('individualScss');
-                       var overrideScss = elById('overrideScss');
-                       
-                       if (isTainted) {
-                               EventHandler.add('com.woltlab.wcf.simpleTabMenu_styleTabMenuContainer', 'select', function(data) {
-                                       individualScss.codemirror.refresh();
-                                       overrideScss.codemirror.refresh();
-                               });
-                       }
-                       else {
-                               EventHandler.add('com.woltlab.wcf.simpleTabMenu_advanced', 'select', function(data) {
-                                       if (data.activeName === 'advanced-custom') {
-                                               elById('individualScssCustom').codemirror.refresh();
-                                               elById('overrideScssCustom').codemirror.refresh();
-                                       }
-                                       else if (data.activeName === 'advanced-original') {
-                                               individualScss.codemirror.refresh();
-                                               overrideScss.codemirror.refresh();
-                                       }
-                               });
-                       }
-               },
-               
-               _handleProtection: function(styleId) {
-                       var button = elById('styleDisableProtectionSubmit');
-                       var checkbox = elById('styleDisableProtectionConfirm');
-                       
-                       checkbox.addEventListener('change', function() {
-                               button.disabled = !checkbox.checked;
-                       });
-                       
-                       button.addEventListener(WCF_CLICK_EVENT, function() {
-                               Ajax.apiOnce({
-                                       data: {
-                                               actionName: 'markAsTainted',
-                                               className: 'wcf\\data\\style\\StyleAction',
-                                               objectIDs: [styleId]
-                                       },
-                                       success: function() {
-                                               window.location.reload();
-                                       }
-                               });
-                       });
-               },
-               
-               _initVisualEditor: function(styleRuleMap) {
-                       var regions = elBySelAll('#spWindow [data-region]');
-                       for (var i = 0, length = regions.length; i < length; i++) {
-                               _stylePreviewRegions.set(elData(regions[i], 'region'), regions[i]);
-                       }
-                       
-                       _stylePreviewRegionMarker = elCreate('div');
-                       _stylePreviewRegionMarker.id = 'stylePreviewRegionMarker';
-                       _stylePreviewRegionMarker.innerHTML = '<div id="stylePreviewRegionMarkerBottom"></div>';
-                       elHide(_stylePreviewRegionMarker);
-                       elById('colors').appendChild(_stylePreviewRegionMarker);
-                       
-                       var container = elById('spSidebar');
-                       var select = elById('spCategories');
-                       var lastValue = select.value;
-                       
-                       function updateRegionMarker() {
-                               if (lastValue === 'none') {
-                                       elHide(_stylePreviewRegionMarker);
-                                       updateWrapperPosition(null);
-                                       scrollToRegion(null);
-                                       return;
-                               }
-                               
-                               var region = _stylePreviewRegions.get(lastValue);
-                               var rect = region.getBoundingClientRect();
-                               
-                               var top = rect.top + window.scrollY;
-                               
-                               DomUtil.setStyles(_stylePreviewRegionMarker, {
-                                       height: (region.clientHeight + 20) + 'px',
-                                       left: (rect.left + document.body.scrollLeft - 10) + 'px',
-                                       top: (top - 10) + 'px',
-                                       width: (region.clientWidth + 20) + 'px'
-                               });
-                               
-                               elShow(_stylePreviewRegionMarker);
-                               
-                               updateWrapperPosition(region);
-                               scrollToRegion(top);
-                       }
-                       
-                       var variablesWrapper = elById('spVariablesWrapper');
-                       function updateWrapperPosition(region) {
-                               var fromTop = 0;
-                               if (region !== null) {
-                                       fromTop = (region.offsetTop - variablesWrapper.offsetTop) - 10;
-                                       
-                                       var styles = window.getComputedStyle(region);
-                                       if (styles.getPropertyValue('position') === 'absolute' || styles.getPropertyValue('position') === 'relative') {
-                                               fromTop += region.offsetParent.offsetTop;
-                                       }
-                               }
-                               
-                               if (fromTop <= 0) {
-                                       variablesWrapper.style.removeProperty('transform');
-                               }
-                               else {
-                                       // ensure that the wrapper does not exceed the bottom boundary
-                                       var maxHeight = variablesWrapper.parentNode.clientHeight;
-                                       var wrapperHeight = variablesWrapper.clientHeight;
-                                       if (wrapperHeight + fromTop > maxHeight) {
-                                               fromTop = maxHeight - wrapperHeight;
-                                       }
-                                       
-                                       variablesWrapper.style.setProperty('transform', 'translateY(' + fromTop + 'px)');
-                               }
-                       }
-                       
-                       var pageHeader = elById('pageHeader');
-                       function scrollToRegion(top) {
-                               if (top === null) {
-                                       top = variablesWrapper.offsetTop - 60;
-                               }
-                               else {
-                                       // use the region marker as an offset
-                                       top -= 60;
-                               }
-                               
-                               // account for sticky header
-                               top -= 60;
-                               
-                               window.scrollTo(0, top);
-                       }
-                       
-                       var selectContainer = elBySel('.spSidebarBox:first-child');
-                       var element;
-                       select.addEventListener('change', function() {
-                               element = elBySel('.spSidebarBox[data-category="' + lastValue + '"]', container);
-                               elHide(element);
-                               
-                               lastValue = select.value;
-                               element = elBySel('.spSidebarBox[data-category="' + lastValue + '"]', container);
-                               elShow(element);
-                               
-                               // set region marker
-                               updateRegionMarker();
-                               
-                               selectContainer.classList[(lastValue === 'none' ? 'remove' : 'add')]('pointer');
-                       });
-                       
-                       
-                       // apply CSS rules
-                       var style = elCreate('style');
-                       style.appendChild(document.createTextNode(''));
-                       elData(style, 'created-by', 'WoltLab/Acp/Ui/Style/Editor');
-                       document.head.appendChild(style);
-                       
-                       function updateCSSRule(identifier, value, isInit) {
-                               if (styleRuleMap[identifier] === undefined) {
-                                       console.debug("Unknown style identifier: " + identifier);
-                                       return;
-                               }
-                               
-                               var rule = styleRuleMap[identifier].replace(/VALUE/g, value + ' !important');
-                               if (!rule) {
-                                       console.debug("Invalid style rule for " + identifier);
-                                       return;
-                               }
-                               
-                               var rules = [];
-                               if (rule.indexOf('__COMBO_RULE__')) {
-                                       rules = rule.split('__COMBO_RULE__');
-                               }
-                               else {
-                                       rules = [rule];
-                               }
-                               
-                               for (var i = 0, length = rules.length; i < length; i++) {
-                                       try {
-                                               style.sheet.insertRule(rules[i], style.sheet.cssRules.length);
-                                       }
-                                       catch (e) {
-                                               // ignore errors for unknown placeholder selectors
-                                               if (!/[a-z]+\-placeholder/.test(rules[i])) {
-                                                       console.debug(e.message);
-                                               }
-                                       }
-                               }
-                       }
-                       
-                       var elements = elByClass('styleVariableColor', variablesWrapper);
-                       [].forEach.call(elements, function(colorField) {
-                               var variableName = elData(colorField, 'store').replace(/_value$/, '');
-                               
-                               var observer = new MutationObserver(function(mutations) {
-                                       mutations.forEach(function(mutation) {
-                                               if (mutation.attributeName === 'style') {
-                                                       updateCSSRule(variableName, colorField.style.getPropertyValue('background-color'));
-                                               }
-                                       });
-                               });
-                               
-                               observer.observe(colorField, {
-                                       attributes: true
-                               });
-                               
-                               updateCSSRule(variableName, colorField.style.getPropertyValue('background-color'));
-                       });
-               }
-       };
-       
-       return AcpUiStyleEditor;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Image/Upload.js b/wcfsetup/install/files/js/WoltLab/WCF/Acp/Ui/Style/Image/Upload.js
deleted file mode 100644 (file)
index 955eef1..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * Handles uploading style preview images.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Acp/Ui/Style/Image/Upload
- */
-define(['Core', 'Dom/Traverse', 'Language', 'Ui/Notification', 'Upload'], function(Core, DomTraverse, Language, UiNotification, Upload) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function AcpUiStyleImageUpload(styleId, tmpHash) {
-               this._styleId = ~~styleId;
-               this._tmpHash = tmpHash;
-               
-               Upload.call(this, 'uploadImage', 'styleImage', {
-                       className: 'wcf\\data\\style\\StyleAction'
-               });
-       }
-       Core.inherit(AcpUiStyleImageUpload, Upload, {
-               /**
-                * @see WoltLab/WCF/Upload#_createFileElement
-                */
-               _createFileElement: function(file) {
-                       return this._target;
-               },
-               
-               /**
-                * @see WoltLab/WCF/Upload#_getParameters
-                */
-               _getParameters: function() {
-                       return {
-                               styleId: this._styleId,
-                               tmpHash: this._tmpHash
-                       };
-               },
-               
-               /**
-                * @see WoltLab/WCF/Upload#_success
-                */
-               _success: function(uploadId, data) {
-                       var error = DomTraverse.childByClass(this._button.parentNode, 'innerError');
-                       if (data.returnValues.url) {
-                               elAttr(this._target, 'src', data.returnValues.url + '?timestamp=' + Date.now());
-                               
-                               if (error) {
-                                       elRemove(error);
-                               }
-                               
-                               UiNotification.show();
-                       }
-                       else if (data.returnValues.errorType) {
-                               if (!error) {
-                                       error = elCreate('small');
-                                       error.className = 'innerError';
-                                       
-                                       this._button.parentNode.appendChild(error);
-                               }
-                               
-                               error.textContent = Language.get('wcf.acp.style.image.error.' + data.returnValues.errorType);
-                       }
-               }
-       });
-       
-       return AcpUiStyleImageUpload;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ajax.js b/wcfsetup/install/files/js/WoltLab/WCF/Ajax.js
deleted file mode 100644 (file)
index 3703f45..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Handles AJAX requests.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ajax
- */
-define(['AjaxRequest', 'Core', 'ObjectMap'], function(AjaxRequest, Core, ObjectMap) {
-       "use strict";
-       
-       var _requests = new ObjectMap();
-       
-       /**
-        * @exports     WoltLab/WCF/Ajax
-        */
-       var Ajax = {
-               /**
-                * Shorthand function to perform a request against the WCF-API with overrides
-                * for success and failure callbacks.
-                * 
-                * @param       {object}                callbackObject  callback object
-                * @param       {object<string, *>=}    data            request data
-                * @param       {function=}             success         success callback
-                * @param       {function=}             failure         failure callback
-                * @return      {AjaxRequest}
-                */
-               api: function(callbackObject, data, success, failure) {
-                       if (typeof data !== 'object') data = {};
-                       
-                       var request = _requests.get(callbackObject);
-                       if (request === undefined) {
-                               if (typeof callbackObject._ajaxSetup !== 'function') {
-                                       throw new TypeError("Callback object must implement at least _ajaxSetup().");
-                               }
-                               
-                               var options = callbackObject._ajaxSetup();
-                               
-                               options.pinData = true;
-                               options.callbackObject = callbackObject;
-                               
-                               if (!options.url) options.url = 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN;
-                               
-                               request = new AjaxRequest(options);
-                               
-                               _requests.set(callbackObject, request);
-                       }
-                       
-                       var oldSuccess = null;
-                       var oldFailure = null;
-                       
-                       if (typeof success === 'function') {
-                               oldSuccess = request.getOption('success');
-                               request.setOption('success', success);
-                       }
-                       if (typeof failure === 'function') {
-                               oldFailure = request.getOption('failure');
-                               request.setOption('failure', failure);
-                       }
-                       
-                       request.setData(data);
-                       request.sendRequest();
-                       
-                       // restore callbacks
-                       if (oldSuccess !== null) request.setOption('success', oldSuccess);
-                       if (oldFailure !== null) request.setOption('failure', oldFailure);
-                       
-                       return request;
-               },
-               
-               /**
-                * Shorthand function to perform a single request against the WCF-API.
-                * 
-                * Please use `Ajax.api` if you're about to repeatedly send requests because this
-                * method will spawn an new and rather expensive `AjaxRequest` with each call.
-                *  
-                * @param       {object<string, *>}     options         request options
-                */
-               apiOnce: function(options) {
-                       // Fetch AjaxRequest, as it cannot be provided because of a circular dependency
-                       if (AjaxRequest === undefined) AjaxRequest = require('AjaxRequest');
-                       
-                       options.pinData = false;
-                       options.callbackObject = null;
-                       if (!options.url) options.url = 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN;
-                       
-                       var request = new AjaxRequest(options);
-                       request.sendRequest();
-               }
-       };
-       
-       return Ajax;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Jsonp.js b/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Jsonp.js
deleted file mode 100644 (file)
index df8efb1..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * Provides a utility class to issue JSONP requests.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ajax/Jsonp
- */
-define(['Core'], function(Core) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Ajax/Jsonp
-        */
-       var AjaxJsonp = {
-               /**
-                * Issues a JSONP request.
-                * 
-                * @param       {string}                url             source URL, must not contain callback parameter
-                * @param       {function}              success         success callback
-                * @param       {function=}             failure         timeout callback
-                * @param       {object<string, *>=}    options         request options
-                */
-               send: function(url, success, failure, options) {
-                       url = (typeof url === 'string') ? url.trim() : '';
-                       if (url.length === 0) {
-                               throw new Error("Expected a non-empty string for parameter 'url'.");
-                       }
-                       
-                       if (typeof success !== 'function') {
-                               throw new TypeError("Expected a valid callback function for parameter 'success'.");
-                       }
-                       
-                       options = Core.extend({
-                               parameterName: 'callback',
-                               timeout: 10
-                       }, options || {});
-                       
-                       var callbackName = 'wcf_jsonp_' + Core.getUuid().replace(/-/g, '').substr(0, 8);
-                       
-                       var timeout = window.setTimeout(function() {
-                               window[callbackName] = function() {};
-                               
-                               if (typeof failure === 'function') {
-                                       failure();
-                               }
-                       }, (~~options.timeout || 10) * 1000);
-                       
-                       window[callbackName] = function() {
-                               window.clearTimeout(timeout);
-                               
-                               success.apply(null, arguments);
-                       };
-                       
-                       url += (url.indexOf('?') === -1) ? '?' : '&';
-                       url += options.parameterName + '=' + callbackName;
-                       
-                       var script = elCreate('script');
-                       script.async = true;
-                       elAttr(script, 'src', url);
-                       
-                       document.head.appendChild(script);
-               }
-       };
-       
-       return AjaxJsonp;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js b/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Request.js
deleted file mode 100644 (file)
index ccb4e19..0000000
+++ /dev/null
@@ -1,324 +0,0 @@
-/**
- * Versatile AJAX request handling.
- * 
- * In case you want to issue JSONP requests, please use `AjaxJsonp` instead.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ajax/Request
- */
-define(['Core', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Dialog', 'WoltLab/WCF/Ajax/Status'], function(Core, Language, DomChangeListener, DomUtil, UiDialog, AjaxStatus) {
-       "use strict";
-       
-       var _didInit = false;
-       var _ignoreAllErrors = false;
-       
-       /**
-        * @constructor
-        */
-       function AjaxRequest(options) {
-               this._data = null;
-               this._options = {};
-               this._previousXhr = null;
-               this._xhr = null;
-               
-               this._init(options);
-       }
-       AjaxRequest.prototype = {
-               /**
-                * Initializes the request options.
-                * 
-                * @param       {Object}        options         request options
-                */
-               _init: function(options) {
-                       this._options = Core.extend({
-                               // request data
-                               data: {},
-                               contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
-                               responseType: 'application/json',
-                               type: 'POST',
-                               url: '',
-                               
-                               // behavior
-                               autoAbort: false,
-                               ignoreError: false,
-                               pinData: false,
-                               silent: false,
-                               
-                               // callbacks
-                               failure: null,
-                               finalize: null,
-                               success: null,
-                               progress: null,
-                               uploadProgress: null,
-                               
-                               callbackObject: null
-                       }, options);
-                       
-                       if (typeof options.callbackObject === 'object') {
-                               this._options.callbackObject = options.callbackObject;
-                       }
-                       
-                       this._options.url = Core.convertLegacyUrl(this._options.url);
-                       
-                       if (this._options.pinData) {
-                               this._data = Core.extend({}, this._options.data);
-                       }
-                       
-                       if (this._options.callbackObject !== null) {
-                               if (typeof this._options.callbackObject._ajaxFailure === 'function') this._options.failure = this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject);
-                               if (typeof this._options.callbackObject._ajaxFinalize === 'function') this._options.finalize = this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject);
-                               if (typeof this._options.callbackObject._ajaxSuccess === 'function') this._options.success = this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject);
-                               if (typeof this._options.callbackObject._ajaxProgress === 'function') this._options.progress = this._options.callbackObject._ajaxProgress.bind(this._options.callbackObject);
-                               if (typeof this._options.callbackObject._ajaxUploadProgress === 'function') this._options.uploadProgress = this._options.callbackObject._ajaxUploadProgress.bind(this._options.callbackObject);
-                       }
-                       
-                       if (_didInit === false) {
-                               _didInit = true;
-                               
-                               window.addEventListener('beforeunload', function() { _ignoreAllErrors = true; });
-                       }
-               },
-               
-               /**
-                * Dispatches a request, optionally aborting a currently active request.
-                * 
-                * @param       {boolean}       abortPrevious   abort currently active request
-                */
-               sendRequest: function(abortPrevious) {
-                       if (abortPrevious === true || this._options.autoAbort) {
-                               this.abortPrevious();
-                       }
-                       
-                       if (!this._options.silent) {
-                               AjaxStatus.show();
-                       }
-                       
-                       if (this._xhr instanceof XMLHttpRequest) {
-                               this._previousXhr = this._xhr;
-                       }
-                       
-                       this._xhr = new XMLHttpRequest();
-                       this._xhr.open(this._options.type, this._options.url, true);
-                       if (this._options.contentType) {
-                               this._xhr.setRequestHeader('Content-Type', this._options.contentType);
-                       }
-                       this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
-                       
-                       var self = this;
-                       var options = Core.clone(this._options);
-                       this._xhr.onload = function() {
-                               if (this.readyState === XMLHttpRequest.DONE) {
-                                       if (this.status >= 200 && this.status < 300 || this.status === 304) {
-                                               if (options.responseType && this.getResponseHeader('Content-Type').indexOf(options.responseType) !== 0) {
-                                                       // request succeeded but invalid response type
-                                                       self._failure(this, options);
-                                               }
-                                               else {
-                                                       self._success(this, options);
-                                               }
-                                       }
-                                       else {
-                                               self._failure(this, options);
-                                       }
-                               }
-                       };
-                       this._xhr.onerror = function() {
-                               self._failure(this, options);
-                       };
-                       
-                       if (this._options.progress) {
-                               this._xhr.onprogress = this._options.progress;
-                       }
-                       if (this._options.uploadProgress) {
-                               this._xhr.upload.onprogress = this._options.uploadProgress;
-                       }
-                       
-                       if (this._options.type === 'POST') {
-                               var data = this._options.data;
-                               if (typeof data === 'object' && Core.getType(data) !== 'FormData') {
-                                       data = Core.serialize(data);
-                               }
-                               
-                               this._xhr.send(data);
-                       }
-                       else {
-                               this._xhr.send();
-                       }
-               },
-               
-               /**
-                * Aborts a previous request.
-                */
-               abortPrevious: function() {
-                       if (this._previousXhr === null) {
-                               return;
-                       }
-                       
-                       this._previousXhr.abort();
-                       this._previousXhr = null;
-                       
-                       if (!this._options.silent) {
-                               AjaxStatus.hide();
-                       }
-               },
-               
-               /**
-                * Sets a specific option.
-                * 
-                * @param       {string}        key     option name
-                * @param       {?}             value   option value
-                */
-               setOption: function(key, value) {
-                       this._options[key] = value;
-               },
-               
-               /**
-                * Returns an option by key or undefined.
-                * 
-                * @param       {string}        key     option name
-                * @return      {(*|null)}      option value or null
-                */
-               getOption: function(key) {
-                       if (objOwns(this._options, key)) {
-                               return this._options[key];
-                       }
-                       
-                       return null;
-               },
-               
-               /**
-                * Sets request data while honoring pinned data from setup callback.
-                * 
-                * @param       {Object}        data    request data
-                */
-               setData: function(data) {
-                       if (this._data !== null && Core.getType(data) !== 'FormData') {
-                               data = Core.extend(this._data, data);
-                       }
-                       
-                       this._options.data = data;
-               },
-               
-               /**
-                * Handles a successful request.
-                * 
-                * @param       {XMLHttpRequest}        xhr             request object
-                * @param       {Object}                options         request options
-                */
-               _success: function(xhr, options) {
-                       if (!options.silent) {
-                               AjaxStatus.hide();
-                       }
-                       
-                       if (typeof options.success === 'function') {
-                               var data = null;
-                               if (xhr.getResponseHeader('Content-Type') === 'application/json') {
-                                       try {
-                                               data = JSON.parse(xhr.responseText);
-                                       }
-                                       catch (e) {
-                                               // invalid JSON
-                                               this._failure(xhr, options);
-                                               
-                                               return;
-                                       }
-                                       
-                                       // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring
-                                       if (data && data.returnValues && data.returnValues.template !== undefined) {
-                                               data.returnValues.template = data.returnValues.template.trim();
-                                       }
-                               }
-                               
-                               options.success(data, xhr.responseText, xhr, options.data);
-                       }
-                       
-                       this._finalize(options);
-               },
-               
-               /**
-                * Handles failed requests, this can be both a successful request with
-                * a non-success status code or an entirely failed request.
-                * 
-                * @param       {XMLHttpRequest}        xhr             request object
-                * @param       {Object}                options         request options
-                */
-               _failure: function (xhr, options) {
-                       if (_ignoreAllErrors) {
-                               return;
-                       }
-                       
-                       if (!options.silent) {
-                               AjaxStatus.hide();
-                       }
-                       
-                       var data = null;
-                       try {
-                               data = JSON.parse(xhr.responseText);
-                       }
-                       catch (e) {}
-                       
-                       var showError = true;
-                       if (data !== null && typeof options.failure === 'function') {
-                               showError = options.failure(data, xhr.responseText, xhr, options.data);
-                       }
-                       
-                       if (options.ignoreError !== true && showError !== false) {
-                               var details = '';
-                               var message = '';
-                               
-                               if (data !== null) {
-                                       if (data.stacktrace) details = '<br /><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
-                                       else if (data.exceptionID) details = '<br /><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
-                                       
-                                       message = data.message;
-                               }
-                               else {
-                                       message = xhr.responseText;
-                               }
-                               
-                               if (!message || message === 'undefined') {
-                                       return;
-                               }
-                               
-                               var html = '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
-                               
-                               if (UiDialog === undefined) UiDialog = require('Ui/Dialog');
-                               UiDialog.openStatic(DomUtil.getUniqueId(), html, {
-                                       title: Language.get('wcf.global.error.title')
-                               });
-                       }
-                       
-                       this._finalize(options);
-               },
-               
-               /**
-                * Finalizes a request.
-                * 
-                * @param       {Object}        options         request options
-                */
-               _finalize: function(options) {
-                       if (typeof options.finalize === 'function') {
-                               options.finalize(this._xhr);
-                       }
-                       
-                       this._previousXhr = null;
-                       
-                       DomChangeListener.trigger();
-                       
-                       // fix anchor tags generated through WCF::getAnchor()
-                       var links = elBySelAll('a[href*="#"]');
-                       for (var i = 0, length = links.length; i < length; i++) {
-                               var link = links[i];
-                               var href = elAttr(link, 'href');
-                               if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) {
-                                       href = href.substr(href.indexOf('#'));
-                                       elAttr(link, 'href', document.location.toString().replace(/#.*/, '') + href);
-                               }
-                       }
-               }
-       };
-       
-       return AjaxRequest;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Status.js b/wcfsetup/install/files/js/WoltLab/WCF/Ajax/Status.js
deleted file mode 100644 (file)
index 5ca1ef5..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Provides the AJAX status 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/Ajax/Status
- */
-define(['Language'], function(Language) {
-       "use strict";
-       
-       var _activeRequests = 0;
-       var _overlay = null;
-       var _timeoutShow = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Ajax/Status
-        */
-       var AjaxStatus = {
-               /**
-                * Initializes the status overlay on first usage.
-                */
-               _init: function() {
-                       _overlay = elCreate('div');
-                       _overlay.classList.add('spinner');
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon48 fa-spinner';
-                       _overlay.appendChild(icon);
-                       
-                       var title = elCreate('span');
-                       title.textContent = Language.get('wcf.global.loading');
-                       _overlay.appendChild(title);
-                       
-                       document.body.appendChild(_overlay);
-               },
-               
-               /**
-                * Shows the loading overlay.
-                */
-               show: function() {
-                       if (_overlay === null) {
-                               this._init();
-                       }
-                       
-                       _activeRequests++;
-                       
-                       if (_timeoutShow === null) {
-                               _timeoutShow = window.setTimeout(function() {
-                                       if (_activeRequests) {
-                                               _overlay.classList.add('active');
-                                       }
-                                       
-                                       _timeoutShow = null;
-                               }, 250);
-                       }
-               },
-               
-               /**
-                * Hides the loading overlay.
-                */
-               hide: function() {
-                       _activeRequests--;
-                       
-                       if (_activeRequests === 0) {
-                               if (_timeoutShow !== null) {
-                                       window.clearTimeout(_timeoutShow);
-                               }
-                               
-                               _overlay.classList.remove('active');
-                       }
-               }
-       };
-       
-       return AjaxStatus;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/Collapsible.js b/wcfsetup/install/files/js/WoltLab/WCF/Bbcode/Collapsible.js
deleted file mode 100644 (file)
index c120611..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Generic handler for collapsible bbcode boxes.
- * 
- * @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/Collapsible
- */
-define([], function() {
-       "use strict";
-       
-       var _containers = elByClass('jsCollapsibleBbcode');
-       
-       /**
-        * @exports     WoltLab/WCF/Bbcode/Collapsible
-        */
-       var BbcodeCollapsible = {
-               observe: function() {
-                       var container, toggleButton;
-                       while (_containers.length) {
-                               container = _containers[0];
-                               container.classList.remove('jsCollapsibleBbcode');
-                               
-                               toggleButton = elBySel('.toggleButton');
-                               if (toggleButton === null) {
-                                       continue;
-                               }
-                               
-                               (function(container, toggleButton) {
-                                       var toggle = function() {
-                                               var expand = container.classList.contains('collapsed');
-                                               container.classList[expand ? 'remove' : 'add']('collapsed');
-                                               toggleButton.textContent = elData(toggleButton, 'title-' + (expand ? 'collapse' : 'expand'));
-                                       };
-                                       
-                                       toggleButton.addEventListener(WCF_CLICK_EVENT, toggle);
-                                       
-                                       // searching in a page causes Google Chrome to scroll
-                                       // the box if something inside it matches
-                                       // 
-                                       // expand the box in this case, to:
-                                       // a) Improve UX
-                                       // b) Hide an ugly misplaced "show all" button
-                                       container.addEventListener('scroll', toggle);
-                                       
-                                       // expand boxes that are initially scrolled
-                                       if (container.scrollTop !== 0) {
-                                               toggle();
-                                       }
-                               })(container, toggleButton);
-                       }
-               }
-       };
-       
-       return BbcodeCollapsible;
-});
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 21f866c..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 = elCreate('div');
-                       container.innerHTML = message;
-                       
-                       // convert line breaks
-                       var elements = elByTag('P', container);
-                       while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
-                       
-                       elements = elByTag('BR', container);
-                       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 = elByTag('*', container);
-                       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 = elBySelAll(_sourceConverter[i].selector, container);
-                               
-                               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 = elCreate('var');
-                       elData(placeholder, '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);
-                                       
-                                       elRemove(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 = elByTag(converter.tagName, container);
-                       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 = elData(element, 'author');
-                       var link = elAttr(element, '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) ? ' ' : '') + elAttr(element, '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 = elData(element, '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 = elByTag('TD', element);
-                       while (elements.length) {
-                               elements[0].outerHTML = '[td]' + elements[0].innerHTML + '[/td]\n';
-                       }
-                       
-                       elements = elByTag('TR', element);
-                       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 = elData(element, 'filename').trim() || '';
-                       var highlighter = elData(element, 'highlighter');
-                       window.dtdesign = element;
-                       var list = DomTraverse.childByTag(element.children[0], 'OL');
-                       var lineNumber = ~~elAttr(list, '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 6facdf3..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 9dd173b..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 objOwns(__REDACTOR_CODE_HIGHLIGHTERS, 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       {int}                   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       {int}           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       {int}           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 (!objOwns(_options.attachments.images, 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 width, 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       {int}           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       {int}           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       {int}           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       {int}           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       {int}           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 = (item.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       {int}           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 (objOwns(__REDACTOR_SMILIES, 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       {int}           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       {int}           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/Bootstrap.js b/wcfsetup/install/files/js/WoltLab/WCF/Bootstrap.js
deleted file mode 100644 (file)
index 32dfb3a..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Bootstraps WCF's JavaScript.
- * It defines globals needed for backwards compatibility
- * and runs modules that are needed on page load.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Bootstrap
- */
-define(
-       [
-               'favico',                  'enquire',                'perfect-scrollbar',      'WoltLab/WCF/Date/Time/Relative',
-               'Ui/SimpleDropdown',       'WoltLab/WCF/Ui/Mobile',  'WoltLab/WCF/Ui/TabMenu', 'WoltLab/WCF/Ui/FlexibleMenu',
-               'Ui/Dialog',               'WoltLab/WCF/Ui/Tooltip', 'WoltLab/WCF/Language',   'WoltLab/WCF/Environment',
-               'WoltLab/WCF/Date/Picker', 'EventHandler',           'Core',                   'WoltLab/WCF/Ui/Page/JumpToTop'
-       ], 
-       function(
-                favico,                   enquire,                  perfectScrollbar,         DateTimeRelative,
-                UiSimpleDropdown,         UiMobile,                 UiTabMenu,                UiFlexibleMenu,
-                UiDialog,                 UiTooltip,                Language,                 Environment,
-                DatePicker,               EventHandler,             Core,                     UiPageJumpToTop
-       )
-{
-       "use strict";
-       
-       // perfectScrollbar does not need to be bound anywhere, it just has to be loaded for WCF.js
-       window.Favico = favico;
-       window.enquire = enquire;
-       // non strict equals by intent
-       if (window.WCF == null) window.WCF = { };
-       if (window.WCF.Language == null) window.WCF.Language = { };
-       window.WCF.Language.get = Language.get;
-       window.WCF.Language.add = Language.add;
-       window.WCF.Language.addObject = Language.addObject;
-       
-       // WCF.System.Event compatibility
-       window.__wcf_bc_eventHandler = EventHandler;
-       
-       /**
-        * @exports     WoltLab/WCF/Bootstrap
-        */
-       return {
-               /**
-                * Initializes the core UI modifications and unblocks jQuery's ready event.
-                * 
-                * @param       {Object=}       options         initialization options
-                */
-               setup: function(options) {
-                       options = Core.extend({
-                               enableMobileMenu: true
-                       }, options);
-                       
-                       Environment.setup();
-                       
-                       DateTimeRelative.setup();
-                       DatePicker.init();
-                       
-                       UiSimpleDropdown.setup();
-                       UiMobile.setup({
-                               enableMobileMenu: options.enableMobileMenu
-                       });
-                       UiTabMenu.setup();
-                       //UiFlexibleMenu.setup();
-                       UiDialog.setup();
-                       UiTooltip.setup();
-                       
-                       new UiPageJumpToTop();
-                       
-                       // convert method=get into method=post
-                       var forms = elBySelAll('form[method=get]');
-                       for (var i = 0, length = forms.length; i < length; i++) {
-                               forms[i].setAttribute('method', 'post');
-                       }
-                       
-                       if (Environment.browser() === 'microsoft') {
-                               window.onbeforeunload = function() {
-                                       /* Prevent "Back navigation caching" (http://msdn.microsoft.com/en-us/library/ie/dn265017%28v=vs.85%29.aspx) */
-                               };
-                       }
-                       
-                       // DEBUG ONLY
-                       var interval = 0;
-                       interval = window.setInterval(function() {
-                               if (typeof window.jQuery === 'function') {
-                                       window.clearInterval(interval);
-                                       
-                                       window.jQuery.holdReady(false);
-                               }
-                       }, 20);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/BootstrapFrontend.js b/wcfsetup/install/files/js/WoltLab/WCF/BootstrapFrontend.js
deleted file mode 100644 (file)
index 4604e0b..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * Bootstraps WCF's JavaScript with additions for the frontend usage.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/BootstrapFrontend
- */
-define(
-       [
-               'Ajax',                           'WoltLab/WCF/Bootstrap',      'WoltLab/WCF/Controller/Style/Changer',
-               'WoltLab/WCF/Controller/Popover', 'WoltLab/WCF/Ui/User/Ignore'
-       ],
-       function(
-               Ajax,                              Bootstrap,                    ControllerStyleChanger,
-               ControllerPopover,                 UiUserIgnore
-       )
-{
-       "use strict";
-       
-       var queueInvocations = 0;
-       
-       /**
-        * @exports     WoltLab/WCF/BootstrapFrontend
-        */
-       return {
-               /**
-                * Bootstraps general modules and frontend exclusive ones.
-                * 
-                * @param       {object<string, *>}     options         bootstrap options
-                */
-               setup: function(options) {
-                       Bootstrap.setup();
-                       
-                       if (options.styleChanger) {
-                               ControllerStyleChanger.setup();
-                       }
-                       
-                       this._initUserPopover();
-                       this._invokeBackgroundQueue(options.backgroundQueue.url, options.backgroundQueue.force);
-                       
-                       UiUserIgnore.init();
-               },
-               
-               /**
-                * Initializes user profile popover.
-                */
-               _initUserPopover: function() {
-                       ControllerPopover.init({
-                               attributeName: 'data-user-id',
-                               className: 'userLink',
-                               identifier: 'com.woltlab.wcf.user',
-                               loadCallback: function(objectId, popover) {
-                                       var callback = function(data) {
-                                               popover.setContent('com.woltlab.wcf.user', objectId, data.returnValues.template);
-                                       };
-                                       
-                                       popover.ajaxApi({
-                                               actionName: 'getUserProfile',
-                                               className: 'wcf\\data\\user\\UserProfileAction',
-                                               objectIDs: [ objectId ]
-                                       }, callback, callback);
-                               }
-                       });
-               },
-               
-               /**
-                * Invokes the background queue roughly every 10th request.
-                * 
-                * @param       {string}        url     background queue url
-                * @param       {boolean}       force   whether execution should be forced
-                */
-               _invokeBackgroundQueue: function(url, force) {
-                       var again = this._invokeBackgroundQueue.bind(this, url, true);
-                       
-                       if (Math.random() < 0.1 || force) {
-                               // 'fire and forget' background queue perform task
-                               Ajax.apiOnce({
-                                       url: url,
-                                       ignoreError: true,
-                                       silent: true,
-                                       success: (function(data) {
-                                               queueInvocations++;
-                                               
-                                               // process up to 5 queue items per page load
-                                               if (data > 0 && queueInvocations < 5) setTimeout(again, 1000);
-                                       }).bind(this)
-                               });
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js b/wcfsetup/install/files/js/WoltLab/WCF/CallbackList.js
deleted file mode 100644 (file)
index 94fdb46..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * Simple API to store and invoke multiple callbacks per identifier.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/CallbackList
- */
-define(['Dictionary'], function(Dictionary) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function CallbackList() {
-               this._dictionary = new Dictionary();
-       }
-       CallbackList.prototype = {
-               /**
-                * Adds a callback for given identifier.
-                * 
-                * @param       {string}        identifier      arbitrary string to group and identify callbacks
-                * @param       {function}      callback        callback function
-                */
-               add: function(identifier, callback) {
-                       if (typeof callback !== 'function') {
-                               throw new TypeError("Expected a valid callback as second argument for identifier '" + identifier + "'.");
-                       }
-                       
-                       if (!this._dictionary.has(identifier)) {
-                               this._dictionary.set(identifier, []);
-                       }
-                       
-                       this._dictionary.get(identifier).push(callback);
-               },
-               
-               /**
-                * Removes all callbacks registered for given identifier
-                * 
-                * @param       {string}        identifier      arbitrary string to group and identify callbacks
-                */
-               remove: function(identifier) {
-                       this._dictionary['delete'](identifier);
-               },
-               
-               /**
-                * Invokes callback function on each registered callback.
-                * 
-                * @param       {string|null}           identifier      arbitrary string to group and identify callbacks.
-                *                                                      null is a wildcard to match every identifier
-                * @param       {function(function)}    callback        function called with the individual callback as parameter
-                */
-               forEach: function(identifier, callback) {
-                       if (identifier === null) {
-                               this._dictionary.forEach(function(callbacks, identifier) {
-                                       callbacks.forEach(callback);
-                               });
-                       }
-                       else {
-                               var callbacks = this._dictionary.get(identifier);
-                               if (callbacks !== undefined) {
-                                       callbacks.forEach(callback);
-                               }
-                       }
-               }
-       };
-       
-       return CallbackList;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/ColorUtil.js b/wcfsetup/install/files/js/WoltLab/WCF/ColorUtil.js
deleted file mode 100644 (file)
index a6d5f8f..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-define([], function() {
-       "use strict";
-       
-       var ColorUtil = {
-               /**
-                * Converts HEX into RGB.
-                *
-                * @param       string          hex     hex value as #ccc or #abc123
-                * @return      object          r-g-b values
-                */
-               hexToRgb: function(hex) {
-                       hex = hex.replace(/^#/, '');
-                       if (/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)) {
-                               // only convert abc and abcdef
-                               hex = hex.split('');
-                               
-                               // parse shorthand #xyz
-                               if (hex.length === 3) {
-                                       return {
-                                               r: parseInt(hex[0] + '' + hex[0], 16),
-                                               g: parseInt(hex[1] + '' + hex[1], 16),
-                                               b: parseInt(hex[2] + '' + hex[2], 16)
-                                       };
-                               }
-                               else {
-                                       return {
-                                               r: parseInt(hex[0] + '' + hex[1], 16),
-                                               g: parseInt(hex[2] + '' + hex[3], 16),
-                                               b: parseInt(hex[4] + '' + hex[5], 16)
-                                       };
-                               }
-                       }
-                       
-                       return Number.NaN;
-               },
-               
-               /**
-                * Converts RGB into HEX.
-                *
-                * @see http://www.linuxtopia.org/online_books/javascript_guides/javascript_faq/rgbtohex.htm
-                * 
-                * @param       {(int|string)}  r       red or rgb(1, 2, 3) or rgba(1, 2, 3, .4)
-                * @param       {int}           g       green
-                * @param       {int}           b       blue
-                * @return      {string}        hex value #abc123
-                */
-               rgbToHex: function(r, g, b) {
-                       var charList = "0123456789ABCDEF";
-                       
-                       if (g === undefined) {
-                               if (r.match(/^rgba?\((\d+), ?(\d+), ?(d\+)(?:, ?[0-9.]+)?\)$/)) {
-                                       r = RegExp.$1;
-                                       g = RegExp.$2;
-                                       b = RegExp.$3;
-                               }
-                       }
-                       
-                       return (charList.charAt((r - r % 16) / 16) + '' + charList.charAt(r % 16)) + '' + (charList.charAt((g - g % 16) / 16) + '' + charList.charAt(g % 16)) + '' + (charList.charAt((b - b % 16) / 16) + '' + charList.charAt(b % 16));
-               }
-       };
-       
-       return ColorUtil;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Captcha.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Captcha.js
deleted file mode 100644 (file)
index cfeea23..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Provides data of the active user.
- *
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Controller/Captcha
- */
-define(['Dictionary'], function(Dictionary) {
-       "use strict";
-       
-       var _captchas = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/Controller/Captcha
-        */
-       return {
-               /**
-                * Registers a captcha with the given identifier and callback used to get captcha data.
-                * 
-                * @param       {string}        captchaId       captcha identifier
-                * @param       {function}      callback        callback to get captcha data
-                */
-               add: function(captchaId, callback) {
-                       if (_captchas.has(captchaId)) {
-                               throw new Error("Captcha with id '" + captchaId + "' is already registered.");
-                       }
-                       
-                       if (typeof callback !== 'function') {
-                               throw new TypeError("Expected a valid callback for parameter 'callback'.");
-                       }
-                       
-                       _captchas.set(captchaId, callback);
-               },
-               
-               /**
-                * Deletes the captcha with the given identifier.
-                * 
-                * @param       {string}        captchaId       identifier of the captcha to be deleted
-                */
-               'delete': function(captchaId) {
-                       if (!_captchas.has(captchaId)) {
-                               throw new Error("Unknown captcha with id '" + captchaId + "'.");
-                       }
-                       
-                       _captchas.delete(captchaId)();
-               },
-               
-               /**
-                * Returns true if a captcha with the given identifier exists.
-                * 
-                * @param       {string}        captchaId       captcha identifier
-                * @return      {boolean}
-                */
-               has: function(captchaId) {
-                       return _captchas.has(captchaId);
-               },
-               
-               /**
-                * Returns the data of the captcha with the given identifier.
-                * 
-                * @param       {string}        captchaId       captcha identifier
-                * @return      {Object}        captcha data
-                */
-               getData: function(captchaId) {
-                       if (!_captchas.has(captchaId)) {
-                               throw new Error("Unknown captcha with id '" + captchaId + "'.");
-                       }
-                       
-                       return _captchas.get(captchaId)();
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Clipboard.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Clipboard.js
deleted file mode 100644 (file)
index 7369986..0000000
+++ /dev/null
@@ -1,633 +0,0 @@
-/**
- * Clipboard API Handler.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Controller/Clipboard
- */
-define(
-       [
-               'Ajax',         'Core',     'Dictionary',      'EventHandler',
-               'Language',     'List',     'ObjectMap',       'Dom/ChangeListener',
-               'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown',
-               'WoltLab/WCF/Ui/Page/Action'
-       ],
-       function(
-               Ajax,            Core,       Dictionary,        EventHandler,
-               Language,        List,       ObjectMap,         DomChangeListener,
-               DomTraverse,     DomUtil,    UiConfirmation,    UiSimpleDropdown,
-               UiPageAction
-       )
-{
-       "use strict";
-       
-       var _containers = new Dictionary();
-       var _editors = new Dictionary();
-       var _editorDropdowns = new Dictionary();
-       var _elements = elByClass('jsClipboardContainer');
-       var _itemData = new ObjectMap();
-       var _knownCheckboxes = new List();
-       var _options = {};
-       
-       var _callbackCheckbox = null;
-       var _callbackItem = null;
-       var _callbackUnmarkAll = null;
-       
-       var _addPageOverlayActiveClass = false;
-       
-       /**
-        * Clipboard API
-        * 
-        * @exports     WoltLab/WCF/Controller/Clipboard
-        */
-       return {
-               /**
-                * Initializes the clipboard API handler.
-                * 
-                * @param       {Object}        options         initialization options
-                */
-               setup: function(options) {
-                       if (!options.pageClassName) {
-                               throw new Error("Expected a non-empty string for parameter 'pageClassName'.");
-                       }
-                       
-                       if (_callbackCheckbox === null) {
-                               _callbackCheckbox = this._mark.bind(this);
-                               _callbackItem = this._executeAction.bind(this);
-                               _callbackUnmarkAll = this._unmarkAll.bind(this);
-                               
-                               _options = Core.extend({
-                                       hasMarkedItems: false,
-                                       pageClassNames: [options.pageClassName],
-                                       pageObjectId: 0
-                               }, options);
-                               
-                               delete _options.pageClassName;
-                       }
-                       else {
-                               if (options.pageObjectId) {
-                                       throw new Error("Cannot load secondary clipboard with page object id set.");
-                               }
-                               
-                               _options.pageClassNames.push(options.pageClassName);
-                       }
-                       
-                       this._initContainers();
-                       
-                       if (_options.hasMarkedItems && _elements.length) {
-                               this._loadMarkedItems();
-                       }
-                       
-                       DomChangeListener.add('WoltLab/WCF/Controller/Clipboard', this._initContainers.bind(this));
-               },
-               
-               /**
-                * Reloads the clipboard data.
-                */
-               reload: function() {
-                       if (_containers.size) {
-                               this._loadMarkedItems();
-                       }
-               },
-               
-               /**
-                * Initializes clipboard containers.
-                */
-               _initContainers: function() {
-                       for (var i = 0, length = _elements.length; i < length; i++) {
-                               var container = _elements[i];
-                               var containerId = DomUtil.identify(container);
-                               var containerData = _containers.get(containerId);
-                               
-                               if (containerData === undefined) {
-                                       var markAll = elBySel('.jsClipboardMarkAll', container);
-                                       if (markAll !== null) {
-                                               elData(markAll, 'container-id', containerId);
-                                               markAll.addEventListener(WCF_CLICK_EVENT, this._markAll.bind(this));
-                                       }
-                                       
-                                       containerData = {
-                                               checkboxes: elByClass('jsClipboardItem', container),
-                                               element: container,
-                                               markAll: markAll,
-                                               markedObjectIds: new List()
-                                       };
-                                       _containers.set(containerId, containerData);
-                               }
-                               
-                               for (var j = 0, innerLength = containerData.checkboxes.length; j < innerLength; j++) {
-                                       var checkbox = containerData.checkboxes[j];
-                                       
-                                       if (!_knownCheckboxes.has(checkbox)) {
-                                               elData(checkbox, 'container-id', containerId);
-                                               checkbox.addEventListener(WCF_CLICK_EVENT, _callbackCheckbox);
-                                               
-                                               _knownCheckboxes.add(checkbox);
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Loads marked items from clipboard.
-                */
-               _loadMarkedItems: function() {
-                       Ajax.api(this, {
-                               actionName: 'getMarkedItems',
-                               parameters: {
-                                       pageClassNames: _options.pageClassNames,
-                                       pageObjectID: _options.pageObjectId
-                               }
-                       });
-               },
-               
-               /**
-                * Marks or unmarks all visible items at once.
-                * 
-                * @param       {object}        event   event object
-                */
-               _markAll: function(event) {
-                       var checkbox = event.currentTarget;
-                       var isMarked = (checkbox.nodeName !== 'INPUT' || checkbox.checked);
-                       var objectIds = [];
-                       
-                       var containerId = elData(checkbox, 'container-id');
-                       var data = _containers.get(containerId);
-                       var type = elData(data.element, 'type');
-                       
-                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                               var item = data.checkboxes[i];
-                               var objectId = ~~elData(item, 'object-id');
-                               
-                               if (isMarked) {
-                                       if (!item.checked) {
-                                               item.checked = true;
-                                               
-                                               data.markedObjectIds.add(objectId);
-                                               objectIds.push(objectId);
-                                       }
-                               }
-                               else {
-                                       if (item.checked) {
-                                               item.checked = false;
-                                               
-                                               data.markedObjectIds['delete'](objectId);
-                                               objectIds.push(objectId);
-                                       }
-                               }
-                               
-                               var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-                               if (clipboardObject !== null) {
-                                       clipboardObject.classList[(isMarked ? 'addClass' : 'removeClass')]('jsMarked');
-                               }
-                       }
-                       
-                       this._saveState(type, objectIds, isMarked);
-               },
-               
-               /**
-                * Marks or unmarks an individual item.
-                * 
-                * @param       {object}        event           event object
-                */
-               _mark: function(event) {
-                       var checkbox = event.currentTarget;
-                       var objectId = ~~elData(checkbox, 'object-id');
-                       var isMarked = checkbox.checked;
-                       var containerId = elData(checkbox, 'container-id');
-                       var data = _containers.get(containerId);
-                       var type = elData(data.element, 'type');
-                       
-                       var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-                       data.markedObjectIds[(isMarked ? 'add' : 'delete')](objectId);
-                       clipboardObject.classList[(isMarked) ? 'add' : 'remove']('jsMarked');
-                       
-                       if (data.markAll !== null) {
-                               var markedAll = true;
-                               for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                                       if (!data.checkboxes[i].checked) {
-                                               markedAll = false;
-                                               
-                                               break;
-                                       }
-                               }
-                               
-                               data.markAll.checked = markedAll;
-                       }
-                       
-                       this._saveState(type, [ objectId ], isMarked);
-               },
-               
-               /**
-                * Saves the state for given item object ids.
-                * 
-                * @param       {string}        type            object type
-                * @param       {int[]}         objectIds       item object ids
-                * @param       {boolean}       isMarked        true if marked
-                */
-               _saveState: function(type, objectIds, isMarked) {
-                       Ajax.api(this, {
-                               actionName: (isMarked ? 'mark' : 'unmark'),
-                               parameters: {
-                                       pageClassNames: _options.pageClassNames,
-                                       pageObjectID: _options.pageObjectId,
-                                       objectIDs: objectIds,
-                                       objectType: type
-                               }
-                       });
-               },
-               
-               /**
-                * Executes an editor action.
-                * 
-                * @param       {object}        event           event object
-                */
-               _executeAction: function(event) {
-                       var listItem = event.currentTarget;
-                       var data = _itemData.get(listItem);
-                       
-                       if (data.url) {
-                               window.location.href = data.url;
-                               return;
-                       }
-                       
-                       var triggerEvent = function() {
-                               var type = elData(listItem, 'type');
-                               
-                               EventHandler.fire('com.woltlab.wcf.clipboard', type, {
-                                       data: data,
-                                       listItem: listItem,
-                                       responseData: null
-                               });
-                       };
-                       
-                       //noinspection JSUnresolvedVariable
-                       var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : '';
-                       var fireEvent = true;
-                       
-                       if (typeof data.parameters === 'object' && data.parameters.actionName && data.parameters.className) {
-                               if (data.parameters.actionName === 'unmarkAll' || Array.isArray(data.parameters.objectIDs)) {
-                                       if (confirmMessage.length) {
-                                               //noinspection JSUnresolvedVariable
-                                               var template = (typeof data.internalData.template === 'string') ? data.internalData.template : '';
-                                               
-                                               UiConfirmation.show({
-                                                       confirm: (function() {
-                                                               var formData = {};
-                                                               
-                                                               if (template.length) {
-                                                                       var items = elBySelAll('input, select, textarea', UiConfirmation.getContentElement());
-                                                                       for (var i = 0, length = items.length; i < length; i++) {
-                                                                               var item = items[i];
-                                                                               var name = elAttr(item, 'name');
-                                                                               
-                                                                               switch (item.nodeName) {
-                                                                                       case 'INPUT':
-                                                                                               if (item.checked) {
-                                                                                                       formData[name] = elAttr(item, 'value');
-                                                                                               }
-                                                                                               break;
-                                                                                       
-                                                                                       case 'SELECT':
-                                                                                               formData[name] = item.value;
-                                                                                               break;
-                                                                                       
-                                                                                       case 'TEXTAREA':
-                                                                                               formData[name] = item.value.trim();
-                                                                                               break;
-                                                                               }
-                                                                       }
-                                                               }
-                                                               
-                                                               //noinspection JSUnresolvedFunction
-                                                               this._executeProxyAction(listItem, data, formData);
-                                                       }).bind(this),
-                                                       message: confirmMessage,
-                                                       template: template
-                                               });
-                                       }
-                                       else {
-                                               this._executeProxyAction(listItem, data);
-                                       }
-                               }
-                       }
-                       else if (confirmMessage.length) {
-                               fireEvent = false;
-                               
-                               UiConfirmation.show({
-                                       confirm: triggerEvent,
-                                       message: confirmMessage
-                               });
-                       }
-                       
-                       if (fireEvent) {
-                               triggerEvent();
-                       }
-               },
-               
-               /**
-                * Forwards clipboard actions to an individual handler.
-                * 
-                * @param       {Element}       listItem        dropdown item element
-                * @param       {Object}        data            action data
-                * @param       {Object?}       formData        form data
-                */
-               _executeProxyAction: function(listItem, data, formData) {
-                       formData = formData || {};
-                       
-                       var objectIds = (data.parameters.actionName !== 'unmarkAll') ? data.parameters.objectIDs : [];
-                       var parameters = { data: formData };
-                       
-                       //noinspection JSUnresolvedVariable
-                       if (typeof data.internalData.parameters === 'object') {
-                               //noinspection JSUnresolvedVariable
-                               for (var key in data.internalData.parameters) {
-                                       //noinspection JSUnresolvedVariable
-                                       if (data.internalData.parameters.hasOwnProperty(key)) {
-                                               //noinspection JSUnresolvedVariable
-                                               parameters[key] = data.internalData.parameters[key];
-                                       }
-                               }
-                       }
-                       
-                       Ajax.api(this, {
-                               actionName: data.parameters.actionName,
-                               className: data.parameters.className,
-                               objectIDs: objectIds,
-                               parameters: parameters
-                       }, (function(responseData) {
-                               if (data.actionName !== 'unmarkAll') {
-                                       var type = elData(listItem, 'type');
-                                       
-                                       EventHandler.fire('com.woltlab.wcf.clipboard', type, {
-                                               data: data,
-                                               listItem: listItem,
-                                               responseData: responseData
-                                       });
-                               }
-                               
-                               this._loadMarkedItems();
-                       }).bind(this));
-               },
-               
-               /**
-                * Unmarks all clipboard items for an object type.
-                * 
-                * @param       {object}        event           event object
-                */
-               _unmarkAll: function(event) {
-                       var type = elData(event.currentTarget, 'type');
-                       
-                       Ajax.api(this, {
-                               actionName: 'unmarkAll',
-                               parameters: {
-                                       objectType: type
-                               }
-                       });
-               },
-               
-               /**
-                * Sets up ajax request object.
-                * 
-                * @return      {object}        request options
-                */
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: 'wcf\\data\\clipboard\\item\\ClipboardItemAction'
-                               }
-                       };
-               },
-               
-               /**
-                * Handles successful AJAX requests.
-                * 
-                * @param       {object}        data    response data
-                */
-               _ajaxSuccess: function(data) {
-                       if (data.actionName === 'unmarkAll') {
-                               _containers.forEach((function(containerData) {
-                                       //noinspection JSUnresolvedVariable
-                                       if (elData(containerData.element, 'type') === data.returnValues.objectType) {
-                                               var clipboardObjects = elByClass('jsMarked', containerData.element);
-                                               while (clipboardObjects.length) {
-                                                       clipboardObjects[0].classList.remove('jsMarked');
-                                               }
-                                               
-                                               if (containerData.markAll !== null) {
-                                                       containerData.markAll.checked = false;
-                                               }
-                                               for (var i = 0, length = containerData.checkboxes.length; i < length; i++) {
-                                                       containerData.checkboxes[i].checked = false;
-                                               }
-                                               
-                                               //noinspection JSUnresolvedVariable
-                                               UiPageAction.remove('wcfClipboard-' + data.returnValues.objectType);
-                                       }
-                               }).bind(this));
-                               
-                               return;
-                       }
-                       
-                       _itemData = new ObjectMap();
-                       
-                       // rebuild markings
-                       _containers.forEach((function(containerData) {
-                               var typeName = elData(containerData.element, 'type');
-                               
-                               //noinspection JSUnresolvedVariable
-                               var objectIds = (data.returnValues.markedItems && data.returnValues.markedItems.hasOwnProperty(typeName)) ? data.returnValues.markedItems[typeName] : [];
-                               this._rebuildMarkings(containerData, objectIds);
-                       }).bind(this));
-                       
-                       var keepEditors = [], typeName;
-                       //noinspection JSUnresolvedVariable
-                       if (data.returnValues && data.returnValues.items) {
-                               //noinspection JSUnresolvedVariable
-                               for (typeName in data.returnValues.items) {
-                                       //noinspection JSUnresolvedVariable
-                                       if (data.returnValues.items.hasOwnProperty(typeName)) {
-                                               keepEditors.push(typeName);
-                                       }
-                               }
-                       }
-                       
-                       // clear editors
-                       _editors.forEach(function(editor, typeName) {
-                               if (keepEditors.indexOf(typeName) === -1) {
-                                       UiPageAction.remove('wcfClipboard-' + typeName);
-                                       
-                                       _editorDropdowns.get(typeName).innerHTML = '';
-                               }
-                       });
-                       
-                       // no items
-                       //noinspection JSUnresolvedVariable
-                       if (!data.returnValues || !data.returnValues.items) {
-                               return;
-                       }
-                       
-                       // rebuild editors
-                       var actionName, created, dropdown, editor, typeData;
-                       var divider, item, itemData, itemIndex, label, unmarkAll;
-                       //noinspection JSUnresolvedVariable
-                       for (typeName in data.returnValues.items) {
-                               //noinspection JSUnresolvedVariable
-                               if (!data.returnValues.items.hasOwnProperty(typeName)) {
-                                       continue;
-                               }
-                               
-                               //noinspection JSUnresolvedVariable
-                               typeData = data.returnValues.items[typeName];
-                               created = false;
-                               
-                               editor = _editors.get(typeName);
-                               dropdown = _editorDropdowns.get(typeName);
-                               if (editor === undefined) {
-                                       created = true;
-                                       
-                                       editor = elCreate('a');
-                                       editor.className = 'dropdownToggle';
-                                       editor.textContent = typeData.label;
-                                       
-                                       _editors.set(typeName, editor);
-                                       
-                                       dropdown = elCreate('ol');
-                                       dropdown.className = 'dropdownMenu';
-                                       
-                                       _editorDropdowns.set(typeName, dropdown);
-                               }
-                               else {
-                                       editor.textContent = typeData.label;
-                                       dropdown.innerHTML = '';
-                               }
-                               
-                               // create editor items
-                               for (itemIndex in typeData.items) {
-                                       if (!typeData.items.hasOwnProperty(itemIndex)) {
-                                               continue;
-                                       }
-                                       
-                                       itemData = typeData.items[itemIndex];
-                                       
-                                       item = elCreate('li');
-                                       label = elCreate('span');
-                                       label.textContent = itemData.label;
-                                       item.appendChild(label);
-                                       dropdown.appendChild(item);
-                                       
-                                       elData(item, 'type', typeName);
-                                       item.addEventListener(WCF_CLICK_EVENT, _callbackItem);
-                                       
-                                       _itemData.set(item, itemData);
-                               }
-                               
-                               divider = elCreate('li');
-                               divider.classList.add('dropdownDivider');
-                               dropdown.appendChild(divider);
-                               
-                               // add 'unmark all'
-                               unmarkAll = elCreate('li');
-                               elData(unmarkAll, 'type', typeName);
-                               label = elCreate('span');
-                               label.textContent = Language.get('wcf.clipboard.item.unmarkAll');
-                               unmarkAll.appendChild(label);
-                               unmarkAll.addEventListener(WCF_CLICK_EVENT, _callbackUnmarkAll);
-                               dropdown.appendChild(unmarkAll);
-                               
-                               if (keepEditors.indexOf(typeName) !== -1) {
-                                       actionName = 'wcfClipboard-' + typeName;
-                                       
-                                       if (UiPageAction.has(actionName)) {
-                                               UiPageAction.show(actionName);
-                                       }
-                                       else {
-                                               UiPageAction.add(actionName, editor);
-                                       }
-                               }
-                               
-                               if (created) {
-                                       editor.parentNode.classList.add('dropdown');
-                                       editor.parentNode.appendChild(dropdown);
-                                       UiSimpleDropdown.init(editor);
-                               }
-                       }
-               },
-               
-               /**
-                * Rebuilds the mark state for each item.
-                * 
-                * @param       {Object}        data            container data
-                * @param       {int[]}         objectIds       item object ids
-                */
-               _rebuildMarkings: function(data, objectIds) {
-                       var markAll = true;
-                       
-                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
-                               var checkbox = data.checkboxes[i];
-                               var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
-                               
-                               var isMarked = (objectIds.indexOf(~~elData(checkbox, 'object-id')) !== -1);
-                               if (!isMarked) markAll = false;
-                               
-                               checkbox.checked = isMarked;
-                               clipboardObject.classList[(isMarked ? 'add' : 'remove')]('jsMarked');
-                       }
-                       
-                       if (data.markAll !== null) {
-                               data.markAll.checked = markAll;
-                               
-                               var parent = data.markAll;
-                               while (parent = parent.parentNode) {
-                                       if (parent instanceof Element && parent.classList.contains('columnMark')) {
-                                               parent = parent.parentNode;
-                                               break;
-                                       }
-                               }
-                               
-                               if (parent) {
-                                       parent.classList[(markAll ? 'add' : 'remove')]('jsMarked');
-                               }
-                       }
-               },
-               
-               /**
-                * Hides the clipboard editor for the given object type.
-                * 
-                * @param       {string}        objectType
-                */
-               hideEditor: function(objectType) {
-                       UiPageAction.remove('wcfClipboard-' + objectType);
-                       
-                       if (_addPageOverlayActiveClass) {
-                               _addPageOverlayActiveClass = false;
-                               
-                               document.documentElement.classList.add('pageOverlayActive');
-                       }
-               },
-               
-               /**
-                * Shows the clipboard editor.
-                */
-               showEditor: function() {
-                       this._loadMarkedItems();
-                       
-                       if (document.documentElement.classList.contains('pageOverlayActive')) {
-                               document.documentElement.classList.remove('pageOverlayActive');
-                               
-                               _addPageOverlayActiveClass = true;
-                       }
-               },
-               
-               /**
-                * Unmarks the objects with given clipboard object type and ids.
-                * 
-                * @param       {string}        objectType
-                * @param       {int[]}         objectIds
-                */
-               unmark: function(objectType, objectIds) {
-                       this._saveState(objectType, objectIds, false);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Condition/Page/Dependence.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Condition/Page/Dependence.js
deleted file mode 100644 (file)
index 6ee856e..0000000
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * Shows and hides an element that depends on certain selected pages when setting up conditions.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Controller/Condition/Page/Dependence
- */
-define(['Dom/ChangeListener', 'Dom/Traverse', 'EventHandler', 'ObjectMap'], function(DomChangeListener, DomTraverse, EventHandler, ObjectMap) {
-       "use strict";
-       
-       var _pages = elBySelAll('input[name="pageIDs[]"]');
-       var _dependentElements = [];
-       var _pageIds = new ObjectMap();
-       var _hiddenElements = new ObjectMap();
-       
-       var _didInit = false;
-       
-       return {
-               register: function(dependentElement, pageIds) {
-                       _dependentElements.push(dependentElement);
-                       _pageIds.set(dependentElement, pageIds);
-                       _hiddenElements.set(dependentElement, []);
-                       
-                       if (!_didInit) {
-                               for (var i = 0, length = _pages.length; i < length; i++) {
-                                       _pages[i].addEventListener('change', this._checkVisibility.bind(this));
-                               }
-                               
-                               _didInit = true;
-                       }
-                       
-                       // remove the dependent element before submit if it is hidden
-                       DomTraverse.parentByTag(dependentElement, 'FORM').addEventListener('submit', function() {
-                               if (dependentElement.style.getPropertyValue('display') === 'none') {
-                                       dependentElement.remove();
-                               }
-                       });
-                       
-                       this._checkVisibility();
-               },
-               
-               /**
-                * Checks if any of the relevant pages is selected. If that is the case, the dependent
-                * element is shown, otherwise it is hidden.
-                * 
-                * @private
-                */
-               _checkVisibility: function() {
-                       var dependentElement, page, pageIds;
-                       
-                       depenentElementLoop: for (var i = 0, length = _dependentElements.length; i < length; i++) {
-                               dependentElement = _dependentElements[i];
-                               pageIds = _pageIds.get(dependentElement);
-                               
-                               for (var j = 0, length2 = _pages.length; j < length2; j++) {
-                                       page = _pages[j];
-                                       
-                                       if (page.checked && pageIds.indexOf(~~page.value) !== -1) {
-                                               this._showDependentElement(dependentElement);
-                                               
-                                               continue depenentElementLoop;
-                                       }
-                               }
-                               
-                               this._hideDependentElement(dependentElement);
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.pageConditionDependence', 'checkVisivility');
-               },
-               
-               _hideDependentElement: function(dependentElement) {
-                       elHide(dependentElement);
-                       
-                       var hiddenElements = _hiddenElements.get(dependentElement);
-                       for (var i = 0, length = hiddenElements.length; i < length; i++) {
-                               elHide(hiddenElements[i]);
-                       }
-                       
-                       _hiddenElements.set(dependentElement, []);
-               },
-               
-               _showDependentElement: function(dependentElement) {
-                       elShow(dependentElement);
-                       
-                       // make sure that all parent elements are also visible
-                       var parentNode = dependentElement;
-                       while ((parentNode = parentNode.parentNode) && parentNode instanceof Element) {
-                               if (parentNode.style.getPropertyValue('display') === 'none') {
-                                       _hiddenElements.get(dependentElement).push(parentNode);
-                               }
-                               
-                               elShow(parentNode);
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Notice/Dismiss.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Notice/Dismiss.js
deleted file mode 100644 (file)
index 223aa25..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Handles dismissible user notices.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Controller/Notice/Dismiss
- */
-define(['Ajax'], function(Ajax) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Controller/Notice/Dismiss
-        */
-       var ControllerNoticeDismiss = {
-               /**
-                * Initializes dismiss buttons.
-                */
-               setup: function() {
-                       var buttons = elByClass('jsDismissNoticeButton');
-                       
-                       if (buttons.length) {
-                               var clickCallback = this._click.bind(this);
-                               for (var i = 0, length = buttons.length; i < length; i++) {
-                                       buttons[i].addEventListener(WCF_CLICK_EVENT, clickCallback);
-                               }
-                       }
-               },
-               
-               /**
-                * Sends a request to dismiss a notice and removes it afterwards.
-                */
-               _click: function(event) {
-                       var button = event.currentTarget;
-                       
-                       Ajax.apiOnce({
-                               data: {
-                                       actionName: 'dismiss',
-                                       className: 'wcf\\data\\notice\\NoticeAction',
-                                       objectIDs: [ elData(button, 'object-id') ]
-                               },
-                               success: function() {
-                                       var parent = button.parentNode;
-                                       
-                                       parent.addEventListener('transitionend', function() {
-                                               elRemove(parent);
-                                       });
-                                       
-                                       parent.classList.remove('active');
-                               }
-                       });
-               }
-       };
-       
-       return ControllerNoticeDismiss;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Popover.js
deleted file mode 100644 (file)
index a9b48a9..0000000
+++ /dev/null
@@ -1,386 +0,0 @@
-/**
- * Versatile popover manager.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Controller/Popover
- */
-define(['Ajax', 'Dictionary', 'Environment', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Alignment'], function(Ajax, Dictionary, Environment, DomChangeListener, DomUtil, UiAlignment) {
-       "use strict";
-       
-       var _activeId = null;
-       var _cache = new Dictionary();
-       var _elements = new Dictionary();
-       var _handlers = new Dictionary();
-       var _hoverId = null;
-       var _suspended = false;
-       var _timeoutEnter = null;
-       var _timeoutLeave = null;
-       
-       var _popover = null;
-       var _popoverContent = null;
-       
-       var _callbackClick = null;
-       var _callbackHide = null;
-       var _callbackMouseEnter = null;
-       var _callbackMouseLeave = null;
-       
-       /** @const */ var STATE_NONE = 0;
-       /** @const */ var STATE_LOADING = 1;
-       /** @const */ var STATE_READY = 2;
-       
-       /** @const */ var DELAY_HIDE = 500;
-       /** @const */ var DELAY_SHOW = 300;
-       
-       /**
-        * @exports     WoltLab/WCF/Controller/Popover
-        */
-       return {
-               /**
-                * Builds popover DOM elements and binds event listeners.
-                */
-               _setup: function() {
-                       if (_popover !== null) {
-                               return;
-                       }
-                       
-                       _popover = elCreate('div');
-                       _popover.className = 'popover forceHide';
-                       
-                       _popoverContent = elCreate('div');
-                       _popoverContent.className = 'popoverContent';
-                       _popover.appendChild(_popoverContent);
-                       
-                       var pointer = elCreate('span');
-                       pointer.className = 'elementPointer';
-                       pointer.appendChild(elCreate('span'));
-                       _popover.appendChild(pointer);
-                       
-                       document.body.appendChild(_popover);
-                       
-                       // static binding for callbacks (they don't change anyway and binding each time is expensive)
-                       _callbackClick = this._hide.bind(this);
-                       _callbackMouseEnter = this._mouseEnter.bind(this);
-                       _callbackMouseLeave = this._mouseLeave.bind(this);
-                       
-                       // event listener
-                       _popover.addEventListener('mouseenter', this._popoverMouseEnter.bind(this));
-                       _popover.addEventListener('mouseleave', _callbackMouseLeave);
-                       
-                       _popover.addEventListener('animationend', this._clearContent.bind(this));
-                       
-                       window.addEventListener('beforeunload', (function() {
-                               _suspended = true;
-                               
-                               if (_timeoutEnter !== null) {
-                                       window.clearTimeout(_timeoutEnter);
-                               }
-                               
-                               this._hide(true);
-                       }).bind(this));
-                       
-                       DomChangeListener.add('WoltLab/WCF/Controller/Popover', this._init.bind(this));
-               },
-               
-               /**
-                * Initializes a popover handler.
-                * 
-                * Usage:
-                * 
-                * ControllerPopover.init({
-                *      attributeName: 'data-object-id',
-                *      className: 'fooLink',
-                *      identifier: 'com.example.bar.foo',
-                *      loadCallback: function(objectId, popover) {
-                *              // request data for object id (e.g. via WoltLab/WCF/Ajax)
-                *              
-                *              // then call this to set the content
-                *              popover.setContent('com.example.bar.foo', objectId, htmlTemplateString);
-                *      }
-                * });
-                * 
-                * @param       {Object}        options         handler options
-                */
-               init: function(options) {
-                       if (Environment.platform() !== 'desktop') {
-                               return;
-                       }
-                       
-                       options.attributeName = options.attributeName || 'data-object-id';
-                       options.legacy = (options.legacy === true);
-                       
-                       this._setup();
-                       
-                       if (_handlers.has(options.identifier)) {
-                               return;
-                       }
-                       
-                       _handlers.set(options.identifier, {
-                               attributeName: options.attributeName,
-                               elements: options.legacy ? options.className : elByClass(options.className),
-                               legacy: options.legacy,
-                               loadCallback: options.loadCallback
-                       });
-                       
-                       this._init(options.identifier);
-               },
-               
-               /**
-                * Initializes a popover handler.
-                * 
-                * @param       {string}        identifier      handler identifier
-                */
-               _init: function(identifier) {
-                       if (typeof identifier === 'string' && identifier.length) {
-                               this._initElements(_handlers.get(identifier), identifier);
-                       }
-                       else {
-                               _handlers.forEach(this._initElements.bind(this));
-                       }
-               },
-               
-               /**
-                * Binds event listeners for popover-enabled elements.
-                * 
-                * @param       {Object}        options         handler options
-                * @param       {string}        identifier      handler identifier
-                */
-               _initElements: function(options, identifier) {
-                       var elements = options.legacy ? elBySelAll(options.elements) : options.elements;
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               var element = elements[i];
-                               
-                               var id = DomUtil.identify(element);
-                               if (_cache.has(id)) {
-                                       return;
-                               }
-                               
-                               var objectId = (options.legacy) ? id : ~~element.getAttribute(options.attributeName);
-                               if (objectId === 0) {
-                                       continue;
-                               }
-                               
-                               element.addEventListener('mouseenter', _callbackMouseEnter);
-                               element.addEventListener('mouseleave', _callbackMouseLeave);
-                               
-                               if (element.nodeName === 'A' && elAttr(element, 'href')) {
-                                       element.addEventListener(WCF_CLICK_EVENT, _callbackClick);
-                               }
-                               
-                               var cacheId = identifier + "-" + objectId;
-                               elData(element, 'cache-id', cacheId);
-                               
-                               _elements.set(id, {
-                                       element: element,
-                                       identifier: identifier,
-                                       objectId: objectId
-                               });
-                               
-                               if (!_cache.has(cacheId)) {
-                                       _cache.set(identifier + "-" + objectId, {
-                                               content: null,
-                                               state: STATE_NONE
-                                       });
-                               }
-                       }
-               },
-               
-               /**
-                * Sets the content for given identifier and object id.
-                * 
-                * @param       {string}        identifier      handler identifier
-                * @param       {int}           objectId        object id
-                * @param       {string}        content         HTML string
-                */
-               setContent: function(identifier, objectId, content) {
-                       var cacheId = identifier + "-" + objectId;
-                       var data = _cache.get(cacheId);
-                       if (data === undefined) {
-                               throw new Error("Unable to find element for object id '" + objectId + "' (identifier: '" + identifier + "').");
-                       }
-                       
-                       data.content = DomUtil.createFragmentFromHtml(content);
-                       data.state = STATE_READY;
-                       
-                       if (_activeId) {
-                               var activeElement = _elements.get(_activeId).element;
-                               
-                               if (elData(activeElement, 'cache-id') === cacheId) {
-                                       this._show();
-                               }
-                       }
-               },
-               
-               /**
-                * Handles the mouse start hovering the popover-enabled element.
-                * 
-                * @param       {object}        event   event object
-                */
-               _mouseEnter: function(event) {
-                       if (_suspended) {
-                               return;
-                       }
-                       
-                       if (_timeoutEnter !== null) {
-                               window.clearTimeout(_timeoutEnter);
-                               _timeoutEnter = null;
-                       }
-                       
-                       var id = DomUtil.identify(event.currentTarget);
-                       if (_activeId === id && _timeoutLeave !== null) {
-                               window.clearTimeout(_timeoutLeave);
-                               _timeoutLeave = null;
-                       }
-                       
-                       _hoverId = id;
-                       
-                       _timeoutEnter = window.setTimeout((function() {
-                               _timeoutEnter = null;
-                               
-                               if (_hoverId === id) {
-                                       this._show();
-                               }
-                       }).bind(this), DELAY_SHOW);
-               },
-               
-               /**
-                * Handles the mouse leaving the popover-enabled element or the popover itself.
-                */
-               _mouseLeave: function() {
-                       _hoverId = null;
-                       
-                       if (_timeoutLeave !== null) {
-                               return;
-                       }
-                       
-                       if (_callbackHide === null) {
-                               _callbackHide = this._hide.bind(this);
-                       }
-                       
-                       if (_timeoutLeave !== null) {
-                               window.clearTimeout(_timeoutLeave);
-                       }
-                       
-                       _timeoutLeave = window.setTimeout(_callbackHide, DELAY_HIDE);
-               },
-               
-               /**
-                * Handles the mouse start hovering the popover element.
-                */
-               _popoverMouseEnter: function() {
-                       if (_timeoutLeave !== null) {
-                               window.clearTimeout(_timeoutLeave);
-                               _timeoutLeave = null;
-                       }
-               },
-               
-               /**
-                * Shows the popover and loads content on-the-fly.
-                */
-               _show: function() {
-                       if (_timeoutLeave !== null) {
-                               window.clearTimeout(_timeoutLeave);
-                               _timeoutLeave = null;
-                       }
-                       
-                       var forceHide = false;
-                       if (_popover.classList.contains('active')) {
-                               this._hide();
-                               
-                               forceHide = true;
-                       }
-                       else if (_popoverContent.childElementCount) {
-                               forceHide = true;
-                       }
-                       
-                       if (forceHide) {
-                               _popover.classList.add('forceHide');
-                               
-                               // force layout
-                               _popover.offsetTop;
-                               
-                               this._clearContent();
-                               
-                               _popover.classList.remove('forceHide');
-                       }
-                       
-                       _activeId = _hoverId;
-                       
-                       var elementData = _elements.get(_activeId);
-                       var data = _cache.get(elData(elementData.element, 'cache-id'));
-                       
-                       if (data.state === STATE_READY) {
-                               _popoverContent.appendChild(data.content);
-                               
-                               this._rebuild(_activeId);
-                       }
-                       else if (data.state === STATE_NONE) {
-                               data.state = STATE_LOADING;
-                               
-                               _handlers.get(elementData.identifier).loadCallback(elementData.objectId, this);
-                       }
-               },
-               
-               /**
-                * Hides the popover element.
-                */
-               _hide: function() {
-                       if (_timeoutLeave !== null) {
-                               window.clearTimeout(_timeoutLeave);
-                               _timeoutLeave = null;
-                       }
-                       
-                       _popover.classList.remove('active');
-               },
-               
-               /**
-                * Clears popover content by moving it back into the cache.
-                */
-               _clearContent: function() {
-                       if (_activeId && _popoverContent.childElementCount && !_popover.classList.contains('active')) {
-                               var activeElData = _cache.get(elData(_elements.get(_activeId).element, 'cache-id'));
-                               while (_popoverContent.childNodes.length) {
-                                       activeElData.content.appendChild(_popoverContent.childNodes[0]);
-                               }
-                       }
-               },
-               
-               /**
-                * Rebuilds the popover.
-                */
-               _rebuild: function() {
-                       if (_popover.classList.contains('active')) {
-                               return;
-                       }
-                       
-                       _popover.classList.remove('forceHide');
-                       _popover.classList.add('active');
-                       
-                       UiAlignment.set(_popover, _elements.get(_activeId).element, {
-                               pointer: true,
-                               vertical: 'top'
-                       });
-               },
-               
-               _ajaxSetup: function() {
-                       // does nothing
-                       return {};
-               },
-               
-               /**
-                * Sends an AJAX requests to the server, simple wrapper to reuse the request object.
-                * 
-                * @param       {Object}        data            request data
-                * @param       {function}      success         success callback
-                * @param       {function=}     failure         error callback
-                */
-               ajaxApi: function(data, success, failure) {
-                       if (typeof success !== 'function') {
-                               throw new TypeError("Expected a valid callback for parameter 'success'.");
-                       }
-                       
-                       Ajax.api(this, data, success, failure);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Style/Changer.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Style/Changer.js
deleted file mode 100644 (file)
index 8913972..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * Dialog based style changer.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Controller/Style/Changer
- */
-define(['Ajax', 'Language', 'Ui/Dialog'], function(Ajax, Language, UiDialog) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Controller/Style/Changer
-        */
-       return {
-               /**
-                * Adds the style changer to the bottom navigation.
-                */
-               setup: function() {
-                       var link = elBySel('.jsButtonStyleChanger');
-                       if (link) {
-                               link.addEventListener(WCF_CLICK_EVENT, this.showDialog.bind(this));
-                       }
-               },
-               
-               /**
-                * Loads and displays the style change dialog.
-                * 
-                * @param       {object}        event   event object
-                */
-               showDialog: function(event) {
-                       event.preventDefault();
-                       
-                       UiDialog.open(this);
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'styleChanger',
-                               options: {
-                                       disableContentPadding: true,
-                                       title: Language.get('wcf.style.changeStyle')
-                               },
-                               source: {
-                                       data: {
-                                               actionName: 'getStyleChooser',
-                                               className: 'wcf\\data\\style\\StyleAction'
-                                       },
-                                       after: (function(content) {
-                                               var styles = elBySelAll('.styleList > li', content);
-                                               for (var i = 0, length = styles.length; i < length; i++) {
-                                                       var style = styles[i];
-                                                       
-                                                       style.classList.add('pointer');
-                                                       style.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-                                               }
-                                       }).bind(this)
-                               }
-                       };
-               },
-               
-               /**
-                * Changes the style and reloads current page.
-                * 
-                * @param       {object}        event   event object
-                */
-               _click: function(event) {
-                       event.preventDefault();
-                       
-                       Ajax.apiOnce({
-                               data: {
-                                       actionName: 'changeStyle',
-                                       className: 'wcf\\data\\style\\StyleAction',
-                                       objectIDs: [ elData(event.currentTarget, 'style-id') ]
-                               },
-                               success: function() { window.location.reload(); }
-                       });
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/User/Notification/Settings.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/User/Notification/Settings.js
deleted file mode 100644 (file)
index cffc119..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-/**
- * Handles email notification type for user notification settings.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Controller/User/Notification/Settings
- */
-define(['Dictionary', 'Language', 'Dom/Traverse', 'Ui/SimpleDropdown'], function(Dictionary, Language, DomTraverse, UiSimpleDropdown) {
-       "use strict";
-       
-       var _data = new Dictionary();
-       
-       var _callbackClick = null;
-       var _callbackSelectType = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Controller/User/Notification/Settings
-        */
-       var ControllerUserNotificationSettings = {
-               /**
-                * Binds event listeners for all notifications supporting emails.
-                */
-               setup: function() {
-                       _callbackClick = this._click.bind(this);
-                       _callbackSelectType = this._selectType.bind(this);
-                       
-                       var group, mailSetting, groups = elBySelAll('#notificationSettings .flexibleButtonGroup');
-                       for (var i = 0, length = groups.length; i < length; i++) {
-                               group = groups[i];
-                               
-                               mailSetting = elBySel('.notificationSettingsEmail', group);
-                               if (mailSetting === null) {
-                                       continue;
-                               }
-                               
-                               this._initGroup(group, mailSetting);
-                       }
-               },
-               
-               /**
-                * Initializes a setting.
-                * 
-                * @param       {Element}       group           button group element
-                * @param       {Element}       mailSetting     mail settings element
-                */
-               _initGroup: function(group, mailSetting) {
-                       var groupId = ~~elData(group, 'object-id');
-                       
-                       var disabledNotification = elById('settings_' + groupId + '_disabled');
-                       disabledNotification.addEventListener(WCF_CLICK_EVENT, function() { mailSetting.classList.remove('active'); });
-                       var enabledNotification = elById('settings_' + groupId + '_enabled');
-                       enabledNotification.addEventListener(WCF_CLICK_EVENT, function() { mailSetting.classList.add('active'); });
-                       
-                       var mailValue = DomTraverse.childByTag(mailSetting, 'INPUT');
-                       
-                       var button = DomTraverse.childByTag(mailSetting, 'A');
-                       elData(button, 'object-id', groupId);
-                       button.addEventListener(WCF_CLICK_EVENT, _callbackClick);
-                       
-                       _data.set(groupId, {
-                               button: button,
-                               dropdownMenu: null,
-                               mailSetting: mailSetting,
-                               mailValue: mailValue
-                       });
-               },
-               
-               /**
-                * Creates and displays the email type dropdown.
-                * 
-                * @param       {Object}        event           event object
-                */
-               _click: function(event) {
-                       event.preventDefault();
-                       
-                       var button = event.currentTarget;
-                       var objectId = ~~elData(button, 'object-id');
-                       var data = _data.get(objectId);
-                       if (data.dropdownMenu === null) {
-                               data.dropdownMenu = this._createDropdown(objectId, data.mailValue.value);
-                               
-                               button.parentNode.classList.add('dropdown');
-                               button.parentNode.appendChild(data.dropdownMenu);
-                               
-                               UiSimpleDropdown.init(button, true);
-                       }
-                       else {
-                               var items = DomTraverse.childrenByTag(data.dropdownMenu, 'LI'), value = data.mailValue.value;
-                               for (var i = 0; i < 4; i++) {
-                                       items[i].classList[(elData(items[i], 'value') === value) ? 'add' : 'remove']('active');
-                               }
-                       }
-               },
-               
-               /**
-                * Creates the email type dropdown.
-                * 
-                * @param       {int}           objectId        notification event id
-                * @param       {string}        initialValue    initial email type
-                * @returns     {Element}       dropdown menu object
-                */
-               _createDropdown: function(objectId, initialValue) {
-                       var dropdownMenu = elCreate('ul');
-                       dropdownMenu.className = 'dropdownMenu';
-                       elData(dropdownMenu, 'object-id', objectId);
-                       
-                       var link, listItem, value, items = ['instant', 'daily', 'divider', 'none'];
-                       for (var i = 0; i < 4; i++) {
-                               value = items[i];
-                               
-                               listItem = elCreate('li');
-                               if (value === 'divider') {
-                                       listItem.className = 'dropdownDivider';
-                               }
-                               else {
-                                       link = elCreate('a');
-                                       link.textContent = Language.get('wcf.user.notification.mailNotificationType.' + value);
-                                       listItem.appendChild(link);
-                                       elData(listItem, 'value', value);
-                                       listItem.addEventListener(WCF_CLICK_EVENT, _callbackSelectType);
-                                       
-                                       if (initialValue === value) {
-                                               listItem.className = 'active';
-                                       }
-                               }
-                               
-                               dropdownMenu.appendChild(listItem);
-                       }
-                       
-                       return dropdownMenu;
-               },
-               
-               /**
-                * Sets the selected email notification type.
-                * 
-                * @param       {Object}        event           event object
-                */
-               _selectType: function(event) {
-                       var value = elData(event.currentTarget, 'value');
-                       var groupId = ~~elData(event.currentTarget.parentNode, 'object-id');
-                       
-                       var data = _data.get(groupId);
-                       data.mailValue.value = value;
-                       elBySel('span.title', data.mailSetting).textContent = Language.get('wcf.user.notification.mailNotificationType.' + value);
-                       
-                       data.button.classList[(value === 'none') ? 'remove' : 'add']('yellow');
-                       data.button.classList[(value === 'none') ? 'remove' : 'add']('active');
-               }
-       };
-       
-       return ControllerUserNotificationSettings;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Core.js b/wcfsetup/install/files/js/WoltLab/WCF/Core.js
deleted file mode 100644 (file)
index 7334663..0000000
+++ /dev/null
@@ -1,250 +0,0 @@
-/**
- * Provides the basic core functionality.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Core
- */
-define([], function() {
-       "use strict";
-       
-       var _clone = function(variable) {
-               if (typeof variable === 'object' && (Array.isArray(variable) || Core.isPlainObject(variable))) {
-                       return _cloneObject(variable);
-               }
-               
-               return variable;
-       };
-       
-       var _cloneObject = function(obj) {
-               if (!obj) {
-                       return null;
-               }
-               
-               if (Array.isArray(obj)) {
-                       return obj.slice();
-               }
-               
-               var newObj = {};
-               for (var key in obj) {
-                       if (objOwns(obj, key) && typeof obj[key] !== 'undefined') {
-                               newObj[key] = _clone(obj[key]);
-                       }
-               }
-               
-               return newObj;
-       };
-       
-       /**
-        * @exports     WoltLab/WCF/Core
-        */
-       var Core = {
-               /**
-                * Deep clones an object.
-                * 
-                * @param       {object}        obj     source object
-                * @return      {object}        cloned object
-                */
-               clone: function(obj) {
-                       return _clone(obj);
-               },
-               
-               /**
-                * Converts WCF 2.0-style URLs into the default URL layout.
-                * 
-                * @param       string  url     target url
-                * @return      rewritten url
-                */
-               convertLegacyUrl: function(url) {
-                       if (URL_LEGACY_MODE) {
-                               return url;
-                       }
-                       
-                       return url.replace(/^index\.php\/(.*?)\/\?/, function(match, controller) {
-                               var parts = controller.split(/([A-Z][a-z0-9]+)/);
-                               controller = '';
-                               for (var i = 0, length = parts.length; i < length; i++) {
-                                       var part = parts[i].trim();
-                                       if (part.length) {
-                                               if (controller.length) controller += '-';
-                                               controller += part.toLowerCase();
-                                       }
-                               }
-                               
-                               return 'index.php?' + controller + '/&';
-                       });
-               },
-               
-               /**
-                * Merges objects with the first argument.
-                * 
-                * @param       {object}        out             destination object
-                * @param       {...object}     arguments       variable number of objects to be merged into the destination object
-                * @return      {object}        destination object with all provided objects merged into
-                */
-               extend: function(out) {
-                       out = out || {};
-                       var newObj = this.clone(out);
-                       
-                       for (var i = 1, length = arguments.length; i < length; i++) {
-                               var obj = arguments[i];
-                               
-                               if (!obj) continue;
-                               
-                               for (var key in obj) {
-                                       if (objOwns(obj, key)) {
-                                               if (!Array.isArray(obj[key]) && typeof obj[key] === 'object') {
-                                                       if (this.isPlainObject(obj[key])) {
-                                                               // object literals have the prototype of Object which in return has no parent prototype
-                                                               newObj[key] = this.extend(out[key], obj[key]);
-                                                       }
-                                                       else {
-                                                               newObj[key] = obj[key];
-                                                       }
-                                               }
-                                               else {
-                                                       newObj[key] = obj[key];
-                                               }
-                                       }
-                               }
-                       }
-                       
-                       return newObj;
-               },
-               
-               /**
-                * Inherits the prototype methods from one constructor to another
-                * constructor.
-                * 
-                * Usage:
-                * 
-                * function MyDerivedClass() {}
-                * Core.inherit(MyDerivedClass, TheAwesomeBaseClass, {
-                *      // regular prototype for `MyDerivedClass`
-                *      
-                *      overwrittenMethodFromBaseClass: function(foo, bar) {
-                *              // do stuff
-                *              
-                *              // invoke parent
-                *              MyDerivedClass._super.prototype.overwrittenMethodFromBaseClass.call(this, foo, bar);
-                *      }
-                * });
-                * 
-                * @see https://github.com/nodejs/node/blob/7d14dd9b5e78faabb95d454a79faa513d0bbc2a5/lib/util.js#L697-L735
-                * @param       {function}      constructor             inheriting constructor function
-                * @param       {function}      superConstructor        inherited constructor function
-                * @param       {object=}       propertiesObject        additional prototype properties
-                */
-               inherit: function(constructor, superConstructor, propertiesObject) {
-                       if (constructor === undefined || constructor === null) {
-                               throw new TypeError("The constructor must not be undefined or null.");
-                       }
-                       if (superConstructor === undefined || superConstructor === null) {
-                               throw new TypeError("The super constructor must not be undefined or null.");
-                       }
-                       if (superConstructor.prototype === undefined) {
-                               throw new TypeError("The super constructor must have a prototype.");
-                       }
-                       
-                       constructor._super = superConstructor;
-                       constructor.prototype = Core.extend(Object.create(superConstructor.prototype, {
-                               constructor: {
-                                       configurable: true,
-                                       enumerable: false,
-                                       value: constructor,
-                                       writable: true
-                               }
-                       }), propertiesObject || {});
-               },
-               
-               /**
-                * Returns true if `obj` is an object literal.
-                * 
-                * @param       {*}     obj     target object
-                * @returns     {boolean}       true if target is an object literal
-                */
-               isPlainObject: function(obj) {
-                       if (typeof obj !== 'object' || obj === null || obj.nodeType) {
-                               return false;
-                       }
-                       
-                       return (Object.getPrototypeOf(obj) === Object.prototype);
-               },
-               
-               /**
-                * Returns the object's class name.
-                * 
-                * @param       {object}        obj     target object
-                * @return      {string}        object class name
-                */
-               getType: function(obj) {
-                       return Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1');
-               },
-               
-               /**
-                * Returns a RFC4122 version 4 compilant UUID.
-                * 
-                * @see         http://stackoverflow.com/a/2117523
-                * @return      {string}
-                */
-               getUuid: function() {
-                       return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
-                               var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
-                               return v.toString(16);
-                       });
-               },
-               
-               /**
-                * Recursively serializes an object into an encoded URI parameter string.
-                *  
-                * @param       {object}        obj     target object
-                * @param       {string=}       prefix  parameter prefix
-                * @return      encoded parameter string
-                */
-               serialize: function(obj, prefix) {
-                       var parameters = [];
-                       
-                       for (var key in obj) {
-                               if (objOwns(obj, key)) {
-                                       var parameterKey = (prefix) ? prefix + '[' + key + ']' : key;
-                                       var value = obj[key];
-                                       
-                                       if (typeof value === 'object') {
-                                               parameters.push(this.serialize(value, parameterKey));
-                                       }
-                                       else {
-                                               parameters.push(encodeURIComponent(parameterKey) + '=' + encodeURIComponent(value));
-                                       }
-                               }
-                       }
-                       
-                       return parameters.join('&');
-               },
-               
-               /**
-                * Triggers a custom or built-in event.
-                * 
-                * @param       {Element}       element         target element
-                * @param       {string}        eventName       event name
-                */
-               triggerEvent: function(element, eventName) {
-                       var event;
-                       
-                       try {
-                               event = new Event(eventName, {
-                                       bubbles: true,
-                                       cancelable: true
-                               });
-                       }
-                       catch (e) {
-                               event = document.createEvent('Event');
-                               event.initEvent(eventName, true, true);
-                       }
-                       
-                       element.dispatchEvent(event);
-               }
-       };
-       
-       return Core;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Date/Picker.js b/wcfsetup/install/files/js/WoltLab/WCF/Date/Picker.js
deleted file mode 100644 (file)
index 85507d4..0000000
+++ /dev/null
@@ -1,783 +0,0 @@
-/**
- * Date picker with time support.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Date/Picker
- */
-define(['DateUtil', 'Language', 'ObjectMap', 'Dom/ChangeListener', 'Ui/Alignment', 'WoltLab/WCF/Ui/CloseOverlay'], function(DateUtil, Language, ObjectMap, DomChangeListener, UiAlignment, UiCloseOverlay) {
-       "use strict";
-       
-       var _didInit = false;
-       var _firstDayOfWeek = 0;
-       
-       var _data = new ObjectMap();
-       var _input = null;
-       var _maxDate = 0;
-       var _minDate = 0;
-       
-       var _dateCells = [];
-       var _dateGrid = null;
-       var _dateHour = null;
-       var _dateMinute = null;
-       var _dateMonth = null;
-       var _dateMonthNext = null;
-       var _dateMonthPrevious = null;
-       var _dateTime = null;
-       var _dateYear = null;
-       var _datePicker = null;
-       
-       var _callbackOpen = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Date/Picker
-        */
-       var DatePicker = {
-               /**
-                * Initializes all date and datetime input fields.
-                */
-               init: function() {
-                       this._setup();
-                       
-                       var elements = elBySelAll('input[type="date"]:not(.inputDatePicker), input[type="datetime"]:not(.inputDatePicker)');
-                       var now = new Date();
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               var element = elements[i];
-                               element.classList.add('inputDatePicker');
-                               element.readOnly = true;
-                               
-                               var isDateTime = (elAttr(element, 'type') === 'datetime');
-                               var isTimeOnly = (isDateTime && elDataBool(element, 'time-only'));
-                               
-                               elData(element, 'is-date-time', isDateTime);
-                               elData(element, 'is-time-only', isTimeOnly);
-                               
-                               // convert value
-                               var date = null, value = elAttr(element, 'value');
-                               if (elAttr(element, 'value')) {
-                                       if (isTimeOnly) {
-                                               date = new Date();
-                                               var tmp = value.split(':');
-                                               date.setHours(tmp[0], tmp[1]);
-                                       }
-                                       else {
-                                               date = new Date(value);
-                                       }
-                                       
-                                       elData(element, 'value', date.getTime());
-                                       var format = (isTimeOnly) ? 'formatTime' : ('formatDate' + (isDateTime ? 'Time' : '')); 
-                                       value = DateUtil[format](date);
-                               }
-                               
-                               var isEmpty = (value.length === 0);
-                               
-                               // handle birthday input
-                               if (element.classList.contains('birthday')) {
-                                       elData(element, 'min-date', '100');
-                                       elData(element, 'max-date', 'now');
-                               }
-                               else {
-                                       if (element.min) elData(element, 'min-date', element.min);
-                                       if (element.max) elData(element, 'max-date', element.max);
-                               }
-                               
-                               this._initDateRange(element, now, true);
-                               this._initDateRange(element, now, false);
-                               
-                               if (elData(element, 'min-date') === elData(element, 'max-date')) {
-                                       throw new Error("Minimum and maximum date cannot be the same (element id '" + element.id + "').");
-                               }
-                               
-                               // change type to prevent browser's datepicker to trigger
-                               element.type = 'text';
-                               element.value = value;
-                               elData(element, 'empty', isEmpty);
-                               
-                               if (elData(element, 'placeholder')) {
-                                       elAttr(element, 'placeholder', elData(element, 'placeholder'));
-                               }
-                               
-                               // add a hidden element to hold the actual date
-                               var shadowElement = elCreate('input');
-                               shadowElement.id = element.id + 'DatePicker';
-                               shadowElement.name = element.name;
-                               shadowElement.type = 'hidden';
-                               
-                               if (date !== null) {
-                                       if (isTimeOnly) {
-                                               shadowElement.value = DateUtil.format(date, 'H:i');
-                                       }
-                                       else {
-                                               shadowElement.value = DateUtil.format(date, (isDateTime) ? 'c' : 'Y-m-d');
-                                       }
-                               }
-                               
-                               element.parentNode.insertBefore(shadowElement, element);
-                               element.removeAttribute('name');
-                               
-                               element.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
-                               
-                               // create input addon
-                               var container = elCreate('div');
-                               container.className = 'inputAddon';
-                               
-                               var button = elCreate('a');
-                               button.className = 'inputSuffix button';
-                               button.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
-                               container.appendChild(button);
-                               
-                               var icon = elCreate('span');
-                               icon.className = 'icon icon16 fa-calendar';
-                               button.appendChild(icon);
-                               
-                               element.parentNode.insertBefore(container, element);
-                               container.insertBefore(element, button);
-                               
-                               button = elCreate('a');
-                               button.className = 'inputSuffix button';
-                               button.addEventListener(WCF_CLICK_EVENT, this.clear.bind(this, element));
-                               if (isEmpty) button.style.setProperty('visibility', 'hidden', '');
-                               
-                               container.appendChild(button);
-                               
-                               icon = elCreate('span');
-                               icon.className = 'icon icon16 fa-times';
-                               button.appendChild(icon);
-                               
-                               // check if the date input has one of the following classes set otherwise default to 'short'
-                               var hasClass = false, knownClasses = ['tiny', 'short', 'medium', 'long'];
-                               for (var j = 0; j < 4; j++) {
-                                       if (element.classList.contains(knownClasses[j])) {
-                                               hasClass = true;
-                                       }
-                               }
-                               
-                               if (!hasClass) {
-                                       element.classList.add('short');
-                               }
-                               
-                               _data.set(element, {
-                                       clearButton: button,
-                                       shadow: shadowElement,
-                                       
-                                       isDateTime: isDateTime,
-                                       isEmpty: isEmpty,
-                                       isTimeOnly: isTimeOnly,
-                                       
-                                       onClose: null
-                               });
-                       }
-               },
-               
-               /**
-                * Initializes the minimum/maximum date range.
-                * 
-                * @param       {Element}       element         input element
-                * @param       {Date}          now             current date
-                * @param       {boolean}       isMinDate       true for the minimum date
-                */
-               _initDateRange: function(element, now, isMinDate) {
-                       var attribute = 'data-' + (isMinDate ? 'min' : 'max') + '-date';
-                       var value = (element.hasAttribute(attribute)) ? elAttr(element, attribute).trim() : '';
-                       
-                       if (value.match(/^(\d{4})-(\d{2})-(\d{2})$/)) {
-                               // YYYY-mm-dd
-                               value = new Date(value).getTime();
-                       }
-                       else if (value === 'now') {
-                               value = now.getTime();
-                       }
-                       else if (value.match(/^\d{1,3}$/)) {
-                               // relative time span in years
-                               var date = new Date(now.getTime());
-                               date.setFullYear(date.getFullYear() + ~~value * (isMinDate ? -1 : 1));
-                               
-                               value = date.getTime();
-                       }
-                       else if (value.match(/^datePicker-(.+)$/)) {
-                               // element id, e.g. `datePicker-someOtherElement`
-                               value = RegExp.$1;
-                               
-                               if (elById(value) === null) {
-                                       throw new Error("Reference date picker identified by '" + value + "' does not exists (element id: '" + element.id + "').");
-                               }
-                       }
-                       else if (/^\d{4}\-\d{2}\-\d{2}T/.test(value)) {
-                               value = new Date(value).getTime();
-                       }
-                       else {
-                               value = new Date((isMinDate ? 1970 : 2038), 0, 1).getTime();
-                       }
-                       
-                       elAttr(element, attribute, value);
-               },
-               
-               /**
-                * Sets up callbacks and event listeners.
-                */
-               _setup: function() {
-                       if (_didInit) return;
-                       _didInit = true;
-                       
-                       _firstDayOfWeek = ~~Language.get('wcf.date.firstDayOfTheWeek');
-                       _callbackOpen = this._open.bind(this);
-                       
-                       DomChangeListener.add('WoltLab/WCF/Date/Picker', this.init.bind(this));
-                       UiCloseOverlay.add('WoltLab/WCF/Date/Picker', this._close.bind(this));
-               },
-               
-               /**
-                * Opens the date picker.
-                * 
-                * @param       {object}        event           event object
-                */
-               _open: function(event) {
-                       event.preventDefault();
-                       event.stopPropagation();
-                       
-                       this._createPicker();
-                       
-                       var input = (event.currentTarget.nodeName === 'INPUT') ? event.currentTarget : event.currentTarget.previousElementSibling;
-                       if (input === _input) {
-                               return;
-                       }
-                       
-                       _input = input;
-                       var data = _data.get(_input), date, value = elData(_input, 'value');
-                       if (value) {
-                               date = new Date(+value);
-                               
-                               if (date.toString() === 'Invalid Date') {
-                                       date = new Date();
-                               }
-                       }
-                       else {
-                               date = new Date();
-                       }
-                       
-                       // set min/max date
-                       _minDate = elData(_input, 'min-date');
-                       if (_minDate.match(/^datePicker-(.+)$/)) _minDate = elData(elById(RegExp.$1), 'value');
-                       _minDate = new Date(+_minDate);
-                       
-                       _maxDate = elData(_input, 'max-date');
-                       if (_maxDate.match(/^datePicker-(.+)$/)) _maxDate = elData(elById(RegExp.$1), 'value');
-                       _maxDate = new Date(+_maxDate);
-                       
-                       if (data.isDateTime) {
-                               _dateHour.value = date.getHours();
-                               _dateMinute.value = date.getMinutes();
-                               
-                               _datePicker.classList.add('datePickerTime');
-                       }
-                       else {
-                               _datePicker.classList.remove('datePickerTime');
-                       }
-                       
-                       _datePicker.classList[(data.isTimeOnly) ? 'add' : 'remove']('datePickerTimeOnly');
-                       
-                       this._renderPicker(date.getDate(), date.getMonth(), date.getFullYear());
-                       
-                       UiAlignment.set(_datePicker, _input);
-               },
-               
-               /**
-                * Closes the date picker.
-                */
-               _close: function() {
-                       if (_datePicker !== null && _datePicker.classList.contains('active')) {
-                               _datePicker.classList.remove('active');
-                               
-                               var data = _data.get(_input);
-                               if (typeof data.onClose === 'function') {
-                                       data.onClose();
-                               }
-                               
-                               _input = null;
-                               _minDate = 0;
-                               _maxDate = 0;
-                       }
-               },
-               
-               /**
-                * Renders the full picker on init.
-                * 
-                * @param       {int}           day
-                * @param       {int}           month
-                * @param       {int}           year
-                */
-               _renderPicker: function(day, month, year) {
-                       this._renderGrid(day, month, year);
-                       
-                       // create options for month and year
-                       var years = '';
-                       for (var i = _minDate.getFullYear(), last = _maxDate.getFullYear(); i <= last; i++) {
-                               years += '<option value="' + i + '">' + i + '</option>';
-                       }
-                       _dateYear.innerHTML = years;
-                       _dateYear.value = year;
-                       
-                       _dateMonth.value = month;
-                       
-                       _datePicker.classList.add('active');
-               },
-               
-               /**
-                * Updates the date grid.
-                * 
-                * @param       {int}           day
-                * @param       {int}           month
-                * @param       {int}           year
-                */
-               _renderGrid: function(day, month, year) {
-                       var cell, hasDay = (day !== undefined), hasMonth = (month !== undefined), i;
-                       
-                       day = ~~day || ~~elData(_dateGrid, 'day');
-                       month = ~~month;
-                       year = ~~year;
-                       
-                       // rebuild cells
-                       if (hasMonth || year) {
-                               var rebuildMonths = (year !== 0);
-                               
-                               // rebuild grid
-                               var fragment = document.createDocumentFragment();
-                               fragment.appendChild(_dateGrid);
-                               
-                               if (!hasMonth) month = ~~elData(_dateGrid, 'month');
-                               year = year || ~~elData(_dateGrid, 'year');
-                               
-                               // check if current selection exceeds min/max date
-                               var date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-' + ('0' + day.toString()).slice(-2));
-                               if (date < _minDate) {
-                                       year = _minDate.getFullYear();
-                                       month = _minDate.getMonth();
-                                       day = _minDate.getDate();
-                                       
-                                       _dateMonth.value = month;
-                                       _dateYear.value = year;
-                                       
-                                       rebuildMonths = true;
-                               }
-                               else if (date > _maxDate) {
-                                       year = _maxDate.getFullYear();
-                                       month = _maxDate.getMonth();
-                                       day = _maxDate.getDate();
-                                       
-                                       _dateMonth.value = month;
-                                       _dateYear.value = year;
-                                       
-                                       rebuildMonths = true;
-                               }
-                               
-                               date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
-                               
-                               // shift until first displayed day equals first day of week
-                               while (date.getDay() !== _firstDayOfWeek) {
-                                       date.setDate(date.getDate() - 1);
-                               }
-                               
-                               var selectable;
-                               for (i = 0; i < 35; i++) {
-                                       cell = _dateCells[i];
-                                       
-                                       cell.textContent = date.getDate();
-                                       selectable = (date.getMonth() === month);
-                                       if (selectable) {
-                                               if (date < _minDate) selectable = false;
-                                               else if (date > _maxDate) selectable = false;
-                                       }
-                                       
-                                       cell.classList[selectable ? 'remove' : 'add']('otherMonth');
-                                       date.setDate(date.getDate() + 1); 
-                               }
-                               
-                               elData(_dateGrid, 'month', month);
-                               elData(_dateGrid, 'year', year);
-                               
-                               _datePicker.insertBefore(fragment, _dateTime);
-                               
-                               if (!hasDay) {
-                                       // check if date is valid
-                                       date = new Date(year, month, day);
-                                       if (date.getDate() !== day) {
-                                               while (date.getMonth() !== month) {
-                                                       date.setDate(date.getDate() - 1);
-                                               }
-                                               
-                                               day = date.getDate();
-                                       }
-                               }
-                               
-                               if (rebuildMonths) {
-                                       for (i = 0; i < 12; i++) {
-                                               var currentMonth = _dateMonth.children[i];
-                                               
-                                               currentMonth.disabled = (year === _minDate.getFullYear() && currentMonth.value < _minDate.getMonth()) || (year === _maxDate.getFullYear() && currentMonth.value > _maxDate.getMonth());
-                                       }
-                                       
-                                       var nextMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
-                                       nextMonth.setMonth(nextMonth.getMonth() + 1);
-                                       
-                                       _dateMonthNext.classList[(nextMonth < _maxDate) ? 'add' : 'remove']('active');
-                                       
-                                       var previousMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
-                                       previousMonth.setDate(previousMonth.getDate() - 1);
-                                       
-                                       _dateMonthPrevious.classList[(previousMonth > _minDate) ? 'add' : 'remove']('active');
-                               }
-                       }
-                       
-                       // update active day
-                       if (day) {
-                               for (i = 0; i < 35; i++) {
-                                       cell = _dateCells[i];
-                                       
-                                       cell.classList[(!cell.classList.contains('otherMonth') && ~~cell.textContent === day) ? 'add' : 'remove']('active');
-                               }
-                               
-                               elData(_dateGrid, 'day', day);
-                       }
-                       
-                       this._formatValue();
-               },
-               
-               /**
-                * Sets the visible and shadow value
-                */
-               _formatValue: function() {
-                       var data = _data.get(_input), date, value, shadowValue;
-                       
-                       if (elData(_input, 'empty') === 'true') {
-                               return;
-                       }
-                       
-                       if (data.isDateTime) {
-                               date = new Date(
-                                       elData(_dateGrid, 'year'),
-                                       elData(_dateGrid, 'month'),
-                                       elData(_dateGrid, 'day'),
-                                       _dateHour.value,
-                                       _dateMinute.value
-                               );
-                               
-                               if (data.isTimeOnly) {
-                                       value = DateUtil.formatTime(date);
-                                       shadowValue = DateUtil.format(date, 'H:i');
-                               }
-                               else {
-                                       value = DateUtil.formatDateTime(date);
-                                       shadowValue = DateUtil.format(date, 'c');
-                               }
-                       }
-                       else {
-                               date = new Date(
-                                       elData(_dateGrid, 'year'),
-                                       elData(_dateGrid, 'month'),
-                                       elData(_dateGrid, 'day')
-                               );
-                               
-                               value = DateUtil.formatDate(date);
-                               shadowValue = DateUtil.format(date, 'Y-m-d');
-                       }
-                       
-                       _input.value = value;
-                       elData(_input, 'value', date.getTime());
-                       data.clearButton.style.removeProperty('visibility');
-                       data.shadow.value = shadowValue;
-               },
-               
-               /**
-                * Creates the date picker DOM.
-                */
-               _createPicker: function() {
-                       if (_datePicker !== null) {
-                               return;
-                       }
-                       
-                       _datePicker = elCreate('div');
-                       _datePicker.className = 'datePicker';
-                       _datePicker.addEventListener(WCF_CLICK_EVENT, function(event) { event.stopPropagation(); });
-                       
-                       var header = elCreate('header');
-                       _datePicker.appendChild(header);
-                       
-                       _dateMonthPrevious = elCreate('a');
-                       _dateMonthPrevious.className = 'previous';
-                       _dateMonthPrevious.innerHTML = '<span class="icon icon16 fa-arrow-left"></span>';
-                       _dateMonthPrevious.addEventListener(WCF_CLICK_EVENT, this.previousMonth.bind(this));
-                       header.appendChild(_dateMonthPrevious);
-                       
-                       var monthYearContainer = elCreate('span');
-                       header.appendChild(monthYearContainer);
-                       
-                       _dateMonth = elCreate('select');
-                       _dateMonth.className = 'month';
-                       _dateMonth.addEventListener('change', this._changeMonth.bind(this));
-                       
-                       var selectWrapper = elCreate('label');
-                       selectWrapper.className = 'selectDropdown';
-                       selectWrapper.appendChild(_dateMonth);
-                       monthYearContainer.appendChild(selectWrapper);
-                       
-                       var i, months = '', monthNames = Language.get('__monthsShort');
-                       for (i = 0; i < 12; i++) {
-                               months += '<option value="' + i + '">' + monthNames[i] + '</option>';
-                       }
-                       _dateMonth.innerHTML = months;
-                       
-                       _dateYear = elCreate('select');
-                       _dateYear.className = 'year';
-                       _dateYear.addEventListener('change', this._changeYear.bind(this));
-                       
-                       selectWrapper = elCreate('label');
-                       selectWrapper.className = 'selectDropdown';
-                       selectWrapper.appendChild(_dateYear);
-                       monthYearContainer.appendChild(selectWrapper);
-                       
-                       _dateMonthNext = elCreate('a');
-                       _dateMonthNext.className = 'next';
-                       _dateMonthNext.innerHTML = '<span class="icon icon16 fa-arrow-right"></span>';
-                       _dateMonthNext.addEventListener(WCF_CLICK_EVENT, this.nextMonth.bind(this));
-                       header.appendChild(_dateMonthNext);
-                       
-                       _dateGrid = elCreate('ul');
-                       _datePicker.appendChild(_dateGrid);
-                       
-                       var item = elCreate('li');
-                       item.className = 'weekdays';
-                       _dateGrid.appendChild(item);
-                       
-                       var span, weekdays = Language.get('__daysShort');
-                       for (i = 0; i < 7; i++) {
-                               var day = i + _firstDayOfWeek;
-                               if (day > 6) day -= 7;
-                               
-                               span = elCreate('span');
-                               span.textContent = weekdays[day];
-                               item.appendChild(span);
-                       }
-                       
-                       // create date grid
-                       var callbackClick = this._click.bind(this), cell, row;
-                       for (i = 0; i < 5; i++) {
-                               row = elCreate('li');
-                               _dateGrid.appendChild(row);
-                               
-                               for (var j = 0; j < 7; j++) {
-                                       cell = elCreate('a');
-                                       cell.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                                       _dateCells.push(cell);
-                                       
-                                       row.appendChild(cell);
-                               }
-                       }
-                       
-                       _dateTime = elCreate('footer');
-                       _datePicker.appendChild(_dateTime);
-                       
-                       _dateHour = elCreate('select');
-                       _dateHour.className = 'hour';
-                       _dateHour.addEventListener('change', this._formatValue.bind(this));
-                       
-                       var tmp = '';
-                       var date = new Date(2000, 0, 1);
-                       var timeFormat = Language.get('wcf.date.timeFormat').replace(/:/, '').replace(/[isu]/g, '');
-                       for (i = 0; i < 24; i++) {
-                               date.setHours(i);
-                               tmp += '<option value="' + i + '">' + DateUtil.format(date, timeFormat) + "</option>";
-                       }
-                       _dateHour.innerHTML = tmp;
-                       
-                       _dateTime.appendChild(_dateHour);
-                       
-                       _dateTime.appendChild(document.createTextNode('\u00A0:\u00A0'));
-                       
-                       _dateMinute = elCreate('select');
-                       _dateMinute.className = 'minute';
-                       _dateMinute.addEventListener('change', this._formatValue.bind(this));
-                       
-                       tmp = '';
-                       for (i = 0; i < 60; i++) {
-                               tmp += '<option value="' + i + '">' + (i < 10 ? '0' + i.toString() : i) + '</option>';
-                       }
-                       _dateMinute.innerHTML = tmp;
-                       
-                       _dateTime.appendChild(_dateMinute);
-                       
-                       document.body.appendChild(_datePicker);
-               },
-               
-               /**
-                * Shows the previous month.
-                */
-               previousMonth: function() {
-                       if (_dateMonth.value === '0') {
-                               _dateMonth.value = 11;
-                               _dateYear.value = ~~_dateYear.value - 1;
-                       }
-                       else {
-                               _dateMonth.value = ~~_dateMonth.value - 1;
-                       }
-                       
-                       this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
-               },
-               
-               /**
-                * Shows the next month.
-                */
-               nextMonth: function() {
-                       if (_dateMonth.value === '11') {
-                               _dateMonth.value = 0;
-                               _dateYear.value = ~~_dateYear.value + 1;
-                       }
-                       else {
-                               _dateMonth.value = ~~_dateMonth.value + 1;
-                       }
-                       
-                       this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
-               },
-               
-               /**
-                * Handles changes to the month select element.
-                * 
-                * @param       {object}        event           event object
-                */
-               _changeMonth: function(event) {
-                       this._renderGrid(undefined, event.currentTarget.value);
-               },
-               
-               /**
-                * Handles changes to the year select element.
-                * 
-                * @param       {object}        event           event object
-                */
-               _changeYear: function(event) {
-                       this._renderGrid(undefined, undefined, event.currentTarget.value);
-               },
-               
-               /**
-                * Handles clicks on an individual day.
-                * 
-                * @param       {object}        event           event object
-                */
-               _click: function(event) {
-                       if (event.currentTarget.classList.contains('otherMonth')) {
-                               return;
-                       }
-                       
-                       elData(_input, 'empty', false);
-                       
-                       this._renderGrid(event.currentTarget.textContent);
-                       
-                       this._close();
-               },
-               
-               /**
-                * Returns the current Date object or null.
-                * 
-                * @param       {(Element|string)}      element         input element or id
-                * @return      {?Date}                 Date object or null
-                */
-               getDate: function(element) {
-                       element = this._getElement(element);
-                       
-                       if (element.hasAttribute('data-value')) {
-                               return new Date(+elData(element, 'value'));
-                       }
-                       
-                       return null;
-               },
-               
-               /**
-                * Sets the date of given element.
-                * 
-                * @param       {(HTMLInputElement|string)}     element         input element or id
-                * @param       {Date}                          date            Date object
-                */
-               setDate: function(element, date) {
-                       element = this._getElement(element);
-                       var data = _data.get(element);
-                       
-                       elData(element, 'value', date.getTime());
-                       element.value = DateUtil['formatDate' + (data.isDateTime ? 'Time' : '')](date);
-                       
-                       data.shadow.value = DateUtil.format(date, (data.isDateTime ? 'c' : 'Y-m-d'));
-               },
-               
-               /**
-                * Clears the date value of given element.
-                * 
-                * @param       {(HTMLInputElement|string)}     element         input element or id
-                */
-               clear: function(element) {
-                       element = this._getElement(element);
-                       var data = _data.get(element);
-                       
-                       element.removeAttribute('data-value');
-                       element.value = '';
-                       
-                       data.clearButton.style.setProperty('visibility', 'hidden', '');
-                       data.isEmpty = true;
-                       data.shadow.value = '';
-               },
-               
-               /**
-                * Reverts the date picker into a normal input field.
-                * 
-                * @param       {(HTMLInputElement|string)}     element         input element or id
-                */
-               destroy: function(element) {
-                       element = this._getElement(element);
-                       var data = _data.get(element);
-                       
-                       var container = element.parentNode;
-                       container.parentNode.insertBefore(element, container);
-                       elRemove(container);
-                       
-                       elAttr(element, 'type', 'date' + (data.isDateTime ? 'time' : ''));
-                       element.value = data.shadow.value;
-                       
-                       element.removeAttribute('data-value');
-                       element.removeEventListener(WCF_CLICK_EVENT, _callbackOpen);
-                       elRemove(data.shadow);
-                       
-                       element.classList.remove('inputDatePicker');
-                       element.readOnly = false;
-                       _data['delete'](element);
-               },
-               
-               /**
-                * Sets the callback invoked on picker close.
-                * 
-                * @param       {(Element|string)}      element         input element or id
-                * @param       {function}              callback        callback function
-                */
-               setCloseCallback: function(element, callback) {
-                       element = this._getElement(element);
-                       _data.get(element).onClose = callback;
-               },
-               
-               /**
-                * Validates given element or id if it represents an active date picker.
-                * 
-                * @param       {(Element|string)}      element         input element or id
-                * @return      {Element}               input element
-                */
-               _getElement: function(element) {
-                       if (typeof element === 'string') element = elById(element);
-                       
-                       if (!(element instanceof Element) || !element.classList.contains('inputDatePicker') || !_data.has(element)) {
-                               throw new Error("Expected a valid date picker input element or id.");
-                       }
-                       
-                       return element;
-               }
-       };
-       
-       // backward-compatibility for `$.ui.datepicker` shim
-       window.__wcf_bc_datePicker = DatePicker;
-       
-       return DatePicker;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Date/Time/Relative.js b/wcfsetup/install/files/js/WoltLab/WCF/Date/Time/Relative.js
deleted file mode 100644 (file)
index 08cf3a7..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Transforms <time> elements to display the elapsed time relative to the current time.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Date/Time/Relative
- */
-define(['Dom/ChangeListener', 'Language', 'WoltLab/WCF/Date/Util', 'WoltLab/WCF/Timer/Repeating'], function(DomChangeListener, Language, DateUtil, Repeating) {
-       "use strict";
-       
-       var _elements = elByTag('time');
-       var _offset = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Date/Time/Relative
-        */
-       return {
-               /**
-                * Transforms <time> elements on init and binds event listeners.
-                */
-               setup: function() {
-                       this._refresh();
-                       
-                       new Repeating(this._refresh.bind(this), 60000);
-                       
-                       DomChangeListener.add('WoltLab/WCF/Date/Time/Relative', this._refresh.bind(this));
-               },
-               
-               _refresh: function() {
-                       var date = new Date();
-                       var timestamp = (date.getTime() - date.getMilliseconds()) / 1000;
-                       if (_offset === null) _offset = timestamp - TIME_NOW;
-                       
-                       for (var i = 0, length = _elements.length; i < length; i++) {
-                               var element = _elements[i];
-                               
-                               if (!element.classList.contains('datetime') || elData(element, 'is-future-date')) continue;
-                               
-                               var elTimestamp = ~~elData(element, 'timestamp') + _offset;
-                               var elDate = elData(element, 'date');
-                               var elTime = elData(element, 'time');
-                               var elOffset = elData(element, 'offset');
-                               
-                               if (!elAttr(element, 'title')) {
-                                       elAttr(element, 'title', Language.get('wcf.date.dateTimeFormat').replace(/%date%/, elDate).replace(/%time%/, elTime));
-                               }
-                               
-                               // timestamp is less than 60 seconds ago
-                               if (elTimestamp >= timestamp || timestamp < (elTimestamp + 60)) {
-                                       element.textContent = Language.get('wcf.date.relative.now');
-                               }
-                               // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago)
-                               else if (timestamp < (elTimestamp + 3540)) {
-                                       var minutes = Math.max(Math.round((timestamp - elTimestamp) / 60), 1);
-                                       element.textContent = Language.get('wcf.date.relative.minutes', { minutes: minutes });
-                               }
-                               // timestamp is less than 24 hours ago
-                               else if (timestamp < (elTimestamp + 86400)) {
-                                       var hours = Math.round((timestamp - elTimestamp) / 3600);
-                                       element.textContent = Language.get('wcf.date.relative.hours', { hours: hours });
-                               }
-                               // timestamp is less than 6 days ago
-                               else if (timestamp < (elTimestamp + 518400)) {
-                                       var midnight = new Date(date.getFullYear(), date.getMonth(), date.getDate());
-                                       var days = Math.ceil((midnight / 1000 - elTimestamp) / 86400);
-                                       
-                                       // get day of week
-                                       var dateObj = DateUtil.getTimezoneDate((elTimestamp * 1000), elOffset * 1000);
-                                       var dow = dateObj.getDay();
-                                       var day = Language.get('__days')[dow];
-                                       
-                                       element.textContent = Language.get('wcf.date.relative.pastDays', { days: days, day: day, time: elTime });
-                               }
-                               // timestamp is between ~700 million years BC and last week
-                               else {
-                                       element.textContent = Language.get('wcf.date.shortDateTimeFormat').replace(/%date%/, elDate).replace(/%time%/, elTime);
-                               }
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Date/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/Date/Util.js
deleted file mode 100644 (file)
index 204562e..0000000
+++ /dev/null
@@ -1,231 +0,0 @@
-/**
- * Provides utility functions for date operations.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Date/Util
- */
-define(['Language'], function(Language) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Date/Util
-        */
-       var DateUtil = {
-               /**
-                * Returns the formatted date.
-                * 
-                * @param       {Date}          date            date object
-                * @returns     {string}        formatted date
-                */
-               formatDate: function(date) {
-                       return this.format(date, Language.get('wcf.date.dateFormat'));
-               },
-               
-               /**
-                * Returns the formatted time.
-                * 
-                * @param       {Date}          date            date object
-                * @returns     {string}        formatted time
-                */
-               formatTime: function(date) {
-                       return this.format(date, Language.get('wcf.date.timeFormat'));
-               },
-               
-               /**
-                * Returns the formatted date time.
-                * 
-                * @param       {Date}          date            date object
-                * @returns     {string}        formatted date time
-                */
-               formatDateTime: function(date) {
-                       return this.format(date, Language.get('wcf.date.dateTimeFormat').replace(/%date%/, Language.get('wcf.date.dateFormat')).replace(/%time%/, Language.get('wcf.date.timeFormat')));
-               },
-               
-               /**
-                * Formats a date using PHP's `date()` modifiers.
-                * 
-                * @param       {Date}          date            date object
-                * @param       {string}        format          output format
-                * @returns     {string}        formatted date
-                */
-               format: function(date, format) {
-                       var char;
-                       var out = '';
-                       
-                       // ISO 8601 date, best recognition by PHP's strtotime()
-                       if (format === 'c') {
-                               format = 'Y-m-dTH:i:sP';
-                       }
-                       
-                       for (var i = 0, length = format.length; i < length; i++) {
-                               switch (format[i]) {
-                                       // seconds
-                                       case 's':
-                                               // `00` through `59`
-                                               char = ('0' + date.getSeconds().toString()).slice(-2);
-                                               break;
-                                       
-                                       // minutes
-                                       case 'i':
-                                               // `00` through `59`
-                                               char = date.getMinutes();
-                                               if (char < 10) char = "0" + char;
-                                               break;
-                                       
-                                       // hours
-                                       case 'a':
-                                               // `am` or `pm`
-                                               char = (date.getHours() > 11) ? 'pm' : 'am';
-                                               break;
-                                       case 'g':
-                                               // `1` through `12`
-                                               char = date.getHours();
-                                               if (char === 0) char = 12;
-                                               else if (char > 12) char -= 12;
-                                               break;
-                                       case 'h':
-                                               // `01` through `12`
-                                               char = date.getHours();
-                                               if (char === 0) char = 12;
-                                               else if (char > 12) char -= 12;
-                                               
-                                               char = ('0' + char.toString()).slice(-2);
-                                               break;
-                                       case 'A':
-                                               // `AM` or `PM`
-                                               char = (date.getHours() > 11) ? 'PM' : 'AM';
-                                               break;
-                                       case 'G':
-                                               // `0` through `23`
-                                               char = date.getHours();
-                                               break;
-                                       case 'H':
-                                               // `00` through `23`
-                                               char = date.getHours();
-                                               char = ('0' + char.toString()).slice(-2);
-                                               break;
-                                       
-                                       // day
-                                       case 'd':
-                                               // `01` through `31`
-                                               char = date.getDate();
-                                               char = ('0' + char.toString()).slice(-2);
-                                               break;
-                                       case 'j':
-                                               // `1` through `31`
-                                               char = date.getDate();
-                                               break;
-                                       case 'l':
-                                               // `Monday` through `Sunday` (localized)
-                                               char = Language.get('__days')[date.getDay()];
-                                               break;
-                                       case 'D':
-                                               // `Mon` through `Sun` (localized)
-                                               char = Language.get('__daysShort')[date.getDay()];
-                                               break;
-                                       case 'S':
-                                               // ignore english ordinal suffix
-                                               char = '';
-                                               break;
-                                       
-                                       // month
-                                       case 'm':
-                                               // `01` through `12`
-                                               char = date.getMonth() + 1;
-                                               char = ('0' + char.toString()).slice(-2);
-                                               break;
-                                       case 'n':
-                                               // `1` through `12`
-                                               char = date.getMonth() + 1;
-                                               break;
-                                       case 'F':
-                                               // `January` through `December` (localized)
-                                               char = Language.get('__months')[date.getMonth()];
-                                               break;
-                                       case 'M':
-                                               // `Jan` through `Dec` (localized)
-                                               char = Language.get('__monthsShort')[date.getMonth()];
-                                               break;
-                                       
-                                       // year
-                                       case 'y':
-                                               // `00` through `99`
-                                               char = date.getYear().toString().replace(/^\d{2}/, '');
-                                               break;
-                                       case 'Y':
-                                               // Examples: `1988` or `2015`
-                                               char = date.getFullYear();
-                                               break;
-                                       
-                                       // timezone
-                                       case 'P':
-                                               var offset = date.getTimezoneOffset();
-                                               char = (offset > 0) ? '-' : '+';
-                                               
-                                               offset = Math.abs(offset);
-                                               
-                                               char += ('0' + (~~(offset / 60)).toString()).slice(-2);
-                                               char += ':';
-                                               char += ('0' + (offset % 60).toString()).slice(-2);
-                                               
-                                               break;
-                                               
-                                       // specials
-                                       case 'r':
-                                               char = date.toString();
-                                               break;
-                                       case 'U':
-                                               char = Math.round(date.getTime() / 1000);
-                                               break;
-                                       
-                                       default:
-                                               char = format[i];
-                                               break;
-                               }
-                               
-                               out += char;
-                       }
-                       
-                       return out;
-               },
-               
-               /**
-                * Returns UTC timestamp, if date is not given, current time will be used.
-                * 
-                * @param       {Date}          date    target date
-                * @return      {int}           UTC timestamp in seconds
-                */
-               gmdate: function(date) {
-                       if (!(date instanceof Date)) {
-                               date = new Date();
-                       }
-                       
-                       return Math.round(Date.UTC(
-                               date.getUTCFullYear(),
-                               date.getUTCMonth(),
-                               date.getUTCDay(),
-                               date.getUTCHours(),
-                               date.getUTCMinutes(),
-                               date.getUTCSeconds()
-                       ) / 1000);
-               },
-               
-               /**
-                * Returns a Date object with precise offset (including timezone and local timezone).
-                * 
-                * @param       {int}           timestamp       timestamp in milliseconds
-                * @param       {int}           offset          timezone offset in milliseconds
-                * @return      {Date}          localized date
-                */
-               getTimezoneDate: function(timestamp, offset) {
-                       var date = new Date(timestamp);
-                       var localOffset = date.getTimezoneOffset() * 60000;
-                       
-                       return new Date((timestamp + localOffset + offset));
-               }
-       };
-       
-       return DateUtil;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js b/wcfsetup/install/files/js/WoltLab/WCF/Dictionary.js
deleted file mode 100644 (file)
index 81d9dd6..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * Dictionary implementation relying on an object or if supported on a Map to hold key => value data.
- * 
- * If you're looking for a dictionary with object keys, please see `WoltLab/WCF/ObjectMap`.
- * 
- * @author     Tim Duesterhus, Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Dictionary
- */
-define(['Core'], function(Core) {
-       "use strict";
-       
-       var _hasMap = objOwns(window, 'Map') && typeof window.Map === 'function';
-       
-       /**
-        * @constructor
-        */
-       function Dictionary() {
-               this._dictionary = (_hasMap) ? new Map() : {};
-       }
-       Dictionary.prototype = {
-               /**
-                * Sets a new key with given value, will overwrite an existing key.
-                * 
-                * @param       {(number|string)}       key     key
-                * @param       {?}                     value   value
-                */
-               set: function(key, value) {
-                       if (typeof key === 'number') key = key.toString();
-                       
-                       if (typeof key !== "string") {
-                               throw new TypeError("Only strings can be used as keys, rejected '" + key + "' (" + typeof key + ").");
-                       }
-                       
-                       if (_hasMap) this._dictionary.set(key, value);
-                       else this._dictionary[key] = value;
-               },
-               
-               /**
-                * Removes a key from the dictionary.
-                * 
-                * @param       {(number|string)}       key     key
-                */
-               'delete': function(key) {
-                       if (typeof key === 'number') key = key.toString();
-                       
-                       if (_hasMap) this._dictionary['delete'](key);
-                       else this._dictionary[key] = undefined;
-               },
-               
-               /**
-                * Returns true if dictionary contains a value for given key and is not undefined.
-                * 
-                * @param       {(number|string)}       key     key
-                * @return      {boolean}       true if key exists and value is not undefined
-                */
-               has: function(key) {
-                       if (typeof key === 'number') key = key.toString();
-                       
-                       if (_hasMap) return this._dictionary.has(key);
-                       else {
-                               return (objOwns(this._dictionary, key) && typeof this._dictionary[key] !== "undefined");
-                       }
-               },
-               
-               /**
-                * Retrieves a value by key, returns undefined if there is no match.
-                * 
-                * @param       {(number|string)}       key     key
-                * @return      {*}
-                */
-               get: function(key) {
-                       if (typeof key === 'number') key = key.toString();
-                       
-                       if (this.has(key)) {
-                               if (_hasMap) return this._dictionary.get(key);
-                               else return this._dictionary[key];
-                       }
-                       
-                       return undefined;
-               },
-               
-               /**
-                * Iterates over the dictionary keys and values, callback function should expect the
-                * value as first parameter and the key name second.
-                * 
-                * @param       {function<*, string>}   callback        callback for each iteration
-                */
-               forEach: function(callback) {
-                       if (typeof callback !== "function") {
-                               throw new TypeError("forEach() expects a callback as first parameter.");
-                       }
-                       
-                       if (_hasMap) {
-                               this._dictionary.forEach(callback);
-                       }
-                       else {
-                               var keys = Object.keys(this._dictionary);
-                               for (var i = 0, length = keys.length; i < length; i++) {
-                                       callback(this._dictionary[keys[i]], keys[i]);
-                               }
-                       }
-               },
-               
-               /**
-                * Merges one or more Dictionary instances into this one.
-                * 
-                * @param       {...Dictionary}         var_args        one or more Dictionary instances
-                */
-               merge: function() {
-                       for (var i = 0, length = arguments.length; i < length; i++) {
-                               var dictionary = arguments[i];
-                               if (!(dictionary instanceof Dictionary)) {
-                                       throw new TypeError("Expected an object of type Dictionary, but argument " + i + " is not.");
-                               }
-                               
-                               dictionary.forEach((function(value, key) {
-                                       this.set(key, value);
-                               }).bind(this));
-                       }
-               },
-               
-               /**
-                * Returns the object representation of the dictionary.
-                * 
-                * @return      {object}        dictionary's object representation
-                */
-               toObject: function() {
-                       if (!_hasMap) return Core.clone(this._dictionary);
-                       
-                       var object = { };
-                       this._dictionary.forEach(function(value, key) {
-                               object[key] = value;
-                       });
-                       
-                       return object;
-               }
-       };
-       
-       /**
-        * Creates a new Dictionary based on the given object.
-        * All properties that are owned by the object will be added
-        * as keys to the resulting Dictionary.
-        * 
-        * @param       {object}        object
-        * @return      {Dictionary}
-        */
-       Dictionary.fromObject = function(object) {
-               var result = new Dictionary();
-               
-               for (var key in object) {
-                       if (objOwns(object, key)) {
-                               result.set(key, object[key]);
-                       }
-               }
-               
-               return result;
-       };
-       
-       Object.defineProperty(Dictionary.prototype, 'size', {
-               enumerable: false,
-               configurable: true,
-               get: function() {
-                       if (_hasMap) {
-                               return this._dictionary.size;
-                       }
-                       else {
-                               return Object.keys(this._dictionary).length;
-                       }
-               }
-       });
-       
-       return Dictionary;
-});
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 0bd02c8..0000000
+++ /dev/null
@@ -1,50 +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
-        */
-       return {
-               /**
-                * @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;
-                       }
-               }
-       };
-});
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 428ea3e..0000000
+++ /dev/null
@@ -1,268 +0,0 @@
-/**
- * Provides helper functions to traverse the DOM.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Dom/Traverse
- */
-define([], function() {
-       "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 el.matches(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
-        */
-       return {
-               /**
-                * 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}        tagName         element tag 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', TAG_NAME, tagName);
-               },
-               
-               /**
-                * 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}        tagName         element tag 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', TAG_NAME, tagName);
-               }
-       };
-});
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 ef64f2b..0000000
+++ /dev/null
@@ -1,431 +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(['Environment', 'StringUtil'], function(Environment, StringUtil) {
-       "use strict";
-       
-       function _isBoundaryNode(element, ancestor, position) {
-               if (!ancestor.contains(element)) {
-                       throw new Error("Ancestor element does not contain target element.");
-               }
-               
-               var node, whichSibling = position + 'Sibling';
-               while (element !== null && element !== ancestor) {
-                       if (element[position + 'ElementSibling'] !== null) {
-                               return false;
-                       }
-                       else if (element[whichSibling]) {
-                               node = element[whichSibling];
-                               while (node) {
-                                       if (node.textContent.trim() !== '') {
-                                               return false;
-                                       }
-                                       
-                                       node = node[whichSibling];
-                               }
-                       }
-                       
-                       element = element.parentNode;
-               }
-               
-               return true;
-       }
-       
-       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 = elCreate('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 (elById(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 instanceof Element)) {
-                               throw new TypeError("Expected a valid DOM element as argument.");
-                       }
-                       
-                       var id = elAttr(el, 'id');
-                       if (!id) {
-                               id = this.getUniqueId();
-                               elAttr(el, 'id', id);
-                       }
-                       
-                       return id;
-               },
-               
-               /**
-                * Returns the outer height of an element including margins.
-                * 
-                * @param       {Element}               el              element
-                * @param       {CSSStyleDeclaration=}  styles          result of window.getComputedStyle()
-                * @return      {int}                   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      {int}   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: int, width: int}}     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: int, top: int}}         offset relative to top left corner
-                */
-               offset: function(el) {
-                       var rect = el.getBoundingClientRect();
-                       
-                       return {
-                               top: Math.round(rect.top + (window.scrollY || window.pageYOffset)),
-                               left: Math.round(rect.left + (window.scrollX || window.pageXOffset))
-                       };
-               },
-               
-               /**
-                * 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.childNodes.length === 0) {
-                               parentEl.appendChild(el);
-                       }
-                       else {
-                               parentEl.insertBefore(el, parentEl.childNodes[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, *>}     styles  list of CSS styles
-                */
-               setStyles: function(el, styles) {
-                       var important = false;
-                       for (var property in styles) {
-                               if (styles.hasOwnProperty(property)) {
-                                       if (/ !important$/.test(styles[property])) {
-                                               important = true;
-                                               
-                                               styles[property] = styles[property].replace(/ !important$/, '');
-                                       }
-                                       else {
-                                               important = false;
-                                       }
-                                       
-                                       // for a set style property with priority = important, some browsers are
-                                       // not able to overwrite it with a property != important; removing the
-                                       // property first solves this issue
-                                       if (el.style.getPropertyPriority(property) === 'important' && !important) {
-                                               el.style.removeProperty(property);
-                                       }
-                                       
-                                       el.style.setProperty(property, styles[property], (important ? 'important' : ''));
-                               }
-                       }
-               },
-               
-               /**
-                * 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      {int}                   property value as integer
-                */
-               styleAsInt: function(styles, propertyName) {
-                       var value = styles.getPropertyValue(propertyName);
-                       if (value === null) {
-                               return 0;
-                       }
-                       
-                       return parseInt(value);
-               },
-               
-               /**
-                * Sets the inner HTML of given element and reinjects <script> elements to be properly executed.
-                * 
-                * @see         http://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0
-                * @param       {Element}       element         target element
-                * @param       {string}        innerHtml       HTML string
-                */
-               setInnerHtml: function(element, innerHtml) {
-                       element.innerHTML = innerHtml;
-                       
-                       var newScript, script, scripts = elBySelAll('script', element);
-                       for (var i = 0, length = scripts.length; i < length; i++) {
-                               script = scripts[i];
-                               newScript = elCreate('script');
-                               if (script.src) {
-                                       newScript.src = script.src;
-                               }
-                               else {
-                                       newScript.textContent = script.textContent;
-                               }
-                               
-                               element.appendChild(newScript);
-                               elRemove(script);
-                       }
-               },
-               
-               /**
-                * 
-                * @param html
-                * @param {Element} referenceElement
-                * @param insertMethod
-                */
-               insertHtml: function(html, referenceElement, insertMethod) {
-                       var element = elCreate('div');
-                       this.setInnerHtml(element, html);
-                       
-                       if (insertMethod === 'append' || insertMethod === 'after') {
-                               while (element.childNodes.length) {
-                                       if (insertMethod === 'append') {
-                                               referenceElement.appendChild(element.childNodes[0]);
-                                       }
-                                       else {
-                                               this.insertAfter(element.childNodes[0], referenceElement);
-                                       }
-                               }
-                       }
-                       else if (insertMethod === 'prepend' || insertMethod === 'before') {
-                               for (var i = element.childNodes.length - 1; i >= 0; i--) {
-                                       if (insertMethod === 'prepend') {
-                                               this.prepend(element.childNodes[i], referenceElement);
-                                       }
-                                       else {
-                                               referenceElement.parentNode.insertBefore(element.childNodes[i], referenceElement);
-                                       }
-                               }
-                       }
-                       else {
-                               throw new Error("Unknown insert method '" + insertMethod + "'.");
-                       }
-               },
-               
-               /**
-                * Returns true if `element` contains the `child` element.
-                * 
-                * @param       {Element}       element         container element
-                * @param       {Element}       child           child element
-                * @returns     {boolean}       true if `child` is a (in-)direct child of `element`
-                */
-               contains: function(element, child) {
-                       while (child !== null) {
-                               child = child.parentNode;
-                               
-                               if (element === child) {
-                                       return true;
-                               }
-                       }
-                       
-                       return false;
-               },
-               
-               /**
-                * Retrieves all data attributes from target element, optionally allowing for
-                * a custom prefix that serves two purposes: First it will restrict the results
-                * for items starting with it and second it will remove that prefix.
-                * 
-                * @param       {Element}       element         target element
-                * @param       {string=}       prefix          attribute prefix
-                * @param       {boolean=}      camelCaseName  transform attribute names into camel case using dashes as separators
-                * @param       {boolean=}      idToUpperCase   transform '-id' into 'ID'
-                * @returns     {object<string, string>}        list of data attributes
-                */
-               getDataAttributes: function(element, prefix, camelCaseName, idToUpperCase) {
-                       prefix = prefix || '';
-                       if (!/^data-/.test(prefix)) prefix = 'data-' + prefix;
-                       camelCaseName = (camelCaseName === true);
-                       idToUpperCase = (idToUpperCase === true);
-                       
-                       var attribute, attributes = {}, name, tmp;
-                       for (var i = 0, length = element.attributes.length; i < length; i++) {
-                               attribute = element.attributes[i];
-                               
-                               if (attribute.name.indexOf(prefix) === 0) {
-                                       name = attribute.name.replace(new RegExp('^' + prefix), '');
-                                       if (camelCaseName) {
-                                               tmp = name.split('-');
-                                               name = '';
-                                               for (var j = 0, innerLength = tmp.length; j < innerLength; j++) {
-                                                       if (name.length) {
-                                                               if (idToUpperCase && tmp[j] === 'id') {
-                                                                       tmp[j] = 'ID';
-                                                               }
-                                                               else {
-                                                                       tmp[j] = StringUtil.ucfirst(tmp[j]);
-                                                               }
-                                                       }
-                                                       
-                                                       name += tmp[j];
-                                               }
-                                       }
-                                       
-                                       attributes[name] = attribute.value;
-                               }
-                       }
-                       
-                       return attributes;
-               },
-               
-               /**
-                * Unwraps contained nodes by moving them out of `element` while
-                * preserving their previous order. Target element will be removed
-                * at the end of the operation.
-                * 
-                * @param       {Element}       element         target element
-                */
-               unwrapChildNodes: function(element) {
-                       var parent = element.parentNode;
-                       while (element.childNodes.length) {
-                               parent.insertBefore(element.childNodes[0], element);
-                       }
-                       
-                       elRemove(element);
-               },
-               
-               /**
-                * Replaces an element by moving all child nodes into the new element
-                * while preserving their previous order. The old element will be removed
-                * at the end of the operation.
-                * 
-                * @param       {Element}       oldElement      old element
-                * @param       {Element}       newElement      old element
-                */
-               replaceElement: function(oldElement, newElement) {
-                       while (oldElement.childNodes.length) {
-                               newElement.appendChild(oldElement.childNodes[0]);
-                       }
-                       
-                       oldElement.parentNode.insertBefore(newElement, oldElement);
-                       elRemove(oldElement);
-               },
-               
-               /**
-                * Returns true if given element is the most left node of the ancestor, that is
-                * a node without any content nor elements before it or its parent nodes.
-                * 
-                * @param       {Element}       element         target element
-                * @param       {Element}       ancestor        ancestor element, must contain the target element
-                * @returns     {boolean}       true if target element is the most left node
-                */
-               isAtNodeStart: function(element, ancestor) {
-                       return _isBoundaryNode(element, ancestor, 'previous');
-               },
-               
-               /**
-                * Returns true if given element is the most right node of the ancestor, that is
-                * a node without any content nor elements after it or its parent nodes.
-                * 
-                * @param       {Element}       element         target element
-                * @param       {Element}       ancestor        ancestor element, must contain the target element
-                * @returns     {boolean}       true if target element is the most right node
-                */
-               isAtNodeEnd: function(element, ancestor) {
-                       return _isBoundaryNode(element, ancestor, 'next');
-               }
-       };
-       
-       // expose on window object for backward compatibility
-       window.bc_wcfDomUtil = DomUtil;
-       
-       return DomUtil;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Environment.js b/wcfsetup/install/files/js/WoltLab/WCF/Environment.js
deleted file mode 100644 (file)
index 078d4ad..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * Provides basic details on the JavaScript environment.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Environment
- */
-define([], function() {
-       "use strict";
-       
-       var _browser = 'other';
-       var _editor = 'none';
-       var _platform = 'desktop';
-       var _touch = false;
-       
-       /**
-        * @exports     WoltLab/WCF/Enviroment
-        */
-       return {
-               /**
-                * Determines environment variables.
-                */
-               setup: function() {
-                       if (typeof window.chrome === 'object') {
-                               // this detects Opera as well, we could check for window.opr if we need to
-                               _browser = 'chrome';
-                       }
-                       else {
-                               var styles = window.getComputedStyle(document.documentElement);
-                               for (var i = 0, length = styles.length; i < length; i++) {
-                                       var property = styles[i];
-                                       
-                                       if (property.indexOf('-ms-') === 0) {
-                                               // it is tempting to use 'msie', but it wouldn't really represent 'Edge'
-                                               _browser = 'microsoft';
-                                       }
-                                       else if (property.indexOf('-moz-') === 0) {
-                                               _browser = 'firefox';
-                                       }
-                                       else if (property.indexOf('-webkit-') === 0) {
-                                               _browser = 'safari';
-                                       }
-                               }
-                       }
-                       
-                       var ua = window.navigator.userAgent.toLowerCase();
-                       if (ua.indexOf('crios') !== -1) {
-                               _browser = 'chrome';
-                               _platform = 'ios';
-                       }
-                       else if (/(?:iphone|ipad|ipod)/.test(ua)) {
-                               _browser = 'safari';
-                               _platform = 'ios';
-                       }
-                       else if (ua.indexOf('android') !== -1) {
-                               _platform = 'android';
-                       }
-                       else if (ua.indexOf('iemobile') !== -1) {
-                               _browser = 'microsoft';
-                               _platform = 'windows';
-                       }
-                       
-                       if (_platform === 'desktop' && (ua.indexOf('mobile') !== -1 || ua.indexOf('tablet') !== -1)) {
-                               _platform = 'mobile';
-                       }
-                       
-                       _editor = 'redactor';
-                       _touch = (!!('ontouchstart' in window) || (!!('msMaxTouchPoints' in window.navigator) && window.navigator.msMaxTouchPoints > 0) || window.DocumentTouch && document instanceof DocumentTouch);
-               },
-               
-               /**
-                * Returns the lower-case browser identifier.
-                * 
-                * Possible values:
-                *  - chrome: Chrome and Opera
-                *  - firefox
-                *  - microsoft: Internet Explorer and Microsoft Edge
-                *  - safari
-                * 
-                * @return      {string}        browser identifier
-                */
-               browser: function() {
-                       return _browser;
-               },
-               
-               /**
-                * Returns the available editor's name or an empty string.
-                * 
-                * @return      {string}        editor name
-                */
-               editor: function() {
-                       return _editor;
-               },
-               
-               /**
-                * Returns the browser platform.
-                * 
-                * Possible values:
-                *  - desktop
-                *  - android
-                *  - ios: iPhone, iPad and iPod
-                *  - windows: Windows on phones/tablets
-                * 
-                * @return      {string}        browser platform
-                */
-               platform: function() {
-                       return _platform;
-               },
-               
-               /**
-                * Returns true if browser is potentially used with a touchscreen.
-                * 
-                * Warning: Detecting touch is unreliable and should be avoided at all cost.
-                * 
-                * @deprecated  3.0 - exists for backward-compatibility only, will be removed in the future
-                * 
-                * @return      {boolean}       true if a touchscreen is present
-                */
-               touch: function() {
-                       return _touch;
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Event/Handler.js
deleted file mode 100644 (file)
index 13ff7b5..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * Versatile event system similar to the WCF-PHP counter part.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Event/Handler
- */
-define(['Core', 'Dictionary'], function(Core, Dictionary) {
-       "use strict";
-       
-       var _listeners = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/Event/Handler
-        */
-       return {
-               /**
-                * Adds an event listener.
-                * 
-                * @param       {string}                identifier      event identifier
-                * @param       {string}                action          action name
-                * @param       {function(object)}      callback        callback function
-                * @return      {string}        uuid required for listener removal
-                */
-               add: function(identifier, action, callback) {
-                       if (typeof callback !== 'function') {
-                               throw new TypeError("[WoltLab/WCF/Event/Handler] Expected a valid callback for '" + action + "@" + identifier + "'.");
-                       }
-                       
-                       var actions = _listeners.get(identifier);
-                       if (actions === undefined) {
-                               actions = new Dictionary();
-                               _listeners.set(identifier, actions);
-                       }
-                       
-                       var callbacks = actions.get(action);
-                       if (callbacks === undefined) {
-                               callbacks = new Dictionary();
-                               actions.set(action, callbacks);
-                       }
-                       
-                       var uuid = Core.getUuid();
-                       callbacks.set(uuid, callback);
-                       
-                       return uuid;
-               },
-               
-               /**
-                * Fires an event and notifies all listeners.
-                * 
-                * @param       {string}        identifier      event identifier
-                * @param       {string}        action          action name
-                * @param       {object=}       data            event data
-                */
-               fire: function(identifier, action, data) {
-                       data = data || {};
-                       
-                       var actions = _listeners.get(identifier);
-                       if (actions !== undefined) {
-                               var callbacks = actions.get(action);
-                               if (callbacks !== undefined) {
-                                       callbacks.forEach(function(callback) {
-                                               callback(data);
-                                       });
-                               }
-                       }
-               },
-               
-               /**
-                * Removes an event listener, requires the uuid returned by add().
-                * 
-                * @param       {string}        identifier      event identifier
-                * @param       {string}        action          action name
-                * @param       {string}        uuid            listener uuid
-                */
-               remove: function(identifier, action, uuid) {
-                       var actions = _listeners.get(identifier);
-                       if (actions === undefined) {
-                               return;
-                       }
-                       
-                       var callbacks = actions.get(action);
-                       if (callbacks === undefined) {
-                               return;
-                       }
-                       
-                       callbacks['delete'](uuid);
-               },
-               
-               /**
-                * Removes all event listeners for given action. Omitting the second parameter will
-                * remove all listeners for this identifier.
-                * 
-                * @param       {string}        identifier      event identifier
-                * @param       {string=}       action          action name
-                */
-               removeAll: function(identifier, action) {
-                       if (typeof action !== 'string') action = undefined;
-                       
-                       var actions = _listeners.get(identifier);
-                       if (actions === undefined) {
-                               return;
-                       }
-                       
-                       if (typeof action === 'undefined') {
-                               _listeners['delete'](identifier);
-                       }
-                       else {
-                               actions['delete'](action);
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Event/Key.js b/wcfsetup/install/files/js/WoltLab/WCF/Event/Key.js
deleted file mode 100644 (file)
index a35a5da..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * Provides reliable checks for common key presses, uses `Event.key` on supported browsers
- * or the deprecated `Event.which`.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Event/Key
- */
-define([], function() {
-       "use strict";
-       
-       function _isKey(event, key, which) {
-               if (!(event instanceof Event)) {
-                       throw new TypeError("Expected a valid event when testing for key '" + key + "'.");
-               }
-               
-               return event.key === key || event.which === which;
-       }
-       
-       /**
-        * @exports     WoltLab/WCF/Event/Key
-        */
-       return {
-               /**
-                * Returns true if pressed key equals 'ArrowDown'.
-                * 
-                * @param       {Event}         event           event object
-                * @return      {boolean}
-                */
-               ArrowDown: function(event) {
-                       return _isKey(event, 'ArrowDown', 40);
-               },
-               
-               /**
-                * Returns true if pressed key equals 'ArrowLeft'.
-                * 
-                * @param       {Event}         event           event object
-                * @return      {boolean}
-                */
-               ArrowLeft: function(event) {
-                       return _isKey(event, 'ArrowLeft', 37);
-               },
-               
-               /**
-                * Returns true if pressed key equals 'ArrowRight'.
-                * 
-                * @param       {Event}         event           event object
-                * @return      {boolean}
-                */
-               ArrowRight: function(event) {
-                       return _isKey(event, 'ArrowRight', 39);
-               },
-               
-               /**
-                * Returns true if pressed key equals 'ArrowUp'.
-                * 
-                * @param       {Event}         event           event object
-                * @return      {boolean}
-                */
-               ArrowUp: function(event) {
-                       return _isKey(event, 'ArrowUp', 38);
-               },
-               
-               /**
-                * Returns true if pressed key equals 'Enter'.
-                * 
-                * @param       {Event}         event           event object
-                * @return      {boolean}
-                */
-               Enter: function(event) {
-                       return _isKey(event, 'Enter', 13);
-               },
-               
-               /**
-                * Returns true if pressed key equals 'Escape'.
-                * 
-                * @param       {Event}         event           event object
-                * @return      {boolean}
-                */
-               Escape: function(event) {
-                       return _isKey(event, 'Escape', 27);
-               },
-               
-               /**
-                * Returns true if pressed key equals 'Tab'.
-                * 
-                * @param       {Event}         event           event object
-                * @return      {boolean}
-                */
-               Tab: function(event) {
-                       return _isKey(event, 'Tab', 9);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/File/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/File/Util.js
deleted file mode 100644 (file)
index 844d722..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Provides helper functions to work with files.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/File/Util
- */
-define([], function() {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/File/Util
-        */
-       var FileUtil = {
-               /**
-                * Returns the FontAwesome icon CSS class name for a mime type.
-                * 
-                * @param       {string}        mimeType        mime type of the relevant file
-                * @return      {string}        FontAwesome icon CSS class name for the mime type
-                */
-               getIconClassByMimeType: function(mimeType) {
-                       if (mimeType.substr(0, 6) == 'image/') {
-                               return 'fa-file-image-o';
-                       }
-                       else if (mimeType.substr(0, 6) == 'video/') {
-                               return 'fa-file-video-o';
-                       }
-                       else if (mimeType.substr(0, 6) == 'audio/') {
-                               return 'fa-file-sound-o';
-                       }
-                       else if (mimeType.substr(0, 5) == 'text/') {
-                               return 'fa-file-text-o';
-                       }
-                       else {
-                               switch (mimeType) {
-                                       case 'application/msword':
-                                       case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
-                                               return 'fa-file-word-o';
-                                       break;
-                                       
-                                       case 'application/pdf':
-                                               return 'fa-file-pdf-o';
-                                       break;
-                                       
-                                       case 'application/vnd.ms-powerpoint':
-                                       case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
-                                               return 'fa-file-powerpoint-o';
-                                       break;
-                                       
-                                       case 'application/vnd.ms-excel':
-                                       case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
-                                               return 'fa-file-excel-o';
-                                       break;
-                                       
-                                       case 'application/zip':
-                                       case 'application/x-tar':
-                                       case 'application/x-gzip':
-                                               return 'fa-file-archive-o';
-                                       break;
-                                       
-                                       case 'application/xml':
-                                               return 'fa-file-text-o';
-                                       break;
-                               }
-                       }
-                       
-                       return 'fa-file-o';
-               }
-       };
-       
-       return FileUtil;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Language.js b/wcfsetup/install/files/js/WoltLab/WCF/Language.js
deleted file mode 100644 (file)
index 97e569c..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-/**
- * Manages language items.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Language
- */
-define(['Dictionary', './Template'], function(Dictionary, Template) {
-       "use strict";
-       
-       var _languageItems = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/Language
-        */
-       var Language = {
-               /**
-                * Adds all the language items in the given object to the store.
-                * 
-                * @param       {Object.<string, string>}       object
-                */
-               addObject: function(object) {
-                       _languageItems.merge(Dictionary.fromObject(object));
-               },
-               
-               /**
-                * Adds a single language item to the store.
-                * 
-                * @param       {string}        key
-                * @param       {string}        value
-                */
-               add: function(key, value) {
-                       _languageItems.set(key, value);
-               },
-               
-               /**
-                * Fetches the language item specified by the given key.
-                * If the language item is a string it will be evaluated as
-                * WoltLab/WCF/Template with the given parameters.
-                * 
-                * @param       {string}        key             Language item to return.
-                * @param       {Object=}       parameters      Parameters to provide to WoltLab/WCF/Template.
-                * @return      {string}
-                */
-               get: function(key, parameters) {
-                       if (!parameters) parameters = { };
-                       
-                       var value = _languageItems.get(key);
-                       
-                       if (value === undefined) {
-                               // TODO
-                               //console.warn("Attempt to retrieve unknown phrase '" + key + "'.");
-                               //console.warn(new Error().stack);
-                               return key;
-                       }
-                       
-                       if (typeof value === 'string') {
-                               // lazily convert to WCF.Template
-                               try {
-                                       _languageItems.set(key, new Template(value));
-                               }
-                               catch (e) {
-                                       _languageItems.set(key, new Template('{literal}' + value.replace(/\{\/literal\}/g, '{/literal}{ldelim}/literal}{literal}') + '{/literal}'));
-                               }
-                               value = _languageItems.get(key);
-                       }
-                       
-                       if (value instanceof Template) {
-                               value = value.fetch(parameters);
-                       }
-                       
-                       return value;
-               }
-       };
-       
-       return Language;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Language/Chooser.js b/wcfsetup/install/files/js/WoltLab/WCF/Language/Chooser.js
deleted file mode 100644 (file)
index b98090d..0000000
+++ /dev/null
@@ -1,298 +0,0 @@
-/**
- * Dropdown language chooser.
- * 
- * @author     Alexander Ebert, Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Language/Chooser
- */
-define(['Dictionary', 'Language', 'Dom/Traverse', 'Dom/Util', 'ObjectMap', 'Ui/SimpleDropdown'], function(Dictionary, Language, DomTraverse, DomUtil, ObjectMap, UiSimpleDropdown) {
-       "use strict";
-       
-       var _choosers = new Dictionary();
-       var _didInit = false;
-       var _forms = new ObjectMap();
-       
-       var _callbackSubmit = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Language/Chooser
-        */
-       return {
-               /**
-                * Initializes a language chooser.
-                * 
-                * @param       {string}                                containerId             input element conainer id
-                * @param       {string}                                chooserId               input element id
-                * @param       {int}                                   languageId              selected language id
-                * @param       {object<int, object<string, string>>}   languages               data of available languages
-                * @param       {function}                              callback                function called after a language is selected
-                * @param       {boolean}                               allowEmptyValue         true if no language may be selected
-                */
-               init: function(containerId, chooserId, languageId, languages, callback, allowEmptyValue) {
-                       if (_choosers.has(chooserId)) {
-                               return;
-                       }
-                       
-                       var container = elById(containerId);
-                       if (container === null) {
-                               throw new Error("Expected a valid container id, cannot find '" + chooserId + "'.");
-                       }
-                       
-                       var element = elById(chooserId);
-                       if (element === null) {
-                               element = elCreate('input');
-                               elAttr(element, 'type', 'hidden');
-                               elAttr(element, 'id', chooserId);
-                               elAttr(element, 'name', chooserId);
-                               elAttr(element, 'value', languageId);
-                               
-                               container.appendChild(element);
-                       }
-                       
-                       this._initElement(chooserId, element, languageId, languages, callback, allowEmptyValue);
-               },
-               
-               /**
-                * Caches common event listener callbacks.
-                */
-               _setup: function() {
-                       if (_didInit) return;
-                       _didInit = true;
-                       
-                       _callbackSubmit = this._submit.bind(this);
-               },
-               
-               /**
-                * Sets up DOM and event listeners for a language chooser.
-                *
-                * @param       {string}                                chooserId               chooser id
-                * @param       {Element}                               element                 chooser element
-                * @param       {int}                                   languageId              selected language id
-                * @param       {object<int, object<string, string>>}   languages               data of available languages
-                * @param       {function}                              callback                callback function invoked on selection change
-                * @param       {boolean}                               allowEmptyValue         true if no language may be selected
-                */
-               _initElement: function(chooserId, element, languageId, languages, callback, allowEmptyValue) {
-                       var container;
-                       
-                       if (element.parentNode.nodeName === 'DD') {
-                               container = elCreate('div');
-                               container.className = 'dropdown';
-                               element.parentNode.insertBefore(container, element);
-                       }
-                       else {
-                               container = element.parentNode;
-                               container.classList.add('dropdown');
-                       }
-                       
-                       elHide(element);
-                       
-                       var dropdownToggle = elCreate('a');
-                       dropdownToggle.className = 'dropdownToggle dropdownIndicator boxFlag box24 inputPrefix' + (element.parentNode.nodeName === 'DD' ? ' button' : '');
-                       container.appendChild(dropdownToggle);
-                       
-                       var dropdownMenu = elCreate('ul');
-                       dropdownMenu.className = 'dropdownMenu';
-                       container.appendChild(dropdownMenu);
-                       
-                       var callbackClick = (function(event) {
-                               var languageId = ~~elData(event.currentTarget, 'language-id');
-                               
-                               var activeItem = DomTraverse.childByClass(dropdownMenu, 'active');
-                               if (activeItem !== null) activeItem.classList.remove('active');
-                               
-                               if (languageId) event.currentTarget.classList.add('active');
-                               
-                               this._select(chooserId, languageId, event.currentTarget);
-                       }).bind(this);
-                       
-                       // add language dropdown items
-                       var link, img, listItem, span;
-                       for (var availableLanguageId in languages) {
-                               if (languages.hasOwnProperty(availableLanguageId)) {
-                                       var language = languages[availableLanguageId];
-                                       
-                                       listItem = elCreate('li');
-                                       listItem.className = 'boxFlag';
-                                       listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                                       elData(listItem, 'language-id', availableLanguageId);
-                                       if (language.languageCode !== undefined) elData(listItem, 'language-code', language.languageCode);
-                                       dropdownMenu.appendChild(listItem);
-                                       
-                                       link = elCreate('a');
-                                       link.className = 'box24';
-                                       listItem.appendChild(link);
-                                       
-                                       img = elCreate('img');
-                                       elAttr(img, 'src', language.iconPath);
-                                       elAttr(img, 'alt', '');
-                                       img.className = 'iconFlag';
-                                       link.appendChild(img);
-                                       
-                                       span = elCreate('span');
-                                       span.textContent = language.languageName;
-                                       link.appendChild(span);
-                                       
-                                       if (availableLanguageId == languageId) {
-                                               dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
-                                       }
-                               }
-                       }
-                       
-                       // add dropdown item for "no selection"
-                       if (allowEmptyValue) {
-                               listItem = elCreate('li');
-                               listItem.className = 'dropdownDivider';
-                               dropdownMenu.appendChild(listItem);
-                               
-                               listItem = elCreate('li');
-                               elData(listItem, 'language-id', 0);
-                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                               dropdownMenu.appendChild(listItem);
-                               
-                               link = elCreate('a');
-                               link.textContent = Language.get('wcf.global.language.noSelection');
-                               listItem.appendChild(link);
-                               
-                               if (languageId === 0) {
-                                       dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
-                               }
-                               
-                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                       }
-                       else if (languageId === 0) {
-                               dropdownToggle.innerHTML = null;
-                               
-                               var div = elCreate('div');
-                               dropdownToggle.appendChild(div);
-                               
-                               span = elCreate('span');
-                               span.className = 'icon icon24 fa-question';
-                               div.appendChild(span);
-                               
-                               span = elCreate('span');
-                               span.textContent = Language.get('wcf.global.language.noSelection');
-                               div.appendChild(span);
-                       }
-                       
-                       UiSimpleDropdown.init(dropdownToggle);
-                       
-                       _choosers.set(chooserId, {
-                               callback: callback,
-                               dropdownMenu: dropdownMenu,
-                               dropdownToggle: dropdownToggle,
-                               element: element
-                       });
-                       
-                       // bind to submit event
-                       var form = DomTraverse.parentByTag(element, 'FORM');
-                       if (form !== null) {
-                               form.addEventListener('submit', _callbackSubmit);
-                               
-                               var chooserIds = _forms.get(form);
-                               if (chooserIds === undefined) {
-                                       chooserIds = [];
-                                       _forms.set(form, chooserIds);
-                               }
-                               
-                               chooserIds.push(chooserId);
-                       }
-               },
-               
-               /**
-                * Selects a language from the dropdown list.
-                * 
-                * @param       {string}        chooserId       input element id
-                * @param       {int}           languageId      language id or `0` to disable i18n
-                * @param       {Element=}      listItem        selected list item
-                */
-               _select: function(chooserId, languageId, listItem) {
-                       var chooser = _choosers.get(chooserId);
-                       
-                       if (listItem === undefined) {
-                               var listItems = chooser.dropdownMenu.childNodes;
-                               for (var i = 0, length = listItems.length; i < length; i++) {
-                                       var _listItem = listItems[i];
-                                       if (~~elData(_listItem, 'language-id') === languageId) {
-                                               listItem = _listItem;
-                                               break;
-                                       }
-                               }
-                               
-                               if (listItem === undefined) {
-                                       throw new Error("Cannot select unknown language id '" + languageId + "'");
-                               }
-                       }
-                       
-                       chooser.element.value = languageId;
-                       
-                       chooser.dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
-                       
-                       _choosers.set(chooserId, chooser);
-                       
-                       // execute callback
-                       if (typeof chooser.callback === 'function') {
-                               chooser.callback(listItem);
-                       }
-               },
-               
-               /**
-                * Inserts hidden fields for the language chooser value on submit.
-                *
-                * @param       {object}        event           event object
-                */
-               _submit: function(event) {
-                       var elementIds = _forms.get(event.currentTarget);
-                       
-                       var input;
-                       for (var i = 0, length = elementIds.length; i < length; i++) {
-                               input = elCreate('input');
-                               input.type = 'hidden';
-                               input.name = elementIds[i];
-                               input.value = this.getLanguageId(elementIds[i]);
-                               
-                               event.currentTarget.appendChild(input);
-                       }
-               },
-               
-               /**
-                * Returns the chooser for an input field.
-                * 
-                * @param       {string}        chooserId       input element id
-                * @return      {Dictionary}    data of the chooser
-                */
-               getChooser: function(chooserId) {
-                       var chooser = _choosers.get(chooserId);
-                       if (chooser === undefined) {
-                               throw new Error("Expected a valid language chooser input element, '" + chooserId + "' is not i18n input field.");
-                       }
-                       
-                       return chooser;
-               },
-               
-               /**
-                * Returns the selected language for a certain chooser.
-                * 
-                * @param       {string}        chooserId       input element id
-                * @return      {int}           chosen language id
-                */
-               getLanguageId: function(chooserId) {
-                       return ~~this.getChooser(chooserId).element.value;
-               },
-               
-               /**
-                * Sets the language for a certain chooser.
-                * 
-                * @param       {string}        chooserId       input element id
-                * @param       {int}           languageId      language id to be set
-                */
-               setLanguageId: function(chooserId, languageId) {
-                       if (_choosers.get(chooserId) === undefined) {
-                               throw new Error("Expected a valid  input element, '" + chooserId + "' is not i18n input field.");
-                       }
-                       
-                       this._select(chooserId, languageId);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js b/wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js
deleted file mode 100644 (file)
index c60f3ff..0000000
+++ /dev/null
@@ -1,460 +0,0 @@
-/**
- * I18n interface for input and textarea fields.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Language/Input
- */
-define(['Core', 'Dictionary', 'Language', 'ObjectMap', 'StringUtil', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function(Core, Dictionary, Language, ObjectMap, StringUtil, DomTraverse, DomUtil, UiSimpleDropdown) {
-       "use strict";
-       
-       var _elements = new Dictionary();
-       var _didInit = false;
-       var _forms = new ObjectMap();
-       var _values = new Dictionary();
-       
-       var _callbackDropdownToggle = null;
-       var _callbackSubmit = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Language/Input
-        */
-       var LanguageInput = {
-               /**
-                * Initializes an input field.
-                * 
-                * @param       {string}                        elementId               input element id
-                * @param       {object<int, string>}           values                  preset values per language id
-                * @param       {object<int, string>}           availableLanguages      language names per language id
-                * @param       {boolean}                       forceSelection          require i18n input
-                */
-               init: function(elementId, values, availableLanguages, forceSelection) {
-                       if (_values.has(elementId)) {
-                               return;
-                       }
-                       
-                       var element = elById(elementId);
-                       if (element === null) {
-                               throw new Error("Expected a valid element id, cannot find '" + elementId + "'.");
-                       }
-                       
-                       this._setup();
-                       
-                       // unescape values
-                       var unescapedValues = new Dictionary();
-                       for (var key in values) {
-                               if (objOwns(values, key)) {
-                                       unescapedValues.set(~~key, StringUtil.unescapeHTML(values[key]));
-                               }
-                       }
-                       
-                       _values.set(elementId, unescapedValues);
-                       
-                       this._initElement(elementId, element, unescapedValues, availableLanguages, forceSelection);
-               },
-               
-               /**
-                * Caches common event listener callbacks.
-                */
-               _setup: function() {
-                       if (_didInit) return;
-                       _didInit = true;
-                       
-                       _callbackDropdownToggle = this._dropdownToggle.bind(this);
-                       _callbackSubmit = this._submit.bind(this);
-               },
-               
-               /**
-                * Sets up DOM and event listeners for an input field.
-                * 
-                * @param       {string}                        elementId               input element id
-                * @param       {Element}                       element                 input or textarea element
-                * @param       {Dictionary}                    values                  preset values per language id
-                * @param       {object<int, string>}           availableLanguages      language names per language id
-                * @param       {boolean}                       forceSelection          require i18n input
-                */
-               _initElement: function(elementId, element, values, availableLanguages, forceSelection) {
-                       var container = element.parentNode;
-                       if (!container.classList.contains('inputAddon')) {
-                               container = elCreate('div');
-                               container.className = 'inputAddon' + (element.nodeName === 'TEXTAREA' ? ' inputAddonTextarea' : '');
-                               elData(container, 'input-id', elementId);
-                               
-                               element.parentNode.insertBefore(container, element);
-                               container.appendChild(element);
-                       }
-                       
-                       container.classList.add('dropdown');
-                       var button = elCreate('span');
-                       button.className = 'button dropdownToggle inputPrefix';
-                       
-                       var span = elCreate('span');
-                       span.textContent = Language.get('wcf.global.button.disabledI18n');
-                       
-                       button.appendChild(span);
-                       container.insertBefore(button, element);
-                       
-                       var dropdownMenu = elCreate('ul');
-                       dropdownMenu.className = 'dropdownMenu';
-                       DomUtil.insertAfter(dropdownMenu, button);
-                       
-                       var callbackClick = (function(event, isInit) {
-                               var languageId = ~~elData(event.currentTarget, 'language-id');
-                               
-                               var activeItem = DomTraverse.childByClass(dropdownMenu, 'active');
-                               if (activeItem !== null) activeItem.classList.remove('active');
-                               
-                               if (languageId) event.currentTarget.classList.add('active');
-                               
-                               this._select(elementId, languageId, isInit || false);
-                       }).bind(this);
-                       
-                       // build language dropdown
-                       for (var languageId in availableLanguages) {
-                               if (objOwns(availableLanguages, languageId)) {
-                                       var listItem = elCreate('li');
-                                       elData(listItem, 'language-id', languageId);
-                                       
-                                       span = elCreate('span');
-                                       span.textContent = availableLanguages[languageId];
-                                       
-                                       listItem.appendChild(span);
-                                       listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                                       dropdownMenu.appendChild(listItem);
-                               }
-                       }
-                       
-                       if (forceSelection !== true) {
-                               var listItem = elCreate('li');
-                               listItem.className = 'dropdownDivider';
-                               dropdownMenu.appendChild(listItem);
-                               
-                               listItem = elCreate('li');
-                               elData(listItem, 'language-id', 0);
-                               span = elCreate('span');
-                               span.textContent = Language.get('wcf.global.button.disabledI18n');
-                               listItem.appendChild(span);
-                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                               dropdownMenu.appendChild(listItem);
-                       }
-                       
-                       var activeItem = null;
-                       if (forceSelection === true || values.size) {
-                               for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
-                                       if (~~elData(dropdownMenu.children[i], 'language-id') === LANGUAGE_ID) {
-                                               activeItem = dropdownMenu.children[i];
-                                               break;
-                                       }
-                               }
-                       }
-                       
-                       UiSimpleDropdown.init(button);
-                       UiSimpleDropdown.registerCallback(container.id, _callbackDropdownToggle);
-                       
-                       _elements.set(elementId, {
-                               buttonLabel: button.children[0],
-                               element: element,
-                               languageId: 0,
-                               isEnabled: true,
-                               forceSelection: forceSelection
-                       });
-                       
-                       // bind to submit event
-                       var submit = DomTraverse.parentByTag(element, 'FORM');
-                       if (submit !== null) {
-                               submit.addEventListener('submit', _callbackSubmit);
-                               
-                               var elementIds = _forms.get(submit);
-                               if (elementIds === undefined) {
-                                       elementIds = [];
-                                       _forms.set(submit, elementIds);
-                               }
-                               
-                               elementIds.push(elementId);
-                       }
-                       
-                       if (activeItem !== null) {
-                               callbackClick({ currentTarget: activeItem }, true);
-                       }
-               },
-               
-               /**
-                * Selects a language or non-i18n from the dropdown list.
-                * 
-                * @param       {string}        elementId       input element id
-                * @param       {int}           languageId      language id or `0` to disable i18n
-                * @param       {boolean}       isInit          triggers pre-selection on init
-                */
-               _select: function(elementId, languageId, isInit) {
-                       var data = _elements.get(elementId);
-                       
-                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(data.element.parentNode.id);
-                       var item, label = '';
-                       for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
-                               item = dropdownMenu.children[i];
-                               
-                               var itemLanguageId = elData(item, 'language-id');
-                               if (itemLanguageId.length && languageId === ~~itemLanguageId) {
-                                       label = item.children[0].textContent;
-                               }
-                       }
-                       
-                       // save current value
-                       if (data.languageId !== languageId) {
-                               var values = _values.get(elementId);
-                               
-                               if (data.languageId) {
-                                       values.set(data.languageId, data.element.value);
-                               }
-                               
-                               if (languageId === 0) {
-                                       _values.set(elementId, new Dictionary());
-                               }
-                               else if (data.buttonLabel.classList.contains('active') || isInit === true) {
-                                       data.element.value = (values.has(languageId)) ? values.get(languageId) : '';
-                               }
-                               
-                               // update label
-                               data.buttonLabel.textContent = label;
-                               data.buttonLabel.classList[(languageId ? 'add' : 'remove')]('active');
-                               
-                               data.languageId = languageId;
-                       }
-                       
-                       data.element.blur();
-                       data.element.focus();
-               },
-               
-               /**
-                * Callback for dropdowns being opened, flags items with a missing value for one or more languages.
-                * 
-                * @param       {string}        containerId     dropdown container id
-                * @param       {string}        action          toggle action, can be `open` or `close`
-                */
-               _dropdownToggle: function(containerId, action) {
-                       if (action !== 'open') {
-                               return;
-                       }
-                       
-                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(containerId);
-                       var elementId = elData(elById(containerId), 'input-id');
-                       var values = _values.get(elementId);
-                       
-                       var item, languageId;
-                       for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
-                               item = dropdownMenu.children[i];
-                               languageId = ~~elData(item, 'language-id');
-                               
-                               if (languageId) {
-                                       item.classList[(values.has(languageId) || !values.size ? 'remove' : 'add')]('missingValue');
-                               }
-                       }
-               },
-               
-               /**
-                * Inserts hidden fields for i18n input on submit.
-                * 
-                * @param       {object}        event           event object
-                */
-               _submit: function(event) {
-                       var elementIds = _forms.get(event.currentTarget);
-                       
-                       var data, elementId, input, values;
-                       for (var i = 0, length = elementIds.length; i < length; i++) {
-                               elementId = elementIds[i];
-                               data = _elements.get(elementId);
-                               if (data.isEnabled) {
-                                       values = _values.get(elementId);
-                                       
-                                       // update with current value
-                                       if (data.languageId) {
-                                               values.set(data.languageId, data.element.value);
-                                       }
-                                       
-                                       if (values.size) {
-                                               values.forEach(function(value, languageId) {
-                                                       input = elCreate('input');
-                                                       input.type = 'hidden';
-                                                       input.name = elementId + '_i18n[' + languageId + ']';
-                                                       input.value = value;
-                                                       
-                                                       event.currentTarget.appendChild(input);
-                                               });
-                                               
-                                               // remove name attribute to enforce i18n values
-                                               data.element.removeAttribute('name');
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Returns the values of an input field.
-                * 
-                * @param       {string}        elementId       input element id
-                * @return      {Dictionary}    values stored for the different languages
-                */
-               getValues: function(elementId) {
-                       var element = _elements.get(elementId);
-                       if (element === undefined) {
-                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
-                       }
-                       
-                       var values = _values.get(elementId);
-                       
-                       // update with current value
-                       values.set(element.languageId, element.element.value);
-                       
-                       return values;
-               },
-               
-               /**
-                * Sets the values of an input field.
-                * 
-                * @param       {string}        elementId       input element id
-                * @param       {Dictionary}    values          values for the different languages
-                */
-               setValues: function(elementId, values) {
-                       var element = _elements.get(elementId);
-                       if (element === undefined) {
-                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
-                       }
-                       
-                       if (Core.isPlainObject(values)) {
-                               values = Dictionary.fromObject(values);
-                       }
-                       
-                       element.element.value = '';
-                       
-                       if (values.has(0)) {
-                               element.element.value = values.get(0);
-                               values['delete'](0);
-                       }
-                       
-                       _values.set(elementId, values);
-                       
-                       element.languageId = 0;
-                       this._select(elementId, LANGUAGE_ID, true);
-               },
-               
-               /**
-                * Disables the i18n interface for an input field.
-                * 
-                * @param       {string}        elementId       input element id
-                */
-               disable: function(elementId) {
-                       var element = _elements.get(elementId);
-                       if (element === undefined) {
-                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
-                       }
-                       
-                       if (!element.isEnabled) return;
-                       
-                       element.isEnabled = false;
-                       
-                       // hide language dropdown
-                       elHide(element.buttonLabel.parentNode);
-                       var dropdownContainer = element.buttonLabel.parentNode.parentNode;
-                       dropdownContainer.classList.remove('inputAddon');
-                       dropdownContainer.classList.remove('dropdown');
-               },
-               
-               /**
-                * Enables the i18n interface for an input field.
-                * 
-                * @param       {string}        elementId       input element id
-                */
-               enable: function(elementId) {
-                       var element = _elements.get(elementId);
-                       if (element === undefined) {
-                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
-                       }
-                       
-                       if (element.isEnabled) return;
-                       
-                       element.isEnabled = true;
-                       
-                       // show language dropdown
-                       elShow(element.buttonLabel.parentNode);
-                       var dropdownContainer = element.buttonLabel.parentNode.parentNode;
-                       dropdownContainer.classList.add('inputAddon');
-                       dropdownContainer.classList.add('dropdown');
-               },
-               
-               /**
-                * Returns true if i18n input is enabled for an input field.
-                * 
-                * @param       {string}        elementId       input element id
-                * @return      {boolean}
-                */
-               isEnabled: function(elementId) {
-                       var element = _elements.get(elementId);
-                       if (element === undefined) {
-                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
-                       }
-                       
-                       return element.isEnabled;
-               },
-               
-               /**
-                * Returns true if the value of an i18n input field is valid.
-                * 
-                * If the element is disabled, true is returned.
-                * 
-                * @param       {string}        elementId               input element id
-                * @param       {boolean}       permitEmptyValue        if true, input may be empty for all languages
-                * @return      {boolean}       true if input is valid
-                */
-               validate: function(elementId, permitEmptyValue) {
-                       var element = _elements.get(elementId);
-                       if (element === undefined) {
-                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
-                       }
-                       
-                       if (!element.isEnabled) return true;
-                       
-                       var values = _values.get(elementId);
-                       
-                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(element.element.parentNode.id);
-                       
-                       if (element.languageId) {
-                               values.set(element.languageId, element.element.value);
-                       }
-                       
-                       var item, languageId;
-                       var hasEmptyValue = false, hasNonEmptyValue = false;
-                       for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
-                               item = dropdownMenu.children[i];
-                               languageId = ~~elData(item, 'language-id');
-                               
-                               if (languageId) {
-                                       if (!values.has(languageId) || values.get(languageId).length === 0) {
-                                               // input has non-empty value for previously checked language
-                                               if (hasNonEmptyValue) {
-                                                       return false;
-                                               }
-                                               
-                                               hasEmptyValue = true;
-                                       }
-                                       else {
-                                               // input has empty value for previously checked language
-                                               if (hasEmptyValue) {
-                                                       return false;
-                                               }
-                                               
-                                               hasNonEmptyValue = true;
-                                       }
-                               }
-                       }
-                       
-                       if (hasEmptyValue && !permitEmptyValue) {
-                               return false;
-                       }
-                       
-                       return true;
-               }
-       };
-       
-       return LanguageInput;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/List.js b/wcfsetup/install/files/js/WoltLab/WCF/List.js
deleted file mode 100644 (file)
index 15a93f8..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-/**
- * List implementation relying on an array or if supported on a Set to hold values.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/List
- */
-define([], function() {
-       "use strict";
-       
-       var _hasSet = objOwns(window, 'Set') && typeof window.Set === 'function';
-       
-       /**
-        * @constructor
-        */
-       function List() {
-               this._set = (_hasSet) ? new Set() : [];
-       }
-       List.prototype = {
-               /**
-                * Appends an element to the list, silently rejects adding an already existing value.
-                * 
-                * @param       {?}     value   unique element
-                */
-               add: function(value) {
-                       if (_hasSet) {
-                               this._set.add(value);
-                       }
-                       else if (!this.has(value)) {
-                               this._set.push(value);
-                       }
-               },
-               
-               /**
-                * Removes all elements from the list.
-                */
-               clear: function() {
-                       if (_hasSet) {
-                               this._set.clear();
-                       }
-                       else {
-                               this._set = [];
-                       }
-               },
-               
-               /**
-                * Removes an element from the list, returns true if the element was in the list.
-                * 
-                * @param       {?}             value   element
-                * @return      {boolean}       true if element was in the list
-                */
-               'delete': function(value) {
-                       if (_hasSet) {
-                               return this._set['delete'](value);
-                       }
-                       else {
-                               var index = this._set.indexOf(value);
-                               if (index === -1) {
-                                       return false;
-                               }
-                               
-                               this._set.splice(index, 1);
-                               return true;
-                       }
-               },
-               
-               /**
-                * Calls `callback` for each element in the list.
-                */
-               forEach: function(callback) {
-                       if (_hasSet) {
-                               this._set.forEach(callback);
-                       }
-                       else {
-                               for (var i = 0, length = this._set.length; i < length; i++) {
-                                       callback(this._set[i]);
-                               }
-                       }
-               },
-               
-               /**
-                * Returns true if the list contains the element.
-                * 
-                * @param       {?}             value   element
-                * @return      {boolean}       true if element is in the list
-                */
-               has: function(value) {
-                       if (_hasSet) {
-                               return this._set.has(value);
-                       }
-                       else {
-                               return (this._set.indexOf(value) !== -1);
-                       }
-               }
-       };
-       
-       Object.defineProperty(List.prototype, 'size', {
-               enumerable: false,
-               configurable: true,
-               get: function() {
-                       if (_hasSet) {
-                               return this._set.size;
-                       }
-                       else {
-                               return this._set.length;
-                       }
-               }
-       });
-       
-       return List;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Editor.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Editor.js
deleted file mode 100644 (file)
index 8f89bc9..0000000
+++ /dev/null
@@ -1,286 +0,0 @@
-/**
- * Handles editing media files via dialog.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Media/Editor
- */
-define(
-       [
-               'Ajax',                         'Core',                       'Dictionary',          'Dom/ChangeListener',
-               'Dom/Traverse',                 'Language',                   'Ui/Dialog',           'Ui/Notification',
-               'WoltLab/WCF/Language/Chooser', 'WoltLab/WCF/Language/Input', 'WoltLab/WCF/File/Util'
-       ],
-       function(
-               Ajax,                            Core,                         Dictionary,            DomChangeListener,
-               DomTraverse,                     Language,                     UiDialog,              UiNotification,
-               LanguageChooser,                 LanguageInput,                FileUtil
-       )
-{
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function MediaEditor(callbackObject) {
-               if (typeof callbackObject !== 'object') {
-                       throw new TypeError("Parameter 'callbackObject' has to be an object, " + typeof callbackObject + " given.");
-               }
-               if (typeof callbackObject._editorClose !== 'function') {
-                       throw new TypeError("Callback object has no function '_editorClose'.");
-               }
-               if (typeof callbackObject._editorSuccess !== 'function') {
-                       throw new TypeError("Callback object has no function '_editorSuccess'.");
-               }
-               
-               this._callbackObject = callbackObject;
-               this._media = null;
-               
-               this._dialogs = new Dictionary();
-       }
-       MediaEditor.prototype = {
-               /**
-                * Returns the data for Ajax to setup the Ajax/Request object.
-                * 
-                * @return      {object}        setup data for Ajax/Request object
-                */
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'update',
-                                       className: 'wcf\\data\\media\\MediaAction'
-                               }
-                       };
-               },
-               
-               /**
-                * Handles successful AJAX requests.
-                * 
-                * @param       {object}        data    response data
-                */
-               _ajaxSuccess: function(data) {
-                       UiNotification.show();
-                       
-                       this._callbackObject._editorSuccess(this._media);
-                       
-                       UiDialog.close('mediaEditor_' + this._media.mediaID);
-                       
-                       this._media = null;
-               },
-               
-               /**
-                * Is called if an editor is manually closed by the user.
-                */
-               _close: function() {
-                       this._media = null;
-                       
-                       this._callbackObject._editorClose();
-               },
-               
-               /**
-                * Handles the `[ENTER]` key to submit the form.
-                * 
-                * @param       {object}        event           event object
-                */
-               _keyPress: function(event) {
-                       // 13 = [ENTER]
-                       if (event.charCode === 13) {
-                               event.preventDefault();
-                               
-                               this._saveData();
-                       }
-               },
-               
-               /**
-                * Saves the data of the currently edited media.
-                */
-               _saveData: function() {
-                       var content = UiDialog.getDialog('mediaEditor_' + this._media.mediaID).content;
-                       
-                       var altText = elBySel('input[name=altText]', content);
-                       var caption = elBySel('textarea[name=caption]', content);
-                       var title = elBySel('input[name=title]', content);
-                       
-                       var hasError = false;
-                       var altTextError = DomTraverse.childByClass(altText.parentNode.parentNode, 'innerError');
-                       var captionError = DomTraverse.childByClass(caption.parentNode.parentNode, 'innerError');
-                       var titleError = DomTraverse.childByClass(title.parentNode.parentNode, 'innerError');
-                       
-                       this._media.isMultilingual = ~~elBySel('input[name=isMultilingual]', content).checked;
-                       this._media.languageID = this._media.isMultilingual ? null : LanguageChooser.getLanguageId('languageID');
-                       
-                       this._media.altText = {};
-                       this._media.caption = {};
-                       this._media.title = {};
-                       if (this._media.isMultilingual) {
-                               if (!LanguageInput.validate('altText_' + this._media.mediaID, true)) {
-                                       hasError = true;
-                                       if (!altTextError) {
-                                               var error = elCreate('small');
-                                               error.className = 'innerError';
-                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
-                                               altText.parentNode.parentNode.appendChild(error);
-                                       }
-                               }
-                               if (!LanguageInput.validate('caption_' + this._media.mediaID, true)) {
-                                       hasError = true;
-                                       if (!captionError) {
-                                               var error = elCreate('small');
-                                               error.className = 'innerError';
-                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
-                                               caption.parentNode.parentNode.appendChild(error);
-                                       }
-                               }
-                               if (!LanguageInput.validate('title_' + this._media.mediaID, true)) {
-                                       hasError = true;
-                                       if (!titleError) {
-                                               var error = elCreate('small');
-                                               error.className = 'innerError';
-                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
-                                               thistitle.parentNode.parentNode.appendChild(error);
-                                       }
-                               }
-                               
-                               this._media.altText = LanguageInput.getValues('altText_' + this._media.mediaID).toObject();
-                               this._media.caption = LanguageInput.getValues('caption_' + this._media.mediaID).toObject();
-                               this._media.title = LanguageInput.getValues('title_' + this._media.mediaID).toObject();
-                       }
-                       else {
-                               this._media.altText[this._media.languageID] = altText.value;
-                               this._media.caption[this._media.languageID] = caption.value;
-                               this._media.title[this._media.languageID] = title.value;
-                       }
-                       
-                       var aclValues = {
-                               allowAll: ~~elById('mediaEditor_' + this._media.mediaID + '_aclAllowAll').checked,
-                               group: [],
-                               user: []
-                       };
-                       
-                       var aclGroups = elBySelAll('input[name="aclValues[group][]"]', content);
-                       for (var i = 0, length = aclGroups.length; i < length; i++) {
-                               aclValues.group.push(~~aclGroups[i].value);
-                       }
-                       
-                       var aclUsers = elBySelAll('input[name="aclValues[user][]"]', content);
-                       for (var i = 0, length = aclUsers.length; i < length; i++) {
-                               aclValues.user.push(~~aclUsers[i].value);
-                       }
-                       
-                       if (!hasError) {
-                               if (altTextError) elRemove(altTextError);
-                               if (captionError) elRemove(captionError);
-                               if (titleError) elRemove(titleError);
-                               
-                               Ajax.api(this, {
-                                       actionName: 'update',
-                                       objectIDs: [ this._media.mediaID ],
-                                       parameters: {
-                                               aclValues: aclValues,
-                                               altText: this._media.altText,
-                                               caption: this._media.caption,
-                                               data: {
-                                                       isMultilingual: this._media.isMultilingual,
-                                                       languageID: this._media.languageID
-                                               },
-                                               title: this._media.title
-                                       }
-                               });
-                       }
-               },
-               
-               /**
-                * Updates language-related input fields depending on whether multilingualism
-                * is enabled.
-                */
-               _updateLanguageFields: function(event, element) {
-                       if (event) element = event.currentTarget;
-                       
-                       var languageChooserContainer = elById('mediaEditor_' + this._media.mediaID + '_languageIDContainer').parentNode;
-                       
-                       if (element.checked) {
-                               LanguageInput.enable('title_' + this._media.mediaID);
-                               LanguageInput.enable('caption_' + this._media.mediaID);
-                               LanguageInput.enable('altText_' + this._media.mediaID);
-                               
-                               elHide(languageChooserContainer);
-                       }
-                       else {
-                               LanguageInput.disable('title_' + this._media.mediaID);
-                               LanguageInput.disable('caption_' + this._media.mediaID);
-                               LanguageInput.disable('altText_' + this._media.mediaID);
-                               
-                               elShow(languageChooserContainer);
-                       }
-               },
-               
-               /**
-                * Edits the media with the given data.
-                * 
-                * @param       {object}        media           data of the edited media
-                */
-               edit: function(media) {
-                       if (this._media !== null) {
-                               throw new Error("Cannot edit media with id '" + media.mediaID + "' while editing media with id '" + this._media.mediaID + "'");
-                       }
-                       
-                       this._media = media;
-                       
-                       if (!this._dialogs.has('mediaEditor_' + media.mediaID)) {
-                               this._dialogs.set('mediaEditor_' + media.mediaID, {
-                                       _dialogSetup: function() {
-                                               return {
-                                                       id: 'mediaEditor_' + media.mediaID,
-                                                       options: {
-                                                               backdropCloseOnClick: false,
-                                                               onClose: this._close.bind(this),
-                                                               title: Language.get('wcf.media.edit')
-                                                       },
-                                                       source: {
-                                                               after: (function(content, data) {
-                                                                       // make sure that the language chooser is initialized first
-                                                                       setTimeout(function() {
-                                                                               LanguageChooser.setLanguageId('languageID', this._media.languageID || LANGUAGE_ID);
-                                                                               
-                                                                               if (this._media.isMultilingual) {
-                                                                                       LanguageInput.setValues('altText_' + this._media.mediaID, Dictionary.fromObject(this._media.altText || { }));
-                                                                                       LanguageInput.setValues('caption_' + this._media.mediaID, Dictionary.fromObject(this._media.caption || { }));
-                                                                                       LanguageInput.setValues('title_' + this._media.mediaID, Dictionary.fromObject(this._media.title || { }));
-                                                                               }
-                                                                               
-                                                                               var isMultilingual = elBySel('input[name=isMultilingual]', content);
-                                                                               isMultilingual.addEventListener('change', this._updateLanguageFields.bind(this));
-                                                                               
-                                                                               this._updateLanguageFields(null, isMultilingual);
-                                                                               
-                                                                               var keyPress = this._keyPress.bind(this);
-                                                                               elBySel('input[name=altText]', content).addEventListener('keypress', keyPress);
-                                                                               elBySel('input[name=title]', content).addEventListener('keypress', keyPress);
-                                                                               
-                                                                               elBySel('button[data-type=submit]', content).addEventListener(WCF_CLICK_EVENT, this._saveData.bind(this));
-                                                                               
-                                                                               // remove focus from input elements and scroll dialog to top
-                                                                               document.activeElement.blur();
-                                                                               elById('mediaEditor_' + this._media.mediaID).parentNode.scrollTop = 0;
-                                                                               
-                                                                               DomChangeListener.trigger();
-                                                                       }.bind(this), 0);
-                                                               }).bind(this),
-                                                               data: {
-                                                                       actionName: 'getEditorDialog',
-                                                                       className: 'wcf\\data\\media\\MediaAction',
-                                                                       objectIDs: [media.mediaID]
-                                                               }
-                                                       }
-                                               };
-                                       }.bind(this)
-                               });
-                       }
-                       
-                       UiDialog.open(this._dialogs.get('mediaEditor_' + media.mediaID));
-               }
-       };
-       
-       return MediaEditor;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Base.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Base.js
deleted file mode 100644 (file)
index c51c51d..0000000
+++ /dev/null
@@ -1,444 +0,0 @@
-/**
- * Provides the media manager dialog.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Media/Manager/Base
- */
-define(
-       [
-               'Core',                     'Dictionary',               'Dom/ChangeListener',              'Dom/Traverse',
-               'Dom/Util',                 'EventHandler',             'Language',                        'List',
-               'Permission',               'Ui/Dialog',                'Ui/Notification',                 'WoltLab/WCF/Controller/Clipboard',
-               'WoltLab/WCF/Media/Editor', 'WoltLab/WCF/Media/Upload', 'WoltLab/WCF/Media/Manager/Search'
-       ],
-       function(
-               Core,                        Dictionary,                 DomChangeListener,                 DomTraverse,
-               DomUtil,                     EventHandler,               Language,                          List,
-               Permission,                  UiDialog,                   UiNotification,                    Clipboard,
-               MediaEditor,                 MediaUpload,                MediaManagerSearch
-       )
-{
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function MediaManagerBase(options) {
-               this._options = Core.extend({
-                       dialogTitle: Language.get('wcf.media.manager'),
-                       fileTypeFilters: {},
-                       minSearchLength: 3
-               }, options);
-               
-               this._media = new Dictionary();
-               this._mediaData = new Dictionary();
-               this._mediaCache = null;
-               this._mediaManagerMediaList = null;
-               this._search = null;
-               
-               if (Permission.get('admin.content.cms.canManageMedia')) {
-                       this._mediaEditor = new MediaEditor(this);
-               }
-               
-               DomChangeListener.add('WoltLab/WCF/Media/Manager', this._addButtonEventListeners.bind(this));
-       }
-       MediaManagerBase.prototype = {
-               /**
-                * Adds click event listeners to media buttons.
-                */
-               _addButtonEventListeners: function() {
-                       if (!this._mediaManagerMediaList) return;
-                       
-                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
-                       for (var i = 0, length = listItems.length; i < length; i++) {
-                               var listItem = listItems[i];
-                               
-                               if (Permission.get('admin.content.cms.canManageMedia')) {
-                                       var editIcon = elByClass('jsMediaEditIcon', listItem)[0];
-                                       if (editIcon) {
-                                               editIcon.classList.remove('jsMediaEditIcon');
-                                               editIcon.addEventListener(WCF_CLICK_EVENT, this._editMedia.bind(this));
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Handles clicks on the media manager button.
-                * 
-                * @param       {object}        event   event object
-                */
-               _click: function(event) {
-                       event.preventDefault();
-                       
-                       UiDialog.open(this);
-               },
-               
-               /**
-                * Reacts to executed clipboard actions.
-                * 
-                * @param       {object<string, *>}     actionData      data of the executed clipboard action
-                */
-               _clipboardAction: function(actionData) {
-                       // only consider events if the action has been executed
-                       if (actionData.data.actionName === 'com.woltlab.wcf.media.delete' && actionData.responseData === null) {
-                               var mediaIds = actionData.responseData.objectIDs;
-                               for (var i = 0, length = mediaIds.length; i < length; i++) {
-                                       this.removeMedia(~~mediaIds[i], true);
-                               }
-                               
-                               UiNotification.show();
-                       }
-               },
-               
-               /**
-                * Is called if the media manager dialog is closed.
-                */
-               _dialogClose: function() {
-                       // only show media clipboard if editor is open
-                       Clipboard.hideEditor('com.woltlab.wcf.media');
-               },
-               
-               /**
-                * Initializes the dialog when first loaded.
-                *
-                * @param       {string}        content         dialog content
-                * @param       {object}        data            AJAX request's response data
-                */
-               _dialogInit: function(content, data) {
-                       // store media data locally
-                       var media = data.returnValues.media || { };
-                       for (var mediaId in media) {
-                               if (objOwns(media, mediaId)) {
-                                       this._mediaData.set(~~mediaId, media[mediaId]);
-                               }
-                       }
-                       
-                       this._mediaManagerMediaList = elById('mediaManagerMediaList');
-                       
-                       // store list items locally
-                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
-                       for (var i = 0, length = listItems.length; i < length; i++) {
-                               var listItem = listItems[i];
-                               
-                               this._media.set(~~elData(listItem, 'object-id'), listItem);
-                       }
-                       
-                       if (Permission.get('admin.content.cms.canManageMedia')) {
-                               new MediaUpload('mediaManagerMediaUploadButton', 'mediaManagerMediaList', {
-                                       mediaManager: this
-                               });
-                               
-                               Clipboard.setup({
-                                       hasMarkedItems: data.returnValues.hasMarkedItems ? true : false,
-                                       pageClassName: 'menuManagerDialog-' + this.getMode()
-                               });
-                               
-                               EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.media', this._clipboardAction.bind(this));
-                       }
-                       
-                       this._search = new MediaManagerSearch(this);
-                       
-                       if (!listItems.length) {
-                               this._search.hideSearch();
-                       }
-                       
-                       this._dialogShow();
-               },
-               
-               /**
-                * Returns all data to setup the media manager dialog.
-                * 
-                * @return      {object}        dialog setup data
-                */
-               _dialogSetup: function() {
-                       return {
-                               id: 'mediaManager',
-                               options: {
-                                       onClose: this._dialogClose.bind(this),
-                                       onShow: this._dialogShow.bind(this),
-                                       title: this._options.dialogTitle
-                               },
-                               source: {
-                                       after: this._dialogInit.bind(this),
-                                       data: {
-                                               actionName: 'getManagementDialog',
-                                               className: 'wcf\\data\\media\\MediaAction',
-                                               parameters: {
-                                                       mode: this.getMode(),
-                                                       fileTypeFilters: this._options.fileTypeFilters
-                                               }
-                                       }
-                               }
-                       };
-               },
-               
-               /**
-                * Is called if the media manager dialog is shown.
-                */
-               _dialogShow: function() {
-                       if (!this._mediaManagerMediaList) return;
-                       
-                       // only show media clipboard if editor is open
-                       Clipboard.showEditor('com.woltlab.wcf.media');
-               },
-               
-               /**
-                * Opens the media editor for a media file.
-                * 
-                * @param       {Event}         event           event object for clicks on edit icons
-                */
-               _editMedia: function(event) {
-                       if (!Permission.get('admin.content.cms.canManageMedia')) {
-                               throw new Error("You are not allowed to edit media files.");
-                       }
-                       
-                       UiDialog.close('mediaManager');
-                       
-                       this._mediaEditor.edit(this._mediaData.get(~~elData(event.currentTarget, 'object-id')));
-               },
-               
-               /**
-                * Re-opens the manager dialog after closing the editor dialog.
-                */
-               _editorClose: function() {
-                       UiDialog.open(this);
-               },
-               
-               /**
-                * Re-opens the manager dialog and updates the media data after
-                * successfully editing a media file.
-                * 
-                * @param       {object}        media           updated media file data
-                */
-               _editorSuccess: function(media) {
-                       UiDialog.open(this);
-                       
-                       this._mediaData.set(~~media.mediaID, media);
-                       
-                       var listItem = this._media.get(~~media.mediaID);
-                       var p = elByClass('mediaTitle', listItem)[0];
-                       if (media.isMultilingual) {
-                               p.textContent = media.title[LANGUAGE_ID] || media.filename;
-                       }
-                       else {
-                               p.textContent = media.title[media.languageID] || media.filename;
-                       }
-               },
-               
-               /**
-                * Sets the displayed media (after a search).
-                * 
-                * @param       {Dictionary}    media           media to be set as active
-                */
-               _setMedia: function(media) {
-                       if (Core.isPlainObject(media)) {
-                               this._media = Dictionary.fromObject(media);
-                       }
-                       else {
-                               this._media = media;
-                       }
-                       
-                       var info = DomTraverse.nextByClass(this._mediaManagerMediaList, 'info');
-                       
-                       if (this._media.size) {
-                               if (info) {
-                                       elHide(info);
-                               }
-                       }
-                       else {
-                               if (info === null) {
-                                       info = elCreate('p');
-                                       info.className = 'info';
-                                       info.textContent = Language.get('wcf.media.search.noResults');
-                               }
-                               
-                               elShow(info);
-                               DomUtil.insertAfter(info, this._mediaManagerMediaList);
-                       }
-                       
-                       var mediaListItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
-                       for (var i = 0, length = mediaListItems.length; i < length; i++) {
-                               var listItem = mediaListItems[i];
-                               
-                               if (!this._media.has(elData(listItem, 'object-id'))) {
-                                       elHide(listItem);
-                               }
-                               else {
-                                       elShow(listItem);
-                               }
-                       }
-                       
-                       DomChangeListener.trigger();
-                       
-                       Clipboard.reload();
-               },
-               
-               /**
-                * Adds a media file to the manager.
-                * 
-                * @param       {object}        media           data of the media file
-                * @param       {Element}       listItem        list item representing the file
-                */
-               addMedia: function(media, listItem) {
-                       if (!media.languageID) media.isMultilingual = 1;
-                       
-                       this._mediaData.set(~~media.mediaID, media);
-                       this._media.set(~~media.mediaID, listItem);
-                       
-                       if (this._media.size === 1) {
-                               this._search.showSearch();
-                       }
-               },
-               
-               /**
-                * Returns the mode of the media manager.
-                *
-                * @return      {string}
-                */
-               getMode: function() {
-                       return '';
-               },
-               
-               /**
-                * Returns the media manager option with the given name.
-                * 
-                * @param       {string}        name            option name
-                * @return      {mixed}         option value or null
-                */
-               getOption: function(name) {
-                       if (this._options[name]) {
-                               return this._options[name];
-                       }
-                       
-                       return null;
-               },
-               
-               /**
-                * Removes a media file.
-                *
-                * @param       {int}                   mediaId         id of the removed media file
-                * @param       {boolean|undefined}     checkCache      media file will also be removed from the local cache if true
-                */
-               removeMedia: function(mediaId, checkCache) {
-                       if (this._media.has(mediaId)) {
-                               // remove list item
-                               elRemove(this._media.get(mediaId));
-                               
-                               this._media.delete(mediaId);
-                               this._mediaData.delete(mediaId);
-                       }
-                       
-                       if (checkCache && this._mediaCache && this._mediaCache.has(mediaId)) {
-                               this._mediaCache.delete(mediaId);
-                       }
-               },
-               
-               /**
-                * Changes the displayed media to the previously displayed media.
-                */
-               resetMedia: function() {
-                       if (this._mediaCache !== null) {
-                               this._setMedia(this._mediaCache);
-                               
-                               this._mediaCache = null;
-                               
-                               this._search.resetSearch();
-                       }
-               },
-               
-               /**
-                * Sets the media files currently displayed.
-                * 
-                * @param       {object}        media           media data
-                * @param       {string}        template        
-                */
-               setMedia: function(media, template) {
-                       if (!this._mediaCache) {
-                               this._mediaCache = this._media;
-                       }
-                       
-                       var hasMedia = false;
-                       for (var mediaId in media) {
-                               if (objOwns(media, mediaId)) {
-                                       hasMedia = true;
-                               }
-                       }
-                       
-                       var newListItems = [];
-                       if (hasMedia) {
-                               var ul = elCreate('ul');
-                               ul.innerHTML = template;
-                               
-                               var listItems = DomTraverse.childrenByTag(ul, 'LI');
-                               for (var i = 0, length = listItems.length; i < length; i++) {
-                                       var listItem = listItems[i];
-                                       if (!this._mediaData.has(~~elData(listItem, 'object-id'))) {
-                                               this._mediaData.set(elData(listItem, 'object-id'), listItem);
-                                               
-                                               this._mediaManagerMediaList.appendChild(listItem);
-                                       }
-                               }
-                       }
-                       
-                       this._setMedia(media);
-               },
-               
-               /**
-                * Sets up a new media element.
-                * 
-                * @param       {object}        media           data of the media file
-                * @param       {HTMLElement}   mediaElement    element representing the media file
-                */
-               setupMediaElement: function(media, mediaElement) {
-                       var mediaInformation = DomTraverse.childByClass(mediaElement, 'mediaInformation');
-                       
-                       var buttonGroupNavigation = elCreate('nav');
-                       buttonGroupNavigation.className = 'buttonGroupNavigation';
-                       mediaInformation.parentNode.appendChild(buttonGroupNavigation);
-                       
-                       var smallButtons = elCreate('ul');
-                       smallButtons.className = 'smallButtons buttonGroup';
-                       buttonGroupNavigation.appendChild(smallButtons);
-                       
-                       var listItem = elCreate('li');
-                       smallButtons.appendChild(listItem);
-                       
-                       var checkbox = elCreate('input');
-                       checkbox.className = 'jsClipboardItem jsMediaCheckbox';
-                       elAttr(checkbox, 'type', 'checkbox');
-                       elData(checkbox, 'object-id', media.mediaID);
-                       listItem.appendChild(checkbox);
-                       
-                       if (Permission.get('admin.content.cms.canManageMedia')) {
-                               listItem = elCreate('li');
-                               smallButtons.appendChild(listItem);
-                               
-                               var a = elCreate('a');
-                               listItem.appendChild(a);
-                               
-                               var icon = elCreate('span');
-                               icon.className = 'icon icon16 fa-pencil jsTooltip jsMediaEditIcon';
-                               elData(icon, 'object-id', media.mediaID);
-                               elAttr(icon, 'title', Language.get('wcf.global.button.edit'));
-                               a.appendChild(icon);
-                               
-                               listItem = elCreate('li');
-                               smallButtons.appendChild(listItem);
-                               
-                               a = elCreate('a');
-                               listItem.appendChild(a);
-                               
-                               icon = elCreate('span');
-                               icon.className = 'icon icon16 fa-times jsTooltip jsMediaDeleteIcon';
-                               elData(icon, 'object-id', media.mediaID);
-                               elAttr(icon, 'title', Language.get('wcf.global.button.delete'));
-                               a.appendChild(icon);
-                       }
-               }
-       };
-       
-       return MediaManagerBase;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Editor.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Editor.js
deleted file mode 100644 (file)
index 6cd2f4b..0000000
+++ /dev/null
@@ -1,340 +0,0 @@
-/**
- * Provides the media manager dialog for selecting media for Redactor editors.
- *
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Media/Manager/Editor
- */
-define(['Core', 'Dictionary', 'Dom/Traverse', 'Language', 'Ui/Dialog', 'WoltLab/WCF/Controller/Clipboard', 'WoltLab/WCF/Media/Manager/Base'],
-       function(Core, Dictionary, DomTraverse, Language, UiDialog, ControllerClipboard, MediaManagerBase) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function MediaManagerEditor(options) {
-               options = Core.extend({
-                       callbackInsert: null
-               }, options);
-               
-               MediaManagerBase.call(this, options);
-               
-               this._activeButton = null;
-               this._buttons = elByClass(this._options.buttonClass || 'jsMediaEditorButton');
-               for (var i = 0, length = this._buttons.length; i < length; i++) {
-                       this._buttons[i].addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-               }
-               this._mediaToInsert = new Dictionary();
-               this._mediaToInsertByClipboard = false;
-       }
-       Core.inherit(MediaManagerEditor, MediaManagerBase, {
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#_addButtonEventListeners
-                */
-               _addButtonEventListeners: function() {
-                       MediaManagerEditor._super.prototype._addButtonEventListeners.call(this);
-                       
-                       if (!this._mediaManagerMediaList) return;
-                       
-                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
-                       for (var i = 0, length = listItems.length; i < length; i++) {
-                               var listItem = listItems[i];
-                               
-                               var insertIcon = elByClass('jsMediaInsertIcon', listItem)[0];
-                               if (insertIcon) {
-                                       insertIcon.classList.remove('jsMediaInsertIcon');
-                                       insertIcon.addEventListener(WCF_CLICK_EVENT, this._openInsertDialog.bind(this));
-                               }
-                       }
-               },
-               
-               /**
-                * Builds the dialog to setup inserting media files.
-                */
-               _buildInsertDialog: function() {
-                       var thumbnailOptions = '';
-                       
-                       var sizes = ['small', 'medium', 'large'];
-                       var size, option;
-                       lengthLoop: for (var i = 0, length = sizes.length; i < length; i++) {
-                               size = sizes[i];
-                               
-                               // make sure that all thumbnails support the thumbnail size
-                               for (var j = 0, mediaLength = this._mediaToInsert.length; j < mediaLength; j++) {
-                                       if (!this._mediaToInsert[i][size + 'ThumbnailType']) {
-                                               continue lengthLoop;
-                                       }
-                               }
-                               
-                               thumbnailOptions += '<option value="' + size + '">' + Language.get('wcf.media.insert.imageSize.' + size) + '</option>';
-                       }
-                       thumbnailOptions += '<option value="original">' + Language.get('wcf.media.insert.imageSize.original') + '</option>';
-                       
-                       var dialog = '<div class="section">'
-                       + (this._mediaToInsert.size > 1 ? '<dl>'
-                               + '<dt>' + Language.get('wcf.media.insert.type') + '</dt>'
-                               + '<dd>'
-                                       + '<select name="insertType">'
-                                               + '<option value="separate">' + Language.get('wcf.media.insert.type.separate') + '</option>'
-                                               + '<option value="gallery">' + Language.get('wcf.media.insert.type.gallery') + '</option>'
-                                       + '</select>'
-                               + '</dd>'
-                       + '</dl>' : '')
-                       + '<dl class="thumbnailSizeSelection">'
-                               + '<dt>' + Language.get('wcf.media.insert.imageSize') + '</dt>'
-                               + '<dd>'
-                                       + '<select name="thumbnailSize">'
-                                               + thumbnailOptions
-                                       + '</select>'
-                               + '</dd>'
-                       + '</dl>'
-                       + '</div>'
-                       + '<div class="formSubmit">'
-                               + '<button class="buttonPrimary">' + Language.get('wcf.global.button.insert') + '</button>'
-                       + '</div>';
-                       
-                       UiDialog.open({
-                               _dialogSetup: (function() {
-                                       return {
-                                               id: this._getInsertDialogId(),
-                                               options: {
-                                                       onClose: this._editorClose.bind(this),
-                                                       onSetup: function(content) {
-                                                               elByClass('buttonPrimary', content)[0].addEventListener(WCF_CLICK_EVENT, this._insertMedia.bind(this));
-                                                               
-                                                               // toggle thumbnail size selection based on selected insert type
-                                                               var insertType = elBySel('select[name=insertType]', content);
-                                                               if (insertType !== null) {
-                                                                       var thumbnailSelection = elByClass('thumbnailSizeSelection', content)[0];
-                                                                       insertType.addEventListener('change', function(event) {
-                                                                               if (event.currentTarget.value === 'gallery') {
-                                                                                       elHide(thumbnailSelection);
-                                                                               }
-                                                                               else {
-                                                                                       elShow(thumbnailSelection);
-                                                                               }
-                                                                       });
-                                                               }
-                                                       }.bind(this),
-                                                       title: Language.get('wcf.media.insert')
-                                               },
-                                               source: dialog
-                                       };
-                               }).bind(this)
-                       });
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#_click
-                */
-               _click: function(event) {
-                       this._activeButton = event.currentTarget;
-                       
-                       MediaManagerEditor._super.prototype._click.call(this, event);
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#_clipboardAction
-                */
-               _clipboardAction: function(actionData) {
-                       MediaManagerEditor._super.prototype._clipboardAction.call(this, actionData);
-                       
-                       if (actionData.data.actionName === 'com.woltlab.wcf.media.insert') {
-                               this.insertMedia(actionData.data.parameters.objectIDs, true);
-                       }
-               },
-               
-               /**
-                * Returns the id of the insert dialog based on the media files to be inserted.
-                * 
-                * @return      {string}        insert dialog id
-                */
-               _getInsertDialogId: function() {
-                       var dialogId = 'mediaInsert';
-                       
-                       this._mediaToInsert.forEach(function(media, mediaId) {
-                               dialogId += '-' + mediaId;
-                       });
-                       
-                       return dialogId;
-               },
-               
-               /**
-                * Inserts media files into redactor.
-                * 
-                * @param       {Event?}        event
-                */
-               _insertMedia: function(event) {
-                       var insertType = 'separate';
-                       var thumbnailSize;
-                       
-                       // update insert options with selected values if method is called by clicking on 'insert' button
-                       // in dialog
-                       if (event) {
-                               UiDialog.close(this._getInsertDialogId());
-                               
-                               var dialogContent = event.currentTarget.closest('.dialogContent');
-                               
-                               if (this._mediaToInsert.size > 1) {
-                                       insertType = elBySel('select[name=insertType]', dialogContent).value;
-                               }
-                               thumbnailSize = elBySel('select[name=thumbnailSize]', dialogContent).value;
-                       }
-                       
-                       if (this._options.callbackInsert !== null) {
-                               this._options.callbackInsert(this._mediaToInsert, insertType, thumbnailSize);
-                       }
-                       else {
-                               if (insertType === 'separate') {
-                                       this._options.editor.buffer.set();
-                                       
-                                       this._mediaToInsert.forEach(this._insertMediaItem.bind(this, thumbnailSize));
-                               }
-                               else {
-                                       this._insertMediaGallery();
-                               }
-                       }
-                       
-                       if (this._mediaToInsertByClipboard) {
-                               var mediaIds = [];
-                               this._mediaToInsert.forEach(function(media) {
-                                       mediaIds.push(media.mediaID);
-                               })
-                               
-                               ControllerClipboard.unmark('com.woltlab.wcf.media', mediaIds);
-                       }
-                       
-                       this._mediaToInsert = new Dictionary();
-                       this._mediaToInsertByClipboard = false;
-                       
-                       // close manager dialog
-                       UiDialog.close(this);
-               },
-               
-               /**
-                * Inserts a series of uploaded images using a slider.
-                * 
-                * @protected
-                */
-               _insertMediaGallery: function() {
-                       var mediaIds = [];
-                       this._mediaToInsert.forEach(function(item) {
-                               mediaIds.push(item.mediaID);
-                       });
-                       
-                       this._options.editor.buffer.set();
-                       this._options.editor.insert.text("[wsmg='" + mediaIds.join(',') + "'][/wsmg]");
-               },
-               
-               /**
-                * Inserts a single media item.
-                * 
-                * @param       {string}        thumbnailSize   preferred image dimension, is ignored for non-images
-                * @param       {Object}        item            media item data
-                * @protected
-                */
-               _insertMediaItem: function(thumbnailSize, item) {
-                       if (item.isImage) {
-                               var sizes = ['small', 'medium', 'large', 'original'];
-                               
-                               // check if size is actually available
-                               var available = '', size;
-                               for (var i = 0; i < 4; i++) {
-                                       size = sizes[i];
-                                       
-                                       if (item[size + 'ThumbnailHeight']) {
-                                               available = size;
-                                               
-                                               if (thumbnailSize == size) {
-                                                       break;
-                                               }
-                                       }
-                               }
-                               
-                               thumbnailSize = available;
-                               
-                               this._options.editor.insert.html('<img src="' + item[thumbnailSize + 'ThumbnailLink'] + '" class="woltlabSuiteMedia" data-media-id="' + item.mediaID + '" data-media-size="' + thumbnailSize + '">');
-                       }
-                       else {
-                               this._options.editor.insert.text("[wsm='" + item.mediaID + "'][/wsm]");
-                       }
-               },
-               
-               /**
-                * Handles clicking on the insert button.
-                * 
-                * @param       {Event}         event           insert button click event
-                */
-               _openInsertDialog: function(event) {
-                       this.insertMedia([~~elData(event.currentTarget, 'object-id')]);
-               },
-               
-               /**
-                * Prepares insertion of the media files with the given ids.
-                * 
-                * @param       {array<int>}    mediaIds                ids of the media files to be inserted
-                * @param       {boolean?}      insertedByClipboard     is true if the media files are inserted by clipboard
-                */
-               insertMedia: function(mediaIds, insertedByClipboard) {
-                       this._mediaToInsert = new Dictionary();
-                       this._mediaToInsertByClipboard = insertedByClipboard || false;
-                       
-                       // open the insert dialog if all media files are images
-                       var imagesOnly = true, media;
-                       for (var i = 0, length = mediaIds.length; i < length; i++) {
-                               media = this._mediaData.get(mediaIds[i]);
-                               this._mediaToInsert.set(media.mediaID, media);
-                               
-                               if (!media.isImage) {
-                                       imagesOnly = false;
-                               }
-                       }
-                       
-                       if (imagesOnly) {
-                               UiDialog.close(this);
-                               var dialogId = this._getInsertDialogId();
-                               if (UiDialog.getDialog(dialogId)) {
-                                       UiDialog.openStatic(dialogId);
-                               }
-                               else {
-                                       this._buildInsertDialog();
-                               }
-                       }
-                       else {
-                               this._insertMedia();
-                       }
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#getMode
-                */
-               getMode: function() {
-                       return 'editor';
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#setupMediaElement
-                */
-               setupMediaElement: function(media, mediaElement) {
-                       MediaManagerEditor._super.prototype.setupMediaElement.call(this, media, mediaElement);
-                       
-                       // add media insertion icon
-                       var smallButtons = elBySel('nav.buttonGroupNavigation > ul.smallButtons', mediaElement);
-                       
-                       var listItem = elCreate('li');
-                       smallButtons.appendChild(listItem);
-                       
-                       var a = elCreate('a');
-                       listItem.appendChild(a);
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon16 fa-plus jsTooltip jsMediaInsertIcon';
-                       elData(icon, 'object-id', media.mediaID);
-                       elAttr(icon, 'title', Language.get('wcf.media.button.insert'));
-                       a.appendChild(icon);
-               }
-       });
-       
-       return MediaManagerEditor;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Search.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Search.js
deleted file mode 100644 (file)
index a8dd504..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-/**
- * Provides the media search for the media manager.
- *
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Media/Manager/Search
- */
-define(['Ajax', 'Core', 'Dom/Traverse', 'Dom/Util', 'Language', 'WoltLab/WCF/Media/Search', 'Ui/SimpleDropdown'], function(Ajax, Core, DomTraverse, DomUtil, Language, MediaSearch, UiSimpleDropdown) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function MediaManagerSearch(mediaManager) {
-               MediaSearch.call(this);
-               
-               this._mediaManager = mediaManager;
-               this._searchMode = false;
-               
-               this._input = elById(this._getIdPrefix() + 'SearchField');
-               this._input.addEventListener('keypress', this._keyPress.bind(this));
-               
-               this._cancelButton = elById(this._getIdPrefix() + 'SearchCancelButton');
-               this._cancelButton.addEventListener(WCF_CLICK_EVENT, this._cancelSearch.bind(this));
-       }
-       Core.inherit(MediaManagerSearch, MediaSearch, {
-               /**
-                * Returns the data for Ajax to setup the Ajax/Request object.
-                *
-                * @return      {object}        setup data for Ajax/Request object
-                */
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'getSearchResultList',
-                                       className: 'wcf\\data\\media\\MediaAction',
-                                       interfaceName: 'wcf\\data\\ISearchAction'
-                               }
-                       };
-               },
-               
-               /**
-                * Handles successful AJAX requests.
-                *
-                * @param       {object}        data    response data
-                */
-               _ajaxSuccess: function(data) {
-                       this._mediaManager.setMedia(data.returnValues.media || { }, data.returnValues.template || '');
-               },
-               
-               /**
-                * Cancels the search after clicking on the cancel search button.
-                */
-               _cancelSearch: function() {
-                       if (this._searchMode) {
-                               this._searchMode = false;
-                               
-                               this._mediaManager.resetMedia();
-                               this.resetSearch();
-                       }
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Search#_getIdPrefix
-                */
-               _getIdPrefix: function() {
-                       return 'mediaManager';
-               },
-               
-               /**
-                * Handles the `[ENTER]` key to submit the form.
-                *
-                * @param       {Event}         event           event object
-                */
-               _keyPress: function(event) {
-                       // 13 = [ENTER]
-                       if (event.charCode === 13) {
-                               event.preventDefault();
-                               
-                               var innerInfo = DomTraverse.childByClass(this._input.parentNode.parentNode, 'innerInfo');
-                               
-                               if (this._input.value.length >= this._mediaManager.getOption('minSearchLength')) {
-                                       if (innerInfo) {
-                                               elHide(innerInfo);
-                                       }
-                                       
-                                       this._search();
-                               }
-                               else {
-                                       if (innerInfo) {
-                                               elShow(innerInfo);
-                                       }
-                                       else {
-                                               innerInfo = elCreate('p');
-                                               innerInfo.className = 'innerInfo';
-                                               innerInfo.textContent = Language.get('wcf.media.search.info.searchStringThreshold');
-                                               
-                                               DomUtil.insertAfter(innerInfo, this._input.parentNode);
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Sends an AJAX request to fetch search results.
-                */
-               _search: function() {
-                       this._searchMode = true;
-                       
-                       Ajax.api(this, {
-                               parameters: {
-                                       fileType: this._fileType,
-                                       fileTypeFilters: this._mediaManager.getOption('fileTypeFilters'),
-                                       mode: this._mediaManager.getMode(),
-                                       searchString: this._input.value
-                               }
-                       });
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Search#_selectFileType
-                */
-               _selectFileType: function(event) {
-                       MediaManagerSearch._super.prototype._selectFileType.call(this, event);
-                       
-                       this._search();
-               },
-               
-               /**
-                * Hides the media search.
-                */
-               hideSearch: function() {
-                       elHide(elById(this._getIdPrefix() + 'Search'));
-               },
-               
-               /**
-                * Resets the media search.
-                */
-               resetSearch: function() {
-                       this._input.value = '';
-                       this._fileType = 'all';
-                       
-                       this._updateDropdownButtonLabel();
-               },
-               
-               /**
-                * Shows the media search.
-                */
-               showSearch: function() {
-                       elShow(elById(this._getIdPrefix() + 'Search'));
-               }
-       });
-       
-       return MediaManagerSearch;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Select.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Manager/Select.js
deleted file mode 100644 (file)
index e52b804..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-/**
- * Provides the media manager dialog for selecting media for input elements.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Media/Manager/Select
- */
-define(['Core', 'Dom/Traverse', 'Dom/Util', 'Language', 'ObjectMap', 'Ui/Dialog', 'WoltLab/WCF/File/Util', 'WoltLab/WCF/Media/Manager/Base'],
-       function(Core, DomTraverse, DomUtil, Language, ObjectMap, UiDialog, FileUtil, MediaManagerBase) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function MediaManagerSelect(options) {
-               MediaManagerBase.call(this, options);
-               
-               this._activeButton = null;
-               this._buttons = elByClass(this._options.buttonClass || 'jsMediaSelectButton');
-               this._storeElements = new ObjectMap();
-               
-               for (var i = 0, length = this._buttons.length; i < length; i++) {
-                       var button = this._buttons[i];
-                       
-                       // only consider buttons with a proper store specified
-                       var store = elData(button, 'store');
-                       if (store) {
-                               var storeElement = elById(store);
-                               if (storeElement && storeElement.tagName === 'INPUT') {
-                                       this._buttons[i].addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-                                       
-                                       this._storeElements.set(button, storeElement);
-                                       
-                                       // add remove button
-                                       var removeButton = elCreate('p');
-                                       removeButton.className = 'button';
-                                       DomUtil.insertAfter(removeButton, button);
-                                       
-                                       var icon = elCreate('span');
-                                       icon.className = 'icon icon16 fa-times';
-                                       removeButton.appendChild(icon);
-                                       
-                                       if (!storeElement.value) elHide(removeButton);
-                                       removeButton.addEventListener(WCF_CLICK_EVENT, this._removeMedia.bind(this));
-                               }
-                       }
-               }
-       }
-       Core.inherit(MediaManagerSelect, MediaManagerBase, {
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#_addButtonEventListeners
-                */
-               _addButtonEventListeners: function() {
-                       MediaManagerSelect._super.prototype._addButtonEventListeners.call(this);
-                       
-                       if (!this._mediaManagerMediaList) return;
-                       
-                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
-                       for (var i = 0, length = listItems.length; i < length; i++) {
-                               var listItem = listItems[i];
-                               
-                               var chooseIcon = elByClass('jsMediaSelectIcon', listItem)[0];
-                               if (chooseIcon) {
-                                       chooseIcon.classList.remove('jsMediaSelectIcon');
-                                       chooseIcon.addEventListener(WCF_CLICK_EVENT, this._chooseMedia.bind(this));
-                               }
-                       }
-               },
-               
-               /**
-                * Handles clicking on a media choose icon.
-                * 
-                * @param       {Event}         event           click event
-                */
-               _chooseMedia: function(event) {
-                       if (this._activeButton === null) {
-                               throw new Error("Media cannot be chosen if no button is active.");
-                       }
-                       
-                       var media = this._mediaData.get(~~elData(event.currentTarget, 'object-id'));
-                       
-                       // save selected media in store element
-                       elById(elData(this._activeButton, 'store')).value = media.mediaID;
-                       
-                       // display selected media
-                       var display = elData(this._activeButton, 'display');
-                       if (display) {
-                               var displayElement = elById(display);
-                               if (displayElement) {
-                                       if (media.isImage) {
-                                               displayElement.innerHTML = '<img src="' + media.smallThumbnailLink + '" alt="' + media.altText + '" />';
-                                       }
-                                       else {
-                                               displayElement.innerHTML = '<div class="box48" style="margin-bottom: 10px;">'
-                                                       + '<span class="icon icon48 ' + FileUtil.getIconClassByMimeType(media.fileType) + '"></span>'
-                                                       + '<div class="containerHeadline">'
-                                                               + '<h3>' + media.filename + '</h3>'
-                                                               + '<p>' + media.formattedFilesize + '</p>'
-                                                       + '</div>'
-                                               + '</div>';
-                                       }
-                               }
-                       }
-                       
-                       // show remove button
-                       elShow(this._activeButton.nextElementSibling);
-                       
-                       UiDialog.close('mediaManager');
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#_click
-                */
-               _click: function(event) {
-                       event.preventDefault();
-                       this._activeButton = event.currentTarget;
-                       
-                       MediaManagerSelect._super.prototype._click.call(this, event);
-                       
-                       if (!this._mediaManagerMediaList) return;
-                       
-                       var storeElement = this._storeElements.get(this._activeButton);
-                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI'), listItem;
-                       for (var i = 0, length = listItems.length; i < length; i++) {
-                               listItem = listItems[i];
-                               if (storeElement.value && storeElement.value == elData(listItem, 'object-id')) {
-                                       listItem.classList.add('jsSelected');
-                               }
-                               else {
-                                       listItem.classList.remove('jsSelected');
-                               }
-                       }
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#getMode
-                */
-               getMode: function() {
-                       return 'select';
-               },
-               
-               /**
-                * @see WoltLab/WCF/Media/Manager/Base#setupMediaElement
-                */
-               setupMediaElement: function(media, mediaElement) {
-                       MediaManagerSelect._super.prototype.setupMediaElement.call(this, media, mediaElement);
-                       
-                       // add media insertion icon
-                       var smallButtons = elBySel('nav.buttonGroupNavigation > ul.smallButtons', mediaElement);
-                       
-                       var listItem = elCreate('li');
-                       smallButtons.appendChild(listItem);
-                       
-                       var a = elCreate('a');
-                       listItem.appendChild(a);
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon16 fa-check jsTooltip jsMediaSelectIcon';
-                       elData(icon, 'object-id', media.mediaID);
-                       elAttr(icon, 'title', Language.get('wcf.media.button.choose'));
-                       a.appendChild(icon);
-               },
-               
-               /**
-                * Handles clicking on the remove button.
-                *
-                * @param       {Event}         event           click event
-                */
-               _removeMedia: function(event) {
-                       event.preventDefault();
-                       
-                       var removeButton = event.currentTarget;
-                       elHide(removeButton);
-                       
-                       var button = removeButton.previousElementSibling;
-                       elById(elData(button, 'store')).value = 0;
-                       var display = elData(button, 'display');
-                       if (display) {
-                               var displayElement = elById(display);
-                               if (displayElement) {
-                                       displayElement.innerHTML = '';
-                               }
-                       }
-               }
-       });
-       
-       return MediaManagerSelect;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Search.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Search.js
deleted file mode 100644 (file)
index aa88a1b..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Provides the media search for the media manager.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Media/Search
- */
-define(['Ajax', 'Dom/Traverse', 'Dom/Util', 'Language', 'Ui/SimpleDropdown'], function(Ajax, DomTraverse, DomUtil, Language, UiSimpleDropdown) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function MediaSearch(initialFileType) {
-               this._fileType = 'all';
-               
-               var dropdown = UiSimpleDropdown.getDropdownMenu(this._getIdPrefix() + 'Search');
-               if (dropdown) {
-                       this._fileTypes = DomTraverse.childrenBySel(dropdown, 'li:not(.dropdownDivider)');
-                       
-                       var selectFileType = this._selectFileType.bind(this);
-                       for (var i = 0, length = this._fileTypes.length; i < length; i++) {
-                               var listItem = this._fileTypes[i];
-                               
-                               if (initialFileType && elData(listItem, 'file-type') == initialFileType) {
-                                       this._fileType = initialFileType;
-                               }
-                               
-                               this._fileTypes[i].addEventListener(WCF_CLICK_EVENT, selectFileType);
-                       }
-                       
-                       if (initialFileType && initialFileType.length) {
-                               this._updateDropdownButtonLabel();
-                       }
-                       
-                       UiSimpleDropdown.registerCallback(this._getIdPrefix() + 'Search', this._updateFileTypeDropdown.bind(this));
-                       
-                       var form = DomTraverse.parentByTag(elById(this._getIdPrefix() + 'Search'), 'FORM');
-                       if (form) {
-                               form.addEventListener('submit', function() {
-                                       var fileTypeInput = elCreate('input');
-                                       elAttr(fileTypeInput, 'type', 'hidden');
-                                       elAttr(fileTypeInput, 'name', 'fileType');
-                                       elAttr(fileTypeInput, 'value', this._fileType);
-                                       
-                                       form.appendChild(fileTypeInput);
-                               }.bind(this));
-                       }
-               }
-               else {
-                       this._fileType = null;
-               }
-       }
-       MediaSearch.prototype = {
-               /**
-                * Returns the prefix to identify search-related elements.
-                * 
-                * @return      {string}
-                */
-               _getIdPrefix: function() {
-                       return 'media';
-               },
-               
-               /**
-                * Selects a certain file type after clicking on it in the dropdown menu.
-                *
-                * @param       {Event}         event
-                */
-               _selectFileType: function(event) {
-                       this._fileType = elData(event.currentTarget, 'file-type');
-                       
-                       this._updateDropdownButtonLabel(event);
-               },
-               
-               /**
-                * Updates the label of the dropdown button based on the currently selected file type.
-                */
-               _updateDropdownButtonLabel: function(event) {
-                       var dropdown = UiSimpleDropdown.getDropdown(this._getIdPrefix() + 'Search');
-                       var buttonLabel = DomTraverse.childBySel(DomTraverse.childByClass(dropdown, 'dropdownToggle'), 'SPAN');
-                       
-                       if (this._fileType !== 'all') {
-                               var listItem;
-                               if (event) {
-                                       listItem = event.currentTarget;
-                               }
-                               else {
-                                       for (var i = 0, length = this._fileTypes.length; i < length; i++) {
-                                               var _listItem = this._fileTypes[i];
-                                               
-                                               if (elData(_listItem, 'file-type') == this._fileType) {
-                                                       listItem = _listItem;
-                                                       break;
-                                               }
-                                       }
-                               }
-                               
-                               buttonLabel.textContent = DomTraverse.childBySel(listItem, 'SPAN').textContent;
-                       }
-                       else {
-                               buttonLabel.textContent = Language.get('wcf.media.search.filetype');
-                       }
-               },
-               
-               /**
-                * Updates the file type dropdown by correctly marking the currently selected file type.
-                */
-               _updateFileTypeDropdown: function() {
-                       for (var i = 0, length = this._fileTypes.length; i < length; i++) {
-                               var listItem = this._fileTypes[i];
-                               
-                               listItem.classList[elData(listItem, 'file-type') === this._fileType ? 'add' : 'remove']('active');
-                       }
-               }
-       };
-       
-       return MediaSearch;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Media/Upload.js b/wcfsetup/install/files/js/WoltLab/WCF/Media/Upload.js
deleted file mode 100644 (file)
index aba3291..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-/**
- * Uploads media files.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Media/Upload
- */
-define(
-       [
-               'Core',                'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util',
-               'EventHandler',        'Language',           'Permission',   'Upload',
-               'WoltLab/WCF/File/Util'
-       ],
-       function(
-               Core,                   DomChangeListener,    DomTraverse,    DomUtil,
-               EventHandler,           Language,             Permission,     Upload,
-               FileUtil
-       )
-{
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function MediaUpload(buttonContainerId, targetId, options) {
-               options = options || {};
-               
-               this._mediaManager = null;
-               if (options.mediaManager) {
-                       this._mediaManager = options.mediaManager;
-                       delete options.mediaManager;
-               }
-               
-               Upload.call(this, buttonContainerId, targetId, Core.extend({
-                       className: 'wcf\\data\\media\\MediaAction',
-                       multiple: this._mediaManager ? true : false,
-                       singleFileRequests: true
-               }, options));
-       }
-       Core.inherit(MediaUpload, Upload, {
-               /**
-                * @see WoltLab/WCF/Upload#_createFileElement
-                */
-               _createFileElement: function(file) {
-                       var fileElement;
-                       if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
-                               fileElement = elCreate('li');
-                       }
-                       else {
-                               fileElement = elCreate('p');
-                       }
-                       
-                       var thumbnail = elCreate('div');
-                       thumbnail.className = 'mediaThumbnail';
-                       fileElement.appendChild(thumbnail);
-                       
-                       var fileIcon = elCreate('span');
-                       fileIcon.className = 'icon icon144 fa-spinner';
-                       thumbnail.appendChild(fileIcon);
-                       
-                       var mediaInformation = elCreate('div');
-                       mediaInformation.className = 'mediaInformation';
-                       fileElement.appendChild(mediaInformation);
-                       
-                       var p = elCreate('p');
-                       p.className = 'mediaTitle';
-                       p.textContent = file.name;
-                       mediaInformation.appendChild(p);
-                       
-                       var progress = elCreate('progress');
-                       elAttr(progress, 'max', 100);
-                       mediaInformation.appendChild(progress);
-                       
-                       DomUtil.prepend(fileElement, this._target);
-                       
-                       DomChangeListener.trigger();
-                       
-                       return fileElement;
-               },
-               
-               /**
-                * @see WoltLab/WCF/Upload#_getParameters
-                */
-               _getParameters: function() {
-                       if (this._mediaManager) {
-                               return Core.extend(MediaUpload._super.prototype._getParameters.call(this), {
-                                       fileTypeFilters: this._mediaManager.getOption('fileTypeFilters')
-                               });
-                       }
-                       
-                       return MediaUpload._super.prototype._getParameters.call(this);
-               },
-               
-               /**
-                * @see WoltLab/WCF/Upload#_success
-                */
-               _success: function(uploadId, data) {
-                       var files = this._fileElements[uploadId];
-                       
-                       for (var i = 0, length = files.length; i < length; i++) {
-                               var file = files[i];
-                               var internalFileId = elData(file, 'internal-file-id');
-                               var media = data.returnValues.media[internalFileId];
-                               
-                               elRemove(DomTraverse.childByTag(DomTraverse.childByClass(file, 'mediaInformation'), 'PROGRESS'));
-                               
-                               if (media) {
-                                       var fileIcon = DomTraverse.childByTag(DomTraverse.childByClass(file, 'mediaThumbnail'), 'SPAN');
-                                       if (media.tinyThumbnailType) {
-                                               var parentNode = fileIcon.parentNode;
-                                               elRemove(fileIcon);
-                                               
-                                               var img = elCreate('img');
-                                               elAttr(img, 'src', media.tinyThumbnailLink);
-                                               elAttr(img, 'alt', '');
-                                               img.style.setProperty('width', '144px');
-                                               img.style.setProperty('height', '144px');
-                                               parentNode.appendChild(img);
-                                       }
-                                       else {
-                                               fileIcon.classList.remove('fa-spinner');
-                                               fileIcon.classList.add(FileUtil.getIconClassByMimeType(media.fileType));
-                                       }
-                                       
-                                       file.className = 'jsClipboardObject';
-                                       elData(file, 'object-id', media.mediaID);
-                                       
-                                       if (this._mediaManager) {
-                                               this._mediaManager.setupMediaElement(media, file);
-                                               this._mediaManager.resetMedia();
-                                               this._mediaManager.addMedia(media, file);
-                                       }
-                               }
-                               else {
-                                       var error = data.returnValues.errors[internalFileId];
-                                       if (!error) {
-                                               error = {
-                                                       errorType: 'uploadFailed',
-                                                       filename: elData(file, 'filename')
-                                               };
-                                       }
-                                       
-                                       var fileIcon = DomTraverse.childByTag(DomTraverse.childByClass(file, 'mediaThumbnail'), 'SPAN');
-                                       fileIcon.classList.remove('fa-spinner');
-                                       fileIcon.classList.add('fa-remove');
-                                       fileIcon.classList.add('pointer');
-                                       
-                                       file.classList.add('uploadFailed');
-                                       file.addEventListener(WCF_CLICK_EVENT, function() {
-                                               elRemove(this);
-                                       });
-                                       
-                                       var title = DomTraverse.childByClass(DomTraverse.childByClass(file, 'mediaInformation'), 'mediaTitle');
-                                       title.innerText = Language.get('wcf.media.upload.error.' + error.errorType, {
-                                               filename: error.filename
-                                       });
-                               }
-                               
-                               DomChangeListener.trigger();
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.media.upload', 'success', {
-                               files: files,
-                               media: data.returnValues.media,
-                               upload: this
-                       });
-               }
-       });
-       
-       return MediaUpload;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/NumberUtil.js b/wcfsetup/install/files/js/WoltLab/WCF/NumberUtil.js
deleted file mode 100644 (file)
index 8997d82..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * Provides helper functions for Number handling.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/NumberUtil
- */
-define([], function() {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/NumberUtil
-        */
-       var NumberUtil = {
-               /**
-                * Decimal adjustment of a number.
-                *
-                * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
-                * @param       {Number}        value   The number.
-                * @param       {Integer}       exp     The exponent (the 10 logarithm of the adjustment base).
-                * @returns     {Number}        The adjusted value.
-                */
-               round: function (value, exp) {
-                       // If the exp is undefined or zero...
-                       if (typeof exp === 'undefined' || +exp === 0) {
-                               return Math.round(value);
-                       }
-                       value = +value;
-                       exp = +exp;
-                       
-                       // If the value is not a number or the exp is not an integer...
-                       if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
-                               return NaN;
-                       }
-                       
-                       // Shift
-                       value = value.toString().split('e');
-                       value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
-                       
-                       // Shift back
-                       value = value.toString().split('e');
-                       return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
-               }
-       };
-       
-       return NumberUtil;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/ObjectMap.js b/wcfsetup/install/files/js/WoltLab/WCF/ObjectMap.js
deleted file mode 100644 (file)
index 1399586..0000000
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * Simple `object` to `object` map using a native WeakMap on supported browsers, otherwise a set of two arrays.
- * 
- * If you're looking for a dictionary with string keys, please see `WoltLab/WCF/Dictionary`.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/ObjectMap
- */
-define([], function() {
-       "use strict";
-       
-       var _hasMap = objOwns(window, 'WeakMap') && typeof window.WeakMap === 'function';
-       
-       /**
-        * @constructor
-        */
-       function ObjectMap() {
-               this._map = (_hasMap) ? new WeakMap() : { key: [], value: [] };
-       }
-       ObjectMap.prototype = {
-               /**
-                * Sets a new key with given value, will overwrite an existing key.
-                * 
-                * @param       {object}        key     key
-                * @param       {object}        value   value
-                */
-               set: function(key, value) {
-                       if (typeof key !== 'object' || key === null) {
-                               throw new TypeError("Only objects can be used as key");
-                       }
-                       
-                       if (typeof value !== 'object' || value === null) {
-                               throw new TypeError("Only objects can be used as value");
-                       }
-                       
-                       if (_hasMap) {
-                               this._map.set(key, value);
-                       }
-                       else {
-                               this._map.key.push(key);
-                               this._map.value.push(value);
-                       }
-               },
-               
-               /**
-                * Removes a key from the map.
-                * 
-                * @param       {object}        key     key
-                */
-               'delete': function(key) {
-                       if (_hasMap) {
-                               this._map['delete'](key);
-                       }
-                       else {
-                               var index = this._map.key.indexOf(key);
-                               this._map.key.splice(index);
-                               this._map.value.splice(index);
-                       }
-               },
-               
-               /**
-                * Returns true if dictionary contains a value for given key.
-                * 
-                * @param       {object}        key     key
-                * @return      {boolean}       true if key exists
-                */
-               has: function(key) {
-                       if (_hasMap) {
-                               return this._map.has(key);
-                       }
-                       else {
-                               return (this._map.key.indexOf(key) !== -1);
-                       }
-               },
-               
-               /**
-                * Retrieves a value by key, returns undefined if there is no match.
-                * 
-                * @param       {object}        key     key
-                * @return      {*}
-                */
-               get: function(key) {
-                       if (_hasMap) {
-                               return this._map.get(key);
-                       }
-                       else {
-                               var index = this._map.key.indexOf(key);
-                               if (index !== -1) {
-                                       return this._map.value[index];
-                               }
-                               
-                               return undefined;
-                       }
-               }
-       };
-       
-       return ObjectMap;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Permission.js b/wcfsetup/install/files/js/WoltLab/WCF/Permission.js
deleted file mode 100644 (file)
index 9fbc322..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Manages user permissions.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Permission
- */
-define(['Dictionary'], function(Dictionary) {
-       "use strict";
-       
-       var _permissions = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/Permission
-        */
-       return {
-               /**
-                * Adds a single permission to the store.
-                * 
-                * @param       {string}        permission      permission name
-                * @param       {boolean}       value           permission value
-                */
-               add: function(permission, value) {
-                       if (typeof value !== "boolean") {
-                               throw new TypeError("Permission value has to be boolean.");
-                       }
-                       
-                       _permissions.set(permission, value);
-               },
-               
-               /**
-                * Adds all the permissions in the given object to the store.
-                * 
-                * @param       {Object.<string, boolean>}      object          permission list
-                */
-               addObject: function(object) {
-                       for (var key in object) {
-                               if (objOwns(object, key)) {
-                                       this.add(key, object[key]);
-                               }
-                       }
-               },
-               
-               /**
-                * Returns the value of a permission.
-                * 
-                * If the permission is unknown, false is returned.
-                * 
-                * @param       {string}        permission      permission name
-                * @return      {boolean}       permission value
-                */
-               get: function(permission) {
-                       if (_permissions.has(permission)) {
-                               return _permissions.get(permission);
-                       }
-                       
-                       return false;
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/StringUtil.js b/wcfsetup/install/files/js/WoltLab/WCF/StringUtil.js
deleted file mode 100644 (file)
index ac70df5..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * Provides helper functions for String handling.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/StringUtil
- */
-define(['Language', './NumberUtil'], function(Language, NumberUtil) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/StringUtil
-        */
-       return {
-               /**
-                * Adds thousands separators to a given number.
-                * 
-                * @see         http://stackoverflow.com/a/6502556/782822
-                * @param       {?}     number
-                * @return      {String}
-                */
-               addThousandsSeparator: function(number) {
-                       // Fetch Language, as it cannot be provided because of a circular dependency
-                       if (Language === undefined) Language = require('Language');
-                       
-                       return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, '$1' + Language.get('wcf.global.thousandsSeparator'));
-               },
-               
-               /**
-                * Escapes special HTML-characters within a string
-                * 
-                * @param       {?}     string
-                * @return      {String}
-                */
-               escapeHTML: function (string) {
-                       return String(string).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
-               },
-               
-               /**
-                * Escapes a String to work with RegExp.
-                * 
-                * @see         https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25
-                * @param       {?}     string
-                * @return      {String}
-                */
-               escapeRegExp: function(string) {
-                       return String(string).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
-               },
-               
-               /**
-                * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands separators.
-                * 
-                * @param       {?}             number
-                * @param       {int}           decimalPlaces   The number of decimal places to leave after rounding.
-                * @return      {String}
-                */
-               formatNumeric: function(number, decimalPlaces) {
-                       // Fetch Language, as it cannot be provided because of a circular dependency
-                       if (Language === undefined) Language = require('Language');
-                       
-                       number = String(NumberUtil.round(number, decimalPlaces || -2));
-                       var numberParts = number.split('.');
-                       
-                       number = this.addThousandsSeparator(numberParts[0]);
-                       if (numberParts.length > 1) number += Language.get('wcf.global.decimalPoint') + numberParts[1];
-                       
-                       number = number.replace('-', '\u2212');
-                       
-                       return number;
-               },
-               
-               /**
-                * Makes a string's first character lowercase.
-                * 
-                * @param       {?}             string
-                * @return      {String}
-                */
-               lcfirst: function(string) {
-                       return String(string).substring(0, 1).toLowerCase() + string.substring(1);
-               },
-               
-               /**
-                * Makes a string's first character uppercase.
-                * 
-                * @param       {?}             string
-                * @return      {String}
-                */
-               ucfirst: function(string) {
-                       return String(string).substring(0, 1).toUpperCase() + string.substring(1);
-               },
-               
-               /**
-                * Unescapes special HTML-characters within a string.
-                * 
-                * @param       {?}             string
-                * @return      {String}
-                */
-               unescapeHTML: function (string) {
-                       return String(string).replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Template.grammar.jison b/wcfsetup/install/files/js/WoltLab/WCF/Template.grammar.jison
deleted file mode 100644 (file)
index 3cd2176..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * Grammar for WoltLab/WCF/Template.
- * 
- * Recompile using:
- *    jison -m amd -o Template.grammar.js Template.grammar.jison
- * after making changes to the grammar.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Template.grammar
- */
-
-%lex
-%s command
-%%
-
-\{\*.*\*\} /* comment */
-\{literal\}.*?\{\/literal\} { yytext = yytext.substring(9, yytext.length - 10); return 'T_LITERAL'; }
-<command>\"([^"]|\\\.)*\" return 'T_QUOTED_STRING';
-<command>\'([^']|\\\.)*\' return 'T_QUOTED_STRING';
-\$ return 'T_VARIABLE';
-[_a-zA-Z][_a-zA-Z0-9]* { return 'T_VARIABLE_NAME'; }
-"."     return '.';
-"["     return '[';
-"]"     return ']';
-"("     return '(';
-")"     return ')';
-"="     return '=';
-"{ldelim}"  return '{ldelim}';
-"{rdelim}"  return '{rdelim}';
-"{#"   return '{#';
-"{@"   return '{@';
-"{if " { this.begin('command'); return '{if'; }
-"{else if " { this.begin('command'); return '{elseif'; }
-"{elseif "  { this.begin('command'); return '{elseif'; }
-"{else}"    return '{else}';
-"{/if}"     return '{/if}';
-"{lang}"    return '{lang}';
-"{/lang}"   return '{/lang}';
-"{include " { this.begin('command'); return '{include'; }
-"{implode " { this.begin('command'); return '{implode'; }
-"{/implode}" return '{/implode}';
-"{foreach "  { this.begin('command'); return '{foreach'; }
-"{foreachelse}"  return '{foreachelse}';
-"{/foreach}"  return '{/foreach}';
-"{"     return '{';
-<command>"}" { this.popState(); return '}';}
-"}"     return '}';
-\s+     return 'T_WS';
-<<EOF>>            return 'EOF';
-[^{]   return 'T_ANY';
-
-/lex
-
-%start TEMPLATE
-%ebnf
-
-%%
-
-// A valid template is any number of CHUNKs.
-TEMPLATE: CHUNK_STAR EOF { return $1 + ";"; };
-
-CHUNK_STAR: CHUNK* {
-       var result = $1.reduce(function (carry, item) {
-               if (item.encode && !carry[1]) carry[0] += " + '" + item.value;
-               else if (item.encode && carry[1]) carry[0] += item.value;
-               else if (!item.encode && carry[1]) carry[0] += "' + " + item.value;
-               else if (!item.encode && !carry[1]) carry[0] += " + " + item.value;
-               
-               carry[1] = item.encode;
-               return carry;
-       }, [ "''", false ]);
-       if (result[1]) result[0] += "'";
-       
-       $$ = result[0];
-};
-
-CHUNK:
-       PLAIN_ANY -> { encode: true, value: $1.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n') }
-|      T_LITERAL -> { encode: true, value: $1.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n') }
-|      COMMAND -> { encode: false, value: $1 }
-;
-
-PLAIN_ANY: T_ANY | '}' | '{' T_WS -> $1 + $2
-| ']' | '[' | ')' | '(' | '.' | '=' | T_VARIABLE | T_VARIABLE_NAME | T_QUOTED_STRING | T_WS;
-
-COMMAND:
-       '{if' COMMAND_PARAMETERS '}' CHUNK_STAR (ELSE_IF)* ELSE? '{/if}' {
-               $$ = "(function() { if (" + $2 + ") { return " + $4 + "; } " + $5.join(' ') + " " + ($6 || '') + " return ''; })()";
-       }
-|      '{include' COMMAND_PARAMETER_LIST '}' {
-               if (!$2['file']) throw new Error('Missing parameter file');
-               
-               $$ = $2['file'] + ".fetch(v)";
-       }
-|      '{implode' COMMAND_PARAMETER_LIST '}' CHUNK_STAR '{/implode}' {
-               if (!$2['from']) throw new Error('Missing parameter from');
-               if (!$2['item']) throw new Error('Missing parameter item');
-               if (!$2['glue']) $2['glue'] = "', '";
-               
-               $$ = "(function() { return " + $2['from'] + ".map(function(item) { v[" + $2['item'] + "] = item; return " + $4 + "; }).join(" + $2['glue'] + "); })()";
-       }
-|      '{foreach' COMMAND_PARAMETER_LIST '}' CHUNK_STAR FOREACH_ELSE? '{/foreach}' {
-               if (!$2['from']) throw new Error('Missing parameter from');
-               if (!$2['item']) throw new Error('Missing parameter item');
-               
-               $$ = "(function() {"
-               + "var looped = false, result = '';"
-               + "if (" + $2['from'] + " instanceof Array) {"
-                       + "for (var i = 0; i < " + $2['from'] + ".length; i++) { looped = true;"
-                               + "v[" + $2['key'] + "] = i;"
-                               + "v[" + $2['item'] + "] = " + $2['from'] + "[i];"
-                               + "result += " + $4 + ";"
-                       + "}"
-               + "} else {"
-                       + "for (var key in " + $2['from'] + ") {"
-                               + "if (!" + $2['from'] + ".hasOwnProperty(key)) continue;"
-                               + "looped = true;"
-                               + "v[" + $2['key'] + "] = key;"
-                               + "v[" + $2['item'] + "] = " + $2['from'] + "[key];"
-                               + "result += " + $4 + ";"
-                       + "}"
-               + "}"
-               + "return (looped ? result : " + ($5 || "''") + "); })()"
-       }
-|      '{lang}' CHUNK_STAR '{/lang}' -> "Language.get(" + $2 + ")"
-|      '{' VARIABLE '}'  -> "StringUtil.escapeHTML(" + $2 + ")"
-|      '{#' VARIABLE '}' -> "StringUtil.formatNumeric(" + $2 + ")"
-|      '{@' VARIABLE '}' -> $2
-|      '{ldelim}' -> "'{'"
-|      '{rdelim}' -> "'}'"
-;
-
-ELSE: '{else}' CHUNK_STAR -> "else { return " + $2 + "; }"
-;
-
-ELSE_IF: '{elseif' COMMAND_PARAMETERS '}' CHUNK_STAR -> "else if (" + $2 + ") { return " + $4 + "; }"
-;
-
-FOREACH_ELSE: '{foreachelse}' CHUNK_STAR -> $2
-;
-
-// VARIABLE parses a valid variable access (with optional property access)
-VARIABLE: T_VARIABLE T_VARIABLE_NAME VARIABLE_SUFFIX* -> "v['" + $2 + "']" + $3.join('');
-;
-
-VARIABLE_SUFFIX:
-       '[' COMMAND_PARAMETERS ']' -> $1 + $2 + $3
-|      '.' T_VARIABLE_NAME -> "['" + $2 + "']"
-|      '(' COMMAND_PARAMETERS? ')' -> $1 + ($2 || '') + $3
-;
-
-COMMAND_PARAMETER_LIST:
-       T_VARIABLE_NAME '=' COMMAND_PARAMETER_VALUE T_WS COMMAND_PARAMETER_LIST { $$ = $5; $$[$1] = $3; }
-|      T_VARIABLE_NAME '=' COMMAND_PARAMETER_VALUE { $$ = {}; $$[$1] = $3; }
-;
-
-COMMAND_PARAMETER_VALUE: T_QUOTED_STRING | VARIABLE;
-
-// COMMAND_PARAMETERS parses anything that is valid between a command name and the closing brace
-COMMAND_PARAMETERS: COMMAND_PARAMETER+ -> $1.join('')
-;
-COMMAND_PARAMETER: T_ANY | T_WS | '=' | T_QUOTED_STRING | VARIABLE | T_VARIABLE_NAME;
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Template.grammar.js b/wcfsetup/install/files/js/WoltLab/WCF/Template.grammar.js
deleted file mode 100644 (file)
index 4dad8f3..0000000
+++ /dev/null
@@ -1,702 +0,0 @@
-
-
-define(function(require){
-var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[2,47],$V1=[5,9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,28,29,31,32,33,35,36,37,39,40,41,42,44,46,48],$V2=[1,33],$V3=[1,37],$V4=[1,38],$V5=[1,39],$V6=[1,42],$V7=[1,40],$V8=[1,44],$V9=[11,12,14,15,17,20,21,22,23],$Va=[11,12,14,15,16,17,18,19,20,21,22,23],$Vb=[9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,28,29,31,33,36,39,40,41,42,44,46],$Vc=[28,44,46],$Vd=[12,14];
-var parser = {trace: function trace() { },
-yy: {},
-symbols_: {"error":2,"TEMPLATE":3,"CHUNK_STAR":4,"EOF":5,"CHUNK_STAR_repetition0":6,"CHUNK":7,"PLAIN_ANY":8,"T_LITERAL":9,"COMMAND":10,"T_ANY":11,"}":12,"{":13,"T_WS":14,"]":15,"[":16,")":17,"(":18,".":19,"=":20,"T_VARIABLE":21,"T_VARIABLE_NAME":22,"T_QUOTED_STRING":23,"{if":24,"COMMAND_PARAMETERS":25,"COMMAND_repetition0":26,"COMMAND_option0":27,"{/if}":28,"{include":29,"COMMAND_PARAMETER_LIST":30,"{implode":31,"{/implode}":32,"{foreach":33,"COMMAND_option1":34,"{/foreach}":35,"{lang}":36,"{/lang}":37,"VARIABLE":38,"{#":39,"{@":40,"{ldelim}":41,"{rdelim}":42,"ELSE":43,"{else}":44,"ELSE_IF":45,"{elseif":46,"FOREACH_ELSE":47,"{foreachelse}":48,"VARIABLE_repetition0":49,"VARIABLE_SUFFIX":50,"VARIABLE_SUFFIX_option0":51,"COMMAND_PARAMETER_VALUE":52,"COMMAND_PARAMETERS_repetition_plus0":53,"COMMAND_PARAMETER":54,"$accept":0,"$end":1},
-terminals_: {2:"error",5:"EOF",9:"T_LITERAL",11:"T_ANY",12:"}",13:"{",14:"T_WS",15:"]",16:"[",17:")",18:"(",19:".",20:"=",21:"T_VARIABLE",22:"T_VARIABLE_NAME",23:"T_QUOTED_STRING",24:"{if",28:"{/if}",29:"{include",31:"{implode",32:"{/implode}",33:"{foreach",35:"{/foreach}",36:"{lang}",37:"{/lang}",39:"{#",40:"{@",41:"{ldelim}",42:"{rdelim}",44:"{else}",46:"{elseif",48:"{foreachelse}"},
-productions_: [0,[3,2],[4,1],[7,1],[7,1],[7,1],[8,1],[8,1],[8,2],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[10,7],[10,3],[10,5],[10,6],[10,3],[10,3],[10,3],[10,3],[10,1],[10,1],[43,2],[45,4],[47,2],[38,3],[50,3],[50,2],[50,3],[30,5],[30,3],[52,1],[52,1],[25,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[6,0],[6,2],[26,0],[26,2],[27,0],[27,1],[34,0],[34,1],[49,0],[49,2],[51,0],[51,1],[53,1],[53,2]],
-performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) {
-/* this == yyval */
-
-var $0 = $$.length - 1;
-switch (yystate) {
-case 1:
- return $$[$0-1] + ";"; 
-break;
-case 2:
-
-       var result = $$[$0].reduce(function (carry, item) {
-               if (item.encode && !carry[1]) carry[0] += " + '" + item.value;
-               else if (item.encode && carry[1]) carry[0] += item.value;
-               else if (!item.encode && carry[1]) carry[0] += "' + " + item.value;
-               else if (!item.encode && !carry[1]) carry[0] += " + " + item.value;
-               
-               carry[1] = item.encode;
-               return carry;
-       }, [ "''", false ]);
-       if (result[1]) result[0] += "'";
-       
-       this.$ = result[0];
-
-break;
-case 3: case 4:
-this.$ = { encode: true, value: $$[$0].replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n') };
-break;
-case 5:
-this.$ = { encode: false, value: $$[$0] };
-break;
-case 8:
-this.$ = $$[$0-1] + $$[$0];
-break;
-case 19:
-
-               this.$ = "(function() { if (" + $$[$0-5] + ") { return " + $$[$0-3] + "; } " + $$[$0-2].join(' ') + " " + ($$[$0-1] || '') + " return ''; })()";
-       
-break;
-case 20:
-
-               if (!$$[$0-1]['file']) throw new Error('Missing parameter file');
-               
-               this.$ = $$[$0-1]['file'] + ".fetch(v)";
-       
-break;
-case 21:
-
-               if (!$$[$0-3]['from']) throw new Error('Missing parameter from');
-               if (!$$[$0-3]['item']) throw new Error('Missing parameter item');
-               if (!$$[$0-3]['glue']) $$[$0-3]['glue'] = "', '";
-               
-               this.$ = "(function() { return " + $$[$0-3]['from'] + ".map(function(item) { v[" + $$[$0-3]['item'] + "] = item; return " + $$[$0-1] + "; }).join(" + $$[$0-3]['glue'] + "); })()";
-       
-break;
-case 22:
-
-               if (!$$[$0-4]['from']) throw new Error('Missing parameter from');
-               if (!$$[$0-4]['item']) throw new Error('Missing parameter item');
-               
-               this.$ = "(function() {"
-               + "var looped = false, result = '';"
-               + "if (" + $$[$0-4]['from'] + " instanceof Array) {"
-                       + "for (var i = 0; i < " + $$[$0-4]['from'] + ".length; i++) { looped = true;"
-                               + "v[" + $$[$0-4]['key'] + "] = i;"
-                               + "v[" + $$[$0-4]['item'] + "] = " + $$[$0-4]['from'] + "[i];"
-                               + "result += " + $$[$0-2] + ";"
-                       + "}"
-               + "} else {"
-                       + "for (var key in " + $$[$0-4]['from'] + ") {"
-                               + "if (!" + $$[$0-4]['from'] + ".hasOwnProperty(key)) continue;"
-                               + "looped = true;"
-                               + "v[" + $$[$0-4]['key'] + "] = key;"
-                               + "v[" + $$[$0-4]['item'] + "] = " + $$[$0-4]['from'] + "[key];"
-                               + "result += " + $$[$0-2] + ";"
-                       + "}"
-               + "}"
-               + "return (looped ? result : " + ($$[$0-1] || "''") + "); })()"
-       
-break;
-case 23:
-this.$ = "Language.get(" + $$[$0-1] + ")";
-break;
-case 24:
-this.$ = "StringUtil.escapeHTML(" + $$[$0-1] + ")";
-break;
-case 25:
-this.$ = "StringUtil.formatNumeric(" + $$[$0-1] + ")";
-break;
-case 26:
-this.$ = $$[$0-1];
-break;
-case 27:
-this.$ = "'{'";
-break;
-case 28:
-this.$ = "'}'";
-break;
-case 29:
-this.$ = "else { return " + $$[$0] + "; }";
-break;
-case 30:
-this.$ = "else if (" + $$[$0-2] + ") { return " + $$[$0] + "; }";
-break;
-case 31:
-this.$ = $$[$0];
-break;
-case 32:
-this.$ = "v['" + $$[$0-1] + "']" + $$[$0].join('');;
-break;
-case 33:
-this.$ = $$[$0-2] + $$[$0-1] + $$[$0];
-break;
-case 34:
-this.$ = "['" + $$[$0] + "']";
-break;
-case 35:
-this.$ = $$[$0-2] + ($$[$0-1] || '') + $$[$0];
-break;
-case 36:
- this.$ = $$[$0]; this.$[$$[$0-4]] = $$[$0-2]; 
-break;
-case 37:
- this.$ = {}; this.$[$$[$0-2]] = $$[$0]; 
-break;
-case 40:
-this.$ = $$[$0].join('');
-break;
-case 47: case 49: case 55:
-this.$ = [];
-break;
-case 48: case 50: case 56: case 60:
-$$[$0-1].push($$[$0]);
-break;
-case 59:
-this.$ = [$$[$0]];
-break;
-}
-},
-table: [o([5,9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,36,39,40,41,42],$V0,{3:1,4:2,6:3}),{1:[3]},{5:[1,4]},o([5,28,32,35,37,44,46,48],[2,2],{7:5,8:6,10:8,9:[1,7],11:[1,9],12:[1,10],13:[1,11],14:[1,21],15:[1,12],16:[1,13],17:[1,14],18:[1,15],19:[1,16],20:[1,17],21:[1,18],22:[1,19],23:[1,20],24:[1,22],29:[1,23],31:[1,24],33:[1,25],36:[1,26],39:[1,27],40:[1,28],41:[1,29],42:[1,30]}),{1:[2,1]},o($V1,[2,48]),o($V1,[2,3]),o($V1,[2,4]),o($V1,[2,5]),o($V1,[2,6]),o($V1,[2,7]),{14:[1,31],21:$V2,38:32},o($V1,[2,9]),o($V1,[2,10]),o($V1,[2,11]),o($V1,[2,12]),o($V1,[2,13]),o($V1,[2,14]),o($V1,[2,15]),o($V1,[2,16]),o($V1,[2,17]),o($V1,[2,18]),{11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7,25:34,38:41,53:35,54:36},{22:$V8,30:43},{22:$V8,30:45},{22:$V8,30:46},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,36,37,39,40,41,42],$V0,{6:3,4:47}),{21:$V2,38:48},{21:$V2,38:49},o($V1,[2,27]),o($V1,[2,28]),o($V1,[2,8]),{12:[1,50]},{22:[1,51]},{12:[1,52]},o([12,15,17],[2,40],{38:41,54:53,11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7}),o($V9,[2,59]),o($V9,[2,41]),o($V9,[2,42]),o($V9,[2,43]),o($V9,[2,44]),o($V9,[2,45]),o($V9,[2,46]),{12:[1,54]},{20:[1,55]},{12:[1,56]},{12:[1,57]},{37:[1,58]},{12:[1,59]},{12:[1,60]},o($V1,[2,24]),o($Va,[2,55],{49:61}),o($Vb,$V0,{6:3,4:62}),o($V9,[2,60]),o($V1,[2,20]),{21:$V2,23:[1,64],38:65,52:63},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,32,33,36,39,40,41,42],$V0,{6:3,4:66}),o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,35,36,39,40,41,42,48],$V0,{6:3,4:67}),o($V1,[2,23]),o($V1,[2,25]),o($V1,[2,26]),o($V9,[2,32],{50:68,16:[1,69],18:[1,71],19:[1,70]}),o($Vc,[2,49],{26:72}),{12:[2,37],14:[1,73]},o($Vd,[2,38]),o($Vd,[2,39]),{32:[1,74]},{34:75,35:[2,53],47:76,48:[1,77]},o($Va,[2,56]),{11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7,25:78,38:41,53:35,54:36},{22:[1,79]},{11:$V3,14:$V4,17:[2,57],20:$V5,21:$V2,22:$V6,23:$V7,25:81,38:41,51:80,53:35,54:36},{27:82,28:[2,51],43:84,44:[1,86],45:83,46:[1,85]},{22:$V8,30:87},o($V1,[2,21]),{35:[1,88]},{35:[2,54]},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,35,36,39,40,41,42],$V0,{6:3,4:89}),{15:[1,90]},o($Va,[2,34]),{17:[1,91]},{17:[2,58]},{28:[1,92]},o($Vc,[2,50]),{28:[2,52]},{11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7,25:93,38:41,53:35,54:36},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,28,29,31,33,36,39,40,41,42],$V0,{6:3,4:94}),{12:[2,36]},o($V1,[2,22]),{35:[2,31]},o($Va,[2,33]),o($Va,[2,35]),o($V1,[2,19]),{12:[1,95]},{28:[2,29]},o($Vb,$V0,{6:3,4:96}),o($Vc,[2,30])],
-defaultActions: {4:[2,1],76:[2,54],81:[2,58],84:[2,52],87:[2,36],89:[2,31],94:[2,29]},
-parseError: function parseError(str, hash) {
-    if (hash.recoverable) {
-        this.trace(str);
-    } else {
-        throw new Error(str);
-    }
-},
-parse: function parse(input) {
-    var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
-    var args = lstack.slice.call(arguments, 1);
-    var lexer = Object.create(this.lexer);
-    var sharedState = { yy: {} };
-    for (var k in this.yy) {
-        if (Object.prototype.hasOwnProperty.call(this.yy, k)) {
-            sharedState.yy[k] = this.yy[k];
-        }
-    }
-    lexer.setInput(input, sharedState.yy);
-    sharedState.yy.lexer = lexer;
-    sharedState.yy.parser = this;
-    if (typeof lexer.yylloc == 'undefined') {
-        lexer.yylloc = {};
-    }
-    var yyloc = lexer.yylloc;
-    lstack.push(yyloc);
-    var ranges = lexer.options && lexer.options.ranges;
-    if (typeof sharedState.yy.parseError === 'function') {
-        this.parseError = sharedState.yy.parseError;
-    } else {
-        this.parseError = Object.getPrototypeOf(this).parseError;
-    }
-    function popStack(n) {
-        stack.length = stack.length - 2 * n;
-        vstack.length = vstack.length - n;
-        lstack.length = lstack.length - n;
-    }
-    _token_stack:
-        function lex() {
-            var token;
-            token = lexer.lex() || EOF;
-            if (typeof token !== 'number') {
-                token = self.symbols_[token] || token;
-            }
-            return token;
-        }
-    var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
-    while (true) {
-        state = stack[stack.length - 1];
-        if (this.defaultActions[state]) {
-            action = this.defaultActions[state];
-        } else {
-            if (symbol === null || typeof symbol == 'undefined') {
-                symbol = lex();
-            }
-            action = table[state] && table[state][symbol];
-        }
-                    if (typeof action === 'undefined' || !action.length || !action[0]) {
-                var errStr = '';
-                expected = [];
-                for (p in table[state]) {
-                    if (this.terminals_[p] && p > TERROR) {
-                        expected.push('\'' + this.terminals_[p] + '\'');
-                    }
-                }
-                if (lexer.showPosition) {
-                    errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\'';
-                } else {
-                    errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\'');
-                }
-                this.parseError(errStr, {
-                    text: lexer.match,
-                    token: this.terminals_[symbol] || symbol,
-                    line: lexer.yylineno,
-                    loc: yyloc,
-                    expected: expected
-                });
-            }
-        if (action[0] instanceof Array && action.length > 1) {
-            throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol);
-        }
-        switch (action[0]) {
-        case 1:
-            stack.push(symbol);
-            vstack.push(lexer.yytext);
-            lstack.push(lexer.yylloc);
-            stack.push(action[1]);
-            symbol = null;
-            if (!preErrorSymbol) {
-                yyleng = lexer.yyleng;
-                yytext = lexer.yytext;
-                yylineno = lexer.yylineno;
-                yyloc = lexer.yylloc;
-                if (recovering > 0) {
-                    recovering--;
-                }
-            } else {
-                symbol = preErrorSymbol;
-                preErrorSymbol = null;
-            }
-            break;
-        case 2:
-            len = this.productions_[action[1]][1];
-            yyval.$ = vstack[vstack.length - len];
-            yyval._$ = {
-                first_line: lstack[lstack.length - (len || 1)].first_line,
-                last_line: lstack[lstack.length - 1].last_line,
-                first_column: lstack[lstack.length - (len || 1)].first_column,
-                last_column: lstack[lstack.length - 1].last_column
-            };
-            if (ranges) {
-                yyval._$.range = [
-                    lstack[lstack.length - (len || 1)].range[0],
-                    lstack[lstack.length - 1].range[1]
-                ];
-            }
-            r = this.performAction.apply(yyval, [
-                yytext,
-                yyleng,
-                yylineno,
-                sharedState.yy,
-                action[1],
-                vstack,
-                lstack
-            ].concat(args));
-            if (typeof r !== 'undefined') {
-                return r;
-            }
-            if (len) {
-                stack = stack.slice(0, -1 * len * 2);
-                vstack = vstack.slice(0, -1 * len);
-                lstack = lstack.slice(0, -1 * len);
-            }
-            stack.push(this.productions_[action[1]][0]);
-            vstack.push(yyval.$);
-            lstack.push(yyval._$);
-            newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
-            stack.push(newState);
-            break;
-        case 3:
-            return true;
-        }
-    }
-    return true;
-}};
-
-/* generated by jison-lex 0.3.4 */
-var lexer = (function(){
-var lexer = ({
-
-EOF:1,
-
-parseError:function parseError(str, hash) {
-        if (this.yy.parser) {
-            this.yy.parser.parseError(str, hash);
-        } else {
-            throw new Error(str);
-        }
-    },
-
-// resets the lexer, sets new input
-setInput:function (input, yy) {
-        this.yy = yy || this.yy || {};
-        this._input = input;
-        this._more = this._backtrack = this.done = false;
-        this.yylineno = this.yyleng = 0;
-        this.yytext = this.matched = this.match = '';
-        this.conditionStack = ['INITIAL'];
-        this.yylloc = {
-            first_line: 1,
-            first_column: 0,
-            last_line: 1,
-            last_column: 0
-        };
-        if (this.options.ranges) {
-            this.yylloc.range = [0,0];
-        }
-        this.offset = 0;
-        return this;
-    },
-
-// consumes and returns one char from the input
-input:function () {
-        var ch = this._input[0];
-        this.yytext += ch;
-        this.yyleng++;
-        this.offset++;
-        this.match += ch;
-        this.matched += ch;
-        var lines = ch.match(/(?:\r\n?|\n).*/g);
-        if (lines) {
-            this.yylineno++;
-            this.yylloc.last_line++;
-        } else {
-            this.yylloc.last_column++;
-        }
-        if (this.options.ranges) {
-            this.yylloc.range[1]++;
-        }
-
-        this._input = this._input.slice(1);
-        return ch;
-    },
-
-// unshifts one char (or a string) into the input
-unput:function (ch) {
-        var len = ch.length;
-        var lines = ch.split(/(?:\r\n?|\n)/g);
-
-        this._input = ch + this._input;
-        this.yytext = this.yytext.substr(0, this.yytext.length - len);
-        //this.yyleng -= len;
-        this.offset -= len;
-        var oldLines = this.match.split(/(?:\r\n?|\n)/g);
-        this.match = this.match.substr(0, this.match.length - 1);
-        this.matched = this.matched.substr(0, this.matched.length - 1);
-
-        if (lines.length - 1) {
-            this.yylineno -= lines.length - 1;
-        }
-        var r = this.yylloc.range;
-
-        this.yylloc = {
-            first_line: this.yylloc.first_line,
-            last_line: this.yylineno + 1,
-            first_column: this.yylloc.first_column,
-            last_column: lines ?
-                (lines.length === oldLines.length ? this.yylloc.first_column : 0)
-                 + oldLines[oldLines.length - lines.length].length - lines[0].length :
-              this.yylloc.first_column - len
-        };
-
-        if (this.options.ranges) {
-            this.yylloc.range = [r[0], r[0] + this.yyleng - len];
-        }
-        this.yyleng = this.yytext.length;
-        return this;
-    },
-
-// When called from action, caches matched text and appends it on next action
-more:function () {
-        this._more = true;
-        return this;
-    },
-
-// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead.
-reject:function () {
-        if (this.options.backtrack_lexer) {
-            this._backtrack = true;
-        } else {
-            return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), {
-                text: "",
-                token: null,
-                line: this.yylineno
-            });
-
-        }
-        return this;
-    },
-
-// retain first n characters of the match
-less:function (n) {
-        this.unput(this.match.slice(n));
-    },
-
-// displays already matched input, i.e. for error messages
-pastInput:function () {
-        var past = this.matched.substr(0, this.matched.length - this.match.length);
-        return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
-    },
-
-// displays upcoming input, i.e. for error messages
-upcomingInput:function () {
-        var next = this.match;
-        if (next.length < 20) {
-            next += this._input.substr(0, 20-next.length);
-        }
-        return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, "");
-    },
-
-// displays the character position where the lexing error occurred, i.e. for error messages
-showPosition:function () {
-        var pre = this.pastInput();
-        var c = new Array(pre.length + 1).join("-");
-        return pre + this.upcomingInput() + "\n" + c + "^";
-    },
-
-// test the lexed token: return FALSE when not a match, otherwise return token
-test_match:function (match, indexed_rule) {
-        var token,
-            lines,
-            backup;
-
-        if (this.options.backtrack_lexer) {
-            // save context
-            backup = {
-                yylineno: this.yylineno,
-                yylloc: {
-                    first_line: this.yylloc.first_line,
-                    last_line: this.last_line,
-                    first_column: this.yylloc.first_column,
-                    last_column: this.yylloc.last_column
-                },
-                yytext: this.yytext,
-                match: this.match,
-                matches: this.matches,
-                matched: this.matched,
-                yyleng: this.yyleng,
-                offset: this.offset,
-                _more: this._more,
-                _input: this._input,
-                yy: this.yy,
-                conditionStack: this.conditionStack.slice(0),
-                done: this.done
-            };
-            if (this.options.ranges) {
-                backup.yylloc.range = this.yylloc.range.slice(0);
-            }
-        }
-
-        lines = match[0].match(/(?:\r\n?|\n).*/g);
-        if (lines) {
-            this.yylineno += lines.length;
-        }
-        this.yylloc = {
-            first_line: this.yylloc.last_line,
-            last_line: this.yylineno + 1,
-            first_column: this.yylloc.last_column,
-            last_column: lines ?
-                         lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length :
-                         this.yylloc.last_column + match[0].length
-        };
-        this.yytext += match[0];
-        this.match += match[0];
-        this.matches = match;
-        this.yyleng = this.yytext.length;
-        if (this.options.ranges) {
-            this.yylloc.range = [this.offset, this.offset += this.yyleng];
-        }
-        this._more = false;
-        this._backtrack = false;
-        this._input = this._input.slice(match[0].length);
-        this.matched += match[0];
-        token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]);
-        if (this.done && this._input) {
-            this.done = false;
-        }
-        if (token) {
-            return token;
-        } else if (this._backtrack) {
-            // recover context
-            for (var k in backup) {
-                this[k] = backup[k];
-            }
-            return false; // rule action called reject() implying the next rule should be tested instead.
-        }
-        return false;
-    },
-
-// return next match in input
-next:function () {
-        if (this.done) {
-            return this.EOF;
-        }
-        if (!this._input) {
-            this.done = true;
-        }
-
-        var token,
-            match,
-            tempMatch,
-            index;
-        if (!this._more) {
-            this.yytext = '';
-            this.match = '';
-        }
-        var rules = this._currentRules();
-        for (var i = 0; i < rules.length; i++) {
-            tempMatch = this._input.match(this.rules[rules[i]]);
-            if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
-                match = tempMatch;
-                index = i;
-                if (this.options.backtrack_lexer) {
-                    token = this.test_match(tempMatch, rules[i]);
-                    if (token !== false) {
-                        return token;
-                    } else if (this._backtrack) {
-                        match = false;
-                        continue; // rule action called reject() implying a rule MISmatch.
-                    } else {
-                        // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
-                        return false;
-                    }
-                } else if (!this.options.flex) {
-                    break;
-                }
-            }
-        }
-        if (match) {
-            token = this.test_match(match, rules[index]);
-            if (token !== false) {
-                return token;
-            }
-            // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
-            return false;
-        }
-        if (this._input === "") {
-            return this.EOF;
-        } else {
-            return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), {
-                text: "",
-                token: null,
-                line: this.yylineno
-            });
-        }
-    },
-
-// return next match that has a token
-lex:function lex() {
-        var r = this.next();
-        if (r) {
-            return r;
-        } else {
-            return this.lex();
-        }
-    },
-
-// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack)
-begin:function begin(condition) {
-        this.conditionStack.push(condition);
-    },
-
-// pop the previously active lexer condition state off the condition stack
-popState:function popState() {
-        var n = this.conditionStack.length - 1;
-        if (n > 0) {
-            return this.conditionStack.pop();
-        } else {
-            return this.conditionStack[0];
-        }
-    },
-
-// produce the lexer rule set which is active for the currently active lexer condition state
-_currentRules:function _currentRules() {
-        if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) {
-            return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules;
-        } else {
-            return this.conditions["INITIAL"].rules;
-        }
-    },
-
-// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available
-topState:function topState(n) {
-        n = this.conditionStack.length - 1 - Math.abs(n || 0);
-        if (n >= 0) {
-            return this.conditionStack[n];
-        } else {
-            return "INITIAL";
-        }
-    },
-
-// alias for begin(condition)
-pushState:function pushState(condition) {
-        this.begin(condition);
-    },
-
-// return the number of states currently on the stack
-stateStackSize:function stateStackSize() {
-        return this.conditionStack.length;
-    },
-options: {},
-performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
-var YYSTATE=YY_START;
-switch($avoiding_name_collisions) {
-case 0:/* comment */
-break;
-case 1: yy_.yytext = yy_.yytext.substring(9, yy_.yytext.length - 10); return 9; 
-break;
-case 2:return 23;
-break;
-case 3:return 23;
-break;
-case 4:return 21;
-break;
-case 5: return 22; 
-break;
-case 6:return 19;
-break;
-case 7:return 16;
-break;
-case 8:return 15;
-break;
-case 9:return 18;
-break;
-case 10:return 17;
-break;
-case 11:return 20;
-break;
-case 12:return 41;
-break;
-case 13:return 42;
-break;
-case 14:return 39;
-break;
-case 15:return 40;
-break;
-case 16: this.begin('command'); return 24; 
-break;
-case 17: this.begin('command'); return 46; 
-break;
-case 18: this.begin('command'); return 46; 
-break;
-case 19:return 44;
-break;
-case 20:return 28;
-break;
-case 21:return 36;
-break;
-case 22:return 37;
-break;
-case 23: this.begin('command'); return 29; 
-break;
-case 24: this.begin('command'); return 31; 
-break;
-case 25:return 32;
-break;
-case 26: this.begin('command'); return 33; 
-break;
-case 27:return 48;
-break;
-case 28:return 35;
-break;
-case 29:return 13;
-break;
-case 30: this.popState(); return 12;
-break;
-case 31:return 12;
-break;
-case 32:return 14;
-break;
-case 33:return 5;
-break;
-case 34:return 11;
-break;
-}
-},
-rules: [/^(?:\{\*.*\*\})/,/^(?:\{literal\}.*?\{\/literal\})/,/^(?:"([^"]|\\\.)*")/,/^(?:'([^']|\\\.)*')/,/^(?:\$)/,/^(?:[_a-zA-Z][_a-zA-Z0-9]*)/,/^(?:\.)/,/^(?:\[)/,/^(?:\])/,/^(?:\()/,/^(?:\))/,/^(?:=)/,/^(?:\{ldelim\})/,/^(?:\{rdelim\})/,/^(?:\{#)/,/^(?:\{@)/,/^(?:\{if )/,/^(?:\{else if )/,/^(?:\{elseif )/,/^(?:\{else\})/,/^(?:\{\/if\})/,/^(?:\{lang\})/,/^(?:\{\/lang\})/,/^(?:\{include )/,/^(?:\{implode )/,/^(?:\{\/implode\})/,/^(?:\{foreach )/,/^(?:\{foreachelse\})/,/^(?:\{\/foreach\})/,/^(?:\{)/,/^(?:\})/,/^(?:\})/,/^(?:\s+)/,/^(?:$)/,/^(?:[^{])/],
-conditions: {"command":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34],"inclusive":true},"INITIAL":{"rules":[0,1,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,31,32,33,34],"inclusive":true}}
-});
-return lexer;
-})();
-parser.lexer = lexer;
-return parser;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Template.js b/wcfsetup/install/files/js/WoltLab/WCF/Template.js
deleted file mode 100644 (file)
index 0620d9a..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * WoltLab/WCF/Template provides a template scripting compiler similar
- * to the PHP one of WoltLab Suite Core. It supports a limited
- * set of useful commands and compiles templates down to a pure
- * JavaScript Function.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Template
- */
-define(['./Template.grammar', './StringUtil', 'Language'], function(parser, StringUtil, Language) {
-       "use strict";
-       
-       // work around bug in AMD module generation of Jison
-       function Parser() {
-               this.yy = {};
-       }
-       Parser.prototype = parser;
-       parser.Parser = Parser;
-       parser = new Parser();
-
-       /**
-        * Compiles the given template.
-        * 
-        * @param       {string}        template        Template to compile.
-        * @constructor
-        */
-       function Template(template) {
-               // Fetch Language/StringUtil, as it cannot be provided because of a circular dependency
-               if (Language === undefined) Language = require('Language');
-               if (StringUtil === undefined) StringUtil = require('StringUtil');
-               
-               try {
-                       template = parser.parse(template);
-                       template = "var tmp = {};\n"
-                       + "for (var key in v) tmp[key] = v[key];\n"
-                       + "v = tmp;\n"
-                       + "v.__wcf = window.WCF; v.__window = window;\n"
-                       + "return " + template;
-                       
-                       this.fetch = new Function("StringUtil", "Language", "v", template).bind(undefined, StringUtil, Language);
-               }
-               catch (e) {
-                       console.debug(e.message);
-                       throw e;
-               }
-       }
-       
-       Object.defineProperty(Template, 'callbacks', {
-               enumerable: false,
-               configurable: false,
-               get: function() {
-                       throw new Error('WCF.Template.callbacks is no longer supported');
-               },
-               set: function(value) {
-                       throw new Error('WCF.Template.callbacks is no longer supported');
-               }
-       });
-       
-       Template.prototype = {
-               /**
-                * Evaluates the Template using the given parameters.
-                * 
-                * @param       {object}        v       Parameters to pass to the template.
-                */
-               fetch: function(v) {
-                       // this will be replaced in the init function
-                       throw new Error('This Template is not initialized.');
-               }
-       };
-       
-       return Template;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Timer/Repeating.js b/wcfsetup/install/files/js/WoltLab/WCF/Timer/Repeating.js
deleted file mode 100644 (file)
index 0fd8697..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * Provides an object oriented API on top of `setInterval`.
- * 
- * @author     Tim Duesterhus
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Timer/Repeating
- */
-define([], function() {
-       "use strict";
-       
-       /**
-        * Creates a new timer that executes the given `callback` every `delta` milliseconds.
-        * It will be created in started mode. Call `stop()` if necessary.
-        * The `callback` will be passed the owning instance of `Repeating`.
-        * 
-        * @constructor
-        * @param       {function(Repeating)}   callback
-        * @param       {int}                   delta
-        */
-       function Repeating(callback, delta) {
-               if (typeof callback !== 'function') {
-                       throw new TypeError("Expected a valid callback as first argument.");
-               }
-               if (delta < 0 || delta > 86400 * 1000) {
-                       throw new RangeError("Invalid delta " + delta + ". Delta must be in the interval [0, 86400000].");
-               }
-               
-               // curry callback with `this` as the first parameter
-               this._callback = callback.bind(undefined, this);
-               
-               this._delta = delta;
-               this._timer = undefined;
-               
-               this.restart();
-       }
-       Repeating.prototype = {
-               /**
-                * Stops the timer and restarts it. The next call will occur in `delta` milliseconds.
-                */
-               restart: function() {
-                       this.stop();
-                       
-                       this._timer = setInterval(this._callback, this._delta);
-               },
-               
-               /**
-                * Stops the timer. It will no longer be called until you call `restart`.
-                */
-               stop: function() {
-                       if (this._timer !== undefined) {
-                               clearInterval(this._timer);
-                               this._timer = undefined;
-                       }
-               },
-               
-               /**
-                * Changes the `delta` of the timer and `restart`s it.
-                * 
-                * @param       {int}   delta   New delta of the timer.
-                */
-               setDelta: function(delta) {
-                       this._delta = delta;
-                       
-                       this.restart();
-               }
-       };
-       
-       return Repeating;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Acl/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Acl/Simple.js
deleted file mode 100644 (file)
index a7bf8e0..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-define(['Language', 'Dom/ChangeListener', 'WoltLab/WCF/Ui/User/Search/Input'], function(Language, DomChangeListener, UiUserSearchInput) {
-       "use strict";
-       
-       function UiAclSimple(prefix) { this.init(prefix); }
-       UiAclSimple.prototype = {
-               init: function(prefix) {
-                       this._prefix = prefix || '';
-                       
-                       this._build();
-               },
-               
-               _build: function () {
-                       var container = elById(this._prefix + 'aclInputContainer');
-                       
-                       elById(this._prefix + 'aclAllowAll').addEventListener('change', (function() {
-                               elHide(container);
-                       }));
-                       elById(this._prefix + 'aclAllowAll_no').addEventListener('change', (function() {
-                               elShow(container);
-                       }));
-                       
-                       new UiUserSearchInput(elById(this._prefix + 'aclSearchInput'), {
-                               callbackSelect: this._select.bind(this),
-                               includeUserGroups: true,
-                               preventSubmit: true
-                       });
-                       
-                       this._aclListContainer = elById(this._prefix + 'aclListContainer');
-                       
-                       this._list = elById(this._prefix + 'aclAccessList');
-                       this._list.addEventListener(WCF_CLICK_EVENT, this._removeItem.bind(this));
-                       
-                       DomChangeListener.trigger();
-               },
-               
-               _select: function(listItem) {
-                       var type = elData(listItem, 'type');
-                       
-                       var html = '<span class="icon icon16 fa-' + (type === 'group' ? 'users' : 'user') + '"></span>';
-                       html += '<span class="aclLabel">' + elData(listItem, 'label') + '</span>';
-                       html += '<span class="icon icon16 fa-times pointer jsTooltip" title="' + Language.get('wcf.global.button.delete') + '"></span>';
-                       html += '<input type="hidden" name="aclValues[' + type + '][]" value="' + elData(listItem, 'object-id') + '">';
-                       
-                       var item = elCreate('li');
-                       item.innerHTML = html;
-                       
-                       var firstUser = elBySel('.fa-user', this._list);
-                       if (firstUser === null) {
-                               this._list.appendChild(item);
-                       }
-                       else {
-                               this._list.insertBefore(item, firstUser.parentNode);
-                       }
-                       
-                       elShow(this._aclListContainer);
-                       
-                       DomChangeListener.trigger();
-                       
-                       return false;
-               },
-               
-               _removeItem: function (event) {
-                       if (event.target.classList.contains('fa-times')) {
-                               elRemove(event.target.parentNode);
-                               
-                               if (this._list.childElementCount === 0) {
-                                       elHide(this._aclListContainer);
-                               }
-                       }
-               }
-       };
-       
-       return UiAclSimple;
-});
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 9b02ee1..0000000
+++ /dev/null
@@ -1,253 +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
-        */
-       return {
-               /**
-                * 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: 0,
-                               
-                               // 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',
-                               visibility: 'hidden !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 pointerBottom = 0;
-                               var pointerRight = 1;
-                               
-                               el.classList[(top === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerBottom]);
-                               el.classList[(left === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerRight]);
-                       }
-                       
-                       if (bottom !== 'auto') bottom = Math.round(bottom) + 'px';
-                       if (left !== 'auto') left = Math.ceil(left) + 'px';
-                       if (right !== 'auto') right = Math.floor(right) + 'px';
-                       if (top !== 'auto') top = Math.round(top) + 'px';
-                       
-                       DomUtil.setStyles(el, {
-                               bottom: bottom,
-                               left: left,
-                               right: right,
-                               top: top
-                       });
-                       
-                       elShow(el);
-                       el.style.removeProperty('visibility');
-               },
-               
-               /**
-                * Calculates left/right position and verifies 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, int>}   elDimensions    element dimensions
-                * @param       {Object<string, int>}   refDimensions   reference element dimensions
-                * @param       {Object<string, int>}   refOffsets      position of reference element relative to the document
-                * @param       {int}                   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, int>}   elDimensions    element dimensions
-                * @param       {Object<string, int>}   refDimensions   reference element dimensions
-                * @param       {Object<string, int>}   refOffsets      position of reference element relative to the document
-                * @param       {int}                   windowHeight    window height
-                * @param       {int}                   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) < window.scrollY) {
-                                       result = false;
-                               }
-                       }
-                       else {
-                               top = refOffsets.top + refDimensions.height + verticalOffset;
-                               if (top + elDimensions.height - window.scrollY > windowHeight) {
-                                       result = false;
-                               }
-                       }
-                       
-                       return {
-                               align: align,
-                               bottom: bottom,
-                               top: top,
-                               result: result
-                       };
-               }
-       };
-});
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 7342fa4..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/CloseOverlay
- */
-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(WCF_CLICK_EVENT, 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/Confirmation.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Confirmation.js
deleted file mode 100644 (file)
index 302fae4..0000000
+++ /dev/null
@@ -1,171 +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: '',
-                               messageIsHtml: false,
-                               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() : '';
-                       if (_options.messageIsHtml) _text.innerHTML = _options.message;
-                       else _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 = elCreate('div');
-                       elAttr(dialog, 'id', 'wcfSystemConfirmation');
-                       dialog.classList.add('systemConfirmation');
-                       
-                       _text = elCreate('p');
-                       dialog.appendChild(_text);
-                       
-                       _content = elCreate('div');
-                       elAttr(_content, 'id', 'wcfSystemConfirmationContent');
-                       dialog.appendChild(_content);
-                       
-                       var formSubmit = elCreate('div');
-                       formSubmit.classList.add('formSubmit');
-                       dialog.appendChild(formSubmit);
-                       
-                       _confirmButton = elCreate('button');
-                       _confirmButton.classList.add('buttonPrimary');
-                       _confirmButton.textContent = Language.get('wcf.global.confirmation.confirm');
-                       _confirmButton.addEventListener(WCF_CLICK_EVENT, this._confirm.bind(this));
-                       formSubmit.appendChild(_confirmButton);
-                       
-                       var cancelButton = elCreate('button');
-                       cancelButton.textContent = Language.get('wcf.global.confirmation.cancel');
-                       cancelButton.addEventListener(WCF_CLICK_EVENT, 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 665a994..0000000
+++ /dev/null
@@ -1,548 +0,0 @@
-/**
- * Modal dialog handler.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 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/Traverse', 'Dom/Util',   'Ui/Confirmation'
-       ],
-       function(
-               enquire,        Ajax,         Core,        Dictionary,
-               Environment,    Language,     ObjectMap,   DomChangeListener,
-               DomTraverse,    DomUtil,      UiConfirmation
-       )
-{
-       "use strict";
-       
-       var _activeDialog = null;
-       var _container = null;
-       var _dialogs = new Dictionary();
-       var _dialogObjects = new ObjectMap();
-       var _dialogFullHeight = false;
-       var _keyupListener = null;
-       var _staticDialogs = elByClass('jsStaticDialog');
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Dialog
-        */
-       return {
-               /**
-                * 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 = elCreate('div');
-                       _container.classList.add('dialogOverlay');
-                       elAttr(_container, 'aria-hidden', 'true');
-                       _container.addEventListener(WCF_CLICK_EVENT, this._closeOnBackdrop.bind(this));
-                       
-                       elById('content').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('(max-width: 767px)', {
-                               match: function() { _dialogFullHeight = true; },
-                               unmatch: function() { _dialogFullHeight = false; },
-                               setup: function() { _dialogFullHeight = true; },
-                               deferSetup: true
-                       });
-                       
-                       this._initStaticDialogs();
-                       DomChangeListener.add('Ui/Dialog', this._initStaticDialogs.bind(this));
-               },
-               
-               _initStaticDialogs: function() {
-                       var button, container, id;
-                       while (_staticDialogs.length) {
-                               button = _staticDialogs[0];
-                               button.classList.remove('jsStaticDialog');
-                               
-                               id = elData(button, 'dialog-id');
-                               if (id && (container = elById(id))) {
-                                       ((function(button, container) {
-                                               container.classList.remove('jsStaticDialogContent');
-                                               elHide(container);
-                                               button.addEventListener(WCF_CLICK_EVENT, this.openStatic.bind(this, container.id, null, { title: elData(container, 'title') }));
-                                       }).bind(this))(button, container);
-                               }
-                       }
-               },
-               
-               /**
-                * 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 = elById(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);
-                               
-                               // remove id and `display: none` from dialog element
-                               dialogElement.removeAttribute('id');
-                               elShow(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)) {
-                               if (typeof html === 'string' && html.trim() !== '') {
-                                       setupData.source = html;
-                               }
-                               else {
-                                       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));
-                                       
-                                       // deferred initialization
-                                       return {};
-                               }
-                       }
-                       else {
-                               if (typeof setupData.source === 'string') {
-                                       var dialogElement = elCreate('div');
-                                       elAttr(dialogElement, 'id', setupData.id);
-                                       DomUtil.setInnerHtml(dialogElement, 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) {
-                       document.documentElement.classList.add('pageOverlayActive');
-                       
-                       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,
-                                       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|object)}       id              element id
-                * @param       {string}                title           dialog title
-                */
-               setTitle: function(id, title) {
-                       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.");
-                       }
-                       
-                       var dialogTitle = elByClass('dialogTitle', data.dialog);
-                       if (dialogTitle.length) {
-                               dialogTitle[0].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 = elById(id);
-                               if (element === null) {
-                                       throw new Error("Expected either a HTML string or an existing element id.");
-                               }
-                       }
-                       
-                       var dialog = elCreate('div');
-                       dialog.classList.add('dialogContainer');
-                       elAttr(dialog, 'aria-hidden', 'true');
-                       elAttr(dialog, 'role', 'dialog');
-                       elData(dialog, 'id', id);
-                       
-                       var header = elCreate('header');
-                       dialog.appendChild(header);
-                       
-                       var titleId = DomUtil.getUniqueId();
-                       elAttr(dialog, 'aria-labelledby', titleId);
-                       
-                       var title = elCreate('span');
-                       title.classList.add('dialogTitle');
-                       title.textContent = options.title;
-                       elAttr(title, 'id', titleId);
-                       header.appendChild(title);
-                       
-                       if (options.closable) {
-                               var closeButton = elCreate('a');
-                               closeButton.className = 'dialogCloseButton jsTooltip';
-                               elAttr(closeButton, 'title', options.closeButtonLabel);
-                               elAttr(closeButton, 'aria-label', options.closeButtonLabel);
-                               closeButton.addEventListener(WCF_CLICK_EVENT, this._close.bind(this));
-                               header.appendChild(closeButton);
-                               
-                               var span = elCreate('span');
-                               span.className = 'icon icon24 fa-times';
-                               closeButton.appendChild(span);
-                       }
-                       
-                       var contentContainer = elCreate('div');
-                       contentContainer.classList.add('dialogContent');
-                       if (options.disableContentPadding) contentContainer.classList.add('dialogContentNoPadding');
-                       dialog.appendChild(contentContainer);
-                       
-                       var content;
-                       if (element === null) {
-                               if (typeof html === 'string') {
-                                       content = elCreate('div');
-                                       content.id = id;
-                                       DomUtil.setInnerHtml(content, html);
-                               }
-                               else if (html instanceof DocumentFragment) {
-                                       if (html.children[0].nodeName !== 'div' || html.childElementCount > 1) {
-                                               content = elCreate('div');
-                                               content.id = id;
-                                               content.appendChild(html);
-                                       }
-                                       else {
-                                               content = html;
-                                       }
-                               }
-                       }
-                       else {
-                               content = element;
-                       }
-                       
-                       contentContainer.appendChild(content);
-                       
-                       if (content.style.getPropertyValue('display') === 'none') {
-                               elShow(content);
-                       }
-                       
-                       _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 (typeof options.onSetup === 'function') {
-                               options.onSetup(content);
-                       }
-                       
-                       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 = elCreate('div');
-                               DomUtil.setInnerHtml(content, html);
-                               
-                               data.content.appendChild(content);
-                       }
-                       
-                       if (elAttr(data.dialog, 'aria-hidden') === 'true') {
-                               if (elAttr(_container, 'aria-hidden') === 'true') {
-                                       window.addEventListener('keyup', _keyupListener);
-                               }
-                               
-                               elAttr(data.dialog, 'aria-hidden', 'false');
-                               elAttr(_container, 'aria-hidden', 'false');
-                               elData(_container, 'close-on-click', (data.backdropCloseOnClick ? 'true' : 'false'));
-                               _activeDialog = id;
-                               
-                               // set focus on first applicable element
-                               var focusElement = elBySel('.jsDialogAutoFocus', data.dialog);
-                               if (focusElement !== null && focusElement.offsetParent !== null) {
-                                       focusElement.focus();
-                               }
-                               
-                               if (typeof data.onShow === 'function') {
-                                       data.onShow(data.content);
-                               }
-                       }
-                       
-                       this.rebuild(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 (elAttr(data.dialog, 'aria-hidden') === 'true') {
-                               return;
-                       }
-                       
-                       var contentContainer = data.content.parentNode;
-                       
-                       var formSubmit = elBySel('.formSubmit', data.content);
-                       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');
-                               contentContainer.style.removeProperty('margin-bottom');
-                       }
-                       
-                       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 (elData(_container, '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);
-                       }
-                       
-                       elAttr(data.dialog, 'aria-hidden', 'true');
-                       
-                       // get next active dialog
-                       _activeDialog = null;
-                       for (var i = 0; i < _container.childElementCount; i++) {
-                               var child = _container.children[i];
-                               if (elAttr(child, 'aria-hidden') === 'false') {
-                                       _activeDialog = elData(child, 'id');
-                                       break;
-                               }
-                       }
-                       
-                       if (_activeDialog === null) {
-                               elAttr(_container, 'aria-hidden', 'true');
-                               elData(_container, 'close-on-click', 'false');
-                               
-                               window.removeEventListener('keyup', _keyupListener);
-                               document.documentElement.classList.remove('pageOverlayActive');
-                       }
-                       else {
-                               data = _dialogs.get(_activeDialog);
-                               elData(_container, '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 {};
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dropdown/Reusable.js
deleted file mode 100644 (file)
index b0acdc2..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/**
- * Simple interface to work with reusable dropdowns that are not bound to a specific item.
- * 
- * @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/Reusable
- */
-define(['Dictionary', 'Ui/SimpleDropdown'], function(Dictionary, UiSimpleDropdown) {
-       "use strict";
-       
-       var _dropdowns = new Dictionary();
-       var _ghostElementId = 0;
-       
-       /**
-        * Returns dropdown name by internal identifier.
-        *
-        * @param       {string}        identifier      internal identifier
-        * @returns     {string}        dropdown name
-        */
-       function _getDropdownName(identifier) {
-               if (!_dropdowns.has(identifier)) {
-                       throw new Error("Unknown dropdown identifier '" + identifier + "'");
-               }
-               
-               return _dropdowns.get(identifier);
-       }
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Dropdown/Reusable
-        */
-       return {
-               /**
-                * Initializes a new reusable dropdown.
-                * 
-                * @param       {string}        identifier      internal identifier
-                * @param       {Element}       menu            dropdown menu element
-                */
-               init: function(identifier, menu) {
-                       if (_dropdowns.has(identifier)) {
-                               return;
-                       }
-                       
-                       var ghostElement = elCreate('div');
-                       ghostElement.id = 'reusableDropdownGhost' + _ghostElementId++;
-                       
-                       UiSimpleDropdown.initFragment(ghostElement, menu);
-                       
-                       _dropdowns.set(identifier, ghostElement.id);
-               },
-               
-               /**
-                * Returns the dropdown menu element.
-                * 
-                * @param       {string}        identifier      internal identifier
-                * @returns     {Element}       dropdown menu element
-                */
-               getDropdownMenu: function(identifier) {
-                       return UiSimpleDropdown.getDropdownMenu(_getDropdownName(identifier));
-               },
-               
-               /**
-                * Registers a callback invoked upon open and close.
-                * 
-                * @param       {string}        identifier      internal identifier
-                * @param       {function}      callback        callback function
-                */
-               registerCallback: function(identifier, callback) {
-                       UiSimpleDropdown.registerCallback(_getDropdownName(identifier), callback);
-               },
-               
-               /**
-                * Toggles a dropdown.
-                * 
-                * @param       {string}        identifier              internal identifier
-                * @param       {Element}       referenceElement        reference element used for alignment
-                */
-               toggleDropdown: function(identifier, referenceElement) {
-                       UiSimpleDropdown.toggleDropdown(_getDropdownName(identifier), referenceElement);
-               }
-       };
-});
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 6500d73..0000000
+++ /dev/null
@@ -1,462 +0,0 @@
-/**
- * Simple dropdown implementation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 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
-        */
-       return {
-               /**
-                * Performs initial setup such as setting up dropdowns and binding listeners.
-                */
-               setup: function() {
-                       if (_didInit) return;
-                       _didInit = true;
-                       
-                       _menuContainer = elCreate('div');
-                       _menuContainer.className = 'dropdownMenuContainer';
-                       document.body.appendChild(_menuContainer);
-                       
-                       _availableDropdowns = elByClass('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') || elData(button, '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(WCF_CLICK_EVENT, this._toggle.bind(this));
-                               
-                               _dropdowns.set(containerId, dropdown);
-                               _menus.set(containerId, menu);
-                               
-                               if (!containerId.match(/^wcf\d+$/)) {
-                                       elData(menu, 'source', containerId);
-                               }
-                               
-                               // prevent page scrolling
-                               if (menu.childElementCount && menu.children[0].classList.contains('scrollableDropdownMenu')) {
-                                       menu = menu.children[0];
-                                       elData(menu, 'scroll-to-active', true);
-                                       
-                                       var menuHeight = null, menuRealHeight = null;
-                                       menu.addEventListener('wheel', function (event) {
-                                               if (menuHeight === null) menuHeight = menu.clientHeight;
-                                               if (menuRealHeight === null) menuRealHeight = menu.scrollHeight;
-                                               
-                                               // positive value: scrolling up
-                                               if (event.wheelDelta > 0 && menu.scrollTop === 0) {
-                                                       event.preventDefault();
-                                               }
-                                               else if (event.wheelDelta < 0 && (menu.scrollTop + menuHeight === menuRealHeight)) {
-                                                       event.preventDefault();
-                                               }
-                                       });
-                               }
-                       }
-                       
-                       elData(button, 'target', containerId);
-                       
-                       if (isLazyInitialization) {
-                               setTimeout(function() { Core.triggerEvent(button, WCF_CLICK_EVENT); }, 10);
-                       }
-               },
-               
-               /**
-                * Initializes a remote-controlled dropdown.
-                * 
-                * @param       {Element}       dropdown        dropdown wrapper element
-                * @param       {Element}       menu            menu list element
-                */
-               initFragment: function(dropdown, menu) {
-                       this.setup();
-                       
-                       var containerId = DomUtil.identify(dropdown);
-                       if (_dropdowns.has(containerId)) {
-                               return;
-                       }
-                       
-                       _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
-                * @param       {Element=}      referenceElement        alternative reference element, used for reusable dropdown menus
-                */
-               toggleDropdown: function(containerId, referenceElement) {
-                       this._toggle(null, containerId, referenceElement);
-               },
-               
-               /**
-                * Calculates and sets the alignment of given dropdown.
-                * 
-                * @param       {Element}       dropdown                dropdown wrapper element
-                * @param       {Element}       dropdownMenu            menu list element
-                * @param       {Element=}      alternateElement        alternative reference element for alignment
-                */
-               setAlignment: function(dropdown, dropdownMenu, alternateElement) {
-                       // check if button belongs to an i18n textarea
-                       var button = elBySel('.dropdownToggle', dropdown), refDimensionsElement;
-                       if (button !== null && button.parentNode.classList.contains('inputAddonTextarea')) {
-                               refDimensionsElement = button;
-                       }
-                       
-                       UiAlignment.set(dropdownMenu, alternateElement || dropdown, {
-                               pointerClassNames: ['dropdownArrowBottom', 'dropdownArrowRight'],
-                               refDimensionsElement: refDimensionsElement || null,
-                               
-                               // alignment
-                               horizontal: (elData(dropdownMenu, 'dropdown-alignment-horizontal') === 'right') ? 'right' : 'left',
-                               vertical: (elData(dropdownMenu, 'dropdown-alignment-vertical') === 'top') ? 'top' : 'bottom'
-                       });
-               },
-               
-               /**
-                * 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);
-                       return (menu !== undefined && menu.classList.contains('dropdownOpen'));
-               },
-               
-               /**
-                * 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;
-                       //noinspection JSCheckFunctionSignatures
-                       var dropdowns = elBySelAll('.dropdown.dropdownOpen', dialogContent);
-                       
-                       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.classList.contains('dropdownOpen')) {
-                                       if (elDataBool(dropdown, 'is-overlay-dropdown-button')) {
-                                               this.setAlignment(dropdown, _menus.get(containerId));
-                                       }
-                                       else {
-                                               this.close(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
-                * @param       {Element=}      alternateElement        alternative reference element for alignment
-                * @return      {boolean}       'false' if event is not null
-                */
-               _toggle: function(event, targetId, alternateElement) {
-                       if (event !== null) {
-                               event.preventDefault();
-                               event.stopPropagation();
-                               
-                               //noinspection JSCheckFunctionSignatures
-                               targetId = elData(event.currentTarget, 'target');
-                       }
-                       
-                       var dropdown = _dropdowns.get(targetId), preventToggle = false;
-                       if (dropdown !== undefined) {
-                               // check if the dropdown is still the same, as some components (e.g. page actions)
-                               // re-create the parent of a button
-                               if (event) {
-                                       var button = event.currentTarget, parent = button.parentNode;
-                                       if (parent !== dropdown) {
-                                               parent.classList.add('dropdown');
-                                               parent.id = dropdown.id;
-                                               
-                                               // remove dropdown class and id from old parent
-                                               dropdown.classList.remove('dropdown');
-                                               dropdown.id = '';
-                                               
-                                               dropdown = parent;
-                                               _dropdowns.set(targetId, parent);
-                                       }
-                               }
-                               
-                               // Repeated clicks on the dropdown button will not cause it to close, the only way
-                               // to close it is by clicking somewhere else in the document or on another dropdown
-                               // toggle. This is used with the search bar to prevent the dropdown from closing by
-                               // setting the caret position in the search input field.
-                               if (elDataBool(dropdown, 'dropdown-prevent-toggle') && dropdown.classList.contains('dropdownOpen')) {
-                                       preventToggle = true;
-                               }
-                               
-                               // check if 'isOverlayDropdownButton' is set which indicates that the dropdown toggle is within an overlay
-                               if (elData(dropdown, 'is-overlay-dropdown-button') === null) {
-                                       var dialogContent = DomTraverse.parentByClass(dropdown, 'dialogContent');
-                                       elData(dropdown, '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')) {
-                                       if (preventToggle === false) {
-                                               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');
-                                       
-                                       if (menu.childElementCount && elDataBool(menu.children[0], 'scroll-to-active')) {
-                                               var list = menu.children[0];
-                                               list.removeAttribute('data-scroll-to-active');
-                                               
-                                               var active = null;
-                                               for (var i = 0, length = list.childElementCount; i < length; i++) {
-                                                       if (list.children[i].classList.contains('active')) {
-                                                               active = list.children[i];
-                                                               break;
-                                                       }
-                                               }
-                                               
-                                               if (active) {
-                                                       list.scrollTop = Math.max((active.offsetTop + active.clientHeight) - menu.clientHeight, 0);
-                                               }
-                                       }
-                                       
-                                       this._notifyCallbacks(containerId, 'open');
-                                       
-                                       this.setAlignment(dropdown, menu, alternateElement);
-                               }
-                       }).bind(this));
-                       
-                       //noinspection JSDeprecatedSymbols
-                       window.WCF.Dropdown.Interactive.Handler.closeAll();
-                       
-                       return (event === null);
-               }
-       };
-});
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 a4f3cd1..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 (elById('mainMenu') !== null) this.register('mainMenu');
-                       var navigationHeader = elBySel('.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 = elById(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 = elBySelAll('.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;
-                                       }
-                                       
-                                       elShow(item);
-                               }
-                               
-                               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);
-                                       elHide(item);
-                                       
-                                       if (list.scrollWidth < availableWidth) {
-                                               break;
-                                       }
-                               }
-                       }
-                       
-                       if (hiddenItems.length) {
-                               var dropdownMenu;
-                               if (dropdown === undefined) {
-                                       dropdown = elCreate('li');
-                                       dropdown.className = 'dropdown jsFlexibleMenuDropdown';
-                                       var icon = elCreate('a');
-                                       icon.className = 'icon icon16 fa-list';
-                                       dropdown.appendChild(icon);
-                                       
-                                       dropdownMenu = elCreate('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 = elCreate('li');
-                                       item.innerHTML = hiddenItem.innerHTML;
-                                       
-                                       item.addEventListener(WCF_CLICK_EVENT, (function(event) {
-                                               event.preventDefault();
-                                               
-                                               Core.triggerEvent(elBySel('a', hiddenItem), WCF_CLICK_EVENT);
-                                               
-                                               // 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) {
-                               elRemove(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 452bd3d..0000000
+++ /dev/null
@@ -1,445 +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
-        */
-       return {
-               /**
-                * 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}         values          list of existing values
-                * @param       {Object}        options         option list
-                */
-               init: function(elementId, values, options) {
-                       var element = elById(elementId);
-                       if (element === null) {
-                               throw new Error("Expected a valid element id, '" + elementId + "' is invalid.");
-                       }
-                       
-                       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 = elCreate('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);
-                       //noinspection JSUnresolvedVariable
-                       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}        elementId       input element id
-                * @return      {Array}         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: elData(item, 'object-id'),
-                                       value: DomTraverse.childByTag(item, 'SPAN').textContent
-                               };
-                               
-                               values.push(value);
-                       }
-                       
-                       return values;
-               },
-               
-               /**
-                * Sets the list of current values.
-                * 
-                * @param       {string}        elementId       input element id
-                * @param       {Array}         values          list of objects containing object id and value
-                */
-               setValues: function(elementId, values) {
-                       if (!_data.has(elementId)) {
-                               throw new Error("Element id '" + elementId + "' is unknown.");
-                       }
-                       
-                       var data = _data.get(elementId);
-                       
-                       // remove all existing items first
-                       var i, length;
-                       var items = DomTraverse.childrenByClass(data.list, 'item');
-                       for (i = 0, length = items.length; i < length; i++) {
-                               this._removeItem(null, items[i], true);
-                       }
-                       
-                       // add new items
-                       for (i = 0, length = values.length; i < length; i++) {
-                               this._addItem(elementId, values[i]);
-                       }
-               },
-               
-               /**
-                * 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}        options         option list
-                */
-               _createUI: function(element, options) {
-                       var list = elCreate('ol');
-                       list.className = 'inputItemList';
-                       elData(list, 'element-id', element.id);
-                       list.addEventListener(WCF_CLICK_EVENT, function(event) {
-                               if (event.target === list) {
-                                       //noinspection JSUnresolvedFunction
-                                       element.focus();
-                               }
-                       });
-                       
-                       var listItem = elCreate('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) {
-                               elAttr(element, 'maxLength', options.maxLength);
-                       }
-                       
-                       var shadow = null, values = [];
-                       if (options.isCSV) {
-                               shadow = elCreate('input');
-                               shadow.className = 'itemListInputShadow';
-                               shadow.type = 'hidden';
-                               //noinspection JSUnresolvedVariable
-                               shadow.name = element.name;
-                               element.removeAttribute('name');
-                               
-                               list.parentNode.insertBefore(shadow, list);
-                               
-                               if (element.nodeName === 'TEXTAREA') {
-                                       //noinspection JSUnresolvedVariable
-                                       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 = elCreate('input');
-                                       element.parentNode.insertBefore(inputElement, element);
-                                       inputElement.id = element.id;
-                                       
-                                       elRemove(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;
-                               elAttr(data.element, '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       {object}        value           item value
-                */
-               _addItem: function(elementId, value) {
-                       var data = _data.get(elementId);
-                       
-                       var listItem = elCreate('li');
-                       listItem.className = 'item';
-                       
-                       var content = elCreate('span');
-                       content.className = 'content';
-                       elData(content, 'object-id', value.objectId);
-                       content.textContent = value.value;
-                       
-                       var button = elCreate('a');
-                       button.className = 'icon icon16 fa-times';
-                       button.addEventListener(WCF_CLICK_EVENT, _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
-                * @param       {boolean?}      noFocus         input element will not be focused if true
-                */
-               _removeItem: function(event, item, noFocus) {
-                       item = (event === null) ? item : event.currentTarget.parentNode;
-                       
-                       var parent = item.parentNode;
-                       //noinspection JSCheckFunctionSignatures
-                       var elementId = elData(parent, 'element-id');
-                       var data = _data.get(elementId);
-                       
-                       data.suggestion.removeExcludedValue(item.children[0].textContent);
-                       parent.removeChild(item);
-                       if (!noFocus) 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;
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/Filter.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/ItemList/Filter.js
deleted file mode 100644 (file)
index ca252b1..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * Provides a filter input for checkbox lists.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Permission
- */
-define(['EventKey', 'Language', 'List', 'StringUtil', 'Dom/Util'], function (EventKey, Language, List, StringUtil, DomUtil) {
-       "use strict";
-       
-       /**
-        * Creates a new filter input.
-        * 
-        * @param       {string}        elementId       list element id
-        * @constructor
-        */
-       function UiItemListFilter(elementId) { this.init(elementId); }
-       UiItemListFilter.prototype = {
-               /**
-                * Creates a new filter input.
-                * 
-                * @param       {string}        elementId       list element id
-                */
-               init: function(elementId) {
-                       this._value = '';
-                       
-                       var element = elById(elementId);
-                       if (element === null) {
-                               throw new Error("Expected a valid element id, '" + elementId + "' does not match anything.");
-                       }
-                       else if (!element.classList.contains('scrollableCheckboxList')) {
-                               throw new Error("Filter only works with elements with the CSS class 'scrollableCheckboxList'.");
-                       }
-                       
-                       var container = elCreate('div');
-                       container.className = 'itemListFilter';
-                       
-                       element.parentNode.insertBefore(container, element);
-                       container.appendChild(element);
-                       
-                       var inputAddon = elCreate('div');
-                       inputAddon.className = 'inputAddon';
-                       
-                       var input = elCreate('input');
-                       input.className = 'long';
-                       input.type = 'text';
-                       input.placeholder = Language.get('wcf.global.filter.placeholder');
-                       input.addEventListener('keydown', function (event) {
-                               if (EventKey.Enter(event)) {
-                                       event.preventDefault();
-                               }
-                       });
-                       input.addEventListener('keyup', this._keyup.bind(this));
-                       
-                       var clearButton = elCreate('a');
-                       clearButton.href = '#';
-                       clearButton.className = 'button inputSuffix jsTooltip';
-                       clearButton.title = Language.get('wcf.global.filter.button.clear');
-                       clearButton.innerHTML = '<span class="icon icon16 fa-times"></span>';
-                       clearButton.addEventListener('click', (function(event) {
-                               event.preventDefault();
-                               
-                               this._input.value = '';
-                               this._keyup();
-                       }).bind(this));
-                       
-                       inputAddon.appendChild(input);
-                       inputAddon.appendChild(clearButton);
-                       
-                       container.appendChild(inputAddon);
-                       
-                       this._container = container;
-                       this._element = element;
-                       this._input = input;
-                       this._items = null;
-                       this._fragment = null;
-               },
-               
-               /**
-                * Builds the item list and rebuilds the items' DOM for easier manipulation.
-                * 
-                * @protected
-                */
-               _buildItems: function() {
-                       this._items = new List();
-                       
-                       var item;
-                       for (var i = 0, length = this._element.childElementCount; i < length; i++) {
-                               item = this._element.children[i];
-                               
-                               var label = item.children[0];
-                               var text = label.textContent.trim();
-                               
-                               var checkbox = label.children[0];
-                               while (checkbox.nextSibling) {
-                                       label.removeChild(checkbox.nextSibling);
-                               }
-                               
-                               label.appendChild(document.createTextNode(' '));
-                               
-                               var span = elCreate('span');
-                               span.textContent = text;
-                               label.appendChild(span);
-                               
-                               this._items.add({
-                                       item: item,
-                                       span: span,
-                                       text: text
-                               });
-                       }
-               },
-               
-               /**
-                * Rebuilds the list on keyup, uses case-insensitive matching.
-                * 
-                * @protected
-                */
-               _keyup: function() {
-                       var value = this._input.value.trim();
-                       if (this._value === value) {
-                               return;
-                       }
-                       
-                       if (this._fragment === null) {
-                               this._fragment = document.createDocumentFragment();
-                               
-                               // set fixed height to avoid layout jumps
-                               this._element.style.setProperty('height', this._element.offsetHeight + 'px', '');
-                       }
-                       
-                       // move list into fragment before editing items, increases performance
-                       // by avoiding the browser to perform repaint/layout over and over again
-                       this._fragment.appendChild(this._element);
-                       
-                       if (this._items === null) {
-                               this._buildItems();
-                       }
-                       
-                       var regexp = new RegExp('(' + StringUtil.escapeRegExp(value) + ')', 'i');
-                       var hasVisibleItems = (value === '');
-                       this._items.forEach(function (item) {
-                               if (value === '') {
-                                       item.span.textContent = item.text;
-                                       
-                                       elShow(item.item);
-                               }
-                               else {
-                                       if (regexp.test(item.text)) {
-                                               item.span.innerHTML = item.text.replace(regexp, '<u>$1</u>');
-                                               
-                                               elShow(item.item);
-                                               hasVisibleItems = true;
-                                       }
-                                       else {
-                                               elHide(item.item);
-                                       }
-                               }
-                       });
-                       
-                       this._container.insertBefore(this._fragment.firstChild, this._container.firstChild);
-                       this._value = value;
-                       
-                       var innerError = this._container.nextElementSibling;
-                       if (innerError && !innerError.classList.contains('innerError')) innerError = null;
-                       
-                       if (hasVisibleItems) {
-                               if (innerError) {
-                                       elRemove(innerError);
-                               }
-                       }
-                       else {
-                               if (!innerError) {
-                                       innerError = elCreate('small');
-                                       innerError.className = 'innerError';
-                                       innerError.textContent = Language.get('wcf.global.filter.error.noMatches');
-                                       DomUtil.insertAfter(innerError, this._container);
-                               } 
-                       }
-               }
-       };
-       
-       return UiItemListFilter;
-});
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 c730d5b..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/Like/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Like/Handler.js
deleted file mode 100644 (file)
index 1af428c..0000000
+++ /dev/null
@@ -1,412 +0,0 @@
-/**
- * Provides interface elements to display and review likes.
- * 
- * @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/Like/Handler
- */
-define(
-       [
-               'Ajax',      'Core',                     'Dictionary',         'Language',
-               'ObjectMap', 'StringUtil',               'Dom/ChangeListener', 'Dom/Util',
-               'Ui/Dialog', 'WoltLab/WCF/Ui/User/List', 'User'
-       ],
-       function(
-               Ajax,        Core,                        Dictionary,           Language,
-               ObjectMap,   StringUtil,                  DomChangeListener,    DomUtil,
-               UiDialog,    UiUserList,                  User
-       )
-{
-       "use strict";
-       
-       var _isBusy = false;
-       
-       /**
-        * @constructor
-        */
-       function UiLikeHandler(objectType, options) { this.init(objectType, options); }
-       UiLikeHandler.prototype = {
-               /**
-                * Initializes the like handler.
-                * 
-                * @param       {string}        objectType      object type
-                * @param       {object}        options         initialization options
-                */
-               init: function(objectType, options) {
-                       if (options.containerSelector === '') {
-                               throw new Error("[WoltLab/WCF/Ui/Like/Handler] Expected a non-empty string for option 'containerSelector'.");
-                       }
-                       
-                       this._containers = new ObjectMap();
-                       this._details = new ObjectMap();
-                       this._objectType = objectType;
-                       this._options = Core.extend({
-                               // settings
-                               badgeClassNames: '',
-                               isSingleItem: false,
-                               markListItemAsActive: false,
-                               renderAsButton: true,
-                               summaryPrepend: true,
-                               summaryUseIcon: true,
-                               
-                               // permissions
-                               canDislike: false,
-                               canLike: false,
-                               canLikeOwnContent: false,
-                               canViewSummary: false,
-                               
-                               // selectors
-                               badgeContainerSelector: '.messageHeader .messageStatus',
-                               buttonAppendToSelector: '.messageFooter .messageFooterButtons',
-                               buttonBeforeSelector: '',
-                               containerSelector: '',
-                               summarySelector: '.messageFooterGroup'
-                       }, options);
-                       
-                       this.initContainers(options, objectType);
-                       
-                       DomChangeListener.add('WoltLab/WCF/Ui/Like/Handler-' + objectType, this.initContainers.bind(this));
-               },
-               
-               /**
-                * Initializes all applicable containers.
-                */
-               initContainers: function() {
-                       var element, elements = elBySelAll(this._options.containerSelector), elementData, triggerChange = false;
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               element = elements[i];
-                               if (this._containers.has(element)) {
-                                       continue;
-                               }
-                               
-                               elementData = {
-                                       badge: null,
-                                       dislikeButton: null,
-                                       likeButton: null,
-                                       summary: null,
-                                       
-                                       dislikes: ~~elData(element, 'like-dislikes'),
-                                       liked: ~~elData(element, 'like-liked'),
-                                       likes: ~~elData(element, 'like-likes'),
-                                       objectId: ~~elData(element, 'object-id'),
-                                       users: JSON.parse(elData(element, 'like-users'))
-                               };
-                               
-                               this._containers.set(element, elementData);
-                               this._buildWidget(element, elementData);
-                               
-                               triggerChange = true;
-                       }
-                       
-                       if (triggerChange) {
-                               DomChangeListener.trigger();
-                       }
-               },
-               
-               /**
-                * Creates the interface elements.
-                * 
-                * @param       {Element}       element         container element
-                * @param       {object}        elementData     like data
-                */
-               _buildWidget: function(element, elementData) {
-                       // build summary
-                       if (this._options.canViewSummary) {
-                               var summary, summaryContent, summaryIcon;
-                               var summaryContainer = (this._options.isSingleItem) ? elBySel(this._options.summarySelector) : elBySel(this._options.summarySelector, element);
-                               if (summaryContainer !== null) {
-                                       summary = elCreate('div');
-                                       summary.className = 'likesSummary';
-                                       
-                                       if (this._options.summaryUseIcon) {
-                                               summaryIcon = elCreate('span');
-                                               summaryIcon.className = 'icon icon16 fa-thumbs-o-up';
-                                               summary.appendChild(summaryIcon);
-                                       }
-                                       
-                                       summaryContent = elCreate('span');
-                                       summaryContent.className = 'likesSummaryContent';
-                                       summaryContent.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));
-                                       summary.appendChild(summaryContent);
-                                       
-                                       if (this._options.summaryPrepend) {
-                                               DomUtil.prepend(summary, summaryContainer);
-                                       }
-                                       else {
-                                               summaryContainer.appendChild(summary);
-                                       }
-                                       
-                                       elementData.summary = summaryContent;
-                                       
-                                       this._updateSummary(element);
-                               }
-                       }
-                       
-                       // cumulative likes
-                       var badge, listItem;
-                       var badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.badgeContainerSelector) : elBySel(this._options.badgeContainerSelector, element);
-                       if (badgeContainer !== null) {
-                               badge = elCreate('a');
-                               badge.href = '#';
-                               badge.className = 'wcfLikeCounter jsTooltip' + (this._options.badgeClassNames ? ' ' + this._options.badgeClassNames : '');
-                               badge.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));
-                               
-                               if (badgeContainer.nodeName === 'OL' || badgeContainer.nodeName === 'UL') {
-                                       listItem = elCreate('li');
-                                       listItem.appendChild(badge);
-                                       badgeContainer.appendChild(listItem);
-                               }
-                               else {
-                                       badgeContainer.appendChild(badge);
-                               }
-                               
-                               elementData.badge = badge;
-                               
-                               this._updateBadge(element);
-                       }
-                       
-                       if (this._options.canLike && (User.userId != elData(element, 'user-id') || this._options.canLikeOwnContent)) {
-                               var appendTo = (this._options.buttonAppendToSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonAppendToSelector) : elBySel(this._options.buttonAppendToSelector, element)) : null;
-                               var insertPosition = (this._options.buttonBeforeSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonBeforeSelector) : elBySel(this._options.buttonBeforeSelector, element)) : null;
-                               if (insertPosition === null && appendTo === null) {
-                                       throw new Error("Unable to find insert location for like/dislike buttons.");
-                               }
-                               else {
-                                       // like button
-                                       elementData.likeButton = this._createButton(element, true, insertPosition, appendTo);
-                                       
-                                       // dislike button
-                                       if (this._options.canDislike) {
-                                               elementData.dislikeButton = this._createButton(element, false, insertPosition, appendTo);
-                                       }
-                                       
-                                       this._updateActiveState(element);
-                               }
-                       }
-               },
-               
-               /**
-                * Creates a like or dislike button.
-                * 
-                * @param       {Element}       element         container element
-                * @param       {boolean}       isLike          false if this is a dislike button
-                * @param       {Element?}      insertBefore    insert button before given element
-                * @param       {Element?}      appendTo        append button to given element
-                * @return      {Element}       button element 
-                */
-               _createButton: function(element, isLike, insertBefore, appendTo) {
-                       var title = Language.get('wcf.like.button.' + (isLike ? 'like' : 'dislike'));
-                       
-                       var listItem = elCreate('li');
-                       listItem.className = 'wcf' + (isLike ? 'Like' : 'Dislike') + 'Button';
-                       
-                       var button = elCreate('a');
-                       button.className = 'jsTooltip' + (this._options.renderAsButton ? ' button' : '');
-                       button.href = '#';
-                       button.title = title;
-                       button.innerHTML = '<span class="icon icon16 fa-thumbs-o-' + (isLike ? 'up' : 'down') + '"></span> <span class="invisible">' + title + '</span>';
-                       button.addEventListener(WCF_CLICK_EVENT, this._like.bind(this, element));
-                       elData(button, 'type', (isLike ? 'like' : 'dislike'));
-                       
-                       listItem.appendChild(button);
-                       
-                       if (insertBefore) {
-                               insertBefore.parentNode.insertBefore(listItem, insertBefore);
-                       }
-                       else {
-                               appendTo.appendChild(listItem);
-                       }
-                       
-                       return button;
-               },
-               
-               /**
-                * Shows the summary of likes/dislikes.
-                * 
-                * @param       {Element}       element         container element
-                * @param       {object}        event           event object
-                */
-               _showSummary: function(element, event) {
-                       event.preventDefault();
-                       
-                       if (!this._details.has(element)) {
-                               this._details.set(element, new UiUserList({
-                                       className: 'wcf\\data\\like\\LikeAction',
-                                       dialogTitle: Language.get('wcf.like.details'),
-                                       parameters: {
-                                               data: {
-                                                       containerID: DomUtil.identify(element),
-                                                       objectID: this._containers.get(element).objectId,
-                                                       objectType: this._objectType
-                                               }
-                                       }
-                               }));
-                       }
-                       
-                       this._details.get(element).open();
-               },
-               
-               /**
-                * Updates the display of cumulative likes.
-                * 
-                * @param       {Element}       element         container element
-                */
-               _updateBadge: function(element) {
-                       var data = this._containers.get(element);
-                       
-                       if (data.likes === 0 && data.dislikes === 0) {
-                               elHide(data.badge);
-                       }
-                       else {
-                               elShow(data.badge);
-                               
-                               // update like counter
-                               var cumulativeLikes = data.likes - data.dislikes;
-                               var content = '<span class="icon icon16 fa-thumbs-o-' + (cumulativeLikes < 0 ? 'down' : 'up' ) + '"></span><span class="wcfLikeValue">';
-                               if (cumulativeLikes > 0) {
-                                       content += '+' + StringUtil.addThousandsSeparator(cumulativeLikes);
-                                       data.badge.classList.add('likeCounterLiked');
-                               }
-                               else if (cumulativeLikes < 0) {
-                                       // U+2212 = minus sign
-                                       content += '\u2212' + StringUtil.addThousandsSeparator(Math.abs(cumulativeLikes));
-                                       data.badge.classList.add('likeCounterDisliked');
-                               }
-                               else {
-                                       // U+00B1 = plus-minus sign
-                                       content += '\u00B1' + '0';
-                               }
-                               
-                               data.badge.innerHTML = content + '</span>';
-                               data.badge.setAttribute('data-tooltip', Language.get('wcf.like.tooltip', {
-                                       dislikes: data.dislikes,
-                                       likes: data.likes
-                               }));
-                       }
-               },
-               
-               /**
-                * Updates the like summary.
-                * 
-                * @param       {Element}       element         container element
-                */
-               _updateSummary: function(element) {
-                       var data = this._containers.get(element);
-                       
-                       if (data.likes) {
-                               elShow(data.summary.parentNode);
-                               
-                               var usernames = [];
-                               var keys = Object.keys(data.users);
-                               for (var i = 0, length = keys.length; i < length; i++) {
-                                       usernames.push(data.users[keys[i]]);
-                               }
-                               
-                               var others = data.likes - usernames.length;
-                               data.summary.innerHTML = Language.get('wcf.like.summary', { users: usernames, others: others });
-                       }
-                       else {
-                               elHide(data.summary.parentNode);
-                       }
-               },
-               
-               /**
-                * Updates the active like/dislike button state.
-                * 
-                * @param       {Element}       element         container element
-                */
-               _updateActiveState: function(element) {
-                       var data = this._containers.get(element);
-                       
-                       var likeTarget = (this._options.markListItemAsActive) ? data.likeButton.parentNode : data.likeButton;
-                       likeTarget.classList.remove('active');
-                       
-                       if (data.liked === 1) {
-                               likeTarget.classList.add('active');
-                       }
-                       
-                       if (this._options.canDislike) {
-                               var dislikeTarget = (this._options.markListItemAsActive) ? data.dislikeButton.parentNode : data.dislikeButton;
-                               dislikeTarget.classList.remove('active');
-                               
-                               if (data.liked === -1) {
-                                       dislikeTarget.classList.add('active');
-                               }
-                       }
-               },
-               
-               /**
-                * Likes or dislikes an element.
-                * 
-                * @param       {Element}       element         container element
-                * @param       {object}        event           event object
-                */
-               _like: function(element, event) {
-                       event.preventDefault();
-                       
-                       if (_isBusy) {
-                               return;
-                       }
-                       
-                       _isBusy = true;
-                       
-                       Ajax.api(this, {
-                               actionName: elData(event.currentTarget, 'type'),
-                               parameters: {
-                                       data: {
-                                               containerID: DomUtil.identify(element),
-                                               objectID: this._containers.get(element).objectId,
-                                               objectType: this._objectType
-                                       }
-                               }
-                       });
-               },
-               
-               _ajaxSuccess: function(data) {
-                       var element = elById(data.returnValues.containerID);
-                       var elementData = this._containers.get(element);
-                       if (elementData === undefined) {
-                               return;
-                       }
-                       
-                       elementData.dislikes = ~~data.returnValues.dislikes;
-                       elementData.likes = ~~data.returnValues.likes;
-                       
-                       var users = data.returnValues.users;
-                       elementData.users = [];
-                       var keys = Object.keys(users);
-                       for (var i = 0, length = keys.length; i < length; i++) {
-                               elementData.users.push(StringUtil.escapeHTML(users[keys[i]].username));
-                       }
-                       
-                       if (data.returnValues.isLiked == 1) elementData.liked = 1;
-                       else if (data.returnValues.isDisliked == 1) elementData.liked = -1;
-                       else elementData.liked = 0;
-                       
-                       // update label
-                       this._updateBadge(element);
-                       
-                       // update summary
-                       if (this._options.canViewSummary) this._updateSummary(element);
-                       
-                       // mark button as active
-                       this._updateActiveState(element);
-                       
-                       // invalidate cache for like details
-                       this._details['delete'](element);
-                       
-                       _isBusy = false;
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: 'wcf\\data\\like\\LikeAction'
-                               }
-                       };
-               }
-       };
-       
-       return UiLikeHandler;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/InlineEditor.js
deleted file mode 100644 (file)
index 68cef5c..0000000
+++ /dev/null
@@ -1,714 +0,0 @@
-/**
- * Flexible message inline editor.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Message/InlineEditor
- */
-define(
-       [
-               'Ajax',         'Core',            'Dictionary',          'Environment',
-               'EventHandler', 'Language',        'ObjectMap',           'Dom/ChangeListener', 'Dom/Traverse',
-               'Dom/Util',     'Ui/Notification', 'Ui/ReusableDropdown', 'WoltLab/WCF/Ui/Scroll'
-       ],
-       function(
-               Ajax,            Core,              Dictionary,            Environment,
-               EventHandler,    Language,          ObjectMap,             DomChangeListener,    DomTraverse,
-               DomUtil,         UiNotification,    UiReusableDropdown,    UiScroll
-       )
-{
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function UiMessageInlineEditor(options) { this.init(options); }
-       UiMessageInlineEditor.prototype = {
-               /**
-                * Initializes the message inline editor.
-                * 
-                * @param       {Object}        options         list of configuration options
-                */
-               init: function(options) {
-                       this._activeDropdownElement = null;
-                       this._activeElement = null;
-                       this._dropdownMenu = null;
-                       this._elements = new ObjectMap();
-                       this._options = Core.extend({
-                               canEditInline: false,
-                               
-                               className: '',
-                               containerId: 0,
-                               dropdownIdentifier: '',
-                               editorPrefix: 'messageEditor',
-                               
-                               messageSelector: '.jsMessage',
-                               
-                               quoteManager: null
-                       }, options);
-                       
-                       this.rebuild();
-                       
-                       DomChangeListener.add('Ui/Message/InlineEdit_' + this._options.className, this.rebuild.bind(this));
-               },
-               
-               /**
-                * Initializes each applicable message, should be called whenever new
-                * messages are being displayed.
-                */
-               rebuild: function() {
-                       var button, canEdit, element, elements = elBySelAll(this._options.messageSelector);
-                       
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               element = elements[i];
-                               if (this._elements.has(element)) {
-                                       continue;
-                               }
-                               
-                               button = elBySel('.jsMessageEditButton', element);
-                               if (button !== null) {
-                                       canEdit = elDataBool(element, 'can-edit');
-                                       
-                                       if (this._options.canEditInline || elDataBool(element, 'can-edit-inline')) {
-                                               button.addEventListener(WCF_CLICK_EVENT, this._clickDropdown.bind(this, element));
-                                               button.classList.add('jsDropdownEnabled');
-                                               
-                                               if (canEdit) {
-                                                       button.addEventListener('dblclick', this._click.bind(this, element));
-                                               }
-                                       }
-                                       else if (canEdit) {
-                                               button.addEventListener(WCF_CLICK_EVENT, this._click.bind(this, element));
-                                       }
-                               }
-                               
-                               var messageBody = elBySel('.messageBody', element);
-                               var messageFooter = elBySel('.messageFooter', element);
-                               var messageHeader = elBySel('.messageHeader', element);
-                               
-                               this._elements.set(element, {
-                                       button: button,
-                                       messageBody: messageBody,
-                                       messageBodyEditor: null,
-                                       messageFooter: messageFooter,
-                                       messageFooterButtons: elBySel('.messageFooterButtons', messageFooter),
-                                       messageHeader: messageHeader,
-                                       messageText: elBySel('.messageText', messageBody)
-                               });
-                       }
-               },
-               
-               /**
-                * Handles clicks on the edit button or the edit dropdown item.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {?Event}        event           event object
-                * @protected
-                */
-               _click: function(element, event) {
-                       if (element === null) element = this._activeDropdownElement;
-                       if (event) event.preventDefault();
-                       
-                       if (this._activeElement === null) {
-                               this._activeElement = element;
-                               
-                               this._prepare();
-                               
-                               Ajax.api(this, {
-                                       actionName: 'beginEdit',
-                                       parameters: {
-                                               containerID: this._options.containerId,
-                                               objectID: this._getObjectId(element)
-                                       }
-                               });
-                       }
-                       else {
-                               UiNotification.show('wcf.message.error.editorAlreadyInUse', null, 'warning');
-                       }
-               },
-               
-               /**
-                * Creates and opens the dropdown on first usage.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {Object}        event           event object
-                * @protected
-                */
-               _clickDropdown: function(element, event) {
-                       event.preventDefault();
-                       
-                       var button = event.currentTarget;
-                       if (button.classList.contains('dropdownToggle')) {
-                               return;
-                       }
-                       
-                       button.classList.add('dropdownToggle');
-                       button.parentNode.classList.add('dropdown');
-                       (function(button, element) {
-                               button.addEventListener(WCF_CLICK_EVENT, (function(event) {
-                                       event.preventDefault();
-                                       event.stopPropagation();
-                                       
-                                       this._activeDropdownElement = element;
-                                       UiReusableDropdown.toggleDropdown(this._options.dropdownIdentifier, button);
-                               }).bind(this));
-                       }).bind(this)(button, element);
-                       
-                       // build dropdown
-                       if (this._dropdownMenu === null) {
-                               this._dropdownMenu = elCreate('ul');
-                               this._dropdownMenu.className = 'dropdownMenu';
-                               
-                               var items = this._dropdownGetItems();
-                               
-                               EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownInit_' + this._options.dropdownIdentifier, {
-                                       items: items
-                               });
-                               
-                               this._dropdownBuild(items);
-                               
-                               UiReusableDropdown.init(this._options.dropdownIdentifier, this._dropdownMenu);
-                               UiReusableDropdown.registerCallback(this._options.dropdownIdentifier, this._dropdownToggle.bind(this));
-                       }
-                       
-                       setTimeout(function() {
-                               Core.triggerEvent(button, WCF_CLICK_EVENT);
-                       }, 10);
-               },
-               
-               /**
-                * Creates the dropdown menu on first usage.
-                * 
-                * @param       {Object}        items   list of dropdown items
-                * @protected
-                */
-               _dropdownBuild: function(items) {
-                       var item, label, listItem;
-                       var callbackClick = this._clickDropdownItem.bind(this);
-                       
-                       for (var i = 0, length = items.length; i < length; i++) {
-                               item = items[i];
-                               listItem = elCreate('li');
-                               elData(listItem, 'item', item.item);
-                               
-                               if (item.item === 'divider') {
-                                       listItem.className = 'dropdownDivider';
-                               }
-                               else {
-                                       label = elCreate('span');
-                                       label.textContent = Language.get(item.label);
-                                       listItem.appendChild(label);
-                                       
-                                       if (item.item === 'editItem') {
-                                               listItem.addEventListener(WCF_CLICK_EVENT, this._click.bind(this, null));
-                                       }
-                                       else {
-                                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                                       }
-                               }
-                               
-                               this._dropdownMenu.appendChild(listItem);
-                       }
-               },
-               
-               /**
-                * Callback for dropdown toggle.
-                * 
-                * @param       {int}           containerId     container id
-                * @param       {string}        action          toggle action, either 'open' or 'close'
-                * @protected
-                */
-               _dropdownToggle: function(containerId, action) {
-                       var elementData = this._elements.get(this._activeDropdownElement);
-                       elementData.button.parentNode.classList[(action === 'open' ? 'add' : 'remove')]('dropdownOpen');
-                       elementData.messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible');
-                       
-                       if (action === 'open') {
-                               var visibility = this._dropdownOpen();
-                               
-                               EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownOpen_' + this._options.dropdownIdentifier, {
-                                       element: this._activeDropdownElement,
-                                       visibility: visibility
-                               });
-                               
-                               var item, listItem, visiblePredecessor = false;
-                               for (var i = 0; i < this._dropdownMenu.childElementCount; i++) {
-                                       listItem = this._dropdownMenu.children[i];
-                                       item = elData(listItem, 'item');
-                                       
-                                       if (item === 'divider') {
-                                               if (visiblePredecessor) {
-                                                       elShow(listItem);
-                                                       
-                                                       visiblePredecessor = false;
-                                               }
-                                               else {
-                                                       elHide(listItem);
-                                               }
-                                       }
-                                       else {
-                                               if (objOwns(visibility, item) && visibility[item] === false) {
-                                                       elHide(listItem);
-                                                       
-                                                       // check if previous item was a divider
-                                                       if (i > 0 && i + 1 === this._dropdownMenu.childElementCount) {
-                                                               if (elData(listItem.previousElementSibling, 'item') === 'divider') {
-                                                                       elHide(listItem.previousElementSibling);
-                                                               }
-                                                       }
-                                               }
-                                               else {
-                                                       elShow(listItem);
-                                                       
-                                                       visiblePredecessor = true;
-                                               }
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Returns the list of dropdown items for this type.
-                * 
-                * @return      {Array<Object>}         list of objects containing the type name and label
-                * @protected
-                */
-               _dropdownGetItems: function() {},
-               
-               /**
-                * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value
-                * to represent the visibility of each item. Items that do not appear in this list will be considered
-                * visible.
-                * 
-                * @return      {Object<string, boolean>}
-                * @protected
-                */
-               _dropdownOpen: function() {},
-               
-               /**
-                * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument.
-                * 
-                * @param       {string}        item    selected dropdown item
-                * @protected
-                */
-               _dropdownSelect: function(item) {},
-               
-               /**
-                * Handles clicks on a dropdown item.
-                * 
-                * @param       {Event}         event   event object
-                * @protected
-                */
-               _clickDropdownItem: function(event) {
-                       event.preventDefault();
-                       
-                       //noinspection JSCheckFunctionSignatures
-                       this._dropdownSelect(elData(event.currentTarget, 'item'));
-               },
-               
-               /**
-                * Prepares the message for editor display.
-                * 
-                * @protected
-                */
-               _prepare: function() {
-                       var data = this._elements.get(this._activeElement);
-                       
-                       var messageBodyEditor = elCreate('div');
-                       messageBodyEditor.className = 'messageBody editor';
-                       data.messageBodyEditor = messageBodyEditor;
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon48 fa-spinner';
-                       messageBodyEditor.appendChild(icon);
-                       
-                       DomUtil.insertAfter(messageBodyEditor, data.messageBody);
-                       
-                       elHide(data.messageBody);
-               },
-               
-               /**
-                * Shows the message editor.
-                * 
-                * @param       {Object}        data            ajax response data
-                * @protected
-                */
-               _showEditor: function(data) {
-                       var id = this._getEditorId();
-                       var elementData = this._elements.get(this._activeElement);
-                       
-                       this._activeElement.classList.add('jsInvalidQuoteTarget');
-                       var icon = DomTraverse.childByClass(elementData.messageBodyEditor, 'icon');
-                       elRemove(icon);
-                       
-                       var messageBody = elementData.messageBodyEditor;
-                       var editor = elCreate('div');
-                       editor.className = 'editorContainer';
-                       //noinspection JSUnresolvedVariable
-                       DomUtil.setInnerHtml(editor, data.returnValues.template);
-                       messageBody.appendChild(editor);
-                       
-                       // bind buttons
-                       var formSubmit = elBySel('.formSubmit', editor);
-                       
-                       var buttonSave = elBySel('button[data-type="save"]', formSubmit);
-                       buttonSave.addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
-                       
-                       var buttonCancel = elBySel('button[data-type="cancel"]', formSubmit);
-                       buttonCancel.addEventListener(WCF_CLICK_EVENT, this._restoreMessage.bind(this));
-                       
-                       EventHandler.add('com.woltlab.wcf.redactor', 'submitEditor_' + id, (function(data) {
-                               data.cancel = true;
-                               
-                               this._save();
-                       }).bind(this));
-                       
-                       // hide message header and footer
-                       elHide(elementData.messageHeader);
-                       elHide(elementData.messageFooter);
-                       
-                       var editorElement = elById(id);
-                       if (Environment.editor() === 'redactor') {
-                               window.setTimeout((function() {
-                                       if (this._options.quoteManager) {
-                                               this._options.quoteManager.setAlternativeEditor(id);
-                                       }
-                                       
-                                       UiScroll.element(this._activeElement);
-                               }).bind(this), 250);
-                       }
-                       else {
-                               editorElement.focus();
-                       }
-               },
-               
-               /**
-                * Restores the message view.
-                * 
-                * @protected
-                */
-               _restoreMessage: function() {
-                       var elementData = this._elements.get(this._activeElement);
-                       
-                       this._destroyEditor();
-                       
-                       elRemove(elementData.messageBodyEditor);
-                       elementData.messageBodyEditor = null;
-                       
-                       elShow(elementData.messageBody);
-                       elShow(elementData.messageFooter);
-                       elShow(elementData.messageHeader);
-                       this._activeElement.classList.remove('jsInvalidQuoteTarget');
-                       
-                       this._activeElement = null;
-                       
-                       if (this._options.quoteManager) {
-                               this._options.quoteManager.clearAlternativeEditor();
-                       }
-               },
-               
-               /**
-                * Saves the editor message.
-                * 
-                * @protected
-                */
-               _save: function() {
-                       var parameters = {
-                               containerID: this._options.containerId,
-                               data: {
-                                       message: ''
-                               },
-                               objectID: this._getObjectId(this._activeElement),
-                               removeQuoteIDs: (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : []
-                       };
-                       
-                       var id = this._getEditorId();
-                       
-                       // add any available settings
-                       var settingsContainer = elById('settings_' + id);
-                       if (settingsContainer) {
-                               elBySelAll('input, select, textarea', settingsContainer, function (element) {
-                                       if (element.nodeName === 'INPUT' && (element.type === 'checkbox' || element.type === 'radio')) {
-                                               if (!element.checked) {
-                                                       return;
-                                               }
-                                       }
-                                       
-                                       var name = element.name;
-                                       if (parameters.hasOwnProperty(name)) {
-                                               throw new Error("Variable overshadowing, key '" + name + "' is already present.");
-                                       }
-                                       
-                                       parameters[name] = element.value.trim();
-                               });
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'getText_' + id, parameters.data);
-                       
-                       if (!this._validate(parameters)) {
-                               // validation failed
-                               return;
-                       }
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_' + id, parameters);
-                       
-                       Ajax.api(this, {
-                               actionName: 'save',
-                               parameters: parameters
-                       });
-                       
-                       this._hideEditor();
-               },
-               
-               /**
-                * Validates the message and invokes listeners to perform additional validation.
-                *
-                * @param       {Object}        parameters      request parameters
-                * @return      {boolean}       validation result
-                * @protected
-                */
-               _validate: function(parameters) {
-                       // remove all existing error elements
-                       var errorMessages = elByClass('innerError', this._activeElement);
-                       while (errorMessages.length) {
-                               elRemove(errorMessages[0]);
-                       }
-                       
-                       var data = {
-                               api: this,
-                               parameters: parameters,
-                               valid: true
-                       };
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_' + this._getEditorId(), data);
-                       
-                       return (data.valid !== false);
-               },
-               
-               /**
-                * Throws an error by adding an inline error to target element.
-                *
-                * @param       {Element}       element         erroneous element
-                * @param       {string}        message         error message
-                */
-               throwError: function(element, message) {
-                       var error = elCreate('small');
-                       error.className = 'innerError';
-                       error.textContent = message;
-                       
-                       DomUtil.insertAfter(error, element);
-               },
-               
-               /**
-                * Shows the update message.
-                * 
-                * @param       {Object}        data            ajax response data
-                * @protected
-                */
-               _showMessage: function(data) {
-                       var activeElement = this._activeElement;
-                       var editorId = this._getEditorId();
-                       var elementData = this._elements.get(activeElement);
-                       var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageFooter);
-                       
-                       // set new content
-                       //noinspection JSUnresolvedVariable
-                       DomUtil.setInnerHtml(elementData.messageBody, data.returnValues.message);
-                       
-                       // handle attachment list
-                       //noinspection JSUnresolvedVariable
-                       if (typeof data.returnValues.attachmentList === 'string') {
-                               for (var i = 0, length = attachmentLists.length; i < length; i++) {
-                                       elRemove(attachmentLists[i]);
-                               }
-                               
-                               var element = elCreate('div');
-                               //noinspection JSUnresolvedVariable
-                               DomUtil.setInnerHtml(element, data.returnValues.attachmentList);
-                               
-                               while (element.childNodes.length) {
-                                       elementData.messageFooter.appendChild(element.childNodes[0]);
-                               }
-                       }
-                       
-                       // handle poll
-                       //noinspection JSUnresolvedVariable
-                       if (typeof data.returnValues.poll === 'string') {
-                               // find current poll
-                               var poll = elBySel('.pollContainer', elementData.messageBody);
-                               if (poll !== null) {
-                                       // poll contain is wrapped inside `.jsInlineEditorHideContent`
-                                       elRemove(poll.parentNode);
-                               }
-                               
-                               var pollContainer = elCreate('div');
-                               pollContainer.className = 'jsInlineEditorHideContent';
-                               //noinspection JSUnresolvedVariable
-                               DomUtil.setInnerHtml(pollContainer, data.returnValues.poll);
-                               
-                               DomUtil.prepend(pollContainer, elementData.messageBody);
-                       }
-                       
-                       this._restoreMessage();
-                       
-                       this._updateHistory(this._getHash(this._getObjectId(activeElement)));
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor', 'autosaveDestroy_' + editorId);
-                       
-                       UiNotification.show();
-                       
-                       if (this._options.quoteManager) {
-                               this._options.quoteManager.clearAlternativeEditor();
-                               this._options.quoteManager.countQuotes();
-                       }
-               },
-               
-               /**
-                * Hides the editor from view.
-                * 
-                * @protected
-                */
-               _hideEditor: function() {
-                       var elementData = this._elements.get(this._activeElement);
-                       elHide(DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer'));
-                       
-                       var icon = elCreate('span');
-                       icon.className = 'icon icon48 fa-spinner';
-                       elementData.messageBodyEditor.appendChild(icon);
-               },
-               
-               /**
-                * Restores the previously hidden editor.
-                * 
-                * @protected
-                */
-               _restoreEditor: function() {
-                       var elementData = this._elements.get(this._activeElement);
-                       var icon = elBySel('.fa-spinner', elementData.messageBodyEditor);
-                       elRemove(icon);
-                       
-                       var editorContainer = DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer');
-                       if (editorContainer !== null) elShow(editorContainer);
-               },
-               
-               /**
-                * Destroys the editor instance.
-                * 
-                * @protected
-                */
-               _destroyEditor: function() {
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this._getEditorId());
-                       EventHandler.fire('com.woltlab.wcf.redactor', 'destroy_' + this._getEditorId());
-               },
-               
-               /**
-                * Returns the hash added to the url after successfully editing a message.
-                * 
-                * @param       {int}   objectId        message object id
-                * @return      string
-                * @protected
-                */
-               _getHash: function(objectId) {
-                       return '#message' + objectId;
-               },
-               
-               /**
-                * Updates the history to avoid old content when going back in the browser
-                * history.
-                * 
-                * @param       {string}        hash    location hash
-                * @protected
-                */
-               _updateHistory: function(hash) {
-                       window.location.hash = hash;
-               },
-               
-               /**
-                * Returns the unique editor id.
-                * 
-                * @return      {string}        editor id
-                * @protected
-                */
-               _getEditorId: function() {
-                       return this._options.editorPrefix + this._getObjectId(this._activeElement);
-               },
-               
-               /**
-                * Returns the element's `data-object-id` value.
-                * 
-                * @param       {Element}       element         target element
-                * @return      {int}
-                * @protected
-                */
-               _getObjectId: function(element) {
-                       return ~~elData(element, 'object-id');
-               },
-               
-               _ajaxFailure: function(data) {
-                       var elementData = this._elements.get(this._activeElement);
-                       var editor = elBySel('.redactor-editor', elementData.messageBodyEditor);
-                       
-                       // handle errors occurring on editor load
-                       if (editor === null) {
-                               this._restoreMessage();
-                               
-                               return true;
-                       }
-                       
-                       this._restoreEditor();
-                       
-                       //noinspection JSUnresolvedVariable
-                       if (!data || data.returnValues === undefined || data.returnValues.errorType === undefined) {
-                               return true;
-                       }
-                       
-                       var innerError = elBySel('.innerError', elementData.messageBodyEditor);
-                       if (innerError === null) {
-                               innerError = elCreate('small');
-                               innerError.className = 'innerError';
-                               
-                               DomUtil.insertAfter(innerError, editor);
-                       }
-                       
-                       //noinspection JSUnresolvedVariable
-                       innerError.textContent = data.returnValues.errorType;
-                       
-                       return false;
-               },
-               
-               _ajaxSuccess: function(data) {
-                       switch (data.actionName) {
-                               case 'beginEdit':
-                                       this._showEditor(data);
-                                       break;
-                                       
-                               case 'save':
-                                       this._showMessage(data);
-                                       break;
-                       }
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: this._options.className,
-                                       interfaceName: 'wcf\\data\\IMessageInlineEditorAction'
-                               }
-                       };
-               },
-               
-               /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
-               legacyGetDropdownMenus: function() { return this._dropdownMenus; },
-               
-               /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
-               legacyGetElements: function() { return this._elements; },
-               
-               /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
-               legacyEdit: function(containerId) {
-                       this._click(elById(containerId), null);
-               }
-       };
-       
-       return UiMessageInlineEditor;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Manager.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Manager.js
deleted file mode 100644 (file)
index 84f07c9..0000000
+++ /dev/null
@@ -1,272 +0,0 @@
-/**
- * Provides access and editing of message properties.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Message/Manager
- */
-define(['Ajax', 'Core', 'Dictionary', 'Language', 'Dom/ChangeListener', 'Dom/Util'], function(Ajax, Core, Dictionary, Language, DomChangeListener, DomUtil) {
-       "use strict";
-       
-       /**
-        * @param       {Object}        options         initilization options
-        * @constructor
-        */
-       function UiMessageManager(options) { this.init(options); }
-       UiMessageManager.prototype = {
-               /**
-                * Initializes a new manager instance.
-                * 
-                * @param       {Object}        options         initilization options
-                */
-               init: function(options) {
-                       this._elements = null;
-                       this._options = Core.extend({
-                               className: '',
-                               selector: ''
-                       }, options);
-                       
-                       this.rebuild();
-                       
-                       DomChangeListener.add('Ui/Message/Manager' + this._options.className, this.rebuild.bind(this));
-               },
-               
-               /**
-                * Rebuilds the list of observed messages. You should call this method whenever a
-                * message has been either added or removed from the document.
-                */
-               rebuild: function() {
-                       this._elements = new Dictionary();
-                       
-                       var element, elements = elBySelAll(this._options.selector);
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               element = elements[i];
-                               
-                               this._elements.set(elData(element, 'object-id'), element);
-                       }
-               },
-               
-               /**
-                * Returns a boolean value for the given permission. The permission should not start
-                * with "can" or "can-" as this is automatically assumed by this method.
-                * 
-                * @param       {int}           objectId        message object id 
-                * @param       {string}        permission      permission name without a leading "can" or "can-"
-                * @return      {boolean}       true if permission was set and is either 'true' or '1'
-                */
-               getPermission: function(objectId, permission) {
-                       permission = 'can-' + this._getAttributeName(permission);
-                       var element = this._elements.get(objectId);
-                       if (element === undefined) {
-                               throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
-                       }
-                       
-                       return elDataBool(element, permission);
-               },
-               
-               /**
-                * Returns the given property value from a message, optionally supporting a boolean return value.
-                * 
-                * @param       {int}           objectId        message object id
-                * @param       {string}        propertyName    attribute name
-                * @param       {boolean}       asBool          attempt to interpret property value as boolean
-                * @return      {(boolean|string)}      raw property value or boolean if requested
-                */
-               getPropertyValue: function(objectId, propertyName, asBool) {
-                       var element = this._elements.get(objectId);
-                       if (element === undefined) {
-                               throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
-                       }
-                       
-                       return window[(asBool ? 'elDataBool' : 'elData')](element, this._getAttributeName(propertyName));
-               },
-               
-               /**
-                * Invokes a method for given message object id in order to alter its state or properties.
-                * 
-                * @param       {int}           objectId        message object id
-                * @param       {string}        actionName      action name used for the ajax api
-                * @param       {Object=}       parameters      optional list of parameters included with the ajax request
-                */
-               update: function(objectId, actionName, parameters) {
-                       Ajax.api(this, {
-                               actionName: actionName,
-                               parameters: parameters || {},
-                               objectIDs: [objectId]
-                       });
-               },
-               
-               /**
-                * Updates properties and states for given object ids. Keep in mind that this method does
-                * not support setting individual properties per message, instead all property changes
-                * are applied to all matching message objects.
-                * 
-                * @param       {Array<int>}    objectIds       list of message object ids
-                * @param       {Object}        data            list of updated properties
-                */
-               updateItems: function(objectIds, data) {
-                       if (!Array.isArray(objectIds)) {
-                               objectIds = [objectIds];
-                       }
-                       
-                       var element;
-                       for (var i = 0, length = objectIds.length; i < length; i++) {
-                               element = this._elements.get(objectIds[i]);
-                               if (element === undefined) {
-                                       continue;
-                               }
-                               
-                               for (var key in data) {
-                                       if (data.hasOwnProperty(key)) {
-                                               this._update(element, key, data[key]);
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Bulk updates the properties and states for all observed messages at once.
-                * 
-                * @param       {Object}        data            list of updated properties
-                */
-               updateAllItems: function(data) {
-                       var objectIds = [];
-                       this._elements.forEach((function(element, objectId) {
-                               objectIds.push(objectId);
-                       }).bind(this));
-                       
-                       this.updateItems(objectIds, data);
-               },
-               
-               /**
-                * Updates a single property of a message element.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {string}        propertyName    property name
-                * @param       {?}             propertyValue   property value, will be implicitly converted to string
-                * @protected
-                */
-               _update: function(element, propertyName, propertyValue) {
-                       elData(element, this._getAttributeName(propertyName), propertyValue);
-                       
-                       // handle special properties
-                       var propertyValueBoolean = (propertyValue == 1 || propertyValue === true || propertyValue === 'true');
-                       this._updateState(element, propertyName, propertyValue, propertyValueBoolean);
-               },
-               
-               /**
-                * Updates the message element's state based upon a property change.
-                * 
-                * @param       {Element}       element                 message element
-                * @param       {string}        propertyName            property name
-                * @param       {?}             propertyValue           property value
-                * @param       {boolean}       propertyValueBoolean    true if `propertyValue` equals either 'true' or '1'
-                * @protected
-                */
-               _updateState: function(element, propertyName, propertyValue, propertyValueBoolean) {
-                       switch (propertyName) {
-                               case 'isDeleted':
-                                       element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDeleted');
-                                       this._toggleMessageStatus(element, 'jsIconDeleted', 'wcf.message.status.deleted', 'red', propertyValueBoolean);
-                                       
-                                       break;
-                               
-                               case 'isDisabled':
-                                       element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDisabled');
-                                       this._toggleMessageStatus(element, 'jsIconDisabled', 'wcf.message.status.disabled', 'green', propertyValueBoolean);
-                                       
-                                       break;
-                       }
-               },
-               
-               /**
-                * Toggles the message status bade for provided element.
-                * 
-                * @param       {Element}       element         message element
-                * @param       {string}        className       badge class name
-                * @param       {string}        phrase          language phrase
-                * @param       {string}        badgeColor      color css class
-                * @param       {boolean}       addBadge        add or remove badge
-                * @protected
-                */
-               _toggleMessageStatus: function(element, className, phrase, badgeColor, addBadge) {
-                       var messageStatus = elBySel('.messageStatus', element);
-                       if (messageStatus === null) {
-                               var messageHeaderMetaData = elBySel('.messageHeaderMetaData', element);
-                               if (messageHeaderMetaData === null) {
-                                       // can't find appropriate location to insert badge
-                                       return;
-                               }
-                               
-                               messageStatus = elCreate('ul');
-                               messageStatus.className = 'messageStatus';
-                               DomUtil.insertAfter(messageStatus, messageHeaderMetaData);
-                       }
-                       
-                       var badge = elBySel('.' + className, messageStatus);
-                       
-                       if (addBadge) {
-                               if (badge !== null) {
-                                       // badge already exists
-                                       return;
-                               }
-                               
-                               badge = elCreate('span');
-                               badge.className = 'badge label ' + badgeColor + ' ' + className;
-                               badge.textContent = Language.get(phrase);
-                               
-                               var listItem = elCreate('li');
-                               listItem.appendChild(badge);
-                               messageStatus.appendChild(listItem);
-                       }
-                       else {
-                               if (badge === null) {
-                                       // badge does not exist
-                                       return;
-                               }
-                               
-                               elRemove(badge.parentNode);
-                       }
-               },
-               
-               /**
-                * Transforms camel-cased property names into their attribute equivalent.
-                * 
-                * @param       {string}        propertyName    camel-cased property name
-                * @return      {string}        equivalent attribute name
-                * @protected
-                */
-               _getAttributeName: function(propertyName) {
-                       if (propertyName.indexOf('-') !== -1) {
-                               return propertyName;
-                       }
-                       
-                       var attributeName = '';
-                       var str, tmp = propertyName.split(/([A-Z][a-z]+)/);
-                       for (var i = 0, length = tmp.length; i < length; i++) {
-                               str = tmp[i];
-                               if (str.length) {
-                                       if (attributeName.length) attributeName += '-';
-                                       attributeName += str.toLowerCase();
-                               }
-                       }
-                       
-                       return attributeName;
-               },
-               
-               _ajaxSuccess: function() {
-                       throw new Error("Method _ajaxSuccess() must be implemented by deriving functions.");
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: this._options.className
-                               }
-                       };
-               }
-       };
-       
-       return UiMessageManager;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js
deleted file mode 100644 (file)
index 8a73c4b..0000000
+++ /dev/null
@@ -1,363 +0,0 @@
-/**
- * Handles user interaction with the quick reply feature.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Message/Reply
- */
-define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', 'WoltLab/WCF/Ui/Scroll', 'EventKey', 'User', 'WoltLab/WCF/Controller/Captcha'],
-       function(Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function UiMessageReply(options) { this.init(options); }
-       UiMessageReply.prototype = {
-               /**
-                * Initializes a new quick reply field.
-                * 
-                * @param       {Object}        options         configuration options
-                */
-               init: function(options) {
-                       this._options = Core.extend({
-                               ajax: {
-                                       className: ''
-                               },
-                               quoteManager: null,
-                               successMessage: 'wcf.global.success.add'
-                       }, options);
-                       
-                       this._container = elById('messageQuickReply');
-                       this._content = elBySel('.messageContent', this._container);
-                       this._textarea = elById('text');
-                       this._editor = null;
-                       this._loadingOverlay = null;
-                       
-                       // prevent marking of text for quoting
-                       elBySel('.message', this._container).classList.add('jsInvalidQuoteTarget');
-                       
-                       // handle submit button
-                       var submitCallback = this._submit.bind(this);
-                       var submitButton = elBySel('button[data-type="save"]');
-                       submitButton.addEventListener(WCF_CLICK_EVENT, submitCallback);
-                       
-                       // bind reply button
-                       var replyButtons = elBySelAll('.jsQuickReply');
-                       for (var i = 0, length = replyButtons.length; i < length; i++) {
-                               replyButtons[i].addEventListener(WCF_CLICK_EVENT, (function(event) {
-                                       event.preventDefault();
-                                       
-                                       UiScroll.element(this._container, (function() {
-                                               this._getEditor().focus.end();
-                                       }).bind(this));
-                               }).bind(this));
-                       }
-               },
-               
-               /**
-                * Submits the guest dialog.
-                * 
-                * @param       {Event}         event
-                * @protected
-                */
-               _submitGuestDialog: function(event) {
-                       // only submit when enter key is pressed
-                       if (event.type === 'keypress' && !EventKey.Enter(event)) {
-                               return;
-                       }
-                       
-                       var usernameInput = elBySel('input[name=username]', event.currentTarget.closest('.dialogContent'));
-                       if (usernameInput.value === '') {
-                               var error = DomTraverse.nextByClass(usernameInput, 'innerError');
-                               if (!error) {
-                                       error = elCreate('small');
-                                       error.className = 'innerError';
-                                       error.innerText = Language.get('wcf.global.form.error.empty');
-                                       
-                                       DomUtil.insertAfter(error, usernameInput);
-                                       
-                                       usernameInput.closest('dl').classList.add('formError');
-                               }
-                               
-                               return;
-                       }
-                       
-                       var parameters = {
-                               parameters: {
-                                       data: {
-                                               username: usernameInput.value
-                                       }
-                               }
-                       };
-                       
-                       //noinspection JSCheckFunctionSignatures
-                       var captchaId = elData(event.currentTarget, 'captcha-id');
-                       if (ControllerCaptcha.has(captchaId)) {
-                               parameters = Core.extend(parameters, ControllerCaptcha.getData(captchaId));
-                       }
-                       
-                       this._submit(undefined, parameters);
-               },
-               
-               /**
-                * Validates the message and submits it to the server.
-                * 
-                * @param       {Event?}        event                   event object
-                * @param       {Object?}       additionalParameters    additional parameters sent to the server
-                * @protected
-                */
-               _submit: function(event, additionalParameters) {
-                       if (event) {
-                               event.preventDefault();
-                       }
-                       
-                       if (!this._validate()) {
-                               // validation failed, bail out
-                               return;
-                       }
-                       
-                       this._showLoadingOverlay();
-                       
-                       // build parameters
-                       var parameters = DomUtil.getDataAttributes(this._container, 'data-', true, true);
-                       parameters.data = { message: this._getEditor().code.get() };
-                       parameters.removeQuoteIDs = (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : [];
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data);
-                       
-                       if (!User.userId && !additionalParameters) {
-                               parameters.requireGuestDialog = true;
-                       }
-                       
-                       Ajax.api(this, Core.extend({
-                               parameters: parameters
-                       }, additionalParameters));
-               },
-               
-               /**
-                * Validates the message and invokes listeners to perform additional validation.
-                * 
-                * @return      {boolean}       validation result
-                * @protected
-                */
-               _validate: function() {
-                       // remove all existing error elements
-                       var errorMessages = elByClass('innerError', this._container);
-                       while (errorMessages.length) {
-                               elRemove(errorMessages[0]);
-                       }
-                       
-                       // check if editor contains actual content
-                       if (this._getEditor().utils.isEmpty()) {
-                               this.throwError(this._textarea, Language.get('wcf.global.form.error.empty'));
-                               return false;
-                       }
-                       
-                       var data = {
-                               api: this,
-                               editor: this._getEditor(),
-                               message: this._getEditor().code.get(),
-                               valid: true
-                       };
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_text', data);
-                       
-                       return (data.valid !== false);
-               },
-               
-               /**
-                * Throws an error by adding an inline error to target element.
-                * 
-                * @param       {Element}       element         erroneous element
-                * @param       {string}        message         error message
-                */
-               throwError: function(element, message) {
-                       var error = elCreate('small');
-                       error.className = 'innerError';
-                       error.textContent = message;
-                       
-                       DomUtil.insertAfter(error, element);
-               },
-               
-               /**
-                * Displays a loading spinner while the request is processed by the server.
-                * 
-                * @protected
-                */
-               _showLoadingOverlay: function() {
-                       if (this._loadingOverlay === null) {
-                               this._loadingOverlay = elCreate('div');
-                               this._loadingOverlay.className = 'messageContentLoadingOverlay';
-                               this._loadingOverlay.innerHTML = '<span class="icon icon96 fa-spinner"></span>';
-                       }
-                       
-                       this._content.classList.add('loading');
-                       this._content.appendChild(this._loadingOverlay);
-               },
-               
-               /**
-                * Hides the loading spinner.
-                * 
-                * @protected
-                */
-               _hideLoadingOverlay: function() {
-                       this._content.classList.remove('loading');
-                       
-                       var loadingOverlay = elBySel('.messageContentLoadingOverlay', this._content);
-                       if (loadingOverlay !== null) {
-                               loadingOverlay.parentNode.removeChild(loadingOverlay);
-                       }
-               },
-               
-               /**
-                * Resets the editor contents and notifies event listeners.
-                * 
-                * @protected
-                */
-               _reset: function() {
-                       this._getEditor().code.set('<p>\u200b</p>');
-                       
-                       EventHandler.fire('com.woltlab.wcf.redactor2', 'reset_text');
-               },
-               
-               /**
-                * Handles errors occured during server processing.
-                * 
-                * @param       {Object}        data    response data
-                * @protected
-                */
-               _handleError: function(data) {
-                       //noinspection JSUnresolvedVariable
-                       this.throwError(this._textarea, data.returnValues.errorType);
-               },
-               
-               /**
-                * Returns the current editor instance.
-                * 
-                * @return      {Object}       editor instance
-                * @protected
-                */
-               _getEditor: function() {
-                       if (this._editor === null) {
-                               if (typeof window.jQuery === 'function') {
-                                       this._editor = window.jQuery(this._textarea).data('redactor');
-                               }
-                               else {
-                                       throw new Error("Unable to access editor, jQuery has not been loaded yet.");
-                               }
-                       }
-                       
-                       return this._editor;
-               },
-               
-               /**
-                * Inserts the rendered message into the post list, unless the post is on the next
-                * page in which case a redirect will be performed instead.
-                * 
-                * @param       {Object}        data    response data
-                * @protected
-                */
-               _insertMessage: function(data) {
-                       this._getEditor().WoltLabAutosave.reset();
-                       
-                       // redirect to new page
-                       //noinspection JSUnresolvedVariable
-                       if (data.returnValues.url) {
-                               //noinspection JSUnresolvedVariable
-                               window.location = data.returnValues.url;
-                       }
-                       else {
-                               //noinspection JSUnresolvedVariable
-                               if (data.returnValues.template) {
-                                       var elementId;
-                                       
-                                       // insert HTML
-                                       if (elData(this._container, 'sort-order') === 'DESC') {
-                                               //noinspection JSUnresolvedVariable
-                                               DomUtil.insertHtml(data.returnValues.template, this._container, 'after');
-                                               elementId = DomUtil.identify(this._container.nextElementSibling);
-                                       }
-                                       else {
-                                               //noinspection JSUnresolvedVariable
-                                               DomUtil.insertHtml(data.returnValues.template, this._container, 'before');
-                                               elementId = DomUtil.identify(this._container.previousElementSibling);
-                                       }
-                                       
-                                       // update last post time
-                                       //noinspection JSUnresolvedVariable
-                                       elData(this._container, 'last-post-time', data.returnValues.lastPostTime);
-                                       
-                                       window.history.replaceState(undefined, '', '#' + elementId);
-                                       UiScroll.element(elById(elementId));
-                               }
-                               
-                               UiNotification.show(Language.get(this._options.successMessage));
-                               
-                               if (this._options.quoteManager) {
-                                       this._options.quoteManager.countQuotes();
-                               }
-                               
-                               DomChangeListener.trigger();
-                       }
-               },
-               
-               /**
-                * @param {{returnValues:{guestDialog:string,guestDialogID:string}}} data
-                * @protected
-                */
-               _ajaxSuccess: function(data) {
-                       if (!User.userId && !data.returnValues.guestDialogID) {
-                               throw new Error("Missing 'guestDialogID' return value for guest.");
-                       }
-                       
-                       if (!User.userId && data.returnValues.guestDialog) {
-                               UiDialog.openStatic(data.returnValues.guestDialogID, data.returnValues.guestDialog, {
-                                       closable: false,
-                                       title: Language.get('wcf.global.confirmation.title')
-                               });
-                               
-                               var dialog = UiDialog.getDialog(data.returnValues.guestDialogID);
-                               elBySel('input[type=submit]', dialog.content).addEventListener(WCF_CLICK_EVENT, this._submitGuestDialog.bind(this));
-                               elBySel('input[type=text]', dialog.content).addEventListener('keypress', this._submitGuestDialog.bind(this));
-                       }
-                       else {
-                               this._insertMessage(data);
-                               
-                               if (!User.userId) {
-                                       UiDialog.close(data.returnValues.guestDialogID);
-                               }
-                               
-                               this._reset();
-                               
-                               this._hideLoadingOverlay();
-                       }
-               },
-               
-               _ajaxFailure: function(data) {
-                       this._hideLoadingOverlay();
-                       
-                       //noinspection JSUnresolvedVariable
-                       if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
-                               return true;
-                       }
-                       
-                       this._handleError(data);
-                       
-                       return false;
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'quickReply',
-                                       className: this._options.ajax.className,
-                                       interfaceName: 'wcf\\data\\IMessageQuickReplyAction'
-                               }
-                       };
-               }
-       };
-       
-       return UiMessageReply;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Share.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Share.js
deleted file mode 100644 (file)
index 15e0b2b..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Provides buttons to share a page through multiple social community sites.
- *
- * @author     Marcel Werk
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Message/Share
- */
-define(['EventHandler'], function(EventHandler) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Message/Share
-        */
-       return {
-               _pageDescription: '',
-               _pageUrl: '',
-               
-               init: function() {
-                       var container = elBySel('.messageShareButtons');
-                       var providers = {
-                               facebook: {
-                                       link: elBySel('.jsShareFacebook', container),
-                                       share: (function() { this._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}', true); }).bind(this)
-                               },
-                               google: {
-                                       link: elBySel('.jsShareGoogle', container),
-                                       share: (function() { this._share('google', 'https://plus.google.com/share?url={pageURL}', false); }).bind(this)
-                               },
-                               reddit: {
-                                       link: elBySel('.jsShareReddit', container),
-                                       share: (function() { this._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}', false); }).bind(this)
-                               },
-                               twitter: {
-                                       link: elBySel('.jsShareTwitter', container),
-                                       share: (function() { this._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}', false); }).bind(this)
-                               },
-                               linkedIn: {
-                                       link: elBySel('.jsShareLinkedIn', container),
-                                       share: (function() { this._share('linkedIn', 'https://www.linkedin.com/cws/share?url={pageURL}', false); }).bind(this)
-                               },
-                               pinterest: {
-                                       link: elBySel('.jsSharePinterest', container),
-                                       share: (function() { this._share('pinterest', 'https://www.pinterest.com/pin/create/link/?url={pageURL}&description={text}', false); }).bind(this)
-                               },
-                               xing: {
-                                       link: elBySel('.jsShareXing', container),
-                                       share: (function() { this._share('xing', 'https://www.xing.com/social_plugins/share?url={pageURL}', false); }).bind(this)
-                               },
-                               whatsApp: {
-                                       link: elBySel('.jsShareWhatsApp', container),
-                                       share: (function() {
-                                               window.location.href = 'whatsapp://send?text=' + this._pageDescription + '%20' + this._pageUrl;
-                                       }).bind(this)
-                               }
-                       };
-
-                       var title = elBySel('meta[property="og:title"]');
-                       if (title !== null) this._pageDescription = encodeURIComponent(title.content);
-                       var url = elBySel('meta[property="og:url"]');
-                       if (url !== null) this._pageUrl = encodeURIComponent(url.content);
-
-                       EventHandler.fire('com.woltlab.wcf.message.share', 'shareProvider', {
-                               container: container,
-                               providers: providers,
-                               pageDescription: this._pageDescription,
-                               pageUrl: this._pageUrl
-                       });
-                       
-                       for (var provider in providers) {
-                               if (providers.hasOwnProperty(provider)) {
-                                       if (providers[provider].link !== null) {
-                                               providers[provider].link.addEventListener(WCF_CLICK_EVENT, providers[provider].share);
-                                       }
-                               }
-                       }
-               },
-               
-               _share: function(objectName, url, appendURL) {
-                       window.open(url.replace(/\{pageURL}/, this._pageUrl).replace(/\{text}/, this._pageDescription + (appendURL ? "%20" + this._pageUrl : "")), objectName, 'height=600,width=600');
-               }
-       };
-});
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 0b2c0d4..0000000
+++ /dev/null
@@ -1,195 +0,0 @@
-/**
- * Modifies the interface to provide a better usability for mobile devices.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Mobile
- */
-define(
-       [        'Core', 'Environment', 'EventHandler', 'Language', 'List', 'Dom/ChangeListener', 'Ui/CloseOverlay', 'Ui/Screen', './Page/Menu/Main', './Page/Menu/User'],
-       function(Core,    Environment,   EventHandler,   Language,   List,   DomChangeListener,    UiCloseOverlay,    UiScreen,    UiPageMenuMain,     UiPageMenuUser)
-{
-       "use strict";
-       
-       var _buttonGroupNavigations = elByClass('buttonGroupNavigation');
-       var _enabled = false;
-       var _knownMessages = new List();
-       var _main = null;
-       var _messages = elByClass('message');
-       var _options = {};
-       var _pageMenuMain = null;
-       var _pageMenuUser = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Mobile
-        */
-       return {
-               /**
-                * Initializes the mobile UI.
-                * 
-                * @param       {Object=}       options         initialization options
-                */
-               setup: function(options) {
-                       _options = Core.extend({
-                               enableMobileMenu: true
-                       }, options);
-                       
-                       _main = elById('main');
-                       
-                       if (Environment.touch()) {
-                               document.documentElement.classList.add('touch');
-                       }
-                       
-                       if (Environment.platform() !== 'desktop') {
-                               document.documentElement.classList.add('mobile');
-                       }
-                       
-                       UiScreen.on('screen-md-down', {
-                               match: this.enable.bind(this),
-                               unmatch: this.disable.bind(this),
-                               setup: this._init.bind(this)
-                       });
-               },
-               
-               /**
-                * Enables the mobile UI.
-                */
-               enable: function() {
-                       _enabled = true;
-                       
-                       if (_options.enableMobileMenu) {
-                               _pageMenuMain.enable();
-                               _pageMenuUser.enable();
-                       }
-               },
-               
-               /**
-                * Disables the mobile UI.
-                */
-               disable: function() {
-                       _enabled = false;
-                       
-                       if (_options.enableMobileMenu) {
-                               _pageMenuMain.disable();
-                               _pageMenuUser.disable();
-                       }
-               },
-               
-               _init: function() {
-                       _enabled = true;
-                       
-                       this._initSearchBar();
-                       this._initButtonGroupNavigation();
-                       this._initMessages();
-                       this._initMobileMenu();
-                       
-                       UiCloseOverlay.add('WoltLab/WCF/Ui/Mobile', this._closeAllMenus.bind(this));
-                       DomChangeListener.add('WoltLab/WCF/Ui/Mobile', this._initButtonGroupNavigation.bind(this));
-               },
-               
-               _initSearchBar: function() {
-                       var _searchBar = elById('pageHeaderSearch');
-                       var _searchInput = elById('pageHeaderSearchInput');
-                       
-                       EventHandler.add('com.woltlab.wcf.MainMenuMobile', 'more', function(data) {
-                               if (data.identifier === 'com.woltlab.wcf.search') {
-                                       _searchBar.style.setProperty('top', elById('pageHeader').offsetHeight + 'px', '');
-                                       _searchBar.classList.add('open');
-                                       _searchInput.focus();
-                                       
-                                       data.handler.close(true);
-                               }
-                       });
-                       
-                       _main.addEventListener(WCF_CLICK_EVENT, function() { _searchBar.classList.remove('open'); });
-               },
-               
-               _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');
-                               
-                               navigation.parentNode.classList.add('hasMobileNavigation');
-                               
-                               var button = elCreate('a');
-                               button.className = 'dropdownLabel';
-                               
-                               var span = elCreate('span');
-                               span.className = 'icon icon24 fa-ellipsis-v';
-                               button.appendChild(span);
-                               
-                               var list = elBySel('.buttonList', navigation);
-                               list.addEventListener(WCF_CLICK_EVENT, function(event) {
-                                       event.stopPropagation();
-                               });
-                               
-                               (function(navigation, button) {
-                                       button.addEventListener(WCF_CLICK_EVENT, function(event) {
-                                               event.preventDefault();
-                                               event.stopPropagation();
-                                               
-                                               navigation.classList.toggle('open');
-                                       });
-                               })(navigation, button);
-                               
-                               navigation.insertBefore(button, navigation.firstChild);
-                       }
-               },
-               
-               _initMessages: function() {
-                       Array.prototype.forEach.call(_messages, function(message) {
-                               if (_knownMessages.has(message)) {
-                                       return;
-                               }
-                               
-                               var navigation = elBySel('.jsMobileNavigation', message);
-                               var quickOptions = elBySel('.messageQuickOptions', message); 
-                               
-                               if (quickOptions) {
-                                       quickOptions.addEventListener(WCF_CLICK_EVENT, function (event) {
-                                               if (_enabled) {
-                                                       event.preventDefault();
-                                                       event.stopPropagation();
-                                                       
-                                                       navigation.classList.toggle('open');
-                                               }
-                                       });
-                                       
-                                       navigation.addEventListener(WCF_CLICK_EVENT, function(event) {
-                                               event.stopPropagation();
-                                       });
-                               }
-                               
-                               _knownMessages.add(message);
-                       });
-               },
-               
-               _initMobileMenu: function() {
-                       if (_options.enableMobileMenu) {
-                               _pageMenuMain = new UiPageMenuMain();
-                               _pageMenuUser = new UiPageMenuUser();
-                       }
-                       
-                       elBySelAll('.boxMenu', null, function(boxMenu) {
-                               boxMenu.addEventListener(WCF_CLICK_EVENT, function(event) {
-                                       event.stopPropagation();
-                                       
-                                       if (event.target === boxMenu) {
-                                               event.preventDefault();
-                                               
-                                               boxMenu.classList.add('open');
-                                       }
-                               });
-                       });
-               },
-               
-               _closeAllMenus: function() {
-                       elBySelAll('.jsMobileButtonGroupNavigation.open, .jsMobileNavigation.open, .boxMenu.open', null, function (menu) {
-                               menu.classList.remove('open');
-                       });
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Notification.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Notification.js
deleted file mode 100644 (file)
index 8e17470..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Simple notification 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/Notification
- */
-define(['Language'], function(Language) {
-       "use strict";
-       
-       var _busy = false;
-       var _callback = null;
-       var _message = null;
-       var _notificationElement = null;
-       var _timeout = null;
-       
-       var _callbackHide = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Notification
-        */
-       var UiNotification = {
-               /**
-                * Shows a notification.
-                * 
-                * @param       {string}        message         message
-                * @param       {function=}     callback        callback function to be executed once notification is being hidden
-                * @param       {string=}       cssClassName    alternate CSS class name, defaults to 'success'
-                */
-               show: function(message, callback, cssClassName) {
-                       if (_busy) {
-                               return;
-                       }
-                       
-                       this._init();
-                       
-                       _callback = (typeof callback === 'function') ? callback : null;
-                       _message.className = cssClassName || 'success';
-                       _message.textContent = Language.get(message || 'wcf.global.success');
-                       
-                       _busy = true;
-                       
-                       _notificationElement.classList.add('active');
-                       
-                       _timeout = setTimeout(_callbackHide, 2000);
-               },
-               
-               /**
-                * Initializes the UI elements.
-                */
-               _init: function() {
-                       if (_notificationElement === null) {
-                               _callbackHide = this._hide.bind(this);
-                               
-                               _notificationElement = elCreate('div');
-                               _notificationElement.id = 'systemNotification';
-                               
-                               _message = elCreate('p');
-                               _message.addEventListener(WCF_CLICK_EVENT, _callbackHide);
-                               _notificationElement.appendChild(_message);
-                               
-                               document.body.appendChild(_notificationElement);
-                       }
-               },
-               
-               /**
-                * Hides the notification and invokes the callback if provided.
-                */
-               _hide: function() {
-                       clearTimeout(_timeout);
-                       
-                       _notificationElement.classList.remove('active');
-                       
-                       if (_callback !== null) {
-                               _callback();
-                       }
-                       
-                       _busy = false;
-               }
-       };
-       
-       return UiNotification;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Action.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Action.js
deleted file mode 100644 (file)
index 12be9fa..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-/**
- * Provides page actions such as "jump to top" and clipboard actions.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Page/Action
- */
-define(['Dictionary', 'Dom/Util'], function(Dictionary, DomUtil) {
-       "use strict";
-       
-       var _buttons = new Dictionary();
-       var _container = null;
-       var _didInit = false;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Page/Action
-        */
-       return {
-               /**
-                * Initializes the page action container.
-                */
-               setup: function() {
-                       _didInit = true;
-                       
-                       _container = elCreate('ul');
-                       _container.className = 'pageAction';
-                       document.body.appendChild(_container);
-               },
-               
-               /**
-                * Adds a button to the page action list. You can optionally provide a button name to
-                * insert the button right before it. Unmatched button names or empty value will cause
-                * the button to be prepended to the list.
-                * 
-                * @param       {string}        buttonName              unique identifier
-                * @param       {Element}       button                  button element, must not be wrapped in a <li>
-                * @param       {string=}       insertBeforeButton      insert button before element identified by provided button name
-                */
-               add: function(buttonName, button, insertBeforeButton) {
-                       if (_didInit === false) this.setup();
-                       
-                       var listItem = elCreate('li');
-                       button.classList.add('button');
-                       button.classList.add('buttonPrimary');
-                       listItem.appendChild(button);
-                       elAttr(listItem, 'aria-hidden', (buttonName === 'toTop' ? 'true' : 'false'));
-                       elData(listItem, 'name', buttonName);
-                       
-                       // force 'to top' button to be always at the most outer position
-                       if (buttonName === 'toTop') {
-                               listItem.className = 'toTop initiallyHidden';
-                               _container.appendChild(listItem);
-                       }
-                       else {
-                               var insertBefore = null;
-                               if (insertBeforeButton) {
-                                       insertBefore = _buttons.get(insertBeforeButton);
-                                       if (insertBefore !== undefined) {
-                                               insertBefore = insertBefore.parentNode;
-                                       }
-                               }
-                               
-                               if (insertBefore === null && _container.childElementCount) {
-                                       insertBefore = _container.children[0];
-                               }
-                               
-                               if (insertBefore === null) {
-                                       DomUtil.prepend(listItem, _container);
-                               }
-                               else {
-                                       _container.insertBefore(listItem, insertBefore);
-                               }
-                       }
-                       
-                       _buttons.set(buttonName, button);
-                       this._renderContainer();
-               },
-               
-               /**
-                * Returns true if there is a registered button with the provided name.
-                * 
-                * @param       {string}        buttonName      unique identifier
-                * @return      {boolean}       true if there is a registered button with this name
-                */
-               has: function (buttonName) {
-                       return _buttons.has(buttonName);
-               },
-               
-               /**
-                * Returns the stored button by name or undefined.
-                * 
-                * @param       {string}        buttonName      unique identifier
-                * @return      {Element}       button element or undefined
-                */
-               get: function(buttonName) {
-                       return _buttons.get(buttonName);
-               },
-               
-               /**
-                * Removes a button by its button name.
-                * 
-                * @param       {string}        buttonName      unique identifier
-                */
-               remove: function(buttonName) {
-                       var button = _buttons.get(buttonName);
-                       if (button !== undefined) {
-                               var listItem = button.parentNode;
-                               listItem.addEventListener('animationend', function () {
-                                       try {
-                                               _container.removeChild(listItem);
-                                               _buttons.delete(buttonName);
-                                       }
-                                       catch (e) {
-                                               // ignore errors if the element has already been removed
-                                       }
-                               });
-                               
-                               this.hide(buttonName);
-                       }
-               },
-               
-               /**
-                * Hides a button by its button name.
-                * 
-                * @param       {string}        buttonName      unique identifier
-                */
-               hide: function(buttonName) {
-                       var button = _buttons.get(buttonName);
-                       if (button) {
-                               elAttr(button.parentNode, 'aria-hidden', 'true');
-                               this._renderContainer();
-                       }
-               },
-               
-               /**
-                * Shows a button by its button name.
-                * 
-                * @param       {string}        buttonName      unique identifier
-                */
-               show: function(buttonName) {
-                       var button = _buttons.get(buttonName);
-                       if (button) {
-                               if (button.parentNode.classList.contains('initiallyHidden')) {
-                                       button.parentNode.classList.remove('initiallyHidden');
-                               }
-                               
-                               elAttr(button.parentNode, 'aria-hidden', 'false');
-                               this._renderContainer();
-                       }
-               },
-               
-               /**
-                * Toggles the container's visibility.
-                * 
-                * @protected
-                */
-               _renderContainer: function() {
-                       var hasVisibleItems = false;
-                       if (_container.childElementCount) {
-                               for (var i = 0, length = _container.childElementCount; i < length; i++) {
-                                       if (elAttr(_container.children[i], 'aria-hidden') === 'false') {
-                                               hasVisibleItems = true;
-                                               break;
-                                       }
-                               }
-                       }
-                       
-                       _container.classList[(hasVisibleItems ? 'add' : 'remove')]('active');
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Header/Fixed.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Header/Fixed.js
deleted file mode 100644 (file)
index e2a6933..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * Manages the sticky page header.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Page/Header/Fixed
- */
-define(['Core', 'EventHandler', 'Ui/Alignment', 'Ui/CloseOverlay', 'Ui/Screen', 'Ui/SimpleDropdown'], function(Core, EventHandler, UiAlignment, UiCloseOverlay, UiScreen, UiSimpleDropdown) {
-       "use strict";
-       
-       var _pageHeader, _pageHeaderContainer, _searchInputContainer, _triggerHeight;
-       var _isFixed = false, _isMobile = false;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Page/Header/Fixed
-        */
-       return {
-               /**
-                * Initializes the sticky page header handler.
-                */
-               init: function() {
-                       _pageHeader = elById('pageHeader');
-                       _pageHeaderContainer = elById('pageHeaderContainer');
-                       
-                       this._initStickyPageHeader();
-                       this._initSearchBar();
-                       
-                       UiScreen.on('screen-md-down', {
-                               match: function() { _isMobile = true; },
-                               unmatch: function() { _isMobile = false; },
-                               setup: function() { _isMobile = true; }
-                       });
-               },
-               
-               /**
-                * Enforces a min-height for the original header's location to prevent collapsing
-                * when setting the header to `position: fixed`.
-                * 
-                * @protected
-                */
-               _initStickyPageHeader: function() {
-                       if (_pageHeader.clientHeight) {
-                               _pageHeader.style.setProperty('min-height', _pageHeader.clientHeight + 'px');
-                       }
-                       
-                       _triggerHeight = _pageHeader.clientHeight - elBySel('.mainMenu', _pageHeader).clientHeight;
-                       
-                       this._scroll();
-                       window.addEventListener('scroll', this._scroll.bind(this));
-               },
-               
-               /**
-                * Provides the collapsible search bar.
-                * 
-                * @protected
-                */
-               _initSearchBar: function() {
-                       var searchContainer = elById('pageHeaderSearch');
-                       searchContainer.addEventListener(WCF_CLICK_EVENT, function(event) {
-                               event.stopPropagation();
-                       });
-                       
-                       var searchInput = elById('pageHeaderSearchInput');
-                       
-                       var searchLabel = elBySel('.pageHeaderSearchLabel');
-                       _searchInputContainer = elById('pageHeaderSearchInputContainer');
-                       
-                       var menu = elById('topMenu');
-                       searchLabel.addEventListener(WCF_CLICK_EVENT, function() {
-                               if ((_isFixed || _isMobile) && !_pageHeader.classList.contains('searchBarOpen')) {
-                                       UiAlignment.set(_searchInputContainer, menu, {
-                                               horizontal: 'right'
-                                       });
-                                       
-                                       _pageHeader.classList.add('searchBarOpen');
-                                       WCF.Dropdown.Interactive.Handler.closeAll();
-                                       searchInput.focus();
-                               }
-                       });
-                       
-                       UiCloseOverlay.add('WoltLab/WCF/Ui/Page/Header/Fixed', function() {
-                               _pageHeader.classList.remove('searchBarOpen');
-                       });
-                       
-                       EventHandler.add('com.woltlab.wcf.MainMenuMobile', 'more', (function(data) {
-                               if (data.identifier === 'com.woltlab.wcf.search') {
-                                       data.handler.close(true);
-                                       
-                                       Core.triggerEvent(elById('pageHeaderSearchInput'), WCF_CLICK_EVENT);
-                               }
-                       }).bind(this));
-               },
-               
-               /**
-                * Updates the page header state after scrolling.
-                * 
-                * @protected
-                */
-               _scroll: function() {
-                       var wasFixed = _isFixed;
-                       
-                       _isFixed = (window.scrollY > _triggerHeight);
-                       
-                       _pageHeader.classList[_isFixed ? 'add' : 'remove']('sticky');
-                       _pageHeaderContainer.classList[_isFixed ? 'add' : 'remove']('stickyPageHeader');
-                       
-                       if (!_isFixed && wasFixed) {
-                               _pageHeader.classList.remove('searchBarOpen');
-                               ['bottom', 'left', 'right', 'top'].forEach(function(propertyName) {
-                                       _searchInputContainer.style.removeProperty(propertyName);
-                               });
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/JumpTo.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/JumpTo.js
deleted file mode 100644 (file)
index f1a024a..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * Utility class to provide a 'Jump To' 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/Page/JumpTo
- */
-define(['Language', 'ObjectMap', 'Ui/Dialog'], function(Language, ObjectMap, UiDialog) {
-       "use strict";
-       
-       var _activeElement = null;
-       var _buttonSubmit = null;
-       var _description = null;
-       var _elements = new ObjectMap();
-       var _input = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Page/JumpTo
-        */
-       var UiPageJumpTo = {
-               /**
-                * Initializes a 'Jump To' element.
-                * 
-                * @param       {Element}       element         trigger element
-                * @param       {function}      callback        callback function, receives the page number as first argument
-                */
-               init: function(element, callback) {
-                       callback = callback || null;
-                       if (callback === null) {
-                               var redirectUrl = elData(element, 'link');
-                               if (redirectUrl) {
-                                       callback = function(pageNo) {
-                                               window.location = redirectUrl.replace(/pageNo=%d/, 'pageNo=' + pageNo);
-                                       };
-                               }
-                               else {
-                                       callback = function() {};
-                               }
-                               
-                       }
-                       else if (typeof callback !== 'function') {
-                               throw new TypeError("Expected a valid function for parameter 'callback'.");
-                       }
-                       
-                       if (!_elements.has(element)) {
-                               elBySelAll('.jumpTo', element, (function(jumpTo) {
-                                       jumpTo.addEventListener(WCF_CLICK_EVENT, this._click.bind(this, element));
-                                       _elements.set(element, { callback: callback });
-                               }).bind(this));
-                       }
-               },
-               
-               /**
-                * Handles clicks on the trigger element.
-                * 
-                * @param       {Element}       element         trigger element
-                * @param       {object}        event           event object
-                */
-               _click: function(element, event) {
-                       _activeElement = element;
-                       
-                       if (typeof event === 'object') {
-                               event.preventDefault();
-                       }
-                       
-                       UiDialog.open(this);
-                       
-                       var pages = elData(element, 'pages');
-                       _input.value = pages;
-                       _input.setAttribute('max', pages);
-                       _input.select();
-                       
-                       _description.textContent = Language.get('wcf.page.jumpTo.description').replace(/#pages#/, pages);
-               },
-               
-               /**
-                * Handles changes to the page number input field.
-                * 
-                * @param       {object}        event           event object
-                */
-               _keyUp: function(event) {
-                       if (event.which === 13 && _buttonSubmit.disabled === false) {
-                               this._submit();
-                               return;
-                       }
-                       
-                       var pageNo = ~~_input.value;
-                       if (pageNo < 1 || pageNo > ~~elAttr(_input, 'max')) {
-                               _buttonSubmit.disabled = true;
-                       }
-                       else {
-                               _buttonSubmit.disabled = false;
-                       }
-               },
-               
-               /**
-                * Invokes the callback with the chosen page number as first argument.
-                * 
-                * @param       {object}        event           event object
-                */
-               _submit: function(event) {
-                       _elements.get(_activeElement).callback(~~_input.value);
-                       
-                       UiDialog.close(this);
-               },
-               
-               _dialogSetup: function() {
-                       var source = '<dl>'
-                                       + '<dt><label for="jsPaginationPageNo">' + Language.get('wcf.page.jumpTo') + '</label></dt>'
-                                       + '<dd>'
-                                               + '<input type="number" id="jsPaginationPageNo" value="1" min="1" max="1" class="tiny">'
-                                               + '<small></small>'
-                                       + '</dd>'
-                               + '</dl>'
-                               + '<div class="formSubmit">'
-                                       + '<button class="buttonPrimary">' + Language.get('wcf.global.button.submit') + '</button>'
-                               + '</div>';
-                       
-                       return {
-                               id: 'paginationOverlay',
-                               options: {
-                                       onSetup: (function(content) {
-                                               _input = elByTag('input', content)[0];
-                                               _input.addEventListener('keyup', this._keyUp.bind(this));
-                                               
-                                               _description = elByTag('small', content)[0];
-                                               
-                                               _buttonSubmit = elByTag('button', content)[0];
-                                               _buttonSubmit.addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
-                                       }).bind(this),
-                                       title: Language.get('wcf.global.page.pagination')
-                               },
-                               source: source
-                       };
-               }
-       };
-       
-       return UiPageJumpTo;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/JumpToTop.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/JumpToTop.js
deleted file mode 100644 (file)
index 6ceaeb7..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Provides a link to scroll to top once the page is scrolled by at least 50% the height of the window.
- * 
- * @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/Page/JumpToTop
- */
-define(['Environment', 'Language', './Action'], function(Environment, Language, PageAction) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function JumpToTop() { this.init(); }
-       JumpToTop.prototype = {
-               /**
-                * Initializes the top link for desktop browsers only.
-                */
-               init: function() {
-                       // top link is not available on smartphones and tablets (they have a built-in function to accomplish this)
-                       if (Environment.platform() !== 'desktop') {
-                               return;
-                       }
-                       
-                       this._callbackScrollEnd = this._afterScroll.bind(this);
-                       this._timeoutScroll = null;
-                       
-                       var button = elCreate('a');
-                       button.className = 'jsTooltip';
-                       button.href = '#';
-                       elAttr(button, 'title', Language.get('wcf.global.scrollUp'));
-                       button.innerHTML = '<span class="icon icon32 fa-angle-up"></span>';
-                       
-                       button.addEventListener(WCF_CLICK_EVENT, this._jump.bind(this));
-                       
-                       PageAction.add('toTop', button);
-                       
-                       window.addEventListener('scroll', this._scroll.bind(this));
-                       
-                       // invoke callback on page load
-                       this._afterScroll();
-               },
-               
-               /**
-                * Handles clicks on the top link.
-                * 
-                * @param       {Event}         event   event object
-                * @protected
-                */
-               _jump: function(event) {
-                       event.preventDefault();
-                       
-                       elById('top').scrollIntoView({ behavior: 'smooth' });
-               },
-               
-               /**
-                * Callback executed whenever the window is being scrolled.
-                * 
-                * @protected
-                */
-               _scroll: function() {
-                       if (this._timeoutScroll !== null) {
-                               window.clearTimeout(this._timeoutScroll);
-                       }
-                       
-                       this._timeoutScroll = window.setTimeout(this._callbackScrollEnd, 100);
-               },
-               
-               /**
-                * Delayed callback executed once the page has not been scrolled for a certain amount of time.
-                * 
-                * @protected
-                */
-               _afterScroll: function() {
-                       this._timeoutScroll = null;
-                       
-                       PageAction[(window.scrollY >= window.innerHeight / 2) ? 'show' : 'hide']('toTop');
-               }
-       };
-       
-       return JumpToTop;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Abstract.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Abstract.js
deleted file mode 100644 (file)
index 185fdf5..0000000
+++ /dev/null
@@ -1,375 +0,0 @@
-/**
- * Provides a touch-friendly fullscreen menu.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Page/Menu/Abstract
- */
-define(['Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', 'Ui/Screen'], function(Environment, EventHandler, ObjectMap, DomTraverse, DomUtil, UiScreen) {
-       "use strict";
-       
-       var _pageContainer = elById('pageContainer');
-       
-       /**
-        * @param       {string}        eventIdentifier         event namespace
-        * @param       {string}        elementId               menu element id
-        * @param       {string}        buttonSelector          CSS selector for toggle button
-        * @constructor
-        */
-       function UiPageMenuAbstract(eventIdentifier, elementId, buttonSelector) { this.init(eventIdentifier, elementId, buttonSelector); }
-       UiPageMenuAbstract.prototype = {
-               /**
-                * Initializes a touch-friendly fullscreen menu.
-                * 
-                * @param       {string}        eventIdentifier         event namespace
-                * @param       {string}        elementId               menu element id
-                * @param       {string}        buttonSelector          CSS selector for toggle button
-                */
-               init: function(eventIdentifier, elementId, buttonSelector) {
-                       this._activeList = [];
-                       this._depth = 0;
-                       this._enabled = true;
-                       this._eventIdentifier = eventIdentifier;
-                       this._items = new ObjectMap();
-                       this._menu = elById(elementId);
-                       this._removeActiveList = false;
-                       
-                       var callbackOpen = this.open.bind(this);
-                       var button = elBySel(buttonSelector);
-                       button.addEventListener(WCF_CLICK_EVENT, callbackOpen);
-                       
-                       this._initItems();
-                       this._initHeader();
-                       
-                       EventHandler.add(this._eventIdentifier, 'open', callbackOpen);
-                       EventHandler.add(this._eventIdentifier, 'close', this.close.bind(this));
-                       
-                       var itemList, itemLists = elByClass('menuOverlayItemList', this._menu);
-                       this._menu.addEventListener('animationend', (function() {
-                               if (!this._menu.classList.contains('open')) {
-                                       for (var i = 0, length = itemLists.length; i < length; i++) {
-                                               itemList = itemLists[i];
-                                               
-                                               // force the main list to be displayed
-                                               itemList.classList.remove('active');
-                                               itemList.classList.remove('hidden');
-                                       }
-                               }
-                       }).bind(this));
-                       
-                       this._menu.children[0].addEventListener('transitionend', (function() {
-                               this._menu.classList.add('allowScroll');
-                               
-                               if (this._removeActiveList) {
-                                       this._removeActiveList = false;
-                                       
-                                       var list = this._activeList.pop();
-                                       if (list) {
-                                               list.classList.remove('activeList');
-                                       }
-                               }
-                       }).bind(this));
-                       
-                       var backdrop = elCreate('div');
-                       backdrop.className = 'menuOverlayMobileBackdrop';
-                       backdrop.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
-                       
-                       DomUtil.insertAfter(backdrop, this._menu);
-               },
-               
-               /**
-                * Opens the menu.
-                * 
-                * @param       {Event}         event   event object
-                * @return      {boolean}       true if menu has been opened
-                */
-               open: function(event) {
-                       if (!this._enabled) {
-                               return false;
-                       }
-                       
-                       if (event instanceof Event) {
-                               event.preventDefault();
-                       }
-                       
-                       this._menu.classList.add('open');
-                       this._menu.classList.add('allowScroll');
-                       this._menu.children[0].classList.add('activeList');
-                       
-                       UiScreen.scrollDisable();
-                       
-                       _pageContainer.classList.add('menuOverlay-' + this._menu.id);
-                       
-                       document.documentElement.classList.add('pageOverlayActive');
-                       
-                       return true;
-               },
-               
-               /**
-                * Closes the menu.
-                * 
-                * @param       {(Event|boolean)}       event   event object or boolean true to force close the menu
-                * @return      {boolean}               true if menu was open
-                */
-               close: function(event) {
-                       if (event instanceof Event) {
-                               event.preventDefault();
-                       }
-                       
-                       if (this._menu.classList.contains('open')) {
-                               this._menu.classList.remove('open');
-                               
-                               UiScreen.scrollEnable();
-                               
-                               _pageContainer.classList.remove('menuOverlay-' + this._menu.id);
-                               
-                               document.documentElement.classList.remove('pageOverlayActive');
-                               
-                               return true;
-                       }
-                       
-                       return false;
-               },
-               
-               /**
-                * Enables the touch menu.
-                */
-               enable: function() {
-                       this._enabled = true;
-               },
-               
-               /**
-                * Disables the touch menu.
-                */
-               disable: function() {
-                       this._enabled = false;
-                       
-                       this.close(true);
-               },
-               
-               /**
-                * Initializes all menu items.
-                * 
-                * @protected
-                */
-               _initItems: function() {
-                       elBySelAll('.menuOverlayItemLink', this._menu, this._initItem.bind(this));
-               },
-               
-               /**
-                * Initializes a single menu item.
-                * 
-                * @param       {Element}       item    menu item
-                * @protected
-                */
-               _initItem: function(item) {
-                       // check if it should contain a 'more' link w/ an external callback
-                       var parent = item.parentNode;
-                       var more = elData(parent, 'more');
-                       if (more) {
-                               item.addEventListener(WCF_CLICK_EVENT, (function(event) {
-                                       event.preventDefault();
-                                       event.stopPropagation();
-                                       
-                                       EventHandler.fire(this._eventIdentifier, 'more', {
-                                               handler: this,
-                                               identifier: more,
-                                               item: item,
-                                               parent: parent
-                                       });
-                               }).bind(this));
-                               
-                               return;
-                       }
-                       
-                       var itemList = item.nextElementSibling, wrapper;
-                       if (itemList === null) {
-                               return;
-                       }
-                       
-                       // handle static items with an icon-type button next to it (acp menu)
-                       if (itemList.nodeName !== 'OL' && itemList.classList.contains('menuOverlayItemLinkIcon')) {
-                               // add wrapper
-                               wrapper = elCreate('span');
-                               wrapper.className = 'menuOverlayItemWrapper';
-                               parent.insertBefore(wrapper, item);
-                               wrapper.appendChild(item);
-                               
-                               while (wrapper.nextElementSibling) {
-                                       wrapper.appendChild(wrapper.nextElementSibling);
-                               }
-                               
-                               return;
-                       }
-                       
-                       var isLink = (elAttr(item, 'href') !== '#');
-                       var parentItemList = parent.parentNode;
-                       var itemTitle = elData(itemList, 'title');
-                       
-                       this._items.set(item, {
-                               itemList: itemList,
-                               parentItemList: parentItemList
-                       });
-                       
-                       if (itemTitle === '') {
-                               itemTitle = DomTraverse.childByClass(item, 'menuOverlayItemTitle').textContent;
-                               elData(itemList, 'title', itemTitle);
-                       }
-                       
-                       var callbackLink = this._showItemList.bind(this, item);
-                       if (isLink) {
-                               wrapper = elCreate('span');
-                               wrapper.className = 'menuOverlayItemWrapper';
-                               parent.insertBefore(wrapper, item);
-                               wrapper.appendChild(item);
-                               
-                               var moreLink = elCreate('a');
-                               elAttr(moreLink, 'href', '#');
-                               moreLink.className = 'menuOverlayItemLinkIcon' + (item.classList.contains('active') ? ' active' : '');
-                               moreLink.innerHTML = '<span class="icon icon24 fa-angle-right"></span>';
-                               moreLink.addEventListener(WCF_CLICK_EVENT, callbackLink);
-                               wrapper.appendChild(moreLink);
-                       }
-                       else {
-                               item.classList.add('menuOverlayItemLinkMore');
-                               item.addEventListener(WCF_CLICK_EVENT, callbackLink);
-                       }
-                       
-                       var backLinkItem = elCreate('li');
-                       backLinkItem.className = 'menuOverlayHeader';
-                       
-                       wrapper = elCreate('span');
-                       wrapper.className = 'menuOverlayItemWrapper';
-                       
-                       var backLink = elCreate('a');
-                       elAttr(backLink, 'href', '#');
-                       backLink.className = 'menuOverlayItemLink menuOverlayBackLink';
-                       backLink.textContent = elData(parentItemList, 'title');
-                       backLink.addEventListener(WCF_CLICK_EVENT, this._hideItemList.bind(this, item));
-                       
-                       var closeLink = elCreate('a');
-                       elAttr(closeLink, 'href', '#');
-                       closeLink.className = 'menuOverlayItemLinkIcon';
-                       closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
-                       closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
-                       
-                       wrapper.appendChild(backLink);
-                       wrapper.appendChild(closeLink);
-                       backLinkItem.appendChild(wrapper);
-                       
-                       itemList.insertBefore(backLinkItem, itemList.firstElementChild);
-                       
-                       if (!backLinkItem.nextElementSibling.classList.contains('menuOverlayTitle')) {
-                               var titleItem = elCreate('li');
-                               titleItem.className = 'menuOverlayTitle';
-                               var title = elCreate('span');
-                               title.textContent = itemTitle;
-                               titleItem.appendChild(title);
-                               
-                               itemList.insertBefore(titleItem, backLinkItem.nextElementSibling);
-                       }
-               },
-               
-               /**
-                * Renders the menu item list header.
-                * 
-                * @protected
-                */
-               _initHeader: function() {
-                       var listItem = elCreate('li');
-                       listItem.className = 'menuOverlayHeader';
-                       
-                       var wrapper = elCreate('span');
-                       wrapper.className = 'menuOverlayItemWrapper';
-                       listItem.appendChild(wrapper);
-                       
-                       var logoWrapper = elCreate('span');
-                       logoWrapper.className = 'menuOverlayLogoWrapper';
-                       wrapper.appendChild(logoWrapper);
-                       
-                       var logo = elCreate('span');
-                       logo.className = 'menuOverlayLogo';
-                       logo.style.setProperty('background-image', 'url("' + elData(this._menu, 'page-logo') + '")', '');
-                       logoWrapper.appendChild(logo);
-                       
-                       var closeLink = elCreate('a');
-                       elAttr(closeLink, 'href', '#');
-                       closeLink.className = 'menuOverlayItemLinkIcon';
-                       closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
-                       closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
-                       wrapper.appendChild(closeLink);
-                       
-                       var list = DomTraverse.childByClass(this._menu, 'menuOverlayItemList');
-                       list.insertBefore(listItem, list.firstElementChild);
-               },
-               
-               /**
-                * Hides an item list, return to the parent item list.
-                * 
-                * @param       {Element}       item    menu item
-                * @param       {Event}         event   event object
-                * @protected
-                */
-               _hideItemList: function(item, event) {
-                       if (event instanceof Event) {
-                               event.preventDefault();
-                       }
-                       
-                       this._menu.classList.remove('allowScroll');
-                       this._removeActiveList = true;
-                       
-                       var data = this._items.get(item);
-                       data.parentItemList.classList.remove('hidden');
-                       
-                       this._updateDepth(false);
-               },
-               
-               /**
-                * Shows the child item list.
-                * 
-                * @param       {Element}       item    menu item
-                * @param event
-                * @private
-                */
-               _showItemList: function(item, event) {
-                       if (event instanceof Event) {
-                               event.preventDefault();
-                       }
-                       
-                       var data = this._items.get(item);
-                       
-                       var load = elData(data.itemList, 'load');
-                       if (load) {
-                               if (!elDataBool(item, 'loaded')) {
-                                       var icon = event.currentTarget.firstElementChild;
-                                       if (icon.classList.contains('fa-angle-right')) {
-                                               icon.classList.remove('fa-angle-right');
-                                               icon.classList.add('fa-spinner');
-                                       }
-                                       
-                                       EventHandler.fire(this._eventIdentifier, 'load_' + load);
-                                       
-                                       return;
-                               }
-                       }
-                       
-                       this._menu.classList.remove('allowScroll');
-                       
-                       data.itemList.classList.add('activeList');
-                       data.parentItemList.classList.add('hidden');
-                       
-                       this._activeList.push(data.itemList);
-                       
-                       this._updateDepth(true);
-               },
-               
-               _updateDepth: function(increase) {
-                       this._depth += (increase) ? 1 : -1;
-                       
-                       this._menu.children[0].style.setProperty('transform', 'translateX(' + (this._depth * -100) + '%)', '');
-               }
-       };
-       
-       return UiPageMenuAbstract;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Main.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/Main.js
deleted file mode 100644 (file)
index 6404656..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * Provides the touch-friendly fullscreen main menu.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Page/Menu/Main
- */
-define(['Core', 'Dom/Traverse', './Abstract'], function(Core, DomTraverse, UiPageMenuAbstract) {
-       "use strict";
-       
-       var _container = null, _hasItems = null, _list = null, _navigationList = null, _spacer = null;
-       
-       /**
-        * @constructor
-        */
-       function UiPageMenuMain() { this.init(); }
-       Core.inherit(UiPageMenuMain, UiPageMenuAbstract, {
-               /**
-                * Initializes the touch-friendly fullscreen main menu.
-                */
-               init: function() {
-                       UiPageMenuMain._super.prototype.init.call(
-                               this,
-                               'com.woltlab.wcf.MainMenuMobile',
-                               'pageMainMenuMobile',
-                               '#pageHeader .mainMenu'
-                       );
-                       
-                       _container = elById('pageMainMenuMobilePageOptionsContainer');
-                       if (_container !== null) {
-                               _list = DomTraverse.childByClass(_container, 'menuOverlayItemList');
-                               _navigationList = elBySel('.jsPageNavigationIcons');
-                               _spacer = _container.nextElementSibling;
-                               
-                               // remove placeholder item
-                               elRemove(DomTraverse.childByClass(_list, 'jsMenuOverlayItemPlaceholder'));
-                       }
-               },
-               
-               open: function (event) {
-                       if (!UiPageMenuMain._super.prototype.open.call(this, event)) {
-                               return false;
-                       }
-                       
-                       if (_container === null) {
-                               return true;
-                       }
-                       
-                       _hasItems = _navigationList.childElementCount > 0;
-                       
-                       if (_hasItems) {
-                               var item, link;
-                               while (_navigationList.childElementCount) {
-                                       item = _navigationList.children[0];
-                                       
-                                       item.classList.add('menuOverlayItem');
-                                       
-                                       link = item.children[0];
-                                       link.classList.add('menuOverlayItemLink');
-                                       link.classList.add('box24');
-                                       
-                                       link.children[1].classList.remove('invisible');
-                                       link.children[1].classList.add('menuOverlayItemTitle');
-                                       
-                                       _list.appendChild(item);
-                               }
-                               
-                               elShow(_container);
-                               elShow(_spacer);
-                       }
-                       else {
-                               elHide(_container);
-                               elHide(_spacer);
-                       }
-                       
-                       return true;
-               },
-               
-               close: function(event) {
-                       if (!UiPageMenuMain._super.prototype.close.call(this, event)) {
-                               return false;
-                       }
-                       
-                       if (_hasItems) {
-                               elHide(_container);
-                               elHide(_spacer);
-                               
-                               var item, link, title = DomTraverse.childByClass(_list, 'menuOverlayTitle');
-                               while (item = title.nextElementSibling) {
-                                       item.classList.remove('menuOverlayItem');
-                                       
-                                       link = item.children[0];
-                                       link.classList.remove('menuOverlayItemLink');
-                                       link.classList.remove('box24');
-                                       
-                                       link.children[1].classList.add('invisible');
-                                       link.children[1].classList.remove('menuOverlayItemTitle');
-                                       
-                                       _navigationList.appendChild(item);
-                               }
-                       }
-                       
-                       return true;
-               }
-       });
-       
-       return UiPageMenuMain;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/User.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Menu/User.js
deleted file mode 100644 (file)
index 031ecbe..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * Provides the touch-friendly fullscreen user menu.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Page/Menu/User
- */
-define(['Core', 'EventHandler', './Abstract'], function(Core, EventHandler, UiPageMenuAbstract) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function UiPageMenuUser() { this.init(); }
-       Core.inherit(UiPageMenuUser, UiPageMenuAbstract, {
-               /**
-                * Initializes the touch-friendly fullscreen user menu.
-                */
-               init: function() {
-                       UiPageMenuUser._super.prototype.init.call(
-                               this,
-                               'com.woltlab.wcf.UserMenuMobile',
-                               'pageUserMenuMobile',
-                               '#pageHeader .userPanel'
-                       );
-               }
-       });
-       
-       return UiPageMenuUser;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search.js
deleted file mode 100644 (file)
index 917a227..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-define(['Ajax', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function(Ajax, EventKey, Language, StringUtil, DomUtil, UiDialog) {
-       "use strict";
-       
-       var _callbackSelect, _resultContainer, _resultList, _searchInput = null;
-       
-       return {
-               open: function(callbackSelect) {
-                       _callbackSelect = callbackSelect;
-                       
-                       UiDialog.open(this);
-               },
-               
-               _search: function (event) {
-                       event.preventDefault();
-                       
-                       var inputContainer = _searchInput.parentNode;
-                       var innerError = inputContainer.nextSibling;
-                       if (innerError && innerError.nodeName === 'SMALL') elRemove(innerError);
-                       
-                       var value = _searchInput.value.trim();
-                       if (value.length < 3) {
-                               innerError = elCreate('small');
-                               innerError.className = 'innerError';
-                               innerError.textContent = Language.get('wcf.page.search.error.tooShort');
-                               DomUtil.insertAfter(innerError, inputContainer);
-                               return;
-                       }
-                       
-                       Ajax.api(this, {
-                               parameters: {
-                                       searchString: value
-                               }
-                       });
-               },
-               
-               _click: function (event) {
-                       event.preventDefault();
-                       
-                       _callbackSelect(elData(event.currentTarget, 'page-id'));
-                       
-                       UiDialog.close(this);
-               },
-               
-               _ajaxSuccess: function(data) {
-                       var html = '', page;
-                       //noinspection JSUnresolvedVariable
-                       for (var i = 0, length = data.returnValues.length; i < length; i++) {
-                               //noinspection JSUnresolvedVariable
-                               page = data.returnValues[i];
-                               
-                               html += '<li>'
-                                               + '<div class="containerHeadline pointer" data-page-id="' + page.pageID + '">'
-                                                       + '<h3>' + StringUtil.escapeHTML(page.name) + '</h3>'
-                                                       + '<small>' + StringUtil.escapeHTML(page.displayLink) + '</small>'
-                                               + '</div>'
-                                       + '</li>';
-                       }
-                       
-                       _resultList.innerHTML = html;
-                       
-                       window[html ? 'elShow' : 'elHide'](_resultContainer);
-                       
-                       if (html) {
-                               elBySelAll('.containerHeadline', _resultList, (function(item) {
-                                       item.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-                               }).bind(this));
-                       }
-                       else {
-                               var innerError = elCreate('small');
-                               innerError.className = 'innerError';
-                               innerError.textContent = Language.get('wcf.page.search.error.noResults');
-                               DomUtil.insertAfter(innerError, _searchInput.parentNode);
-                       }
-               },
-               
-               _ajaxSetup: function () {
-                       return {
-                               data: {
-                                       actionName: 'search',
-                                       className: 'wcf\\data\\page\\PageAction'
-                               }
-                       };
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'wcfUiPageSearch',
-                               options: {
-                                       onSetup: (function() {
-                                               var callbackSearch = this._search.bind(this);
-                                               
-                                               _searchInput = elById('wcfUiPageSearchInput');
-                                               _searchInput.addEventListener('keydown', function(event) {
-                                                       if (EventKey.Enter(event)) {
-                                                               callbackSearch(event);
-                                                       }
-                                               });
-                                               
-                                               _searchInput.nextElementSibling.addEventListener(WCF_CLICK_EVENT, callbackSearch);
-                                               
-                                               _resultContainer = elById('wcfUiPageSearchResultContainer');
-                                               _resultList = elById('wcfUiPageSearchResultList');
-                                       }).bind(this),
-                                       onShow: function() {
-                                               _searchInput.focus();
-                                       },
-                                       title: Language.get('wcf.page.search')
-                               },
-                               source: '<div class="section">'
-                                       + '<dl>'
-                                               + '<dt><label for="wcfUiPageSearchInput">' + Language.get('wcf.page.search.name') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<div class="inputAddon">'
-                                                               + '<input type="text" id="wcfUiPageSearchInput" class="long">'
-                                                               + '<a href="#" class="inputSuffix"><span class="icon icon16 fa-search"></span></a>'
-                                                       + '</div>'
-                                               + '</dd>'
-                                       + '</dl>'
-                               + '</div>'
-                               + '<section id="wcfUiPageSearchResultContainer" class="section" style="display: none;">'
-                                       + '<header class="sectionHeader">'
-                                               + '<h2 class="sectionTitle">' + Language.get('wcf.page.search.results') + '</h2>'
-                                               + '<p class="sectionDescription">' + Language.get('wcf.page.search.results.description') + '</p>'
-                                       + '</header>'
-                                       + '<ol id="wcfUiPageSearchResultList" class="containerList"></ol>'
-                               + '</section>'
-                       };
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search/Handler.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search/Handler.js
deleted file mode 100644 (file)
index 501cde4..0000000
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * Provides access to the lookup function of page handlers, allowing the user to search and
- * select page object ids.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Page/Search/Handler
- */
-define(['Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', './Input'], function(Language, StringUtil, DomUtil, UiDialog, UiPageSearchInput) {
-       "use strict";
-       
-       var _callback = null;
-       var _searchInput = null;
-       var _searchInputHandler = null;
-       var _resultList = null;
-       var _resultListContainer = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Page/Search/Handler
-        */
-       return {
-               /**
-                * Opens the lookup overlay for provided page id.
-                * 
-                * @param       {int}           pageId          page id
-                * @param       {string}        title           dialog title
-                * @param       {function}      callback        callback function provided with the user-selected object id
-                */
-               open: function (pageId, title, callback) {
-                       _callback = callback;
-                       
-                       UiDialog.open(this);
-                       UiDialog.setTitle(this, title);
-                       
-                       this._getSearchInputHandler().setPageId(pageId);
-               },
-               
-               /**
-                * Builds the result list.
-                * 
-                * @param       {Object}        data            AJAX response data
-                * @protected
-                */
-               _buildList: function(data) {
-                       this._resetList();
-                       
-                       // no matches
-                       if (!Array.isArray(data.returnValues) || data.returnValues.length === 0) {
-                               var innerError = elCreate('small');
-                               innerError.className = 'innerError';
-                               innerError.textContent = Language.get('wcf.page.pageObjectID.search.noResults');
-                               DomUtil.insertAfter(innerError, _searchInput);
-                               
-                               return;
-                       }
-                       
-                       var image, item, listItem;
-                       for (var i = 0, length = data.returnValues.length; i < length; i++) {
-                               item = data.returnValues[i];
-                               image = item.image;
-                               if (/^fa-/.test(image)) {
-                                       image = '<span class="icon icon48 ' + image + '"></span>';
-                               }
-                               
-                               listItem = elCreate('li');
-                               elData(listItem, 'object-id', item.objectID);
-                               
-                               listItem.innerHTML = '<div class="box48">'
-                                       + image
-                                       + '<div>'
-                                               + '<div class="containerHeadline">'
-                                                       + '<h3><a href="' + StringUtil.escapeHTML(item.link) + '">' + StringUtil.escapeHTML(item.title) + '</a></h3>'
-                                                       + (item.description ? '<p>' + item.description + '</p>' : '')
-                                               + '</div>'
-                                       + '</div>'
-                               + '</div>';
-                               
-                               listItem.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-                               
-                               _resultList.appendChild(listItem);
-                       }
-                       
-                       elShow(_resultListContainer);
-               },
-               
-               /**
-                * Resets the list and removes any error elements.
-                * 
-                * @protected
-                */
-               _resetList: function() {
-                       var innerError = _searchInput.nextElementSibling;
-                       if (innerError && innerError.classList.contains('innerError')) elRemove(innerError);
-                       
-                       _resultList.innerHTML = '';
-                       
-                       elHide(_resultListContainer);
-               },
-               
-               /**
-                * Initializes the search input handler and returns the instance.
-                * 
-                * @returns     {UiPageSearchInput}     search input handler
-                * @protected
-                */
-               _getSearchInputHandler: function() {
-                       if (_searchInputHandler === null) {
-                               var callback = this._buildList.bind(this);
-                               _searchInputHandler = new UiPageSearchInput(elById('wcfUiPageSearchInput'), {
-                                       callbackSuccess: callback
-                               });
-                       }
-                       
-                       return _searchInputHandler;
-               },
-               
-               /**
-                * Handles clicks on the item unless the click occured directly on a link.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _click: function(event) {
-                       if (event.target.nodeName === 'A') {
-                               return;
-                       }
-                       
-                       event.stopPropagation();
-                       
-                       _callback(elData(event.currentTarget, 'object-id'));
-                       UiDialog.close(this);
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'wcfUiPageSearchHandler',
-                               options: {
-                                       onShow: function() {
-                                               if (_searchInput === null) {
-                                                       _searchInput = elById('wcfUiPageSearchInput');
-                                                       _resultList = elById('wcfUiPageSearchResultList');
-                                                       _resultListContainer = elById('wcfUiPageSearchResultListContainer');
-                                               }
-                                               
-                                               // clear search input
-                                               _searchInput.value = '';
-                                               
-                                               // reset results
-                                               elHide(_resultListContainer);
-                                               _resultList.innerHTML = '';
-                                               
-                                               _searchInput.focus();
-                                       },
-                                       title: ''
-                               },
-                               source: '<div class="section">'
-                                               + '<dl>'
-                                                       + '<dt><label for="wcfUiPageSearchInput">' + Language.get('wcf.page.pageObjectID.search.terms') + '</label></dt>'
-                                                       + '<dd>'
-                                                               + '<input type="text" id="wcfUiPageSearchInput" class="long">'
-                                                               + '<small>' + Language.get('wcf.page.pageObjectID.search.terms.description') + '</small>'
-                                                       + '</dd>'
-                                               + '</dl>'
-                                       + '</div>'
-                                       + '<section id="wcfUiPageSearchResultListContainer" class="section sectionContainerList">'
-                                               + '<header class="sectionHeader">'
-                                                       + '<h2 class="sectionTitle">' + Language.get('wcf.page.pageObjectID.search.results') + '</h2>'
-                                                       + '<p class="sectionDescription">' + Language.get('wcf.page.pageObjectID.search.results.description') + '</p>'
-                                               + '</header>'
-                                               + '<ul id="wcfUiPageSearchResultList" class="containerList wcfUiPageSearchResultList"></ul>'
-                                       + '</section>'
-                       };
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search/Input.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Page/Search/Input.js
deleted file mode 100644 (file)
index 70c09b5..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * Suggestions for page object ids with external response data processing.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Page/Search/Input
- * @extends     module:WoltLab/WCF/Ui/Search/Input
- */
-define(['Core', 'WoltLab/WCF/Ui/Search/Input'], function(Core, UiSearchInput) {
-       "use strict";
-       
-       /**
-        * @param       {Element}       element         input element
-        * @param       {Object=}       options         search options and settings
-        * @constructor
-        */
-       function UiPageSearchInput(element, options) { this.init(element, options); }
-       Core.inherit(UiPageSearchInput, UiSearchInput, {
-               init: function(element, options) {
-                       options = Core.extend({
-                               ajax: {
-                                       className: 'wcf\\data\\page\\PageAction'
-                               },
-                               callbackSuccess: null
-                       }, options);
-                       
-                       if (typeof options.callbackSuccess !== 'function') {
-                               throw new Error("Expected a valid callback function for 'callbackSuccess'.");
-                       }
-                       
-                       UiPageSearchInput._super.prototype.init.call(this, element, options);
-                       
-                       this._pageId = 0;
-               },
-               
-               /**
-                * Sets the target page id.
-                * 
-                * @param       {int}   pageId  target page id
-                */
-               setPageId: function(pageId) {
-                       this._pageId = pageId;
-               },
-               
-               _getParameters: function(value) {
-                       var data = UiPageSearchInput._super.prototype._getParameters.call(this, value);
-                       
-                       data.objectIDs = [this._pageId];
-                       
-                       return data;
-               },
-               
-               _ajaxSuccess: function(data) {
-                       this._options.callbackSuccess(data);
-               }
-       });
-       
-       return UiPageSearchInput;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Pagination.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Pagination.js
deleted file mode 100644 (file)
index dca2915..0000000
+++ /dev/null
@@ -1,237 +0,0 @@
-/**
- * Callback-based pagination.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Pagination
- */
-define(['Core', 'Language', 'ObjectMap', 'StringUtil', 'WoltLab/WCF/Ui/Page/JumpTo'], function(Core, Language, ObjectMap, StringUtil, UiPageJumpTo) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function UiPagination(element, options) { this.init(element, options); }
-       UiPagination.prototype = {
-               /**
-                * maximum number of displayed page links, should match the PHP implementation
-                * @var {int}
-                */
-               SHOW_LINKS: 11,
-               
-               /**
-                * Initializes the pagination.
-                * 
-                * @param       {Element}       element         container element
-                * @param       {object}        options         list of initilization options
-                */
-               init: function(element, options) {
-                       this._element = element;
-                       this._options = Core.extend({
-                               activePage: 1,
-                               maxPage: 1,
-                               
-                               callbackShouldSwitch: null,
-                               callbackSwitch: null
-                       }, options);
-                       
-                       if (typeof this._options.callbackShouldSwitch !== 'function') this._options.callbackShouldSwitch = null;
-                       if (typeof this._options.callbackSwitch !== 'function') this._options.callbackSwitch = null;
-                       
-                       this._element.classList.add('pagination');
-                       
-                       this._rebuild(this._element);
-               },
-               
-               /**
-                * Rebuilds the entire pagination UI.
-                */
-               _rebuild: function() {
-                       var hasHiddenPages = false;
-                       
-                       // clear content
-                       this._element.innerHTML = '';
-                       
-                       var list = elCreate('ul'), link;
-                       
-                       var listItem = elCreate('li');
-                       listItem.className = 'skip';
-                       list.appendChild(listItem);
-                       
-                       var iconClassNames = 'icon icon16 fa-chevron-left';
-                       if (this._options.activePage > 1) {
-                               link = elCreate('a');
-                               link.className = iconClassNames + ' jsTooltip';
-                               link.href = '#';
-                               link.title = Language.get('wcf.global.page.previous');
-                               listItem.appendChild(link);
-                               
-                               link.addEventListener(WCF_CLICK_EVENT, this.switchPage.bind(this, this._options.activePage - 1));
-                       }
-                       else {
-                               listItem.innerHTML = '<span class="' + iconClassNames + '"></span>';
-                               listItem.classList.add('disabled');
-                       }
-                       
-                       // add first page
-                       list.appendChild(this._createLink(1));
-                       
-                       // calculate page links
-                       var maxLinks = this.SHOW_LINKS - 4;
-                       var linksBefore = this._options.activePage - 2;
-                       if (linksBefore < 0) linksBefore = 0;
-                       var linksAfter = this._options.maxPage - (this._options.activePage + 1);
-                       if (linksAfter < 0) linksAfter = 0;
-                       if (this._options.activePage > 1 && this._options.activePage < this._options.maxPage) maxLinks--;
-                       
-                       var half = maxLinks / 2;
-                       var left = this._options.activePage;
-                       var right = this._options.activePage;
-                       if (left < 1) left = 1;
-                       if (right < 1) right = 1;
-                       if (right > this._options.maxPage - 1) right = this._options.maxPage - 1;
-                       
-                       if (linksBefore >= half) {
-                               left -= half;
-                       }
-                       else {
-                               left -= linksBefore;
-                               right += half - linksBefore;
-                       }
-                       
-                       if (linksAfter >= half) {
-                               right += half;
-                       }
-                       else {
-                               right += linksAfter;
-                               left -= half - linksAfter;
-                       }
-                       
-                       right = Math.ceil(right);
-                       left = Math.ceil(left);
-                       if (left < 1) left = 1;
-                       if (right > this._options.maxPage) right = this._options.maxPage;
-                       
-                       // left ... links
-                       var jumpToHtml = '<a class="jsTooltip" title="' + Language.get('wcf.page.jumpTo') + '">&hellip;</a>';
-                       if (left > 1) {
-                               if (left - 1 < 2) {
-                                       list.appendChild(this._createLink(2));
-                               }
-                               else {
-                                       listItem = elCreate('li');
-                                       listItem.className = 'jumpTo';
-                                       listItem.innerHTML = jumpToHtml;
-                                       list.appendChild(listItem);
-                                       
-                                       hasHiddenPages = true;
-                               }
-                       }
-                       
-                       // visible links
-                       for (var i = left + 1; i < right; i++) {
-                               list.appendChild(this._createLink(i));
-                       }
-                       
-                       // right ... links
-                       if (right < this._options.maxPage) {
-                               if (this._options.maxPage - right < 2) {
-                                       list.appendChild(this._createLink(this._options.maxPage - 1));
-                               }
-                               else {
-                                       listItem = elCreate('li');
-                                       listItem.className = 'jumpTo';
-                                       listItem.innerHTML = jumpToHtml;
-                                       list.appendChild(listItem);
-                                       
-                                       hasHiddenPages = true;
-                               }
-                       }
-                       
-                       // add last page
-                       list.appendChild(this._createLink(this._options.maxPage));
-                       
-                       // add next button
-                       listItem = elCreate('li');
-                       listItem.className = 'skip';
-                       list.appendChild(listItem);
-                       
-                       iconClassNames = 'icon icon16 fa-chevron-right';
-                       if (this._options.activePage < this._options.maxPage) {
-                               link = elCreate('a');
-                               link.className = iconClassNames + ' jsTooltip';
-                               link.href = '#';
-                               link.title = Language.get('wcf.global.page.next');
-                               listItem.appendChild(link);
-                               
-                               link.addEventListener(WCF_CLICK_EVENT, this.switchPage.bind(this, this._options.activePage + 1));
-                       }
-                       else {
-                               listItem.innerHTML = '<span class="' + iconClassNames + '"></span>';
-                               listItem.classList.add('disabled');
-                       }
-                       
-                       if (hasHiddenPages) {
-                               elData(list, 'pages', this._options.maxPage);
-                               
-                               UiPageJumpTo.init(list, this.switchPage.bind(this));
-                       }
-                       
-                       this._element.appendChild(list);
-               },
-               
-               /**
-                * Creates a link to a specific page.
-                * 
-                * @param       {int}           pageNo          page number
-                * @return      {Element}       link element
-                */
-               _createLink: function(pageNo) {
-                       var listItem = elCreate('li');
-                       if (pageNo !== this._options.activePage) {
-                               var link = elCreate('a');
-                               link.textContent = StringUtil.addThousandsSeparator(pageNo);
-                               link.addEventListener(WCF_CLICK_EVENT, this.switchPage.bind(this, pageNo));
-                               listItem.appendChild(link);
-                       }
-                       else {
-                               listItem.classList.add('active');
-                               listItem.innerHTML = '<span>' + StringUtil.addThousandsSeparator(pageNo) + '</span><span class="invisible">' + Language.get('wcf.page.pagePosition', { pageNo: pageNo, pages: this._options.maxPage }) + '</span>';
-                       }
-                       
-                       return listItem;
-               },
-               
-               /**
-                * Switches to given page number.
-                * 
-                * @param       {int}           pageNo          page number
-                * @param       {object}        event           event object
-                */
-               switchPage: function(pageNo, event) {
-                       if (typeof event === 'object') {
-                               event.preventDefault();
-                       }
-                       
-                       pageNo = ~~pageNo;
-                       
-                       if (pageNo > 0 && this._options.activePage !== pageNo && pageNo <= this._options.maxPage) {
-                               if (this._options.callbackShouldSwitch !== null) {
-                                       if (this._options.callbackShouldSwitch(pageNo) !== true) {
-                                               return;
-                                       }
-                               }
-                               
-                               this._options.activePage = pageNo;
-                               this._rebuild();
-                               
-                               if (this._options.callbackSwitch !== null) {
-                                       this._options.callbackSwitch(pageNo);
-                               }
-                       }
-               }
-       };
-       
-       return UiPagination;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Autosave.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Autosave.js
deleted file mode 100644 (file)
index 2871bbc..0000000
+++ /dev/null
@@ -1,200 +0,0 @@
-/**
- * Manages the autosave process storing the current editor message in the local
- * storage to recover it on browser crash or accidental navigation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Redactor/Autosave
- */
-define(['Dom/Traverse'], function(DomTraverse) {
-       "use strict";
-       
-       // time between save requests in seconds
-       var _frequency = 15;
-       
-       //noinspection JSUnresolvedVariable
-       var _prefix = 'wsc' + window.WCF_PATH.hashCode() + '-';
-       
-       /**
-        * @param       {Element}       element         textarea element
-        * @constructor
-        */
-       function UiRedactorAutosave(element) { this.init(element); }
-       UiRedactorAutosave.prototype = {
-               /**
-                * Initializes the autosave handler and removes outdated messages from storage.
-                * 
-                * @param       {Element}       element         textarea element
-                */
-               init: function (element) {
-                       this._editor = null;
-                       this._element = element;
-                       this._key = _prefix + elData(this._element, 'autosave');
-                       this._lastMessage = '';
-                       this._timer = null;
-                       
-                       this._cleanup();
-                       
-                       // remove attribute to prevent Redactor's built-in autosave to kick in
-                       this._element.removeAttribute('data-autosave');
-                       
-                       var form = DomTraverse.parentByTag(this._element, 'FORM');
-                       if (form !== null) {
-                               form.addEventListener('submit', this.destroy.bind(this));
-                       }
-               },
-               
-               /**
-                * Returns the initial value for the textarea, used to inject message
-                * from storage into the editor before initialization.
-                * 
-                * @return      {string}        message content
-                */
-               getInitialValue: function() {
-                       var value = '';
-                       try {
-                               value = window.localStorage.getItem(this._key);
-                       }
-                       catch (e) {
-                               window.console.warn("Unable to access local storage: " + e.message);
-                       }
-                       
-                       try {
-                               value = JSON.parse(value);
-                       }
-                       catch (e) {
-                               value = '';
-                       }
-                       
-                       // check if storage is outdated
-                       if (value !== null && typeof value === 'object') {
-                               var lastEditTime = ~~elData(this._element, 'autosave-last-edit-time');
-                               if (lastEditTime * 1000 > value.timestamp) {
-                                       //noinspection JSUnresolvedVariable
-                                       return this._element.value;
-                               }
-                               
-                               return value.content;
-                       }
-                       
-                       //noinspection JSUnresolvedVariable
-                       return this._element.value;
-               },
-               
-               /**
-                * Enables periodical save of editor contents to local storage.
-                * 
-                * @param       {$.Redactor}    editor  redactor instance
-                */
-               watch: function(editor) {
-                       this._editor = editor;
-                       
-                       if (this._timer !== null) {
-                               throw new Error("Autosave timer is already active.");
-                       }
-                       
-                       this._timer = window.setInterval(this._saveToStorage.bind(this), _frequency * 1000);
-                       
-                       this._saveToStorage();
-               },
-               
-               /**
-                * Disables autosave handler, for use on editor destruction.
-                */
-               destroy: function () {
-                       this.clear();
-                       
-                       this._editor = null;
-                       
-                       window.clearInterval(this._timer);
-                       this._timer = null;
-               },
-               
-               /**
-                * Removed the stored message, for use after a message has been submitted.
-                */
-               clear: function () {
-                       this._lastMessage = '';
-                       
-                       try {
-                               window.localStorage.removeItem(this._key);
-                       }
-                       catch (e) {
-                               window.console.warn("Unable to remove from local storage: " + e.message);
-                       }
-               },
-               
-               /**
-                * Saves the current message to storage unless there was no change.
-                * 
-                * @protected
-                */
-               _saveToStorage: function() {
-                       var content = this._editor.code.get();
-                       if (this._editor.utils.isEmpty(content)) {
-                               content = '';
-                       }
-                       
-                       if (this._lastMessage === content) {
-                               // break if content hasn't changed
-                               return;
-                       }
-                       
-                       try {
-                               window.localStorage.setItem(this._key, JSON.stringify({
-                                       content: content,
-                                       timestamp: Date.now()
-                               }));
-                               
-                               this._lastMessage = content;
-                       }
-                       catch (e) {
-                               window.console.warn("Unable to write to local storage: " + e.message);
-                       }
-               },
-               
-               /**
-                * Removes stored messages older than one week.
-                * 
-                * @protected
-                */
-               _cleanup: function () {
-                       var oneWeekAgo = Date.now() - (7 * 24 * 3600 * 1000);
-                       var key, value;
-                       for (var i = 0, length = window.localStorage.length; i < length; i++) {
-                               key = window.localStorage.key(i);
-                               
-                               // check if key matches our prefix
-                               if (key.indexOf(_prefix) !== 0) {
-                                       continue;
-                               }
-                               
-                               try {
-                                       value = window.localStorage.getItem(key);
-                               }
-                               catch (e) {
-                                       window.console.warn("Unable to access local storage: " + e.message);
-                               }
-                               
-                               try {
-                                       value = JSON.parse(value);
-                               }
-                               catch (e) {
-                                       value = { timestamp: 0 };
-                               }
-                               
-                               if (!value || value.timestamp < oneWeekAgo) {
-                                       try {
-                                               window.localStorage.removeItem(key);
-                                       }
-                                       catch (e) {
-                                               window.console.warn("Unable to remove from local storage: " + e.message);
-                                       }
-                               }
-                       }
-               }
-       };
-       
-       return UiRedactorAutosave;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Code.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Code.js
deleted file mode 100644 (file)
index 51c1f0e..0000000
+++ /dev/null
@@ -1,233 +0,0 @@
-/**
- * Manages code blocks.
- *
- * @author      Alexander Ebert
- * @copyright   2001-2016 WoltLab GmbH
- * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module      WoltLab/WCF/Ui/Redactor/Code
- */
-define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
-       "use strict";
-       
-       var _headerHeight = 0;
-       
-       /**
-        * @param       {Object}        editor  editor instance
-        * @constructor
-        */
-       function UiRedactorCode(editor) { this.init(editor); }
-       UiRedactorCode.prototype = {
-               /**
-                * Initializes the source code management.
-                * 
-                * @param       {Object}        editor  editor instance
-                */
-               init: function(editor) {
-                       this._editor = editor;
-                       this._elementId = this._editor.$element[0].id;
-                       this._pre = null;
-                       
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_code_' + this._elementId, this._bbcodeCode.bind(this));
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
-                       
-                       // support for active button marking
-                       this._editor.opts.activeButtonsStates.pre = 'code';
-                       
-                       // static bind to ensure that removing works
-                       this._callbackEdit = this._edit.bind(this);
-                       
-                       // bind listeners on init
-                       this._observeLoad();
-               },
-               
-               /**
-                * Intercepts the insertion of `[code]` tags and uses a native `<pre>` instead.
-                * 
-                * @param       {Object}        data    event data
-                * @protected
-                */
-               _bbcodeCode: function(data) {
-                       data.cancel = true;
-                       
-                       this._editor.button.toggle({}, 'pre', 'func', 'block.format');
-                       
-                       var pre = this._editor.selection.block();
-                       if (pre && pre.nodeName === 'PRE') {
-                               this._setTitle(pre);
-                               
-                               pre.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
-                       }
-               },
-               
-               /**
-                * Binds event listeners and sets quote title on both editor
-                * initialization and when switching back from code view.
-                * 
-                * @protected
-                */
-               _observeLoad: function() {
-                       elBySelAll('pre', this._editor.$editor[0], (function(pre) {
-                               pre.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
-                               this._setTitle(pre);
-                       }).bind(this));
-               },
-               
-               /**
-                * Opens the dialog overlay to edit the code's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _edit: function(event) {
-                       var pre = event.currentTarget;
-                       
-                       if (_headerHeight === 0) {
-                               _headerHeight = ~~window.getComputedStyle(pre).paddingTop.replace(/px$/, '');
-                               
-                               var styles = window.getComputedStyle(pre, '::before');
-                               _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
-                               _headerHeight += ~~styles.height.replace(/px$/, '');
-                               _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
-                       }
-                       
-                       // check if the click hit the header
-                       var offset = DomUtil.offset(pre);
-                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
-                               event.preventDefault();
-                               
-                               this._pre = pre;
-                               
-                               UiDialog.open(this);
-                       }
-               },
-               
-               /**
-                * Saves the changes to the code's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _save: function(event) {
-                       event.preventDefault();
-                       
-                       var id = 'redactor-code-' + this._elementId;
-                       
-                       ['file', 'highlighter', 'line'].forEach((function (attr) {
-                               elData(this._pre, attr, elById(id + '-' + attr).value);
-                       }).bind(this));
-                       
-                       this._setTitle(this._pre);
-                       this._editor.caret.after(this._pre);
-                       
-                       UiDialog.close(this);
-               },
-               
-               /**
-                * Sets or updates the code's header title.
-                * 
-                * @param       {Element}       pre     code element
-                * @protected
-                */
-               _setTitle: function(pre) {
-                       var file = elData(pre, 'file'),
-                           highlighter = elData(pre, 'highlighter');
-                       
-                       //noinspection JSUnresolvedVariable
-                       highlighter = (this._editor.opts.woltlab.highlighters.hasOwnProperty(highlighter)) ? this._editor.opts.woltlab.highlighters[highlighter] : '';
-                       
-                       var title = Language.get('wcf.editor.code.title', {
-                               file: file,
-                               highlighter: highlighter
-                       });
-                       
-                       if (elData(pre, 'title') !== title) {
-                               elData(pre, 'title', title);
-                       }
-               },
-               
-               _dialogSetup: function() {
-                       var id = 'redactor-code-' + this._elementId,
-                           idButtonSave = id + '-button-save',
-                           idFile = id + '-file',
-                           idHighlighter = id + '-highlighter',
-                           idLine = id + '-line';
-                       
-                       return {
-                               id: id,
-                               options: {
-                                       onSetup: (function() {
-                                               elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
-                                               
-                                               // set highlighters
-                                               var highlighters = '<option value="">' + Language.get('wcf.editor.code.highlighter.detect') + '</option>';
-                                               
-                                               var value, values = [];
-                                               //noinspection JSUnresolvedVariable
-                                               for (var highlighter in this._editor.opts.woltlab.highlighters) {
-                                                       //noinspection JSUnresolvedVariable
-                                                       if (this._editor.opts.woltlab.highlighters.hasOwnProperty(highlighter)) {
-                                                               //noinspection JSUnresolvedVariable
-                                                               values.push([highlighter, this._editor.opts.woltlab.highlighters[highlighter]]);
-                                                       }
-                                               }
-                                               
-                                               // sort by label
-                                               values.sort(function(a, b) {
-                                                       if (a[1] < b[1]) {
-                                                               return  -1;
-                                                       }
-                                                       else if (a[1] > b[1]) {
-                                                               return 1;
-                                                       }
-                                                       
-                                                       return 0;
-                                               });
-                                               
-                                               values.forEach((function(value) {
-                                                       highlighters += '<option value="' + value[0] + '">' + StringUtil.escapeHTML(value[1]) + '</option>';
-                                               }).bind(this));
-                                               
-                                               elById(idHighlighter).innerHTML = highlighters;
-                                       }).bind(this),
-                                       
-                                       onShow: (function() {
-                                               elById(idHighlighter).value = elData(this._pre, 'highlighter');
-                                               var line = elData(this._pre, 'line');
-                                               elById(idLine).value = (line === '') ? 1 : ~~line;
-                                               elById(idFile).value = elData(this._pre, 'file');
-                                       }).bind(this),
-                                       
-                                       title: Language.get('wcf.editor.code.edit')
-                               },
-                               source: '<div class="section">'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idHighlighter + '">' + Language.get('wcf.editor.code.highlighter') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<select id="' + idHighlighter + '"></select>'
-                                                       + '<small>' + Language.get('wcf.editor.code.highlighter.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idLine + '">' + Language.get('wcf.editor.code.line') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="number" id="' + idLine + '" min="0" value="1" class="long">'
-                                                       + '<small>' + Language.get('wcf.editor.code.line.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idFile + '">' + Language.get('wcf.editor.code.file') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="text" id="' + idFile + '" class="long">'
-                                                       + '<small>' + Language.get('wcf.editor.code.file.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                               + '</div>'
-                               + '<div class="formSubmit">'
-                                       + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
-                               + '</div>'
-                       };
-               }
-       };
-       
-       return UiRedactorCode;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Format.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Format.js
deleted file mode 100644 (file)
index 6f83209..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-/**
- * Provides helper methods to add and remove format elements. These methods should in
- * theory work with non-editor elements but has not been tested and any usage outside
- * the editor is not recommended.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Redactor/Format
- */
-define(['Dom/Util'], function(DomUtil) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Redactor/Format
-        */
-       return {
-               /**
-                * Applies format elements to the selected text.
-                * 
-                * @param       {Element}       editorElement   editor element
-                * @param       {string}        tagName         format tag name
-                * @param       {string=}       className       optional CSS class for the format tag
-                * @param       {Object=}       attributes      optional list of attributes for the format tag
-                */
-               format: function(editorElement, tagName, className, attributes) {
-                       var selection = window.getSelection();
-                       if (!selection.rangeCount) {
-                               // no active selection
-                               return;
-                       }
-                       
-                       var range = selection.getRangeAt(0);
-                       var tmpElement = null;
-                       if (range.collapsed) {
-                               tmpElement = elCreate('strike');
-                               tmpElement.textContent = '\u200B';
-                               range.insertNode(tmpElement);
-                               
-                               range = document.createRange();
-                               range.selectNodeContents(tmpElement);
-                               
-                               selection.removeAllRanges();
-                               selection.addRange(range);
-                       }
-                       
-                       if (tmpElement === null) {
-                               document.execCommand('strikethrough');
-                       }
-                       
-                       var elements = elBySelAll('strike', editorElement), formatElement, property, strike;
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               strike = elements[i];
-                               
-                               formatElement = elCreate(tagName);
-                               if (className) formatElement.className = className;
-                               if (typeof attributes === 'object') {
-                                       for (property in attributes) {
-                                               if (attributes.hasOwnProperty(property)) {
-                                                       elAttr(formatElement, key, attributes[key]);
-                                               }
-                                       }
-                               }
-                               
-                               DomUtil.replaceElement(strike, formatElement);
-                       }
-               },
-               
-               /**
-                * Removes a format element from the current selection.
-                * 
-                * The removal uses a few techniques to remove the target element(s) without harming
-                * nesting nor any other formatting present. The steps taken are described below:
-                * 
-                * 1. The browser will wrap all parts of the selection into <strike> tags
-                * 
-                *      This isn't the most efficient way to isolate each selected node, but is the
-                *      most reliable way to accomplish this because the browser will insert them
-                *      exactly where the range spans without harming the node nesting.
-                *      
-                *      Basically it is a trade-off between efficiency and reliability, the performance
-                *      is still excellent but could be better at the expense of an increased complexity,
-                *      which simply doesn't exactly pay off.
-                * 
-                * 2. Iterate over each inserted <strike> and isolate all relevant ancestors
-                * 
-                *      Format tags can appear both as a child of the <strike> as well as once or multiple
-                *      times as an ancestor.
-                *      
-                *      It uses ranges to select the contents before the <strike> element up to the start
-                *      of the last matching ancestor and cuts out the nodes. The browser will ensure that
-                *      the resulting fragment will include all relevant ancestors that were present before.
-                *      
-                *      The example below will use the fictional <bar> elements as the tag to remove, the
-                *      pipe ("|") is used to denote the outer node boundaries.
-                *      
-                *      Before:
-                *      |<bar>This is <foo>a <strike>simple <bar>example</bar></strike></foo></bar>|
-                *      After:
-                *      |<bar>This is <foo>a </foo></bar>|<bar><foo>simple <bar>example</bar></strike></foo></bar>|
-                *      
-                *      As a result we can now remove <bar> both inside the <strike> element as well as
-                *      the outer <bar> without harming the effect of <bar> for the preceding siblings.
-                *      
-                *      This process is repeated for siblings appearing after the <strike> element too, it
-                *      works as described above but flipped. This is an expensive operation and will only
-                *      take place if there are any matching ancestors that need to be considered.
-                *      
-                *      Inspired by http://stackoverflow.com/a/12899461
-                * 
-                * 3. Remove all matching ancestors, child elements and last the <strike> element itself
-                * 
-                *      Depending on the amount of nested matching nodes, this process will move a lot of
-                *      nodes around. Removing the <bar> element will require all its child nodes to be moved
-                *      in front of <bar>, they will actually become a sibling of <bar>. Afterwards the
-                *      (now empty) <bar> element can be safely removed without losing any nodes.
-                * 
-                * 
-                * One last hint: This method will not check if the selection at some point contains at
-                * least one target element, it assumes that the user will not take any action that invokes
-                * this method for no reason (unless they want to waste CPU cycles, in that case they're
-                * welcome).
-                * 
-                * This is especially important for developers as this method shouldn't be called for
-                * no good reason. Even though it is super fast, it still comes with expensive DOM operations
-                * and especially low-end devices (such as cheap smartphones) might not exactly like executing
-                * this method on large documents.
-                * 
-                * If you fell the need to invoke this method anyway, go ahead. I'm a comment, not a cop.
-                * 
-                * @param       {Element}       editorElement   editor element
-                * @param       {string}        tagName         format tag name that should be removed
-                */
-               removeFormat: function(editorElement, tagName) {
-                       tagName = tagName.toUpperCase();
-                       
-                       var strikeElements = elByTag('strike', editorElement);
-                       
-                       // remove any <strike> element first, all though there shouldn't be any at all
-                       while (strikeElements.length) {
-                               DomUtil.unwrapChildNodes(strikeElements[0]);
-                       }
-                       
-                       document.execCommand('strikethrough');
-                       
-                       var elements, lastMatchingParent, strikeElement;
-                       while (strikeElements.length) {
-                               strikeElement = strikeElements[0];
-                               lastMatchingParent = this._getLastMatchingParent(strikeElement, editorElement, tagName);
-                               
-                               if (lastMatchingParent !== null) {
-                                       this._handleParentNodes(strikeElement, lastMatchingParent, tagName);
-                               }
-                               
-                               // remove offending elements from child nodes
-                               elements = elByTag(tagName.toLowerCase(), strikeElement);
-                               while (elements.length) {
-                                       DomUtil.unwrapChildNodes(elements[0]);
-                               }
-                               
-                               // remove strike element itself
-                               DomUtil.unwrapChildNodes(strikeElement);
-                       }
-               },
-               
-               /**
-                * Slices relevant parent nodes and removes matching ancestors.
-                * 
-                * @param       {Element}       strikeElement           strike element representing the text selection
-                * @param       {Element}       lastMatchingParent      last matching ancestor element
-                * @param       {string}        tagName                 format tag name that should be removed
-                * @protected
-                */
-               _handleParentNodes: function(strikeElement, lastMatchingParent, tagName) {
-                       var range;
-                       
-                       // selection does not begin at parent node start, slice all relevant parent
-                       // nodes to ensure that selection is then at the beginning while preserving
-                       // all proper ancestor elements
-                       // 
-                       // before: (the pipe represents the node boundary)
-                       // |otherContent <-- selection -->
-                       // after:
-                       // |otherContent| |<-- selection -->
-                       if (!DomUtil.isAtNodeStart(strikeElement, lastMatchingParent)) {
-                               range = document.createRange();
-                               range.setStartBefore(lastMatchingParent);
-                               range.setEndBefore(strikeElement);
-                               
-                               var fragment = range.extractContents();
-                               lastMatchingParent.parentNode.insertBefore(fragment, lastMatchingParent);
-                       }
-                       
-                       // selection does not end at parent node end, slice all relevant parent nodes
-                       // to ensure that selection is then at the end while preserving all proper
-                       // ancestor elements
-                       // 
-                       // before: (the pipe represents the node boundary)
-                       // <-- selection --> otherContent|
-                       // after:
-                       // <-- selection -->| |otherContent|
-                       if (!DomUtil.isAtNodeEnd(strikeElement, lastMatchingParent)) {
-                               range = document.createRange();
-                               range.setStartAfter(strikeElement);
-                               range.setEndAfter(lastMatchingParent);
-                               
-                               fragment = range.extractContents();
-                               lastMatchingParent.parentNode.insertBefore(fragment, lastMatchingParent.nextSibling);
-                       }
-                       
-                       // the strike element is now some kind of isolated, meaning we can now safely
-                       // remove all offending parent nodes without influcing formatting of any content
-                       // before or after the element
-                       var elements = elByTag(tagName, lastMatchingParent);
-                       while (elements.length) {
-                               DomUtil.unwrapChildNodes(elements[0]);
-                       }
-                       
-                       // finally remove the parent itself
-                       DomUtil.unwrapChildNodes(lastMatchingParent);
-               },
-               
-               /**
-                * Finds the last matching ancestor until it reaches the editor element.
-                * 
-                * @param       {Element}               strikeElement   strike element representing the text selection
-                * @param       {Element}               editorElement   editor element
-                * @param       {string}                tagName         format tag name that should be removed
-                * @returns     {(Element|null)}        last matching ancestor element or null if there is none
-                * @protected
-                */
-               _getLastMatchingParent: function(strikeElement, editorElement, tagName) {
-                       var parent = strikeElement.parentNode, match = null;
-                       while (parent !== editorElement) {
-                               if (parent.nodeName === tagName) {
-                                       match = parent;
-                               }
-                               
-                               parent = parent.parentNode;
-                       }
-                       
-                       return match;
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Link.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Link.js
deleted file mode 100644 (file)
index f278af6..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-define(['Language', 'Ui/Dialog'], function(Language, UiDialog) {
-       "use strict";
-       
-       var _boundListener = false;
-       var _callback = null;
-       
-       return {
-               showDialog: function(options) {
-                       UiDialog.open(this);
-                       
-                       UiDialog.setTitle(this, Language.get('wcf.editor.link.' + (options.insert ? 'add' : 'edit')));
-                       
-                       var submitButton = elById('redactor-modal-button-action');
-                       submitButton.textContent = Language.get('wcf.global.button.' + (options.insert ? 'insert' : 'save'));
-                       
-                       _callback = options.submitCallback;
-                       
-                       if (!_boundListener) {
-                               _boundListener = true;
-                               
-                               submitButton.addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
-                       }
-               },
-               
-               _submit: function() {
-                       if (_callback()) {
-                               UiDialog.close(this);
-                       }
-                       else {
-                               var url = elById('redactor-link-url');
-                               var small = (url.nextElementSibling && url.nextElementSibling.nodeName === 'SMALL') ? url.nextElementSibling : null;
-                               
-                               if (small === null) {
-                                       small = elCreate('small');
-                                       small.className = 'innerError';
-                                       small.textContent = Language.get('wcf.global.form.error.empty');
-                                       url.parentNode.appendChild(small);
-                               }
-                       }
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'redactorDialogLink',
-                               options: {
-                                       onClose: function() {
-                                               var url = elById('redactor-link-url');
-                                               var small = (url.nextElementSibling && url.nextElementSibling.nodeName === 'SMALL') ? url.nextElementSibling : null;
-                                               if (small !== null) {
-                                                       elRemove(small);
-                                               }
-                                       }
-                               },
-                               source: '<dl>'
-                                               + '<dt><label for="redactor-link-url">' + Language.get('wcf.editor.link.url') + '</label></dt>'
-                                               + '<dd><input type="url" id="redactor-link-url" class="long"></dd>'
-                                       + '</dl>'
-                                       + '<dl>'
-                                               + '<dt><label for="redactor-link-url-text">' + Language.get('wcf.editor.link.text') + '</label></dt>'
-                                               + '<dd><input type="text" id="redactor-link-url-text" class="long"></dd>'
-                                       + '</dl>'
-                                       + '<div class="formSubmit">'
-                                               + '<button id="redactor-modal-button-action" class="buttonPrimary"></button>'
-                                       + '</div>'
-                       };
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Mention.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Mention.js
deleted file mode 100644 (file)
index b23defc..0000000
+++ /dev/null
@@ -1,345 +0,0 @@
-define(['Ajax', 'Environment', 'EventHandler', 'Ui/Alignment'], function(Ajax, Environment, EventHandler, UiAlignment) {
-       "use strict";
-       
-       function UiRedactorMention(redactor) { this.init(redactor); }
-       UiRedactorMention.prototype = {
-               init: function(redactor) {
-                       this._active = false;
-                       this._caret = null;
-                       this._dropdownActive = false;
-                       this._dropdownMenu = null;
-                       this._itemIndex = 0;
-                       this._lineHeight = null;
-                       this._mentionStart = '';
-                       this._redactor = redactor;
-                       this._timer = null;
-                       
-                       redactor.WoltLabEvent.register('keydown', this._keyDown.bind(this));
-                       redactor.WoltLabEvent.register('keyup', this._keyUp.bind(this));
-               },
-               
-               _keyDown: function(data) {
-                       if (!this._dropdownActive) {
-                               return;
-                       }
-                       
-                       /** @var Event event */
-                       var event = data.event;
-                       
-                       switch (event.which) {
-                               // enter
-                               case 13:
-                                       this._setUsername(null, this._dropdownMenu.children[this._itemIndex].children[0]);
-                                       break;
-                               
-                               // arrow up
-                               case 38:
-                                       this._selectItem(-1);
-                                       break;
-                               
-                               // arrow down
-                               case 40:
-                                       this._selectItem(1);
-                                       break;
-                               
-                               default:
-                                       return;
-                                       break;
-                       }
-                       
-                       event.preventDefault();
-                       data.cancel = true;
-               },
-               
-               _keyUp: function(data) {
-                       /** @var Event event */
-                       var event = data.event;
-                       
-                       // ignore return key
-                       if (event.which === 13) {
-                               this._active = false;
-                               
-                               return;
-                       }
-                       
-                       var text = this._getTextLineInFrontOfCaret();
-                       if (text.length) {
-                               var match = text.match(/@([^,]{3,})$/);
-                               if (match) {
-                                       // if mentioning is at text begin or there's a whitespace character
-                                       // before the '@', everything is fine
-                                       if (!match.index || text[match.index - 1].match(/\s/)) {
-                                               this._mentionStart = match[1];
-                                               
-                                               if (this._timer !== null) {
-                                                       window.clearTimeout(this._timer);
-                                                       this._timer = null;
-                                               }
-                                               
-                                               this._timer = window.setTimeout((function() {
-                                                       Ajax.api(this, {
-                                                               parameters: {
-                                                                       data: {
-                                                                               searchString: this._mentionStart
-                                                                       }
-                                                               }
-                                                       });
-                                                       
-                                                       this._timer = null;
-                                               }).bind(this), 500);
-                                       }
-                               }
-                               else {
-                                       this._hideDropdown();
-                               }
-                       }
-                       else {
-                               this._hideDropdown();
-                       }
-               },
-               
-               _setUsername: function(event, item) {
-                       if (event) {
-                               event.preventDefault();
-                               item = event.currentTarget;
-                       }
-                       
-                       /*if (this._timer !== null) {
-                               this._timer.stop();
-                               this._timer = null;
-                       }
-                       this._proxy.abortPrevious();*/
-                       
-                       var selection = window.getSelection();
-                       
-                       // restore caret position
-                       selection.removeAllRanges();
-                       selection.addRange(this._caret);
-                       
-                       var orgRange = selection.getRangeAt(0).cloneRange();
-                       
-                       // allow redactor to undo this
-                       this._redactor.buffer.set();
-                       
-                       var startContainer = orgRange.startContainer;
-                       var startOffset = orgRange.startOffset - (this._mentionStart.length + 1);
-                       
-                       // navigating with the keyboard before hitting enter will cause the text node to be split
-                       if (startOffset < 0) {
-                               startContainer = startContainer.previousSibling;
-                               startOffset = startContainer.length - (this._mentionStart.length + 1) - (orgRange.startOffset - 1);
-                       }
-                       
-                       var newRange = document.createRange();
-                       newRange.setStart(startContainer, startOffset);
-                       newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
-                       
-                       selection.removeAllRanges();
-                       selection.addRange(newRange);
-                       
-                       var range = getSelection().getRangeAt(0);
-                       range.deleteContents();
-                       range.collapse(true);
-                       
-                       var mention = elCreate('woltlab-mention');
-                       elAttr(mention, 'contenteditable', 'false');
-                       elData(mention, 'user-id', elData(item, 'user-id'));
-                       elData(mention, 'username', elData(item, 'username'));
-                       mention.textContent = elData(item, 'username');
-                       
-                       // U+200C = zero width non-joiner
-                       var text = document.createTextNode('\u200c');
-                       
-                       range.insertNode(text);
-                       range.insertNode(mention);
-                       
-                       newRange = document.createRange();
-                       newRange.selectNode(text);
-                       newRange.collapse(false);
-                       
-                       selection.removeAllRanges();
-                       selection.addRange(newRange);
-                       
-                       this._redactor.selection.save();
-                       
-                       this._hideDropdown();
-               },
-               
-               _getTextLineInFrontOfCaret: function() {
-                       /** @var Range range */
-                       var range = window.getSelection().getRangeAt(0);
-                       if (!range.collapsed) {
-                               return '';
-                       }
-                       
-                       // in Firefox, blurring and refocusing the browser creates separate text nodes
-                       if (Environment.browser() === 'firefox' && range.startContainer.nodeType === Node.TEXT_NODE) {
-                               range.startContainer.parentNode.normalize();
-                       }
-                       
-                       var text = range.startContainer.textContent.substr(0, range.startOffset);
-                       
-                       // remove unicode zero-width space and non-breaking space
-                       var textBackup = text;
-                       text = '';
-                       var hadSpace = false;
-                       for (var i = 0; i < textBackup.length; i++) {
-                               var byte = textBackup.charCodeAt(i).toString(16);
-                               if (byte !== '200b' && (!/\s/.test(textBackup[i]) || ((byte === 'a0' || byte === '20') && !hadSpace))) {
-                                       if (byte === 'a0' || byte === '20') {
-                                               hadSpace = true;
-                                       }
-                                       
-                                       if (textBackup[i] === '@' && i && /\s/.test(textBackup[i - 1])) {
-                                               hadSpace = false;
-                                               text = '';
-                                       }
-                                       
-                                       text += textBackup[i];
-                               }
-                               else {
-                                       hadSpace = false;
-                                       text = '';
-                               }
-                       }
-                       
-                       return text;
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'getSearchResultList',
-                                       className: 'wcf\\data\\user\\UserAction',
-                                       interfaceName: 'wcf\\data\\ISearchAction',
-                                       parameters: {
-                                               data: {
-                                                       includeUserGroups: false
-                                               }
-                                       }
-                               }
-                       };
-               },
-               
-               _ajaxSuccess: function(data) {
-                       if (!Array.isArray(data.returnValues) || !data.returnValues.length) {
-                               this._hideDropdown();
-                               
-                               return;
-                       }
-                       
-                       if (this._dropdownMenu === null) {
-                               this._dropdownMenu = elCreate('ol');
-                               this._dropdownMenu.className = 'dropdownMenu';
-                               elById('dropdownMenuContainer').appendChild(this._dropdownMenu);
-                       }
-                       
-                       this._dropdownMenu.innerHTML = '';
-                       
-                       var callbackClick = this._setUsername.bind(this), link, listItem, user;
-                       for (var i = 0, length = data.returnValues.length; i < length; i++) {
-                               user = data.returnValues[i];
-                               
-                               listItem = elCreate('li');
-                               link = elCreate('a');
-                               link.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                               link.className = 'box16';
-                               link.innerHTML = '<span>' + user.icon + '</span> <span>' + user.label + '</span>';
-                               elData(link, 'user-id', user.objectID);
-                               elData(link, 'username', user.label);
-                               
-                               listItem.appendChild(link);
-                               this._dropdownMenu.appendChild(listItem);
-                       }
-                       
-                       this._dropdownMenu.classList.add('dropdownOpen');
-                       this._dropdownActive = true;
-                       
-                       this._updateDropdownPosition();
-               },
-               
-               _getDropdownMenuPosition: function() {
-                       this._redactor.selection.save();
-                       
-                       var selection = window.getSelection();
-                       var orgRange = selection.getRangeAt(0).cloneRange();
-                       
-                       // mark the entire text, starting from the '@' to the current cursor position
-                       var newRange = document.createRange();
-                       newRange.setStart(orgRange.startContainer, orgRange.startOffset - (this._mentionStart.length + 1));
-                       newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
-                       
-                       selection.removeAllRanges();
-                       selection.addRange(newRange);
-                       
-                       // get the offsets of the bounding box of current text selection
-                       var rect = selection.getRangeAt(0).getBoundingClientRect();
-                       var offsets = {
-                               top: Math.round(rect.bottom) + window.scrollY,
-                               left: Math.round(rect.left) + document.body.scrollLeft
-                       };
-                       
-                       if (this._lineHeight === null) {
-                               this._lineHeight = Math.round(rect.bottom - rect.top - window.scrollY);
-                       }
-                       
-                       // restore caret position
-                       this._redactor.selection.restore();
-                       
-                       this._caret = orgRange;
-                       
-                       return offsets;
-               },
-               
-               _updateDropdownPosition: function() {
-                       try {
-                               var offset = this._getDropdownMenuPosition();
-                               offset.top += 7; // add a little vertical gap
-                               
-                               this._dropdownMenu.style.setProperty('left', offset.left + 'px', '');
-                               this._dropdownMenu.style.setProperty('top', offset.top + 'px', '');
-                               
-                               this._selectItem(0);
-                               
-                               if (offset.top + this._dropdownMenu.offsetHeight + 10 > window.innerHeight + window.scrollY) {
-                                       this._dropdownMenu.classList.add('dropdownArrowBottom');
-                                       
-                                       this._dropdownMenu.style.setProperty('top', offset.top - this._dropdownMenu.offsetHeight - 2 * this._lineHeight + 7 + 'px', '');
-                               }
-                               else {
-                                       this._dropdownMenu.classList.remove('dropdownArrowBottom');
-                               }
-                       }
-                       catch (e) {
-                               console.debug(e);
-                               // ignore errors that are caused by pressing enter to
-                               // often in a short period of time
-                       }
-               },
-               
-               _selectItem: function(step) {
-                       // find currently active item
-                       var item = elBySel('.active', this._dropdownMenu);
-                       if (item !== null) {
-                               item.classList.remove('active');
-                       }
-                       
-                       this._itemIndex += step;
-                       if (this._itemIndex === -1) {
-                               this._itemIndex = this._dropdownMenu.childElementCount - 1;
-                       }
-                       else if (this._itemIndex === this._dropdownMenu.childElementCount) {
-                               this._itemIndex = 0;
-                       }
-                       
-                       this._dropdownMenu.children[this._itemIndex].classList.add('active');
-               },
-               
-               _hideDropdown: function() {
-                       if (this._dropdownMenu !== null) this._dropdownMenu.classList.remove('dropdownOpen');
-                       this._dropdownActive = false;
-               }
-       };
-       
-       return UiRedactorMention;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Metacode.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Metacode.js
deleted file mode 100644 (file)
index fba6bb1..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/**
- * Converts `<woltlab-metacode>` into the bbcode representation.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Redactor/Metacode
- */
-define(['Dom/Util'], function(DomUtil) {
-       "use strict";
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Redactor/Metacode
-        */
-       return {
-               /**
-                * Converts `<woltlab-metacode>` into the bbcode representation.
-                * 
-                * @param       {Element}       element         textarea element
-                */
-               convert: function(element) {
-                       var div = elCreate('div');
-                       div.innerHTML = element.textContent;
-                       
-                       var attributes, metacode, metacodes = elByTag('woltlab-metacode', div), name, tagClose, tagOpen;
-                       while (metacodes.length) {
-                               metacode = metacodes[0];
-                               name = elData(metacode, 'name');
-                               attributes = elData(metacode, 'attributes');
-                               
-                               tagOpen = this._getOpeningTag(name, attributes);
-                               tagClose = this._getClosingTag(name);
-                               
-                               if (metacode.parentNode === div) {
-                                       DomUtil.prepend(tagOpen, this._getFirstParagraph(metacode));
-                                       this._getLastParagraph(metacode).appendChild(tagClose);
-                               }
-                               else {
-                                       DomUtil.prepend(tagOpen, metacode);
-                                       metacode.appendChild(tagClose);
-                               }
-                               
-                               DomUtil.unwrapChildNodes(metacode);
-                       }
-                       
-                       element.textContent = div.innerHTML;
-               },
-               
-               /**
-                * Returns a text node representing the opening bbcode tag.
-                * 
-                * @param       {string}        name            bbcode tag
-                * @param       {string}        attributes      base64- and JSON-encoded attributes
-                * @returns     {Text}          text node containing the opening bbcode tag
-                * @protected
-                */
-               _getOpeningTag: function(name, attributes) {
-                       try {
-                               attributes = JSON.parse(atob(attributes));
-                       }
-                       catch (e) { /* invalid base64 data or invalid json */ }
-                       
-                       if (!Array.isArray(attributes)) {
-                               attributes = [];
-                       }
-                       
-                       var buffer = '[' + name;
-                       if (attributes.length) {
-                               for (var i = 0, length = attributes.length; i < length; i++) {
-                                       if (!/^'.*'$/.test(attributes[i])) {
-                                               attributes[i] = "'" + attributes[i] + "'";
-                                       }
-                               }
-                               
-                               buffer += '=' + attributes.join(',');
-                       }
-                       
-                       return document.createTextNode(buffer + ']');
-               },
-               
-               /**
-                * Returns a text node representing the closing bbcode tag.
-                * 
-                * @param       {string}        name            bbcode tag
-                * @returns     {Text}          text node containing the closing bbcode tag
-                * @protected
-                */
-               _getClosingTag: function(name) {
-                       return document.createTextNode('[/' + name + ']');
-               },
-               
-               /**
-                * Returns the first paragraph of provided element. If there are no children or
-                * the first child is not a paragraph, a new paragraph is created and inserted
-                * as first child.
-                * 
-                * @param       {Element}       element         metacode element
-                * @returns     {Element}       paragraph that is the first child of provided element
-                * @protected
-                */
-               _getFirstParagraph: function (element) {
-                       var firstChild, paragraph;
-                       
-                       if (element.childElementCount === 0) {
-                               paragraph = elCreate('p');
-                               element.appendChild(paragraph);
-                       }
-                       else {
-                               firstChild = element.children[0];
-                               
-                               if (firstChild.nodeName === 'P') {
-                                       paragraph = firstChild;
-                               }
-                               else {
-                                       paragraph = elCreate('p');
-                                       element.insertBefore(paragraph, firstChild);
-                               }
-                       }
-                       
-                       return paragraph;
-               },
-               
-               /**
-                * Returns the last paragraph of provided element. If there are no children or
-                * the last child is not a paragraph, a new paragraph is created and inserted
-                * as last child.
-                * 
-                * @param       {Element}       element         metacode element
-                * @returns     {Element}       paragraph that is the last child of provided element
-                * @protected
-                */
-               _getLastParagraph: function (element) {
-                       var count = element.childElementCount, lastChild, paragraph;
-                       
-                       if (count === 0) {
-                               paragraph = elCreate('p');
-                               element.appendChild(paragraph);
-                       }
-                       else {
-                               lastChild = element.children[count - 1];
-                               
-                               if (lastChild.nodeName === 'P') {
-                                       paragraph = lastChild;
-                               }
-                               else {
-                                       paragraph = elCreate('p');
-                                       element.appendChild(paragraph);
-                               }
-                       }
-                       
-                       return paragraph;
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Page.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Page.js
deleted file mode 100644 (file)
index 210c5ad..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Converts `<woltlab-metacode>` into the bbcode representation.
- *
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Redactor/Metacode
- */
-define(['WoltLab/WCF/Ui/Page/Search'], function(UiPageSearch) {
-       "use strict";
-       
-       function UiRedactorPage(editor, button) { this.init(editor, button); }
-       UiRedactorPage.prototype = {
-               init: function (editor, button) {
-                       this._editor = editor;
-                       
-                       button.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-               },
-               
-               _click: function (event) {
-                       event.preventDefault();
-                       
-                       UiPageSearch.open(this._insert.bind(this));
-               },
-               
-               _insert: function (pageID) {
-                       this._editor.buffer.set();
-                       
-                       this._editor.insert.text("[wsp='" + pageID + "'][/wsp]");
-               }
-       };
-       
-       return UiRedactorPage;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Quote.js
deleted file mode 100644 (file)
index b9871e6..0000000
+++ /dev/null
@@ -1,262 +0,0 @@
-/**
- * Manages quotes.
- *
- * @author      Alexander Ebert
- * @copyright   2001-2016 WoltLab GmbH
- * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module      WoltLab/WCF/Ui/Redactor/Quote
- */
-define(['Core', 'EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (Core, EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
-       "use strict";
-       
-       var _headerHeight = 0;
-       
-       /**
-        * @param       {Object}        editor  editor instance
-        * @param       {jQuery}        button  toolbar button
-        * @constructor
-        */
-       function UiRedactorQuote(editor, button) { this.init(editor, button); }
-       UiRedactorQuote.prototype = {
-               /**
-                * Initializes the quote management.
-                * 
-                * @param       {Object}        editor  editor instance
-                * @param       {jQuery}        button  toolbar button
-                */
-               init: function(editor, button) {
-                       this._blockquote = null;
-                       this._editor = editor;
-                       this._elementId = this._editor.$element[0].id;
-                       
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
-                       
-                       this._editor.button.addCallback(button, this._click.bind(this));
-                       
-                       // support for active button marking
-                       this._editor.opts.activeButtonsStates.blockquote = 'woltlabQuote';
-                       
-                       // static bind to ensure that removing works
-                       this._callbackEdit = this._edit.bind(this);
-                       
-                       // bind listeners on init
-                       this._observeLoad();
-                       
-                       // quote manager
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'insertQuote_' + this._elementId, this._insertQuote.bind(this));
-               },
-               
-               /**
-                * Inserts a quote.
-                * 
-                * @param       {Object}        data            quote data
-                * @protected
-                */
-               _insertQuote: function (data) {
-                       this._editor.buffer.set();
-                       
-                       // caret must be within a `<p>`, if it is not move it
-                       /** @type Node */
-                       var block = this._editor.selection.block();
-                       if (block === false) {
-                               this._editor.selection.restore();
-                               
-                               block = this._editor.selection.block();
-                       }
-                       
-                       if (block.nodeName !== 'P') {
-                               var redactor = this._editor.core.editor()[0];
-                               
-                               // find parent before Redactor
-                               while (block.parentNode !== redactor) {
-                                       block = block.parentNode;
-                               }
-                               
-                               // caret.after() requires a following element
-                               var next = this._editor.caret.next(block);
-                               if (next === undefined || next.nodeName !== 'P') {
-                                       var p = elCreate('p');
-                                       p.textContent = '\u200B';
-                                       
-                                       DomUtil.insertAfter(p, block);
-                               }
-                               
-                               this._editor.caret.after(block);
-                       }
-                       
-                       var content = '';
-                       if (data.isText) content = this._editor.marker.html();
-                       else content = data.content;
-                       
-                       var quoteId = Core.getUuid();
-                       this._editor.insert.html('<blockquote id="' + quoteId + '">' + content + '</blockquote>');
-                       
-                       var quote = elById(quoteId);
-                       elData(quote, 'author', data.author);
-                       elData(quote, 'link', data.link);
-                       
-                       if (data.isText) {
-                               this.insert.text(data.content);
-                       }
-                       
-                       quote.removeAttribute('id');
-                       
-                       this._editor.caret.after(quote);
-                       this._editor.selection.save();
-               },
-               
-               /**
-                * Toggles the quote block on button click.
-                * 
-                * @protected
-                */
-               _click: function() {
-                       this._editor.button.toggle({}, 'blockquote', 'func', 'block.format');
-                       
-                       var blockquote = this._editor.selection.block();
-                       if (blockquote && blockquote.nodeName === 'BLOCKQUOTE') {
-                               this._setTitle(blockquote);
-                               
-                               blockquote.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
-                       }
-               },
-               
-               /**
-                * Binds event listeners and sets quote title on both editor
-                * initialization and when switching back from code view.
-                * 
-                * @protected
-                */
-               _observeLoad: function() {
-                       elBySelAll('blockquote', this._editor.$editor[0], (function(blockquote) {
-                               blockquote.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
-                               this._setTitle(blockquote);
-                       }).bind(this));
-               },
-               
-               /**
-                * Opens the dialog overlay to edit the quote's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _edit: function(event) {
-                       var blockquote = event.currentTarget;
-                       
-                       if (_headerHeight === 0) {
-                               _headerHeight = ~~window.getComputedStyle(blockquote).paddingTop.replace(/px$/, '');
-                               
-                               var styles = window.getComputedStyle(blockquote, '::before');
-                               _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
-                               _headerHeight += ~~styles.height.replace(/px$/, '');
-                               _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
-                       }
-                       
-                       // check if the click hit the header
-                       var offset = DomUtil.offset(blockquote);
-                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
-                               event.preventDefault();
-                               
-                               this._blockquote = blockquote;
-                               
-                               UiDialog.open(this);
-                       }
-               },
-               
-               /**
-                * Saves the changes to the quote's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _save: function(event) {
-                       event.preventDefault();
-                       
-                       var id = 'redactor-quote-' + this._elementId;
-                       var urlInput = elById(id + '-url');
-                       var innerError = elBySel('.innerError', urlInput.parentNode);
-                       if (innerError !== null) elRemove(innerError);
-                       
-                       var url = urlInput.value.replace(/\u200B/g, '').trim();
-                       // simple test to check if it at least looks like it could be a valid url
-                       if (url.length && !/^https?:\/\/[^\/]+/.test(url)) {
-                               innerError = elCreate('small');
-                               innerError.className = 'innerError';
-                               innerError.textContent = Language.get('wcf.editor.quote.url.error.invalid');
-                               urlInput.parentNode.insertBefore(innerError, urlInput.nextElementSibling);
-                               return;
-                       }
-                       
-                       // set author
-                       elData(this._blockquote, 'author', elById(id + '-author').value);
-                       
-                       // set url
-                       elData(this._blockquote, 'url', url);
-                       
-                       this._setTitle(this._blockquote);
-                       this._editor.caret.after(this._blockquote);
-                       
-                       UiDialog.close(this);
-               },
-               
-               /**
-                * Sets or updates the quote's header title.
-                * 
-                * @param       {Element}       blockquote     quote element
-                * @protected
-                */
-               _setTitle: function(blockquote) {
-                       var title = Language.get('wcf.editor.quote.title', {
-                               author: elData(blockquote, 'author'),
-                               url: elData(blockquote, 'url')
-                       });
-                       
-                       if (elData(blockquote, 'title') !== title) {
-                               elData(blockquote, 'title', title);
-                       }
-               },
-               
-               _dialogSetup: function() {
-                       var id = 'redactor-quote-' + this._elementId,
-                           idAuthor = id + '-author',
-                           idButtonSave = id + '-button-save',
-                           idUrl = id + '-url';
-                       
-                       return {
-                               id: id,
-                               options: {
-                                       onSetup: (function() {
-                                               elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
-                                       }).bind(this),
-                                       
-                                       onShow: (function() {
-                                               elById(idAuthor).value = elData(this._blockquote, 'author');
-                                               elById(idUrl).value = elData(this._blockquote, 'url');
-                                       }).bind(this),
-                                       
-                                       title: Language.get('wcf.editor.quote.edit')
-                               },
-                               source: '<div class="section">'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idAuthor + '">' + Language.get('wcf.editor.quote.author') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="text" id="' + idAuthor + '" class="long">'
-                                               + '</dd>'
-                                       + '</dl>'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idUrl + '">' + Language.get('wcf.editor.quote.url') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="text" id="' + idUrl + '" class="long">'
-                                                       + '<small>' + Language.get('wcf.editor.quote.url.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                               + '</div>'
-                               + '<div class="formSubmit">'
-                                       + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
-                               + '</div>'
-                       };
-               }
-       };
-       
-       return UiRedactorQuote;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Spoiler.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Redactor/Spoiler.js
deleted file mode 100644 (file)
index 23044fe..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-/**
- * Manages spoilers.
- *
- * @author      Alexander Ebert
- * @copyright   2001-2016 WoltLab GmbH
- * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module      WoltLab/WCF/Ui/Redactor/Spoiler
- */
-define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
-       "use strict";
-       
-       var _headerHeight = 0;
-       
-       /**
-        * @param       {Object}        editor  editor instance
-        * @constructor
-        */
-       function UiRedactorSpoiler(editor) { this.init(editor); }
-       UiRedactorSpoiler.prototype = {
-               /**
-                * Initializes the spoiler management.
-                * 
-                * @param       {Object}        editor  editor instance
-                */
-               init: function(editor) {
-                       this._editor = editor;
-                       this._elementId = this._editor.$element[0].id;
-                       this._spoiler = null;
-                       
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_spoiler_' + this._elementId, this._bbcodeSpoiler.bind(this));
-                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
-                       
-                       // register custom block element
-                       this._editor.WoltLabBlock.register('woltlab-spoiler', true);
-                       
-                       // support for active button marking
-                       this._editor.opts.activeButtonsStates['woltlab-spoiler'] = 'woltlabSpoiler';
-                       
-                       // static bind to ensure that removing works
-                       this._callbackEdit = this._edit.bind(this);
-                       
-                       // bind listeners on init
-                       this._observeLoad();
-               },
-               
-               /**
-                * Intercepts the insertion of `[spoiler]` tags and uses
-                * the custom `<woltlab-spoiler>` element instead.
-                * 
-                * @param       {Object}        data    event data
-                * @protected
-                */
-               _bbcodeSpoiler: function(data) {
-                       data.cancel = true;
-                       
-                       this._editor.button.toggle({}, 'woltlab-spoiler', 'func', 'block.format');
-                       
-                       var spoiler = this._editor.selection.block();
-                       if (spoiler && spoiler.nodeName === 'WOLTLAB-SPOILER') {
-                               this._setTitle(spoiler);
-                               
-                               spoiler.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
-                       }
-               },
-               
-               /**
-                * Binds event listeners and sets quote title on both editor
-                * initialization and when switching back from code view.
-                * 
-                * @protected
-                */
-               _observeLoad: function() {
-                       elBySelAll('woltlab-spoiler', this._editor.$editor[0], (function(spoiler) {
-                               spoiler.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
-                               this._setTitle(spoiler);
-                       }).bind(this));
-               },
-               
-               /**
-                * Opens the dialog overlay to edit the spoiler's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _edit: function(event) {
-                       var spoiler = event.currentTarget;
-                       
-                       if (_headerHeight === 0) {
-                               _headerHeight = ~~window.getComputedStyle(spoiler).paddingTop.replace(/px$/, '');
-                               
-                               var styles = window.getComputedStyle(spoiler, '::before');
-                               _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
-                               _headerHeight += ~~styles.height.replace(/px$/, '');
-                               _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
-                       }
-                       
-                       // check if the click hit the header
-                       var offset = DomUtil.offset(spoiler);
-                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
-                               event.preventDefault();
-                               
-                               this._spoiler = spoiler;
-                               
-                               UiDialog.open(this);
-                       }
-               },
-               
-               /**
-                * Saves the changes to the spoiler's properties.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _save: function(event) {
-                       event.preventDefault();
-                       
-                       elData(this._spoiler, 'label', elById('redactor-spoiler-' + this._elementId + '-label').value);
-                       
-                       this._setTitle(this._spoiler);
-                       this._editor.caret.after(this._spoiler);
-                       
-                       UiDialog.close(this);
-               },
-               
-               /**
-                * Sets or updates the spoiler's header title.
-                * 
-                * @param       {Element}       spoiler     spoiler element
-                * @protected
-                */
-               _setTitle: function(spoiler) {
-                       var title = Language.get('wcf.editor.spoiler.title', { label: elData(spoiler, 'label') });
-                       
-                       if (elData(spoiler, 'title') !== title) {
-                               elData(spoiler, 'title', title);
-                       }
-               },
-               
-               _dialogSetup: function() {
-                       var id = 'redactor-spoiler-' + this._elementId,
-                           idButtonSave = id + '-button-save',
-                           idLabel = id + '-label';
-                       
-                       return {
-                               id: id,
-                               options: {
-                                       onSetup: (function() {
-                                               elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
-                                       }).bind(this),
-                                       
-                                       onShow: (function() {
-                                               elById(idLabel).value = elData(this._spoiler, 'label');
-                                       }).bind(this),
-                                       
-                                       title: Language.get('wcf.editor.spoiler.edit')
-                               },
-                               source: '<div class="section">'
-                                       + '<dl>'
-                                               + '<dt><label for="' + idLabel + '">' + Language.get('wcf.editor.spoiler.label') + '</label></dt>'
-                                               + '<dd>'
-                                                       + '<input type="text" id="' + idLabel + '" class="long">'
-                                                       + '<small>' + Language.get('wcf.editor.spoiler.label.description') + '</small>'
-                                               + '</dd>'
-                                       + '</dl>'
-                               + '</div>'
-                               + '<div class="formSubmit">'
-                                       + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
-                               + '</div>'
-                       };
-               }
-       };
-       
-       return UiRedactorSpoiler;
-});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Screen.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Screen.js
deleted file mode 100644 (file)
index 21a26fa..0000000
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- * Provides consistent support for media queries and body scrolling.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Screen
- */
-define(['Core', 'Dictionary'], function(Core, Dictionary) {
-       "use strict";
-       
-       var _mql = new Dictionary();
-       var _scrollDisableCounter = 0;
-       
-       var _mqMap = Dictionary.fromObject({
-               'screen-xs': '(max-width: 544px)',                              /* smartphone */
-               'screen-sm': '(min-width: 545px) and (max-width: 768px)',       /* tablet (portrait) */
-               'screen-sm-down': '(max-width: 768px)',                         /* smartphone + tablet (portrait) */
-               'screen-sm-up': '(min-width: 545px)',                           /* tablet (portrait) + tablet (landscape) + desktop */
-               'screen-sm-md': '(min-width: 545px) and (max-width: 1024px)',   /* tablet (portrait) + tablet (landscape) */
-               'screen-md': '(min-width: 769px) and (max-width: 1024px)',      /* tablet (landscape) */
-               'screen-md-down': '(max-width: 1024px)',                        /* smartphone + tablet (portrait) + tablet (landscape) */
-               'screen-md-up': '(min-width: 1024px)',                          /* tablet (landscape) + desktop */
-               'screen-lg': '(min-width: 1025px)'                              /* desktop */
-       });
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Screen
-        */
-       return {
-               /**
-                * Registers event listeners for media query match/unmatch.
-                * 
-                * The `callbacks` object may contain the following keys:
-                *  - `match`, triggered when media query matches
-                *  - `unmatch`, triggered when media query no longer matches
-                *  - `setup`, invoked when media query first matches
-                * 
-                * Returns a UUID that is used to internal identify the callbacks, can be used
-                * to remove binding by calling the `remove` method.
-                * 
-                * @param       {string}        query           media query
-                * @param       {object}        callbacks       callback functions
-                * @return      {string}        UUID for listener removal
-                */
-               on: function(query, callbacks) {
-                       var uuid = Core.getUuid(), queryObject = this._getQueryObject(query);
-                       
-                       if (typeof callbacks.match === 'function') {
-                               queryObject.callbacksMatch.set(uuid, callbacks.match);
-                       }
-                       
-                       if (typeof callbacks.unmatch === 'function') {
-                               queryObject.callbacksUnmatch.set(uuid, callbacks.unmatch);
-                       }
-                       
-                       if (typeof callbacks.setup === 'function') {
-                               if (queryObject.mql.matches) {
-                                       callbacks.setup();
-                               }
-                               else {
-                                       queryObject.callbacksSetup.set(uuid, callbacks.setup);
-                               }
-                       }
-                       
-                       return uuid;
-               },
-               
-               /**
-                * Removes all listeners identified by their common UUID.
-                *
-                * @param       {string}        query   must match the `query` argument used when calling `on()`
-                * @param       {string}        uuid    UUID received when calling `on()`
-                */
-               remove: function(query, uuid) {
-                       var queryObject = this._getQueryObject(query);
-                       
-                       queryObject.callbacksMatch.delete(uuid);
-                       queryObject.callbacksUnmatch.delete(uuid);
-                       queryObject.callbacksSetup.delete(uuid);
-               },
-               
-               /**
-                * Returns a boolean value if a media query expression currently matches.
-                * 
-                * @param       {string}        query   CSS media query
-                * @returns     {boolean}       true if query matches
-                */
-               is: function(query) {
-                       return this._getQueryObject(query).mql.matches;
-               },
-               
-               /**
-                * Disables scrolling of body element.
-                */
-               scrollDisable: function() {
-                       if (_scrollDisableCounter === 0) {
-                               document.documentElement.classList.add('disableScrolling');
-                       }
-                       
-                       _scrollDisableCounter++;
-               },
-               
-               /**
-                * Re-enables scrolling of body element.
-                */
-               scrollEnable: function() {
-                       if (_scrollDisableCounter) {
-                               _scrollDisableCounter--;
-                               
-                               if (_scrollDisableCounter === 0) {
-                                       document.documentElement.classList.remove('disableScrolling');
-                               }
-                       }
-               },
-               
-               /**
-                * 
-                * @param       {string}        query   CSS media query
-                * @return      {Object}        object containing callbacks and MediaQueryList
-                * @protected
-                */
-               _getQueryObject: function(query) {
-                       if (typeof query !== 'string' || query.trim() === '') {
-                               throw new TypeError("Expected a non-empty string for parameter 'query'.");
-                       }
-                       
-                       if (_mqMap.has(query)) query = _mqMap.get(query);
-                       
-                       var queryObject = _mql.get(query);
-                       if (!queryObject) {
-                               queryObject = {
-                                       callbacksMatch: new Dictionary(),
-                                       callbacksUnmatch: new Dictionary(),
-                                       callbacksSetup: new Dictionary(),
-                                       mql: window.matchMedia(query)
-                               };
-                               queryObject.mql.addListener(this._mqlChange.bind(this));
-                               
-                               _mql.set(query, queryObject);
-                       }
-                       
-                       return queryObject;
-               },
-               
-               /**
-                * Triggered whenever a registered media query now matches or no longer matches.
-                * 
-                * @param       {Event} event   event object
-                * @protected
-                */
-               _mqlChange: function(event) {
-                       var queryObject = this._getQueryObject(event.media);
-                       if (event.matches) {
-                               if (queryObject.callbacksSetup.size) {
-                                       queryObject.callbacksSetup.forEach(function(callback) {
-                                               callback();
-                                       });
-                                       
-                                       // discard all setup callbacks after execution
-                                       queryObject.callbacksSetup = new Dictionary();
-                               }
-                               
-                               queryObject.callbacksMatch.forEach(function(callback) {
-                                       callback();
-                               });
-                       }
-                       else {
-                               queryObject.callbacksUnmatch.forEach(function(callback) {
-                                       callback();
-                               });
-                       }
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Scroll.js
deleted file mode 100644 (file)
index a872499..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Smoothly scrolls to an element while accounting for potential sticky headers.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Scroll
- */
-define(['Dom/Util'], function(DomUtil) {
-       "use strict";
-       
-       var _callback = null;
-       var _callbackScroll = null;
-       var _timeoutScroll = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/Scroll
-        */
-       return {
-               /**
-                * Scrolls to target element, optionally invoking the provided callback once scrolling has ended.
-                * 
-                * @param       {Element}       element         target element
-                * @param       {function=}     callback        callback invoked once scrolling has ended
-                */
-               element: function(element, callback) {
-                       if (!(element instanceof Element)) {
-                               throw new TypeError("Expected a valid DOM element.");
-                       }
-                       else if (callback !== undefined && typeof callback !== 'function') {
-                               throw new TypeError("Expected a valid callback function.");
-                       }
-                       else if (!document.body.contains(element)) {
-                               throw new Error("Element must be part of the visible DOM.");
-                       }
-                       else if (_callback !== null) {
-                               throw new Error("Cannot scroll to element, a concurrent request is running.");
-                       }
-                       
-                       if (callback) {
-                               _callback = callback;
-                               
-                               if (_callbackScroll === null) {
-                                       _callbackScroll = this._onScroll.bind(this);
-                               }
-                               
-                               window.addEventListener('scroll', _callbackScroll);
-                       }
-                       
-                       var y = DomUtil.offset(element).top;
-                       
-                       if (y <= 50) {
-                               y = 0;
-                       }
-                       else {
-                               // add an offset of 50 pixel to account for a sticky header
-                               y -= 50;
-                       }
-                       
-                       window.scrollTo({
-                               left: 0,
-                               top: y,
-                               behavior: 'smooth'
-                       });
-               },
-               
-               /**
-                * Monitors scroll event to only execute the callback once scrolling has ended.
-                * 
-                * @protected
-                */
-               _onScroll: function() {
-                       if (_timeoutScroll !== null) window.clearTimeout(_timeoutScroll);
-                       
-                       _timeoutScroll = window.setTimeout(function() {
-                               _callback();
-                               
-                               window.removeEventListener('scroll', _callbackScroll);
-                               _callback = null;
-                               _timeoutScroll = null;
-                       }, 100);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Search/Input.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Search/Input.js
deleted file mode 100644 (file)
index 93dd9f7..0000000
+++ /dev/null
@@ -1,364 +0,0 @@
-/**
- * Provides suggestions using an input field, designed to work with `wcf\data\ISearchAction`.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Search/Input
- */
-define(['Ajax', 'Core', 'EventKey', 'Dom/Util', 'Ui/SimpleDropdown'], function(Ajax, Core, EventKey, DomUtil, UiSimpleDropdown) {
-       "use strict";
-       
-       /**
-        * @param       {Element}       element         target input[type="text"]
-        * @param       {Object}        options         search options and settings
-        * @constructor
-        */
-       function UiSearchInput(element, options) { this.init(element, options); }
-       UiSearchInput.prototype = {
-               /**
-                * Initializes the search input field.
-                * 
-                * @param       {Element}       element         target input[type="text"]
-                * @param       {Object}        options         search options and settings
-                */
-               init: function(element, options) {
-                       this._element = element;
-                       if (!(this._element instanceof Element)) {
-                               throw new TypeError("Expected a valid DOM element.");
-                       }
-                       else if (this._element.nodeName !== 'INPUT' || (this._element.type !== 'search' && this._element.type !== 'text')) {
-                               throw new Error('Expected an input[type="text"].');
-                       }
-                       
-                       this._activeItem = null;
-                       this._dropdownContainerId = '';
-                       this._lastValue = '';
-                       this._list = null;
-                       this._request = null;
-                       this._timerDelay = null;
-                       
-                       this._options = Core.extend({
-                               ajax: {
-                                       actionName: 'getSearchResultList',
-                                       className: '',
-                                       interfaceName: 'wcf\\data\\ISearchAction'
-                               },
-                               callbackDropdownInit: null,
-                               callbackSelect: null,
-                               delay: 500,
-                               minLength: 3,
-                               noResultPlaceholder: '',
-                               preventSubmit: false
-                       }, options);
-                       
-                       // disable auto-complete as it collides with the suggestion dropdown
-                       elAttr(this._element, 'autocomplete', 'off');
-                       
-                       this._element.addEventListener('keydown', this._keydown.bind(this));
-                       this._element.addEventListener('keyup', this._keyup.bind(this));
-               },
-               
-               /**
-                * Handles the 'keydown' event.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _keydown: function(event) {
-                       if ((this._activeItem !== null && UiSimpleDropdown.isOpen(this._dropdownContainerId)) || this._options.preventSubmit) {
-                               if (EventKey.Enter(event)) {
-                                       event.preventDefault();
-                               }
-                       }
-                       
-                       if (EventKey.ArrowUp(event) || EventKey.ArrowDown(event) || EventKey.Escape(event)) {
-                               event.preventDefault();
-                       }
-               },
-               
-               /**
-                * Handles the 'keyup' event, provides keyboard navigation and executes search queries.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _keyup: function(event) {
-                       // handle dropdown keyboard navigation
-                       if (this._activeItem !== null) {
-                               if (!UiSimpleDropdown.isOpen(this._dropdownContainerId)) {
-                                       return;
-                               }
-                               
-                               if (EventKey.ArrowUp(event)) {
-                                       event.preventDefault();
-                                       
-                                       return this._keyboardPreviousItem();
-                               }
-                               else if (EventKey.ArrowDown(event)) {
-                                       event.preventDefault();
-                                       
-                                       return this._keyboardNextItem();
-                               }
-                               else if (EventKey.Enter(event)) {
-                                       event.preventDefault();
-                                       
-                                       return this._keyboardSelectItem();
-                               }
-                       }
-                       
-                       // close list on escape
-                       if (EventKey.Escape(event)) {
-                               UiSimpleDropdown.close(this._dropdownContainerId);
-                               
-                               return;
-                       }
-                       
-                       var value = this._element.value.trim();
-                       if (this._lastValue === value) {
-                               // value did not change, e.g. previously it was "Test" and now it is "Test ",
-                               // but the trailing whitespace has been ignored
-                               return;
-                       }
-                       
-                       this._lastValue = value;
-                       
-                       if (value.length < this._options.minLength) {
-                               if (this._dropdownContainerId) {
-                                       UiSimpleDropdown.close(this._dropdownContainerId);
-                               }
-                               
-                               // value below threshold
-                               return;
-                       }
-                       
-                       if (this._options.delay) {
-                               if (this._timerDelay !== null) {
-                                       window.clearTimeout(this._timerDelay);
-                               }
-                               
-                               this._timerDelay = window.setTimeout((function() {
-                                       this._search(value);
-                               }).bind(this), this._options.delay);
-                       }
-                       else {
-                               this._search(value);
-                       }
-               },
-               
-               /**
-                * Queries the server with the provided search string.
-                * 
-                * @param       {string}        value   search string
-                * @protected
-                */
-               _search: function(value) {
-                       if (this._request) {
-                               this._request.abortPrevious();
-                       }
-                       
-                       this._request = Ajax.api(this, this._getParameters(value));
-               },
-               
-               /**
-                * Returns additional AJAX parameters.
-                * 
-                * @param       {string}        value   search string
-                * @return      {Object}        additional AJAX parameters
-                * @protected
-                */
-               _getParameters: function(value) {
-                       return {
-                               parameters: {
-                                       data: {
-                                               searchString: value
-                                       }
-                               }
-                       };
-               },
-               
-               /**
-                * Selects the next dropdown item.
-                * 
-                * @protected
-                */
-               _keyboardNextItem: function() {
-                       this._activeItem.classList.remove('active');
-                       
-                       if (this._activeItem.nextElementSibling) {
-                               this._activeItem = this._activeItem.nextElementSibling;
-                       }
-                       else {
-                               this._activeItem = this._list.children[0];
-                       }
-                       
-                       this._activeItem.classList.add('active');
-               },
-               
-               /**
-                * Selects the previous dropdown item.
-                * 
-                * @protected
-                */
-               _keyboardPreviousItem: function() {
-                       this._activeItem.classList.remove('active');
-                       
-                       if (this._activeItem.previousElementSibling) {
-                               this._activeItem = this._activeItem.previousElementSibling;
-                       }
-                       else {
-                               this._activeItem = this._list.children[this._list.childElementCount - 1];
-                       }
-                       
-                       this._activeItem.classList.add('active');
-               },
-               
-               /**
-                * Selects the active item from the dropdown.
-                * 
-                * @protected
-                */
-               _keyboardSelectItem: function() {
-                       this._selectItem(this._activeItem);
-               },
-               
-               /**
-                * Selects an item from the dropdown by clicking it.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _clickSelectItem: function(event) {
-                       this._selectItem(event.currentTarget);
-               },
-               
-               /**
-                * Selects an item.
-                * 
-                * @param       {Element}       item    selected item
-                * @protected
-                */
-               _selectItem: function(item) {
-                       if (this._options.callbackSelect && this._options.callbackSelect(item) === false) {
-                               this._element.value = '';
-                       }
-                       else {
-                               this._element.value = elData(item, 'label');
-                       }
-                       
-                       this._activeItem = null;
-                       UiSimpleDropdown.close(this._dropdownContainerId);
-               },
-               
-               /**
-                * Handles successful AJAX requests.
-                * 
-                * @param       {Object}        data    response data
-                * @protected
-                */
-               _ajaxSuccess: function(data) {
-                       var createdList = false;
-                       if (this._list === null) {
-                               this._list = elCreate('ul');
-                               this._list.className = 'dropdownMenu';
-                               
-                               createdList = true;
-                               
-                               if (typeof this._options.callbackDropdownInit === 'function') {
-                                       this._options.callbackDropdownInit(this._list);
-                               }
-                       }
-                       else {
-                               // reset current list
-                               this._list.innerHTML = '';
-                       }
-                       
-                       if (typeof data.returnValues === 'object') {
-                               var callbackClick = this._clickSelectItem.bind(this), listItem;
-                               
-                               for (var key in data.returnValues) {
-                                       if (data.returnValues.hasOwnProperty(key)) {
-                                               listItem = this._createListItem(data.returnValues[key]);
-                                               
-                                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
-                                               this._list.appendChild(listItem);
-                                       }
-                               }
-                       }
-                       
-                       if (createdList) {
-                               DomUtil.insertAfter(this._list, this._element);
-                               UiSimpleDropdown.initFragment(this._element.parentNode, this._list);
-                               
-                               this._dropdownContainerId = DomUtil.identify(this._element.parentNode);
-                       }
-                       
-                       if (this._dropdownContainerId) {
-                               this._activeItem = null;
-                               
-                               if (!this._list.childElementCount && this._handleEmptyResult() === false) {
-                                       UiSimpleDropdown.close(this._dropdownContainerId);
-                               }
-                               else {
-                                       UiSimpleDropdown.open(this._dropdownContainerId);
-                                       
-                                       // mark first item as active
-                                       if (this._list.childElementCount && ~~elData(this._list.children[0], 'object-id')) {
-                                               this._activeItem = this._list.children[0];
-                                               this._activeItem.classList.add('active');
-                                       }
-                               }
-                       }
-               },
-               
-               /**
-                * Handles an empty result set, return a boolean false to hide the dropdown.
-                * 
-                * @return      {boolean}      false to close the dropdown
-                * @protected
-                */
-               _handleEmptyResult: function() {
-                       if (!this._options.noResultPlaceholder) {
-                               return false;
-                       }
-                       
-                       var listItem = elCreate('li');
-                       listItem.className = 'dropdownText';
-                       
-                       var span = elCreate('span');
-                       span.textContent = this._options.noResultPlaceholder;
-                       listItem.appendChild(span);
-                       
-                       this._list.appendChild(listItem);
-                       
-                       return true;
-               },
-               
-               /**
-                * Creates an list item from response data.
-                * 
-                * @param       {Object}        item    response data
-                * @return      {Element}       list item
-                * @protected
-                */
-               _createListItem: function(item) {
-                       var listItem = elCreate('li');
-                       elData(listItem, 'object-id', item.objectID);
-                       elData(listItem, 'label', item.label);
-                       
-                       var span = elCreate('span');
-                       span.textContent = item.label;
-                       listItem.appendChild(span);
-                       
-                       return listItem;
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: this._options.ajax
-                       };
-               }
-       };
-       
-       return UiSearchInput;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Search/Page.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Search/Page.js
deleted file mode 100644 (file)
index 522352a..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-define(['Core', 'Dom/Util', 'Ui/SimpleDropdown', './Input'], function(Core, DomUtil, UiSimpleDropdown, UiSearchInput) {
-       "use strict";
-       
-       return {
-               init: function (objectType) {
-                       var searchInput = elById('pageHeaderSearchInput');
-                       
-                       new UiSearchInput(searchInput, {
-                               ajax: {
-                                       className: 'wcf\\data\\search\\keyword\\SearchKeywordAction'
-                               },
-                               callbackDropdownInit: function(dropdownMenu) {
-                                       dropdownMenu.classList.add('dropdownMenuPageSearch');
-                                       
-                                       elData(dropdownMenu, 'dropdown-alignment-horizontal', 'right');
-                                       
-                                       var minWidth = searchInput.clientWidth;
-                                       dropdownMenu.style.setProperty('min-width', minWidth + 'px', '');
-                                       
-                                       // calculate offset to ignore the width caused by the submit button
-                                       var parent = searchInput.parentNode;
-                                       var offsetRight = (DomUtil.offset(parent).left + parent.clientWidth) - (DomUtil.offset(searchInput).left + minWidth);
-                                       var offsetTop = DomUtil.styleAsInt(window.getComputedStyle(parent), 'padding-bottom');
-                                       dropdownMenu.style.setProperty('transform', 'translateX(-' + Math.ceil(offsetRight) + 'px) translateY(-' + offsetTop + 'px)', '');
-                               }
-                       });
-                       
-                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(DomUtil.identify(elBySel('.pageHeaderSearchType')));
-                       var callback = this._click.bind(this);
-                       elBySelAll('a[data-object-type]', dropdownMenu, function(link) {
-                               link.addEventListener(WCF_CLICK_EVENT, callback);
-                       });
-                       
-                       // trigger click on init
-                       var link = elBySel('a[data-object-type="' + objectType + '"]', dropdownMenu);
-                       Core.triggerEvent(link, WCF_CLICK_EVENT);
-               },
-               
-               _click: function(event) {
-                       event.preventDefault();
-                       
-                       var objectType = elData(event.currentTarget, 'object-type');
-                       
-                       var container = elById('pageHeaderSearchParameters');
-                       container.innerHTML = '';
-                       
-                       var parameters = elData(event.currentTarget, 'parameters');
-                       if (parameters) {
-                               parameters = JSON.parse(parameters);
-                       }
-                       else {
-                               parameters = {};
-                       }
-                       
-                       if (objectType) parameters['types[]'] = objectType;
-                       
-                       for (var key in parameters) {
-                               if (parameters.hasOwnProperty(key)) {
-                                       var input = elCreate('input');
-                                       input.type = 'hidden';
-                                       input.name = key;
-                                       input.value = parameters[key];
-                                       container.appendChild(input);
-                               }
-                       }
-                       
-                       // update label
-                       var button = elBySel('.pageHeaderSearchType > .button', elById('pageHeaderSearchInputContainer'));
-                       button.textContent = event.currentTarget.textContent;
-               }
-       };
-});
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 3aed6ef..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 = elById(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(WCF_CLICK_EVENT, 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: elData(item.children[0], 'object-id'), value: item.textContent });
-                       
-                       if (isEvent) {
-                               this._element.focus();
-                       }
-               },
-               
-               /**
-                * Performs a search for the input value unless it is below the threshold.
-                * 
-                * @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 = elCreate('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 = elCreate('a');
-                                       anchor.textContent = item.label;
-                                       elData(anchor, 'object-id', item.objectID);
-                                       anchor.addEventListener(WCF_CLICK_EVENT, this._select.bind(this));
-                                       
-                                       listItem = elCreate('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 d903722..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * Common interface for tab menu access.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 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', 'Ui/CloseOverlay', './TabMenu/Simple'], function(Dictionary, DomChangeListener, DomUtil, UiCloseOverlay, SimpleTabMenu) {
-       "use strict";
-       
-       var _activeList = null;
-       var _tabMenus = new Dictionary();
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/TabMenu
-        */
-       return {
-               /**
-                * Sets up tab menus and binds listeners.
-                */
-               setup: function() {
-                       this._init();
-                       this._selectErroneousTabs();
-                       
-                       DomChangeListener.add('WoltLab/WCF/Ui/TabMenu', this._init.bind(this));
-                       UiCloseOverlay.add('WoltLab/WCF/Ui/TabMenu', function() {
-                               if (_activeList) {
-                                       _activeList.classList.remove('active');
-                                       
-                                       _activeList = null;
-                               }
-                       });
-               },
-               
-               /**
-                * Initializes available tab menus.
-                */
-               _init: function() {
-                       var container, containerId, list, returnValue, tabMenu, tabMenus = elBySelAll('.tabMenuContainer:not(.staticTabMenuContainer)');
-                       for (var i = 0, length = tabMenus.length; i < length; i++) {
-                               container = tabMenus[i];
-                               containerId = DomUtil.identify(container);
-                               
-                               if (_tabMenus.has(containerId)) {
-                                       continue;
-                               }
-                               
-                               tabMenu = new SimpleTabMenu(container);
-                               if (tabMenu.validate()) {
-                                       returnValue = tabMenu.init();
-                                       
-                                       _tabMenus.set(containerId, tabMenu);
-                                       
-                                       if (returnValue instanceof Element) {
-                                               tabMenu = this.getTabMenu(returnValue.parentNode.id);
-                                               tabMenu.select(returnValue.id, null, true);
-                                       }
-                                       
-                                       list = elBySel('#' + containerId + ' > nav > ul');
-                                       (function(list) {
-                                               list.addEventListener(WCF_CLICK_EVENT, function(event) {
-                                                       event.preventDefault();
-                                                       event.stopPropagation();
-                                                       
-                                                       if (event.target === list) {
-                                                               list.classList.add('active');
-                                                               
-                                                               _activeList = list;
-                                                       }
-                                                       else {
-                                                               list.classList.remove('active');
-                                                               
-                                                               _activeList = null;
-                                                       }
-                                               });
-                                       })(list);
-                               }
-                       }
-               },
-               
-               /**
-                * 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 && elByClass('formError', container).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);
-               }
-       };
-});
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 7da8f77..0000000
+++ /dev/null
@@ -1,358 +0,0 @@
-/**
- * Simple tab menu implementation with a straight-forward logic.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/TabMenu/Simple
- */
-define(['Dictionary', 'EventHandler', 'Dom/Traverse', 'Dom/Util'], function(Dictionary, EventHandler, DomTraverse, DomUtil) {
-       "use strict";
-       
-       /**
-        * @param       {Element}       container       container element
-        * @constructor
-        */
-       function TabMenuSimple(container) {
-               this._container = container;
-               this._containers = new Dictionary();
-               this._isLegacy = null;
-               this._store = 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 = elByTag('li', nav);
-                       if (tabs.length === 0) {
-                               return false;
-                       }
-                       
-                       var container, containers = DomTraverse.childrenByTag(this._container, 'DIV'), name, i, length;
-                       for (i = 0, length = containers.length; i < length; i++) {
-                               container = containers[i];
-                               name = elData(container, 'name');
-                               
-                               if (!name) {
-                                       name = DomUtil.identify(container);
-                               }
-                               
-                               elData(container, 'name', name);
-                               this._containers.set(name, container);
-                       }
-                       
-                       var containerId = this._container.id, tab;
-                       for (i = 0, length = tabs.length; i < length; i++) {
-                               tab = tabs[i];
-                               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: '" + containerId + "') exists more than once.");
-                               }
-                               
-                               container = this._containers.get(name);
-                               if (container === undefined) {
-                                       throw new Error("Expected content element for li[data-name='" + name + "'] (tab menu id: '" + containerId + "').");
-                               }
-                               else if (container.parentNode !== this._container) {
-                                       throw new Error("Expected content element '" + name + "' (tab menu id: '" + 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: '" + containerId + "').");
-                               }
-                               
-                               this._tabs.set(name, tab);
-                       }
-                       
-                       if (!this._tabs.size) {
-                               throw new Error("Expected at least one tab (tab menu id: '" + containerId + "').");
-                       }
-                       
-                       if (this._isLegacy) {
-                               elData(this._container, 'is-legacy', true);
-                               
-                               this._tabs.forEach(function(tab, name) {
-                                       elAttr(tab, 'aria-controls', name);
-                               });
-                       }
-                       
-                       return true;
-               },
-               
-               /**
-                * Initializes this tab menu.
-                * 
-                * @param       {Dictionary=}   oldTabs         previous list of tabs
-                * @return      {?Element}      parent tab for selection or null
-                */
-               init: function(oldTabs) {
-                       oldTabs = oldTabs || null;
-                       
-                       // bind listeners
-                       this._tabs.forEach((function(tab) {
-                               if (!oldTabs || oldTabs.get(elData(tab, 'name')) !== tab) {
-                                       tab.children[0].addEventListener(WCF_CLICK_EVENT, this._onClick.bind(this));
-                               }
-                       }).bind(this));
-                       
-                       var returnValue = null;
-                       if (!oldTabs) {
-                               var hash = window.location.hash.replace(/^#/, ''), selectTab = null;
-                               if (hash !== '') {
-                                       selectTab = this._tabs.get(hash);
-                                       
-                                       // check for parent tab menu
-                                       if (selectTab && this._container.parentNode.classList.contains('tabMenuContainer')) {
-                                               returnValue = this._container;
-                                       }
-                               }
-                               
-                               if (!selectTab) {
-                                       var preselect = elData(this._container, 'preselect') || elData(this._container, 'active');
-                                       if (preselect === "true" || !preselect) preselect = true;
-                                       
-                                       if (preselect === true) {
-                                               this._tabs.forEach(function(tab) {
-                                                       if (!selectTab && !tab.previousElementSibling) {
-                                                               selectTab = tab;
-                                                       }
-                                               });
-                                       }
-                                       else if (preselect !== "false") {
-                                               selectTab = this._tabs.get(preselect);
-                                       }
-                               }
-                               
-                               if (selectTab) {
-                                       this._containers.forEach(function(container) {
-                                               container.classList.add('hidden');
-                                       });
-                                       
-                                       this.select(null, selectTab, true);
-                               }
-                               
-                               var store = elData(this._container, 'store');
-                               if (store) {
-                                       var input = elCreate('input');
-                                       input.type = 'hidden';
-                                       input.name = store;
-                                       
-                                       this._container.appendChild(input);
-                                       
-                                       this._store = input;
-                               }
-                       }
-                       
-                       return returnValue;
-               },
-               
-               /**
-                * Selects a tab.
-                * 
-                * @param       {?(string|int)}         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);
-                       
-                       if (!tab) {
-                               // 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) {
-                                       throw new Error("Expected a valid tab name, '" + name + "' given (tab menu id: '" + this._container.id + "').");
-                               }
-                       }
-                       
-                       name = name || elData(tab, 'name');
-                       
-                       // unmark active tab
-                       var oldTab = this.getActiveTab();
-                       var oldContent = null;
-                       if (oldTab) {
-                               if (elData(oldTab, 'name') === name) {
-                                       // same tab
-                                       return;
-                               }
-                               
-                               oldTab.classList.remove('active');
-                               oldContent = this._containers.get(elData(oldTab, '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');
-                       newContent.classList.remove('hidden');
-                       
-                       if (this._isLegacy) {
-                               tab.classList.add('ui-state-active');
-                               newContent.classList.add('ui-state-active');
-                       }
-                       
-                       if (this._store) {
-                               this._store.value = name;
-                       }
-                       
-                       if (!disableEvent) {
-                               EventHandler.fire('com.woltlab.wcf.simpleTabMenu_' + this._container.id, 'select', {
-                                       active: tab,
-                                       activeName: name,
-                                       previous: oldTab,
-                                       previousName: oldTab ? elData(oldTab, 'name') : null
-                               });
-                               
-                               var jQuery = (this._isLegacy && typeof window.jQuery === 'function') ? window.jQuery : null;
-                               if (jQuery) {
-                                       // simulate jQuery UI Tabs event
-                                       jQuery(this._container).trigger('wcftabsbeforeactivate', {
-                                               newTab: jQuery(tab),
-                                               oldTab: jQuery(oldTab),
-                                               newPanel: jQuery(newContent),
-                                               oldPanel: jQuery(oldContent)
-                                       });
-                               }
-                               
-                               // update history
-                               window.history.replaceState(
-                                       undefined,
-                                       undefined,
-                                       window.location.href.replace(/#[^#]+$/, '') + '#' + name
-                               );
-                       }
-               },
-               
-               /**
-                * 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 = new Dictionary();
-                       oldTabs.merge(this._tabs);
-                       
-                       this.validate();
-                       this.init(oldTabs);
-               },
-               
-               /**
-                * Handles clicks on a tab.
-                * 
-                * @param       {object}        event   event object
-                */
-               _onClick: function(event) {
-                       event.preventDefault();
-                       
-                       this.select(null, event.currentTarget.parentNode);
-               },
-               
-               /**
-                * Returns the tab name.
-                * 
-                * @param       {Element}       tab     tab element
-                * @return      {string}        tab name
-                */
-               _getTabName: function(tab) {
-                       var name = elData(tab, 'name');
-                       
-                       // handle legacy tab menus
-                       if (!name) {
-                               if (tab.childElementCount === 1 && tab.children[0].nodeName === 'A') {
-                                       if (tab.children[0].href.match(/#([^#]+)$/)) {
-                                               name = RegExp.$1;
-                                               
-                                               if (elById(name) === null) {
-                                                       name = null;
-                                               }
-                                               else {
-                                                       this._isLegacy = true;
-                                                       elData(tab, 'name', name);
-                                               }
-                                       }
-                               }
-                       }
-                       
-                       return name;
-               },
-               
-               /**
-                * Returns the currently active tab.
-                *
-                * @return      {Element}       active tab
-                */
-               getActiveTab: function() {
-                       return elBySel('#' + this._container.id + ' > nav > ul > li.active');
-               },
-               
-               /**
-                * 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/Toggle/Input.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Toggle/Input.js
deleted file mode 100644 (file)
index 16a1c1a..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * Provides a simple toggle to show or hide certain elements when the
- * target element is checked.
- * 
- * Be aware that the list of elements to show or hide accepts selectors
- * which will be passed to `elBySel()`, causing only the first matched
- * element to be used. If you require a whole list of elements identified
- * by a single selector to be handled, please provide the actual list of
- * elements instead.
- * 
- * Usage:
- * 
- * new UiToggleInput('input[name="foo"][value="bar"]', {
- *      show: ['#showThisContainer', '.makeThisVisibleToo'],
- *      hide: ['.notRelevantStuff', elById('fooBar')]
- * });
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/Toggle/Input
- */
-define(['Core'], function(Core) {
-       "use strict";
-       
-       /**
-        * @param       {string}        elementSelector         element selector used with `elBySel()`
-        * @param       {Object}        options                 toggle options
-        * @constructor
-        */
-       function UiToggleInput(elementSelector, options) { this.init(elementSelector, options); }
-       UiToggleInput.prototype = {
-               /**
-                * Initializes a new input toggle.
-                * 
-                * @param       {string}        elementSelector         element selector used with `elBySel()`
-                * @param       {Object}        options                 toggle options
-                */
-               init: function(elementSelector, options) {
-                       this._element = elBySel(elementSelector);
-                       if (this._element === null) {
-                               throw new Error("Unable to find element by selector '" + elementSelector + "'.");
-                       }
-                       
-                       var type = (this._element.nodeName === 'INPUT') ? elAttr(this._element, 'type') : '';
-                       if (type !== 'checkbox' && type !== 'radio') {
-                               throw new Error("Illegal element, expected input[type='checkbox'] or input[type='radio'].");
-                       }
-                       
-                       this._options = Core.extend({
-                               hide: [],
-                               show: []
-                       }, options);
-                       
-                       ['hide', 'show'].forEach((function(type) {
-                               var element, i, length;
-                               for (i = 0, length = this._options[type].length; i < length; i++) {
-                                       element = this._options[type][i];
-                                       
-                                       if (typeof element !== 'string' && !(element instanceof Element)) {
-                                               throw new TypeError("The array '" + type + "' may only contain string selectors or DOM elements.");
-                                       }
-                               }
-                       }).bind(this));
-                       
-                       this._element.addEventListener('change', this._change.bind(this));
-               },
-               
-               /**
-                * Triggered when element is checked / unchecked.
-                * 
-                * @param       {Event}         event   event object
-                * @protected
-                */
-               _change: function(event) {
-                       var showElements = event.currentTarget.checked;
-                       
-                       this._handleElements(this._options.show, showElements);
-                       this._handleElements(this._options.hide, !showElements);
-               },
-               
-               /**
-                * Loops through the target elements and shows / hides them.
-                * 
-                * @param       {Array}         elements        list of elements or selectors
-                * @param       {boolean}       showElement     true if elements should be shown
-                * @protected
-                */
-               _handleElements: function(elements, showElement) {
-                       var element, tmp;
-                       for (var i = 0, length = elements.length; i < length; i++) {
-                               element = elements[i];
-                               if (typeof element === 'string') {
-                                       tmp = elBySel(element);
-                                       if (tmp === null) {
-                                               throw new Error("Unable to find element by selector '" + element + "'.");
-                                       }
-                                       
-                                       elements[i] = element = tmp;
-                               }
-                               
-                               window[(showElement ? 'elShow' : 'elHide')](element);
-                       }
-               }
-       };
-       
-       return UiToggleInput;
-});
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 3a96674..0000000
+++ /dev/null
@@ -1,119 +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
-        */
-       return {
-               /**
-                * Initializes the tooltip element and binds event listener.
-                */
-               setup: function() {
-                       if (Environment.platform() !== 'desktop') return;
-                       
-                       _tooltip = elCreate('div');
-                       elAttr(_tooltip, 'id', 'balloonTooltip');
-                       _tooltip.classList.add('balloonTooltip');
-                       
-                       _text = elCreate('span');
-                       elAttr(_text, 'id', 'balloonTooltipText');
-                       _tooltip.appendChild(_text);
-                       
-                       _pointer = elCreate('span');
-                       _pointer.classList.add('elementPointer');
-                       _pointer.appendChild(elCreate('span'));
-                       _tooltip.appendChild(_pointer);
-                       
-                       document.body.appendChild(_tooltip);
-                       
-                       _elements = elByClass('jsTooltip');
-                       
-                       this.init();
-                       
-                       DomChangeListener.add('WoltLab/WCF/Ui/Tooltip', this.init.bind(this));
-                       window.addEventListener('scroll', this._mouseLeave.bind(this));
-               },
-               
-               /**
-                * Initializes tooltip elements.
-                */
-               init: function() {
-                       var element, title;
-                       while (_elements.length) {
-                               element = _elements[0];
-                               element.classList.remove('jsTooltip');
-                               
-                               title = elAttr(element, 'title').trim();
-                               if (title.length) {
-                                       elData(element, 'tooltip', title);
-                                       element.removeAttribute('title');
-                                       
-                                       element.addEventListener('mouseenter', this._mouseEnter.bind(this));
-                                       element.addEventListener('mouseleave', this._mouseLeave.bind(this));
-                                       element.addEventListener(WCF_CLICK_EVENT, this._mouseLeave.bind(this));
-                               }
-                       }
-               },
-               
-               /**
-                * Displays the tooltip on mouse enter.
-                * 
-                * @param       {Event}         event   event object
-                */
-               _mouseEnter: function(event) {
-                       var element = event.currentTarget;
-                       var title = elAttr(element, 'title');
-                       title = (typeof title === 'string') ? title.trim() : '';
-                       
-                       if (title !== '') {
-                               elData(element, 'tooltip', title);
-                               element.removeAttribute('title');
-                       }
-                       
-                       title = elData(element, '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',
-                               verticalOffset: 4,
-                               pointer: true,
-                               pointerClassNames: ['inverse'],
-                               vertical: 'top'
-                       });
-               },
-               
-               /**
-                * Hides the tooltip once the mouse leaves the element.
-                */
-               _mouseLeave: function() {
-                       _tooltip.classList.remove('active');
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Editor.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Editor.js
deleted file mode 100644 (file)
index 05c6d70..0000000
+++ /dev/null
@@ -1,228 +0,0 @@
-/**
- * Simple notification overlay.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/User/Editor
- */
-define(['Ajax', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', 'Ui/Notification'], function(Ajax, Language, StringUtil, DomUtil, UiDialog, UiNotification) {
-       "use strict";
-       
-       var _actionName = '';
-       var _userHeader = null;
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/User/Editor
-        */
-       return {
-               /**
-                * Initializes the user editor.
-                */
-               init: function() {
-                       _userHeader = elBySel('.userProfileUser');
-                       
-                       // init buttons
-                       ['ban', 'disableAvatar', 'disableSignature', 'enable'].forEach((function(action) {
-                               var button = elBySel('.userProfileButtonMenu .jsButtonUser' + StringUtil.ucfirst(action));
-                               
-                               // button is missing if users lacks the permission
-                               if (button) {
-                                       elData(button, 'action', action);
-                                       button.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
-                               }
-                       }).bind(this));
-               },
-               
-               /**
-                * Handles clicks on action buttons.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _click: function(event) {
-                       event.preventDefault();
-                       
-                       //noinspection JSCheckFunctionSignatures
-                       var action = elData(event.currentTarget, 'action');
-                       var actionName = '';
-                       switch (action) {
-                               case 'ban':
-                                       if (elDataBool(_userHeader, 'banned')) {
-                                               actionName = 'unban';
-                                       }
-                                       break;
-                               
-                               case 'disableAvatar':
-                                       if (elDataBool(_userHeader, 'disable-avatar')) {
-                                               actionName = 'enableAvatar';
-                                       }
-                                       break;
-                               
-                               case 'disableSignature':
-                                       if (elDataBool(_userHeader, 'disable-signature')) {
-                                               actionName = 'enableSignature';
-                                       }
-                                       break;
-                               
-                               case 'enable':
-                                       actionName = (elDataBool(_userHeader, 'is-disabled')) ? 'enable' : 'disable';
-                                       break;
-                       }
-                       
-                       if (actionName === '') {
-                               _actionName = action;
-                               
-                               UiDialog.open(this);
-                       }
-                       else {
-                               Ajax.api(this, {
-                                       actionName: actionName
-                               });
-                       }
-               },
-               
-               /**
-                * Handles form submit and input validation.
-                * 
-                * @param       {Event}         event           event object
-                * @protected
-                */
-               _submit: function(event) {
-                       event.preventDefault();
-                       
-                       var label = elById('wcfUiUserEditorExpiresLabel');
-                       var innerError = label.previousElementSibling;
-                       if (innerError.classList.contains('innerError')) elRemove(innerError);
-                       
-                       var expires = '';
-                       if (!elById('wcfUiUserEditorNeverExpires').checked) {
-                               expires = elById('wcfUiUserEditorExpiresDatePicker').value;
-                               if (expires === '') {
-                                       innerError = elCreate('small');
-                                       innerError.className = 'innerError';
-                                       innerError.textContent = Language.get('wcf.global.form.error.empty');
-                                       label.parentNode.insertBefore(innerError, label);
-                               }
-                       }
-                       
-                       var parameters = {};
-                       parameters[_actionName + 'Expires'] = expires;
-                       parameters[_actionName + 'Reason'] = elById('wcfUiUserEditorReason').value.trim();
-                       
-                       Ajax.api(this, {
-                               actionName: _actionName,
-                               parameters: parameters
-                       });
-               },
-               
-               _ajaxSuccess: function(data) {
-                       switch (data.actionName) {
-                               case 'ban':
-                               case 'unban':
-                                       elData(_userHeader, 'banned', (data.actionName === 'ban'));
-                                       elBySel('.userProfileButtonMenu .jsButtonUserBan').textContent = Language.get('wcf.user.' + (data.actionName === 'ban' ? 'unban' : 'ban'));
-                                       
-                                       var contentTitle = elBySel('.contentTitle', _userHeader);
-                                       var banIcon = elBySel('.jsUserBanned', contentTitle);
-                                       if (data.actionName === 'ban') {
-                                               banIcon = elCreate('span');
-                                               banIcon.className = 'icon icon16 fa-lock jsUserBanned jsTooltip';
-                                               banIcon.title = Language.get('wcf.user.banned');
-                                               contentTitle.appendChild(banIcon);
-                                       }
-                                       else if (banIcon) {
-                                               elRemove(banIcon);
-                                       }
-                                       
-                                       break;
-                               
-                               case 'disableAvatar':
-                               case 'enableAvatar':
-                                       elData(_userHeader, 'disable-avatar', (data.actionName === 'disableAvatar'));
-                                       elBySel('.userProfileButtonMenu .jsButtonUserDisableAvatar').textContent = Language.get('wcf.user.' + (data.actionName === 'disableAvatar' ? 'enable' : 'disable') + 'Avatar');
-                                       
-                                       break;
-                               
-                               case 'disableSignature':
-                               case 'enableSignature':
-                                       elData(_userHeader, 'disable-signature', (data.actionName === 'disableSignature'));
-                                       elBySel('.userProfileButtonMenu .jsButtonUserDisableSignature').textContent = Language.get('wcf.user.' + (data.actionName === 'disableSignature' ? 'enable' : 'disable') + 'Signature');
-                                       
-                                       break;
-                               
-                               case 'enable':
-                               case 'disable':
-                                       elData(_userHeader, 'is-disabled', (data.actionName === 'disable'));
-                                       elBySel('.userProfileButtonMenu .jsButtonUserEnable').textContent = Language.get('wcf.acp.user.' + (data.actionName === 'enable' ? 'disable' : 'enable'));
-                                       
-                                       break;
-                       }
-                       
-                       if (data.actionName === 'ban' || data.actionName === 'disableAvatar' || data.actionName === 'disableSignature') {
-                               UiDialog.close(this);
-                       }
-                       
-                       UiNotification.show();
-               },
-               
-               _ajaxSetup: function () {
-                       return {
-                               data: {
-                                       className: 'wcf\\data\\user\\UserAction',
-                                       objectIDs: [ elData(_userHeader, 'object-id') ]
-                               }
-                       };
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: 'wcfUiUserEditor',
-                               options: {
-                                       onSetup: (function (content) {
-                                               elById('wcfUiUserEditorNeverExpires').addEventListener('change', function () {
-                                                       window[(this.checked) ? 'elHide' : 'elShow'](elById('wcfUiUserEditorExpiresSettings'));
-                                               });
-                                               
-                                               elBySel('button.buttonPrimary', content).addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
-                                       }).bind(this),
-                                       onShow: function(content) {
-                                               UiDialog.setTitle('wcfUiUserEditor', Language.get('wcf.user.' + _actionName + '.confirmMessage'));
-                                               
-                                               var label = elById('wcfUiUserEditorReason').nextElementSibling;
-                                               var phrase = 'wcf.user.' + _actionName + '.reason.description';
-                                               label.textContent = Language.get(phrase);
-                                               window[(label.textContent === phrase) ? 'elHide' : 'elShow'](label);
-                                               
-                                               label = elById('wcfUiUserEditorNeverExpires').nextElementSibling;
-                                               label.textContent = Language.get('wcf.user.' + _actionName + '.neverExpires');
-                                               
-                                               label = elBySel('label[for="wcfUiUserEditorExpires"]', content);
-                                               label.textContent = Language.get('wcf.user.' + _actionName + '.expires');
-                                               
-                                               label = elById('wcfUiUserEditorExpiresLabel');
-                                               label.textContent = Language.get('wcf.user.' + _actionName + '.expires.description');
-                                       }
-                               },
-                               source: '<div class="section">'
-                                               + '<dl>'
-                                                       + '<dt><label for="wcfUiUserEditorReason">' + Language.get('wcf.global.reason') + '</label></dt>'
-                                                       + '<dd><textarea id="wcfUiUserEditorReason" cols="40" rows="3"></textarea><small></small></dd>'
-                                               + '</dl>'
-                                               + '<dl>'
-                                                       + '<dt></dt>'
-                                                       + '<dd><label><input type="checkbox" id="wcfUiUserEditorNeverExpires" checked> <span></span></label></dd>'
-                                               + '</dl>'
-                                               + '<dl id="wcfUiUserEditorExpiresSettings" style="display: none">'
-                                                       + '<dt><label for="wcfUiUserEditorExpires"></label></dt>'
-                                                       + '<dd>'
-                                                               + '<input type="date" name="wcfUiUserEditorExpires" id="wcfUiUserEditorExpires" class="medium" min="' + new Date(TIME_NOW * 1000).toISOString() + '" data-ignore-timezone="true">'
-                                                               + '<small id="wcfUiUserEditorExpiresLabel"></small>'
-                                                       + '</dd>'
-                                               +'</dl>'
-                                       + '</div>'
-                                       + '<div class="formSubmit"><button class="buttonPrimary">' + Language.get('wcf.global.button.submit') + '</button></div>'
-                       };
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Ignore.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Ignore.js
deleted file mode 100644 (file)
index da515c2..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * Provides global helper methods to interact with ignored content.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/User/Ignore
- */
-define(['List', 'Dom/ChangeListener'], function(List, DomChangeListener) {
-       "use strict";
-       
-       var _availableMessages = elByClass('ignoredUserMessage');
-       var _callback = null;
-       var _knownMessages = new List();
-       
-       /**
-        * @exports     WoltLab/WCF/Ui/User/Ignore
-        */
-       return {
-               /**
-                * Initializes the click handler for each ignored message and listens for
-                * newly inserted messages.
-                */
-               init: function () {
-                       _callback = this._removeClass.bind(this);
-                       
-                       this._rebuild();
-                       
-                       DomChangeListener.add('WoltLab/WCF/Ui/User/Ignore', this._rebuild.bind(this));
-               },
-               
-               /**
-                * Adds ignored messages to the collection.
-                * 
-                * @protected
-                */
-               _rebuild: function() {
-                       var message;
-                       for (var i = 0, length = _availableMessages.length; i < length; i++) {
-                               message = _availableMessages[i];
-                               
-                               if (!_knownMessages.has(message)) {
-                                       message.addEventListener(WCF_CLICK_EVENT, _callback);
-                                       
-                                       _knownMessages.add(message);
-                               }
-                       }
-               },
-               
-               /**
-                * Reveals a message on click/tap and disables the listener.
-                * 
-                * @param       {Event}         event   event object
-                * @protected
-                */
-               _removeClass: function(event) {
-                       event.preventDefault();
-                       
-                       var message = event.currentTarget;
-                       message.classList.remove('ignoredUserMessage');
-                       message.removeEventListener(WCF_CLICK_EVENT, _callback);
-                       _knownMessages.delete(message);
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/List.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/List.js
deleted file mode 100644 (file)
index 11c9150..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-/**
- * Object-based user list.
- * 
- * @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/User/List
- */
-define(['Ajax', 'Core', 'Dictionary', 'Dom/Util', 'Ui/Dialog', 'WoltLab/WCF/Ui/Pagination'], function(Ajax, Core, Dictionary, DomUtil, UiDialog, UiPagination) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function UiUserList(options) { this.init(options); }
-       UiUserList.prototype = {
-               /**
-                * Initializes the user list.
-                * 
-                * @param       {object}        options         list of initialization options
-                */
-               init: function(options) {
-                       this._cache = new Dictionary();
-                       this._pageCount = 0;
-                       this._pageNo = 1;
-                       
-                       this._options = Core.extend({
-                               className: '',
-                               dialogTitle: '',
-                               parameters: {}
-                       }, options);
-               },
-               
-               /**
-                * Opens the user list.
-                */
-               open: function() {
-                       this._pageNo = 1;
-                       this._showPage();
-               },
-               
-               /**
-                * Shows the current or given page.
-                * 
-                * @param       {int=}          pageNo          page number
-                */
-               _showPage: function(pageNo) {
-                       if (typeof pageNo === 'number') {
-                               this._pageNo = ~~pageNo;
-                       }
-                       
-                       if (this._pageCount !== 0 && (this._pageNo < 1 || this._pageNo > this._pageCount)) {
-                               throw new RangeError("pageNo must be between 1 and " + this._pageCount + " (" + this._pageNo + " given).");
-                       }
-                       
-                       if (this._cache.has(this._pageNo)) {
-                               var dialog = UiDialog.open(this, this._cache.get(this._pageNo));
-                               
-                               if (this._pageCount > 1) {
-                                       var element = elBySel('.jsPagination', dialog.content);
-                                       if (element !== null) {
-                                               new UiPagination(element, {
-                                                       activePage: this._pageNo,
-                                                       maxPage: this._pageCount,
-                                                       
-                                                       callbackSwitch: this._showPage.bind(this)
-                                               });
-                                       }
-                               }
-                       }
-                       else {
-                               this._options.parameters.pageNo = this._pageNo;
-                               
-                               Ajax.api(this, {
-                                       parameters: this._options.parameters
-                               });
-                       }
-               },
-               
-               _ajaxSuccess: function(data) {
-                       if (data.returnValues.pageCount !== undefined) {
-                               this._pageCount = ~~data.returnValues.pageCount;
-                       }
-                       
-                       this._cache.set(this._pageNo, data.returnValues.template);
-                       this._showPage();
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       actionName: 'getGroupedUserList',
-                                       className: this._options.className,
-                                       interfaceName: 'wcf\\data\\IGroupedUserListAction'
-                               }
-                       };
-               },
-               
-               _dialogSetup: function() {
-                       return {
-                               id: DomUtil.getUniqueId(),
-                               options: {
-                                       title: this._options.dialogTitle
-                               },
-                               source: null
-                       };
-               }
-       };
-       
-       return UiUserList;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Abstract.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Abstract.js
deleted file mode 100644 (file)
index 4cefd20..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-/**
- * Default implementation for user interaction menu items used in the user profile.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/User/Profile/Menu/Item/Abstract
- */
-define(['Ajax', 'Dom/Util'], function(Ajax, DomUtil) {
-       "use strict";
-       
-       /**
-        * Creates a new user profile menu item.
-        * 
-        * @param       {int}           userId          user id
-        * @param       {boolean}       isActive        true if item is initially active
-        * @constructor
-        */
-       function UiUserProfileMenuItemAbstract(userId, isActive) {}
-       UiUserProfileMenuItemAbstract.prototype = {
-               /**
-                * Creates a new user profile menu item.
-                * 
-                * @param       {int}           userId          user id
-                * @param       {boolean}       isActive        true if item is initially active
-                */
-               init: function(userId, isActive) {
-                       this._userId = userId;
-                       this._isActive = (isActive !== false);
-                       
-                       this._initButton();
-                       this._updateButton();
-               },
-               
-               /**
-                * Initializes the menu item.
-                * 
-                * @protected
-                */
-               _initButton: function() {
-                       var button = elCreate('a');
-                       button.href = '#';
-                       button.addEventListener(WCF_CLICK_EVENT, this._toggle.bind(this));
-                       
-                       var listItem = elCreate('li');
-                       listItem.appendChild(button);
-                       
-                       var menu = elBySel('.userProfileButtonMenu[data-menu="interaction"]');
-                       DomUtil.prepend(listItem, menu);
-                       
-                       this._button = button;
-                       this._listItem = listItem;
-               },
-               
-               /**
-                * Handles clicks on the menu item button.
-                * 
-                * @param       {Event}         event   event object
-                * @protected
-                */
-               _toggle: function(event) {
-                       event.preventDefault();
-                       
-                       Ajax.api(this, {
-                               actionName: this._getAjaxActionName(),
-                               parameters: {
-                                       data: {
-                                               userID: this._userId
-                                       }
-                               }
-                       });
-               },
-               
-               /**
-                * Updates the button state and label.
-                * 
-                * @protected
-                */
-               _updateButton: function() {
-                       this._button.textContent = this._getLabel();
-                       this._listItem.classList[(this._isActive ? 'add' : 'remove')]('active');
-               },
-               
-               /**
-                * Returns the button label.
-                * 
-                * @return      {string}        button label
-                * @protected
-                * @abstract
-                */
-               _getLabel: function() {
-                       throw new Error("Implement me!");
-               },
-               
-               /**
-                * Returns the Ajax action name.
-                * 
-                * @return      {string}        ajax action name
-                * @protected
-                * @abstract
-                */
-               _getAjaxActionName: function() {
-                       throw new Error("Implement me!");
-               },
-               
-               /**
-                * Handles successful Ajax requests.
-                * 
-                * @protected
-                * @abstract
-                */
-               _ajaxSuccess: function() {
-                       throw new Error("Implement me!");
-               },
-               
-               /**
-                * Returns the default Ajax request data
-                * 
-                * @return      {Object}        ajax request data
-                * @protected
-                * @abstract
-                */
-               _ajaxSetup: function() {
-                       throw new Error("Implement me!");
-               }
-       };
-       
-       return UiUserProfileMenuItemAbstract;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Follow.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Follow.js
deleted file mode 100644 (file)
index 67fa968..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-define(['Core', 'Language', 'Ui/Notification', './Abstract'], function(Core, Language, UiNotification, UiUserProfileMenuItemAbstract) {
-       "use strict";
-       
-       function UiUserProfileMenuItemFollow(userId, isActive) { this.init(userId, isActive); }
-       Core.inherit(UiUserProfileMenuItemFollow, UiUserProfileMenuItemAbstract, {
-               _getLabel: function() {
-                       return Language.get('wcf.user.button.' + (this._isActive ? 'un' : '') + 'follow');
-               },
-               
-               _getAjaxActionName: function() {
-                       return this._isActive ? 'unfollow' : 'follow';
-               },
-               
-               _ajaxSuccess: function(data) {
-                       this._isActive = (data.returnValues.following ? true : false);
-                       this._updateButton();
-                       
-                       UiNotification.show();
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: 'wcf\\data\\user\\follow\\UserFollowAction'
-                               }
-                       };
-               }
-       });
-       
-       return UiUserProfileMenuItemFollow;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Ignore.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Profile/Menu/Item/Ignore.js
deleted file mode 100644 (file)
index 92e5a13..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-define(['Core', 'Language', 'Ui/Notification', './Abstract'], function(Core, Language, UiNotification, UiUserProfileMenuItemAbstract) {
-       "use strict";
-       
-       function UiUserProfileMenuItemIgnore(userId, isActive) { this.init(userId, isActive); }
-       Core.inherit(UiUserProfileMenuItemIgnore, UiUserProfileMenuItemAbstract, {
-               _getLabel: function() {
-                       return Language.get('wcf.user.button.' + (this._isActive ? 'un' : '') + 'ignore');
-               },
-               
-               _getAjaxActionName: function() {
-                       return this._isActive ? 'unignore' : 'ignore';
-               },
-               
-               _ajaxSuccess: function(data) {
-                       this._isActive = (data.returnValues.isIgnoredUser ? true : false);
-                       this._updateButton();
-                       
-                       UiNotification.show();
-               },
-               
-               _ajaxSetup: function() {
-                       return {
-                               data: {
-                                       className: 'wcf\\data\\user\\ignore\\UserIgnoreAction'
-                               }
-                       };
-               }
-       });
-       
-       return UiUserProfileMenuItemIgnore;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Search/Input.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/User/Search/Input.js
deleted file mode 100644 (file)
index bf3550b..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Provides suggestions for users, optionally supporting groups.
- * 
- * @author     Alexander Ebert
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Ui/User/Search/Input
- * @see         module:WoltLab/WCF/Ui/Search/Input
- */
-define(['Core', 'WoltLab/WCF/Ui/Search/Input'], function(Core, UiSearchInput) {
-       "use strict";
-       
-       /**
-        * @param       {Element}       element         input element
-        * @param       {Object=}       options         search options and settings
-        * @constructor
-        */
-       function UiUserSearchInput(element, options) { this.init(element, options); }
-       Core.inherit(UiUserSearchInput, UiSearchInput, {
-               init: function(element, options) {
-                       var includeUserGroups = (Core.isPlainObject(options) && options.includeUserGroups === true);
-                       
-                       options = Core.extend({
-                               ajax: {
-                                       className: 'wcf\\data\\user\\UserAction',
-                                       parameters: {
-                                               data: {
-                                                       includeUserGroups: (includeUserGroups ? 1 : 0)
-                                               }
-                                       }
-                               }
-                       }, options);
-                       
-                       UiUserSearchInput._super.prototype.init.call(this, element, options);
-               },
-               
-               _createListItem: function(item) {
-                       var listItem = UiUserSearchInput._super.prototype._createListItem.call(this, item);
-                       elData(listItem, 'type', item.type);
-                       
-                       var box = elCreate('div');
-                       box.className = 'box16';
-                       box.innerHTML = (item.type === 'group') ? '<span class="icon icon16 fa-users"></span>' : item.icon;
-                       box.appendChild(listItem.children[0]);
-                       listItem.appendChild(box);
-                       
-                       return listItem;
-               }
-       });
-       
-       return UiUserSearchInput;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Upload.js b/wcfsetup/install/files/js/WoltLab/WCF/Upload.js
deleted file mode 100644 (file)
index 55a3bd7..0000000
+++ /dev/null
@@ -1,341 +0,0 @@
-/**
- * Uploads file via AJAX.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2015 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/Upload
- */
-define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse'], function(AjaxRequest, Core, DomChangeListener, Language, DomUtil, DomTraverse) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function Upload(buttonContainerId, targetId, options) {
-               options = options || {};
-               
-               if (options.className === undefined) {
-                       throw new Error("Missing class name.");
-               }
-               
-               // set default options
-               this._options = Core.extend({
-                       // name of the PHP action
-                       action: 'upload',
-                       // is true if multiple files can be uploaded at once
-                       multiple: false,
-                       // name if the upload field
-                       name: '__files[]',
-                       // is true if every file from a multi-file selection is uploaded in its own request
-                       singleFileRequests: false,
-                       // url for uploading file
-                       url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN
-               }, options);
-               
-               this._options.url = WCF.convertLegacyURL(this._options.url);
-               
-               this._buttonContainer = elById(buttonContainerId);
-               if (this._buttonContainer === null) {
-                       throw new Error("Element id '" + buttonContainerId + "' is unknown.");
-               }
-               
-               this._target = elById(targetId);
-               if (targetId === null) {
-                       throw new Error("Element id '" + targetId + "' is unknown.");
-               }
-               if (options.multiple && this._target.nodeName !== 'UL' && this._target.nodeName !== 'OL') {
-                       throw new Error("Target element has to be list when allowing upload of multiple files.");
-               }
-               
-               this._fileElements = [];
-               this._internalFileId = 0;
-               
-               this._createButton();
-       }
-       Upload.prototype = {
-               /**
-                * Creates the upload button.
-                */
-               _createButton: function() {
-                       this._fileUpload = elCreate('input');
-                       elAttr(this._fileUpload, 'type', 'file');
-                       elAttr(this._fileUpload, 'name', this._options.name);
-                       if (this._options.multiple) {
-                               elAttr(this._fileUpload, 'multiple', 'true');
-                       }
-                       this._fileUpload.addEventListener('change', this._upload.bind(this));
-                       
-                       this._button = elCreate('p');
-                       this._button.classList.add('button');
-                       this._button.classList.add('uploadButton');
-                       
-                       var span = elCreate('span');
-                       span.textContent = Language.get('wcf.global.button.upload');
-                       this._button.appendChild(span);
-                       
-                       DomUtil.prepend(this._fileUpload, this._button);
-                       
-                       this._insertButton();
-                       
-                       DomChangeListener.trigger();
-               },
-               
-               /**
-                * Creates the document element for an uploaded file.
-                * 
-                * @param       {File}          file            uploaded file
-                */
-               _createFileElement: function(file) {
-                       var progress = elCreate('progress');
-                       elAttr(progress, 'max', 100);
-                       
-                       if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
-                               var li = elCreate('li');
-                               li.innerText = file.name;
-                               li.appendChild(progress);
-                               
-                               this._target.appendChild(li);
-                               
-                               return li;
-                       }
-                       else {
-                               var p = elCreate('p');
-                               p.appendChild(progress);
-                               
-                               this._target.appendChild(p);
-                               
-                               return p;
-                       }
-               },
-               
-               /**
-                * Creates the document elements for uploaded files.
-                * 
-                * @param       {(FileList|Array.<File>)}       files           uploaded files
-                */
-               _createFileElements: function(files) {
-                       if (files.length) {
-                               var uploadId = this._fileElements.length;
-                               this._fileElements[uploadId] = [];
-                               
-                               for (var i = 0, length = files.length; i < length; i++) {
-                                       var file = files[i];
-                                       var fileElement = this._createFileElement(file);
-                                       
-                                       if (!fileElement.classList.contains('uploadFailed')) {
-                                               elData(fileElement, 'filename', file.name);
-                                               elData(fileElement, 'internal-file-id', this._internalFileId++);
-                                               this._fileElements[uploadId][i] = fileElement;
-                                       }
-                               }
-                               
-                               DomChangeListener.trigger();
-                               
-                               return uploadId;
-                       }
-                       
-                       return null;
-               },
-               
-               /**
-                * Handles a failed file upload.
-                * 
-                * @param       {int}                   uploadId        identifier of a file upload
-                * @param       {object<string, *>}     data            response data
-                * @param       {string}                responseText    response
-                * @param       {XMLHttpRequest}        xhr             request object
-                * @param       {object<string, *>}     requestOptions  options used to send AJAX request
-                * @return      {boolean}       true if the error message should be shown
-                */
-               _failure: function(uploadId, data, responseText, xhr, requestOptions) {
-                       // does nothing
-                       return true;
-               },
-               
-               /**
-                * Return additional parameters for upload requests.
-                * 
-                * @return      {object<string, *>}     additional parameters
-                */
-               _getParameters: function() {
-                       return {};
-               },
-               
-               /**
-                * Inserts the created button to upload files into the button container.
-                */
-               _insertButton: function() {
-                       DomUtil.prepend(this._button, this._buttonContainer);
-               },
-               
-               /**
-                * Updates the progress of an upload.
-                * 
-                * @param       {int}                           uploadId        internal upload identifier
-                * @param       {XMLHttpRequestProgressEvent}   event           progress event object
-                */
-               _progress: function(uploadId, event) {
-                       var percentComplete = Math.round(event.loaded / event.total * 100);
-                       
-                       for (var i in this._fileElements[uploadId]) {
-                               var progress = elByTag('PROGRESS', this._fileElements[uploadId][i]);
-                               if (progress.length === 1) {
-                                       elAttr(progress[0], 'value', percentComplete);
-                               }
-                       }
-               },
-               
-               /**
-                * Removes the button to upload files.
-                */
-               _removeButton: function() {
-                       elRemove(this._button);
-                       
-                       DomChangeListener.trigger();
-               },
-               
-               /**
-                * Handles a successful file upload.
-                * 
-                * @param       {int}                   uploadId        identifier of a file upload
-                * @param       {object<string, *>}     data            response data
-                * @param       {string}                responseText    response
-                * @param       {XMLHttpRequest}        xhr             request object
-                * @param       {object<string, *>}     requestOptions  options used to send AJAX request
-                */
-               _success: function(uploadId, data, responseText, xhr, requestOptions) {
-                       // does nothing
-               },
-               
-               /**
-                * File input change callback to upload files.
-                * 
-                * @param       {Event}         event           input change event object
-                * @param       {File}          file            uploaded file
-                * @param       {Blob}          blob            file blob
-                * @return      {(int|Array.<int>|null)}        identifier(s) for the uploaded files
-                */
-               _upload: function(event, file, blob) {
-                       // remove failed upload elements first
-                       var failedUploads = DomTraverse.childrenByClass(this._target, 'uploadFailed');
-                       for (var i = 0, length = failedUploads.length; i < length; i++) {
-                               elRemove(failedUploads[i]);
-                       }
-                       
-                       var uploadId = null;
-                       
-                       var files = [];
-                       if (file) {
-                               files.push(file);
-                       }
-                       else if (blob) {
-                               var fileExtension = '';
-                               switch (blob.type) {
-                                       case 'image/jpeg':
-                                               fileExtension = '.jpg';
-                                       break;
-                                       
-                                       case 'image/gif':
-                                               fileExtension = '.gif';
-                                       break;
-                                       
-                                       case 'image/png':
-                                               fileExtension = '.png';
-                                       break;
-                               }
-                               
-                               files.push({
-                                       name: 'pasted-from-clipboard' + fileExtension
-                               });
-                       }
-                       else {
-                               files = this._fileUpload.files;
-                       }
-                       
-                       if (files.length) {
-                               if (this._options.singleFileRequests) {
-                                       uploadId = [];
-                                       for (var i = 0, length = files.length; i < length; i++) {
-                                               uploadId.push(this._uploadFiles([ files[i] ], blob));
-                                       }
-                               }
-                               else {
-                                       uploadId = this._uploadFiles(files, blob);
-                               }
-                       }
-                       
-                       // re-create upload button to effectively reset the 'files'
-                       // property of the input element
-                       this._removeButton();
-                       this._createButton();
-                       
-                       return uploadId;
-               },
-               
-               /**
-                * Sends the request to upload files.
-                * 
-                * @param       {(FileList|Array.<File>)}       files           uploaded files
-                * @param       {Blob}                          blob            file blob
-                * @return      {(int|null)}    identifier for the uploaded files
-                */
-               _uploadFiles: function(files, blob) {
-                       var uploadId = this._createFileElements(files);
-                       
-                       // no more files left, abort
-                       if (!this._fileElements[uploadId].length) {
-                               return null;
-                       }
-                       
-                       var formData = new FormData();
-                       for (var i = 0, length = files.length; i < length; i++) {
-                               if (this._fileElements[uploadId][i]) {
-                                       var internalFileId = elData(this._fileElements[uploadId][i], 'internal-file-id');
-                                       
-                                       if (blob) {
-                                               formData.append('__files[' + internalFileId + ']', blob, files[i].name);
-                                       }
-                                       else {
-                                               formData.append('__files[' + internalFileId + ']', files[i]);
-                                       }
-                               }
-                       }
-                       
-                       formData.append('actionName', this._options.action);
-                       formData.append('className', this._options.className);
-                       formData.append('interfaceName', 'wcf\\data\\IUploadAction');
-                       
-                       // recursively append additional parameters to form data
-                       var appendFormData = function(parameters, prefix) {
-                               prefix = prefix || '';
-                               
-                               for (var name in parameters) {
-                                       if (typeof parameters[name] === 'object') {
-                                               appendFormData(parameters[name], prefix + '[' + name + ']');
-                                       }
-                                       else {
-                                               formData.append('parameters' + prefix + '[' + name + ']', parameters[name]);
-                                       }
-                               }
-                       };
-                       
-                       appendFormData(this._getParameters());
-                       
-                       var request = new AjaxRequest({
-                               data: formData,
-                               contentType: false,
-                               failure: this._failure.bind(this, uploadId),
-                               silent: true,
-                               success: this._success.bind(this, uploadId),
-                               uploadProgress: this._progress.bind(this, uploadId),
-                               url: this._options.url
-                       });
-                       request.sendRequest();
-                       
-                       return uploadId;
-               }
-       };
-       
-       return Upload;
-});
diff --git a/wcfsetup/install/files/js/WoltLab/WCF/User.js b/wcfsetup/install/files/js/WoltLab/WCF/User.js
deleted file mode 100644 (file)
index 17f1662..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Provides data of the active user.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2016 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLab/WCF/User
- */
-define([], function() {
-       "use strict";
-       
-       var _didInit = false;
-       
-       /**
-        * @exports     WoltLab/WCF/User
-        */
-       return {
-               /**
-                * Initializes the user object.
-                * 
-                * @param       {int}           userId          id of the user, `0` for guests
-                * @param       {string}        username        name of the user, empty for guests
-                */
-               init: function(userId, username) {
-                       if (_didInit) {
-                               throw new Error('User has already been initialized.');
-                       }
-                       
-                       // define non-writeable properties for userId and username
-                       Object.defineProperty(this, 'userId', {
-                               value: userId,
-                               writable: false
-                       });
-                       Object.defineProperty(this, 'username', {
-                               value: username,
-                               writable: false
-                       });
-                       
-                       _didInit = true;
-               }
-       };
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Bootstrap.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Bootstrap.js
new file mode 100644 (file)
index 0000000..76525f5
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Bootstraps WCF's JavaScript with additions for the ACP usage.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Bootstrap
+ */
+define(['Core', 'WoltLabSuite/Core/Bootstrap', './Ui/Page/Menu'], function(Core, Bootstrap, UiPageMenu) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Bootstrap
+        */
+       return {
+               /**
+                * Bootstraps general modules and frontend exclusive ones.
+                * 
+                * @param       {Object=}       options         bootstrap options
+                */
+               setup: function(options) {
+                       options = Core.extend({
+                               bootstrap: {
+                                       enableMobileMenu: true
+                               }
+                       }, options);
+                       
+                       Bootstrap.setup(options.bootstrap);
+                       UiPageMenu.init();
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/Add.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Article/Add.js
new file mode 100644 (file)
index 0000000..00f257c
--- /dev/null
@@ -0,0 +1,63 @@
+/**
+ * Provides the dialog overlay to add a new article.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Article/Add
+ */
+define(['Core', 'Language', 'Ui/Dialog'], function(Core, Language, UiDialog) {
+       "use strict";
+       
+       var _link;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Ui/Article/Add
+        */
+       return {
+               /**
+                * Initializes the article add handler.
+                * 
+                * @param       {string}        link    redirect URL
+                */
+               init: function(link) {
+                       _link = link;
+                       
+                       var buttons = elBySelAll('.jsButtonArticleAdd');
+                       for (var i = 0, length = buttons.length; i < length; i++) {
+                               buttons[i].addEventListener(WCF_CLICK_EVENT, this.openDialog.bind(this));
+                       }
+               },
+               
+               /**
+                * Opens the 'Add Article' dialog.
+                * 
+                * @param       {Event=}        event   event object
+                */
+               openDialog: function(event) {
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       UiDialog.open(this);
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'articleAddDialog',
+                               options: {
+                                       onSetup: function(content) {
+                                               elBySel('button', content).addEventListener(WCF_CLICK_EVENT, function(event) {
+                                                       event.preventDefault();
+                                                       
+                                                       var isMultilingual = elBySel('input[name="isMultilingual"]:checked', content).value;
+                                                       
+                                                       window.location = _link.replace(/{\$isMultilingual}/, isMultilingual);
+                                               });
+                                       },
+                                       title: Language.get('wcf.acp.article.add')
+                               }
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Add.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Add.js
new file mode 100644 (file)
index 0000000..6bbeda2
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Provides the dialog overlay to add a new box.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Box/Add
+ */
+define(['Core', 'Language', 'Ui/Dialog'], function(Core, Language, UiDialog) {
+       "use strict";
+       
+       var _link;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Ui/Box/Add
+        */
+       return {
+               /**
+                * Initializes the box add handler.
+                * 
+                * @param       {string}        link    redirect URL
+                */
+               init: function(link) {
+                       _link = link;
+                       
+                       var buttons = elBySelAll('.jsButtonBoxAdd');
+                       for (var i = 0, length = buttons.length; i < length; i++) {
+                               buttons[i].addEventListener(WCF_CLICK_EVENT, this.openDialog.bind(this));
+                       }
+               },
+               
+               /**
+                * Opens the 'Add Box' dialog.
+                * 
+                * @param       {Event=}        event   event object
+                */
+               openDialog: function(event) {
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       UiDialog.open(this);
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'boxAddDialog',
+                               options: {
+                                       onSetup: function(content) {
+                                               elBySel('button', content).addEventListener(WCF_CLICK_EVENT, function(event) {
+                                                       event.preventDefault();
+                                                       
+                                                       var boxType = elBySel('input[name="boxType"]:checked', content).value;
+                                                       var isMultilingual = 0;
+                                                       if (boxType !== 'system') isMultilingual = elBySel('input[name="isMultilingual"]:checked', content).value;
+                                                       
+                                                       window.location = _link.replace(/{\$boxType}/, boxType).replace(/{\$isMultilingual}/, isMultilingual);
+                                               });
+                                               
+                                               elBySelAll('input[type="radio"][name="boxType"]', content, function(element) {
+                                                       element.addEventListener('change', function(event) {
+                                                               elBySelAll('input[type="radio"][name="isMultilingual"]', content, function(element) {
+                                                                       element.disabled = (event.currentTarget.value === 'system');
+                                                               });
+                                                       });
+                                               });
+                                       },
+                                       title: Language.get('wcf.acp.box.add')
+                               }
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Controller/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Controller/Handler.js
new file mode 100644 (file)
index 0000000..f668a19
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * Provides the interface logic to add and edit menu items.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Box/Controller/Handler
+ */
+define(['Ajax', 'Dictionary'], function(Ajax, Dictionary) {
+       "use strict";
+       
+       var _boxControllerContainer = elById('boxControllerContainer');
+       var _boxController = elById('boxControllerID');
+       var _boxConditions = elById('boxConditions');
+       var _templates = new Dictionary();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Ui/Box/Controller/Handler
+        */
+       return {
+               init: function(initialObjectTypeId) {
+                       _boxController.addEventListener('change', this._updateConditions.bind(this));
+                       
+                       if (initialObjectTypeId) {
+                               _templates.set(~~initialObjectTypeId, _boxConditions.innerHTML);
+                       }
+                       
+                       elShow(_boxControllerContainer);
+                       
+                       this._updateConditions();
+               },
+               
+               /**
+                * Sets up ajax request object.
+                *
+                * @return      {object}        request options
+                */
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'getBoxConditionsTemplate',
+                                       className: 'wcf\\data\\box\\BoxAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                *
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       _templates.set(~~data.returnValues.objectTypeID, data.returnValues.template);
+                       
+                       _boxConditions.innerHTML = data.returnValues.template;
+               },
+               
+               /**
+                * Updates the displayed box conditions based on the selected dynamic box controller.
+                * 
+                * @protected
+                */
+               _updateConditions: function() {
+                       var objectTypeId = ~~_boxController.value;
+                       
+                       if (_templates.has(objectTypeId)) {
+                               if (_templates.get(objectTypeId) !== null) {
+                                       _boxConditions.innerHTML = _templates.get(objectTypeId);
+                               }
+                       }
+                       else {
+                               _templates.set(objectTypeId, null);
+                               
+                               Ajax.api(this, {
+                                       parameters: {
+                                               objectTypeID: objectTypeId
+                                       }
+                               });
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Box/Handler.js
new file mode 100644 (file)
index 0000000..360d2c1
--- /dev/null
@@ -0,0 +1,160 @@
+/**
+ * Provides the interface logic to add and edit boxes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Box/Handler
+ */
+define(['Dictionary', 'WoltLabSuite/Core/Ui/Page/Search/Handler'], function(Dictionary, UiPageSearchHandler) {
+       "use strict";
+       
+       var _activePageId = 0;
+       var _boxController;
+       var _cache;
+       var _containerExternalLink;
+       var _containerPageID;
+       var _containerPageObjectId = null;
+       var _handlers;
+       var _pageId;
+       var _pageObjectId;
+       var _position;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Ui/Box/Handler
+        */
+       return {
+               /**
+                * Initializes the interface logic.
+                * 
+                * @param       {Dictionary}    handlers        list of handlers by page id supporting page object ids
+                */
+               init: function(handlers) {
+                       _handlers = handlers;
+                       
+                       _boxController = elById('boxControllerID');
+                       
+                       _containerPageID = elById('linkPageIDContainer');
+                       _containerExternalLink = elById('externalURLContainer');
+                       _containerPageObjectId = elById('linkPageObjectIDContainer');
+                       
+                       if (_handlers.size) {
+                               _pageId = elById('linkPageID');
+                               _pageId.addEventListener('change', this._togglePageId.bind(this));
+                               
+                               _pageObjectId = elById('linkPageObjectID');
+                               
+                               _cache = new Dictionary();
+                               _activePageId = ~~_pageId.value;
+                               if (_activePageId && _handlers.has(_activePageId)) {
+                                       _cache.set(_activePageId, ~~_pageObjectId.value);
+                               }
+                               
+                               elById('searchLinkPageObjectID').addEventListener(WCF_CLICK_EVENT, this._openSearch.bind(this));
+                               
+                               // toggle page object id container on init
+                               if (_handlers.has(~~_pageId.value)) {
+                                       elShow(_containerPageObjectId);
+                               }
+                       }
+                       
+                       elBySelAll('input[name="linkType"]', null, (function(input) {
+                               input.addEventListener('change', this._toggleLinkType.bind(this, input.value));
+                               
+                               if (input.checked) {
+                                       this._toggleLinkType(input.value);
+                               }
+                       }).bind(this));
+                       
+                       if (_boxController !== null) {
+                               _position = elById('position');
+                               _boxController.addEventListener('change', this._setAvailableBoxPositions.bind(this));
+                               
+                               // update positions on init
+                               this._setAvailableBoxPositions();
+                       }
+               },
+               
+               /**
+                * Toggles between the interface for internal and external links.
+                * 
+                * @param       {string}        value   selected option value
+                * @protected
+                */
+               _toggleLinkType: function(value) {
+                       if (value == 'none') {
+                               elHide(_containerPageID);
+                               elHide(_containerPageObjectId);
+                               elHide(_containerExternalLink);
+                       }
+                       if (value == 'internal') {
+                               elShow(_containerPageID);
+                               elHide(_containerExternalLink);
+                               if (_handlers.size) this._togglePageId();
+                       }
+                       if (value == 'external') {
+                               elHide(_containerPageID);
+                               elHide(_containerPageObjectId);
+                               elShow(_containerExternalLink);
+                       }
+               },
+               
+               /**
+                * Handles the changed page selection.
+                * 
+                * @protected
+                */
+               _togglePageId: function() {
+                       if (_handlers.has(_activePageId)) {
+                               _cache.set(_activePageId, ~~_pageObjectId.value);
+                       }
+                       
+                       _activePageId = ~~_pageId.value;
+                       
+                       // page w/o pageObjectID support, discard value
+                       if (!_handlers.has(_activePageId)) {
+                               _pageObjectId.value = '';
+                               
+                               elHide(_containerPageObjectId);
+                               
+                               return;
+                       }
+                               
+                       var newValue = ~~_cache.get(_activePageId);
+                       _pageObjectId.value = (newValue) ? newValue : '';
+                       
+                       elShow(_containerPageObjectId);
+               },
+               
+               /**
+                * Opens the handler lookup dialog.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _openSearch: function(event) {
+                       event.preventDefault();
+                       
+                       UiPageSearchHandler.open(_activePageId, _pageId.options[_pageId.selectedIndex].textContent.trim(), function(objectId) {
+                               _pageObjectId.value = objectId;
+                               _cache.set(_activePageId, objectId);
+                       });
+               },
+               
+               /**
+                * Updates the available box positions per box controller.
+                * 
+                * @protected
+                */
+               _setAvailableBoxPositions: function() {
+                       var supportedPositions = JSON.parse(elData(_boxController.options[_boxController.selectedIndex], 'supported-positions'));
+                       
+                       var option;
+                       for (var i = 0, length = _position.childElementCount; i < length; i++) {
+                               option = _position.children[i];
+                               
+                               option.disabled = (supportedPositions.indexOf(option.value) === -1);
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Media.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Media.js
new file mode 100644 (file)
index 0000000..9a23f57
--- /dev/null
@@ -0,0 +1,41 @@
+define(['WoltLabSuite/Core/Media/Manager/Editor'], function(MediaManagerEditor) {
+       "use strict";
+       
+       function AcpUiCodeMirrorMedia(elementId) { this.init(elementId); }
+       AcpUiCodeMirrorMedia.prototype = {
+               init: function(elementId) {
+                       this._element = elById(elementId);
+                       
+                       var button = elById('codemirror-' + elementId + '-media');
+                       button.classList.add(button.id);
+                       
+                       new MediaManagerEditor({
+                               buttonClass: button.id,
+                               callbackInsert: this._insert.bind(this),
+                               editor: null
+                       });
+               },
+               
+               _insert: function (mediaList, insertType, thumbnailSize) {
+                       var content = '';
+                       
+                       if (insertType === 'gallery') {
+                               var mediaIds = [];
+                               mediaList.forEach(function(item) {
+                                       mediaIds.push(item.mediaID);
+                               });
+                               
+                               content = '{{ mediaGallery="' + mediaIds.join(',') + '" }}';
+                       }
+                       else {
+                               mediaList.forEach(function(item) {
+                                       content += '{{ media="' + item.mediaID + '" size="' + thumbnailSize + '" }}';
+                               });
+                       }
+                       
+                       this._element.codemirror.replaceSelection(content);
+               }
+       };
+       
+       return AcpUiCodeMirrorMedia;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Page.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Page.js
new file mode 100644 (file)
index 0000000..4ead1e5
--- /dev/null
@@ -0,0 +1,24 @@
+define(['WoltLabSuite/Core/Ui/Page/Search'], function(UiPageSearch) {
+       "use strict";
+       
+       function AcpUiCodeMirrorPage(elementId) { this.init(elementId); }
+       AcpUiCodeMirrorPage.prototype = {
+               init: function(elementId) {
+                       this._element = elById(elementId);
+                       
+                       elById('codemirror-' + elementId + '-page').addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+               },
+               
+               _click: function (event) {
+                       event.preventDefault();
+                       
+                       UiPageSearch.open(this._insert.bind(this));
+               },
+               
+               _insert: function (pageID) {
+                       this._element.codemirror.replaceSelection('{{ page="' + pageID + '" }}');
+               }
+       };
+       
+       return AcpUiCodeMirrorPage;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Menu/Item/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Menu/Item/Handler.js
new file mode 100644 (file)
index 0000000..76e9051
--- /dev/null
@@ -0,0 +1,127 @@
+/**
+ * Provides the interface logic to add and edit menu items.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Menu/Item/Handler
+ */
+define(['Dictionary', 'WoltLabSuite/Core/Ui/Page/Search/Handler'], function(Dictionary, UiPageSearchHandler) {
+       "use strict";
+       
+       var _activePageId = 0;
+       var _cache;
+       var _containerExternalLink;
+       var _containerInternalLink;
+       var _containerPageObjectId = null;
+       var _handlers;
+       var _pageId;
+       var _pageObjectId;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Ui/Menu/Item/Handler
+        */
+       return {
+               /**
+                * Initializes the interface logic.
+                * 
+                * @param       {Dictionary}    handlers        list of handlers by page id supporting page object ids
+                */
+               init: function(handlers) {
+                       _handlers = handlers;
+                       
+                       _containerInternalLink = elById('pageIDContainer');
+                       _containerExternalLink = elById('externalURLContainer');
+                       _containerPageObjectId = elById('pageObjectIDContainer');
+                       
+                       if (_handlers.size) {
+                               _pageId = elById('pageID');
+                               _pageId.addEventListener('change', this._togglePageId.bind(this));
+                               
+                               _pageObjectId = elById('pageObjectID');
+                               
+                               _cache = new Dictionary();
+                               _activePageId = ~~_pageId.value;
+                               if (_activePageId && _handlers.has(_activePageId)) {
+                                       _cache.set(_activePageId, ~~_pageObjectId.value);
+                               }
+                               
+                               elById('searchPageObjectID').addEventListener(WCF_CLICK_EVENT, this._openSearch.bind(this));
+                               
+                               // toggle page object id container on init
+                               if (_handlers.has(~~_pageId.value)) {
+                                       elShow(_containerPageObjectId);
+                               }
+                       }
+                       
+                       elBySelAll('input[name="isInternalLink"]', null, (function(input) {
+                               input.addEventListener('change', this._toggleIsInternalLink.bind(this, input.value));
+                               
+                               if (input.checked) {
+                                       this._toggleIsInternalLink(input.value);
+                               }
+                       }).bind(this));
+               },
+               
+               /**
+                * Toggles between the interface for internal and external links.
+                * 
+                * @param       {string}        value   selected option value
+                * @protected
+                */
+               _toggleIsInternalLink: function(value) {
+                       if (~~value) {
+                               elShow(_containerInternalLink);
+                               elHide(_containerExternalLink);
+                               if (_handlers.size) this._togglePageId();
+                       }
+                       else {
+                               elHide(_containerInternalLink);
+                               elHide(_containerPageObjectId);
+                               elShow(_containerExternalLink);
+                       }
+               },
+               
+               /**
+                * Handles the changed page selection.
+                * 
+                * @protected
+                */
+               _togglePageId: function() {
+                       if (_handlers.has(_activePageId)) {
+                               _cache.set(_activePageId, ~~_pageObjectId.value);
+                       }
+                       
+                       _activePageId = ~~_pageId.value;
+                       
+                       // page w/o pageObjectID support, discard value
+                       if (!_handlers.has(_activePageId)) {
+                               _pageObjectId.value = '';
+                               
+                               elHide(_containerPageObjectId);
+                               
+                               return;
+                       }
+                               
+                       var newValue = ~~_cache.get(_activePageId);
+                       _pageObjectId.value = (newValue) ? newValue : '';
+                       
+                       elShow(_containerPageObjectId);
+               },
+               
+               /**
+                * Opens the handler lookup dialog.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _openSearch: function(event) {
+                       event.preventDefault();
+                       
+                       UiPageSearchHandler.open(_activePageId, _pageId.options[_pageId.selectedIndex].textContent.trim(), function(objectId) {
+                               _pageObjectId.value = objectId;
+                               _cache.set(_activePageId, objectId);
+                       });
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Page/Add.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Page/Add.js
new file mode 100644 (file)
index 0000000..89fd340
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Provides the dialog overlay to add a new page.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Page/Add
+ */
+define(['Core', 'Language', 'Ui/Dialog'], function(Core, Language, UiDialog) {
+       "use strict";
+       
+       var _languages, _link;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Ui/Page/Add
+        */
+       return {
+               /**
+                * Initializes the page add handler.
+                * 
+                * @param       {string}        link            redirect URL
+                * @param       {int}           languages       number of available languages
+                */
+               init: function(link, languages) {
+                       _languages = languages;
+                       _link = link;
+                       
+                       var buttons = elBySelAll('.jsButtonPageAdd');
+                       for (var i = 0, length = buttons.length; i < length; i++) {
+                               buttons[i].addEventListener(WCF_CLICK_EVENT, this.openDialog.bind(this));
+                       }
+               },
+               
+               /**
+                * Opens the 'Add Page' dialog.
+                * 
+                * @param       {Event=}        event   event object
+                */
+               openDialog: function(event) {
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       UiDialog.open(this);
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'pageAddDialog',
+                               options: {
+                                       onSetup: function(content) {
+                                               elBySel('button', content).addEventListener(WCF_CLICK_EVENT, function(event) {
+                                                       event.preventDefault();
+                                                       
+                                                       var pageType = elBySel('input[name="pageType"]:checked', content).value;
+                                                       var isMultilingual = (_languages > 1) ? elBySel('input[name="isMultilingual"]:checked', content).value : 0;
+                                                       
+                                                       window.location = _link.replace(/{\$pageType}/, pageType).replace(/{\$isMultilingual}/, isMultilingual);
+                                               });
+                                       },
+                                       title: Language.get('wcf.acp.page.add')
+                               }
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Page/Menu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Page/Menu.js
new file mode 100644 (file)
index 0000000..16d2750
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Provides the ACP menu navigation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Page/Menu
+ */
+define(['Dictionary'], function(Dictionary) {
+       "use strict";
+       
+       var _activeMenuItem = '';
+       var _menuItems = new Dictionary();
+       var _menuItemContainers = new Dictionary();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Acp/Ui/Page/Menu
+        */
+       return {
+               /**
+                * Initializes the ACP menu navigation.
+                */
+               init: function() {
+                       elBySelAll('.acpPageMenuLink', null, (function(link) {
+                               var menuItem = elData(link, 'menu-item');
+                               if (link.classList.contains('active')) {
+                                       _activeMenuItem = menuItem;
+                               }
+                               
+                               link.addEventListener(WCF_CLICK_EVENT, this._toggle.bind(this));
+                               
+                               _menuItems.set(menuItem, link);
+                       }).bind(this));
+                       
+                       elBySelAll('.acpPageSubMenuCategoryList', null, function(container) {
+                               _menuItemContainers.set(elData(container, 'menu-item'), container);
+                       });
+               },
+               
+               /**
+                * Toggles a menu item.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _toggle: function(event) {
+                       event.preventDefault();
+                       event.stopPropagation();
+                       
+                       var link = event.currentTarget;
+                       var menuItem = elData(link, 'menu-item');
+                       
+                       // remove active marking from currently active menu
+                       if (_activeMenuItem) {
+                               _menuItems.get(_activeMenuItem).classList.remove('active');
+                               _menuItemContainers.get(_activeMenuItem).classList.remove('active');
+                       }
+                       
+                       if (_activeMenuItem === menuItem) {
+                               // current item was active before
+                               _activeMenuItem = '';
+                       }
+                       else {
+                               link.classList.add('active');
+                               _menuItemContainers.get(menuItem).classList.add('active');
+                               
+                               _activeMenuItem = menuItem;
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Editor.js
new file mode 100644 (file)
index 0000000..5600a99
--- /dev/null
@@ -0,0 +1,269 @@
+/**
+ * Provides the style editor.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Style/Editor
+ */
+define(['Ajax', 'Dictionary', 'Dom/Util', 'EventHandler'], function(Ajax, Dictionary, DomUtil, EventHandler) {
+       "use strict";
+       
+       var _stylePreviewRegions = new Dictionary();
+       var _stylePreviewRegionMarker = null;
+       
+       /**
+        * @module      WoltLabSuite/Core/Acp/Ui/Style/Editor
+        */
+       var AcpUiStyleEditor = {
+               /**
+                * Sets up dynamic style options.
+                */
+               setup: function(options) {
+                       this._handleLayoutWidth();
+                       this._handleScss(options.isTainted);
+                       
+                       if (!options.isTainted) {
+                               this._handleProtection(options.styleId);
+                       }
+                       
+                       this._initVisualEditor(options.styleRuleMap);
+               },
+               
+               /**
+                * Handles the switch between static and fluid layout.
+                */
+               _handleLayoutWidth: function() {
+                       var useFluidLayout = elById('useFluidLayout');
+                       var fluidLayoutMinWidth = elById('fluidLayoutMinWidth');
+                       var fluidLayoutMaxWidth = elById('fluidLayoutMaxWidth');
+                       var fixedLayoutVariables = elById('fixedLayoutVariables');
+                       
+                       function change() {
+                               var checked = useFluidLayout.checked;
+                               
+                               fluidLayoutMinWidth.style[(checked ? 'remove' : 'set') + 'Property']('display', 'none');
+                               fluidLayoutMaxWidth.style[(checked ? 'remove' : 'set') + 'Property']('display', 'none');
+                               fixedLayoutVariables.style[(checked ? 'set' : 'remove') + 'Property']('display', 'none');
+                       }
+                       
+                       useFluidLayout.addEventListener('change', change);
+                       
+                       change();
+               },
+               
+               /**
+                * Handles SCSS input fields.
+                * 
+                * @param       {boolean}       isTainted       false if style is in protected mode
+                */
+               _handleScss: function(isTainted) {
+                       var individualScss = elById('individualScss');
+                       var overrideScss = elById('overrideScss');
+                       
+                       if (isTainted) {
+                               EventHandler.add('com.woltlab.wcf.simpleTabMenu_styleTabMenuContainer', 'select', function(data) {
+                                       individualScss.codemirror.refresh();
+                                       overrideScss.codemirror.refresh();
+                               });
+                       }
+                       else {
+                               EventHandler.add('com.woltlab.wcf.simpleTabMenu_advanced', 'select', function(data) {
+                                       if (data.activeName === 'advanced-custom') {
+                                               elById('individualScssCustom').codemirror.refresh();
+                                               elById('overrideScssCustom').codemirror.refresh();
+                                       }
+                                       else if (data.activeName === 'advanced-original') {
+                                               individualScss.codemirror.refresh();
+                                               overrideScss.codemirror.refresh();
+                                       }
+                               });
+                       }
+               },
+               
+               _handleProtection: function(styleId) {
+                       var button = elById('styleDisableProtectionSubmit');
+                       var checkbox = elById('styleDisableProtectionConfirm');
+                       
+                       checkbox.addEventListener('change', function() {
+                               button.disabled = !checkbox.checked;
+                       });
+                       
+                       button.addEventListener(WCF_CLICK_EVENT, function() {
+                               Ajax.apiOnce({
+                                       data: {
+                                               actionName: 'markAsTainted',
+                                               className: 'wcf\\data\\style\\StyleAction',
+                                               objectIDs: [styleId]
+                                       },
+                                       success: function() {
+                                               window.location.reload();
+                                       }
+                               });
+                       });
+               },
+               
+               _initVisualEditor: function(styleRuleMap) {
+                       var regions = elBySelAll('#spWindow [data-region]');
+                       for (var i = 0, length = regions.length; i < length; i++) {
+                               _stylePreviewRegions.set(elData(regions[i], 'region'), regions[i]);
+                       }
+                       
+                       _stylePreviewRegionMarker = elCreate('div');
+                       _stylePreviewRegionMarker.id = 'stylePreviewRegionMarker';
+                       _stylePreviewRegionMarker.innerHTML = '<div id="stylePreviewRegionMarkerBottom"></div>';
+                       elHide(_stylePreviewRegionMarker);
+                       elById('colors').appendChild(_stylePreviewRegionMarker);
+                       
+                       var container = elById('spSidebar');
+                       var select = elById('spCategories');
+                       var lastValue = select.value;
+                       
+                       function updateRegionMarker() {
+                               if (lastValue === 'none') {
+                                       elHide(_stylePreviewRegionMarker);
+                                       updateWrapperPosition(null);
+                                       scrollToRegion(null);
+                                       return;
+                               }
+                               
+                               var region = _stylePreviewRegions.get(lastValue);
+                               var rect = region.getBoundingClientRect();
+                               
+                               var top = rect.top + window.scrollY;
+                               
+                               DomUtil.setStyles(_stylePreviewRegionMarker, {
+                                       height: (region.clientHeight + 20) + 'px',
+                                       left: (rect.left + document.body.scrollLeft - 10) + 'px',
+                                       top: (top - 10) + 'px',
+                                       width: (region.clientWidth + 20) + 'px'
+                               });
+                               
+                               elShow(_stylePreviewRegionMarker);
+                               
+                               updateWrapperPosition(region);
+                               scrollToRegion(top);
+                       }
+                       
+                       var variablesWrapper = elById('spVariablesWrapper');
+                       function updateWrapperPosition(region) {
+                               var fromTop = 0;
+                               if (region !== null) {
+                                       fromTop = (region.offsetTop - variablesWrapper.offsetTop) - 10;
+                                       
+                                       var styles = window.getComputedStyle(region);
+                                       if (styles.getPropertyValue('position') === 'absolute' || styles.getPropertyValue('position') === 'relative') {
+                                               fromTop += region.offsetParent.offsetTop;
+                                       }
+                               }
+                               
+                               if (fromTop <= 0) {
+                                       variablesWrapper.style.removeProperty('transform');
+                               }
+                               else {
+                                       // ensure that the wrapper does not exceed the bottom boundary
+                                       var maxHeight = variablesWrapper.parentNode.clientHeight;
+                                       var wrapperHeight = variablesWrapper.clientHeight;
+                                       if (wrapperHeight + fromTop > maxHeight) {
+                                               fromTop = maxHeight - wrapperHeight;
+                                       }
+                                       
+                                       variablesWrapper.style.setProperty('transform', 'translateY(' + fromTop + 'px)');
+                               }
+                       }
+                       
+                       var pageHeader = elById('pageHeader');
+                       function scrollToRegion(top) {
+                               if (top === null) {
+                                       top = variablesWrapper.offsetTop - 60;
+                               }
+                               else {
+                                       // use the region marker as an offset
+                                       top -= 60;
+                               }
+                               
+                               // account for sticky header
+                               top -= 60;
+                               
+                               window.scrollTo(0, top);
+                       }
+                       
+                       var selectContainer = elBySel('.spSidebarBox:first-child');
+                       var element;
+                       select.addEventListener('change', function() {
+                               element = elBySel('.spSidebarBox[data-category="' + lastValue + '"]', container);
+                               elHide(element);
+                               
+                               lastValue = select.value;
+                               element = elBySel('.spSidebarBox[data-category="' + lastValue + '"]', container);
+                               elShow(element);
+                               
+                               // set region marker
+                               updateRegionMarker();
+                               
+                               selectContainer.classList[(lastValue === 'none' ? 'remove' : 'add')]('pointer');
+                       });
+                       
+                       
+                       // apply CSS rules
+                       var style = elCreate('style');
+                       style.appendChild(document.createTextNode(''));
+                       elData(style, 'created-by', 'WoltLab/Acp/Ui/Style/Editor');
+                       document.head.appendChild(style);
+                       
+                       function updateCSSRule(identifier, value, isInit) {
+                               if (styleRuleMap[identifier] === undefined) {
+                                       console.debug("Unknown style identifier: " + identifier);
+                                       return;
+                               }
+                               
+                               var rule = styleRuleMap[identifier].replace(/VALUE/g, value + ' !important');
+                               if (!rule) {
+                                       console.debug("Invalid style rule for " + identifier);
+                                       return;
+                               }
+                               
+                               var rules = [];
+                               if (rule.indexOf('__COMBO_RULE__')) {
+                                       rules = rule.split('__COMBO_RULE__');
+                               }
+                               else {
+                                       rules = [rule];
+                               }
+                               
+                               for (var i = 0, length = rules.length; i < length; i++) {
+                                       try {
+                                               style.sheet.insertRule(rules[i], style.sheet.cssRules.length);
+                                       }
+                                       catch (e) {
+                                               // ignore errors for unknown placeholder selectors
+                                               if (!/[a-z]+\-placeholder/.test(rules[i])) {
+                                                       console.debug(e.message);
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       var elements = elByClass('styleVariableColor', variablesWrapper);
+                       [].forEach.call(elements, function(colorField) {
+                               var variableName = elData(colorField, 'store').replace(/_value$/, '');
+                               
+                               var observer = new MutationObserver(function(mutations) {
+                                       mutations.forEach(function(mutation) {
+                                               if (mutation.attributeName === 'style') {
+                                                       updateCSSRule(variableName, colorField.style.getPropertyValue('background-color'));
+                                               }
+                                       });
+                               });
+                               
+                               observer.observe(colorField, {
+                                       attributes: true
+                               });
+                               
+                               updateCSSRule(variableName, colorField.style.getPropertyValue('background-color'));
+                       });
+               }
+       };
+       
+       return AcpUiStyleEditor;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Image/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/Style/Image/Upload.js
new file mode 100644 (file)
index 0000000..5f829d1
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Handles uploading style preview images.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Acp/Ui/Style/Image/Upload
+ */
+define(['Core', 'Dom/Traverse', 'Language', 'Ui/Notification', 'Upload'], function(Core, DomTraverse, Language, UiNotification, Upload) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function AcpUiStyleImageUpload(styleId, tmpHash) {
+               this._styleId = ~~styleId;
+               this._tmpHash = tmpHash;
+               
+               Upload.call(this, 'uploadImage', 'styleImage', {
+                       className: 'wcf\\data\\style\\StyleAction'
+               });
+       }
+       Core.inherit(AcpUiStyleImageUpload, Upload, {
+               /**
+                * @see WoltLabSuite/Core/Upload#_createFileElement
+                */
+               _createFileElement: function(file) {
+                       return this._target;
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_getParameters
+                */
+               _getParameters: function() {
+                       return {
+                               styleId: this._styleId,
+                               tmpHash: this._tmpHash
+                       };
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_success
+                */
+               _success: function(uploadId, data) {
+                       var error = DomTraverse.childByClass(this._button.parentNode, 'innerError');
+                       if (data.returnValues.url) {
+                               elAttr(this._target, 'src', data.returnValues.url + '?timestamp=' + Date.now());
+                               
+                               if (error) {
+                                       elRemove(error);
+                               }
+                               
+                               UiNotification.show();
+                       }
+                       else if (data.returnValues.errorType) {
+                               if (!error) {
+                                       error = elCreate('small');
+                                       error.className = 'innerError';
+                                       
+                                       this._button.parentNode.appendChild(error);
+                               }
+                               
+                               error.textContent = Language.get('wcf.acp.style.image.error.' + data.returnValues.errorType);
+                       }
+               }
+       });
+       
+       return AcpUiStyleImageUpload;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax.js
new file mode 100644 (file)
index 0000000..4f38e2e
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Handles AJAX requests.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ajax
+ */
+define(['AjaxRequest', 'Core', 'ObjectMap'], function(AjaxRequest, Core, ObjectMap) {
+       "use strict";
+       
+       var _requests = new ObjectMap();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ajax
+        */
+       var Ajax = {
+               /**
+                * Shorthand function to perform a request against the WCF-API with overrides
+                * for success and failure callbacks.
+                * 
+                * @param       {object}                callbackObject  callback object
+                * @param       {object<string, *>=}    data            request data
+                * @param       {function=}             success         success callback
+                * @param       {function=}             failure         failure callback
+                * @return      {AjaxRequest}
+                */
+               api: function(callbackObject, data, success, failure) {
+                       if (typeof data !== 'object') data = {};
+                       
+                       var request = _requests.get(callbackObject);
+                       if (request === undefined) {
+                               if (typeof callbackObject._ajaxSetup !== 'function') {
+                                       throw new TypeError("Callback object must implement at least _ajaxSetup().");
+                               }
+                               
+                               var options = callbackObject._ajaxSetup();
+                               
+                               options.pinData = true;
+                               options.callbackObject = callbackObject;
+                               
+                               if (!options.url) options.url = 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN;
+                               
+                               request = new AjaxRequest(options);
+                               
+                               _requests.set(callbackObject, request);
+                       }
+                       
+                       var oldSuccess = null;
+                       var oldFailure = null;
+                       
+                       if (typeof success === 'function') {
+                               oldSuccess = request.getOption('success');
+                               request.setOption('success', success);
+                       }
+                       if (typeof failure === 'function') {
+                               oldFailure = request.getOption('failure');
+                               request.setOption('failure', failure);
+                       }
+                       
+                       request.setData(data);
+                       request.sendRequest();
+                       
+                       // restore callbacks
+                       if (oldSuccess !== null) request.setOption('success', oldSuccess);
+                       if (oldFailure !== null) request.setOption('failure', oldFailure);
+                       
+                       return request;
+               },
+               
+               /**
+                * Shorthand function to perform a single request against the WCF-API.
+                * 
+                * Please use `Ajax.api` if you're about to repeatedly send requests because this
+                * method will spawn an new and rather expensive `AjaxRequest` with each call.
+                *  
+                * @param       {object<string, *>}     options         request options
+                */
+               apiOnce: function(options) {
+                       // Fetch AjaxRequest, as it cannot be provided because of a circular dependency
+                       if (AjaxRequest === undefined) AjaxRequest = require('AjaxRequest');
+                       
+                       options.pinData = false;
+                       options.callbackObject = null;
+                       if (!options.url) options.url = 'index.php/AJAXProxy/?t=' + SECURITY_TOKEN;
+                       
+                       var request = new AjaxRequest(options);
+                       request.sendRequest();
+               }
+       };
+       
+       return Ajax;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Jsonp.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Jsonp.js
new file mode 100644 (file)
index 0000000..fac5c1a
--- /dev/null
@@ -0,0 +1,67 @@
+/**
+ * Provides a utility class to issue JSONP requests.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ajax/Jsonp
+ */
+define(['Core'], function(Core) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ajax/Jsonp
+        */
+       var AjaxJsonp = {
+               /**
+                * Issues a JSONP request.
+                * 
+                * @param       {string}                url             source URL, must not contain callback parameter
+                * @param       {function}              success         success callback
+                * @param       {function=}             failure         timeout callback
+                * @param       {object<string, *>=}    options         request options
+                */
+               send: function(url, success, failure, options) {
+                       url = (typeof url === 'string') ? url.trim() : '';
+                       if (url.length === 0) {
+                               throw new Error("Expected a non-empty string for parameter 'url'.");
+                       }
+                       
+                       if (typeof success !== 'function') {
+                               throw new TypeError("Expected a valid callback function for parameter 'success'.");
+                       }
+                       
+                       options = Core.extend({
+                               parameterName: 'callback',
+                               timeout: 10
+                       }, options || {});
+                       
+                       var callbackName = 'wcf_jsonp_' + Core.getUuid().replace(/-/g, '').substr(0, 8);
+                       
+                       var timeout = window.setTimeout(function() {
+                               window[callbackName] = function() {};
+                               
+                               if (typeof failure === 'function') {
+                                       failure();
+                               }
+                       }, (~~options.timeout || 10) * 1000);
+                       
+                       window[callbackName] = function() {
+                               window.clearTimeout(timeout);
+                               
+                               success.apply(null, arguments);
+                       };
+                       
+                       url += (url.indexOf('?') === -1) ? '?' : '&';
+                       url += options.parameterName + '=' + callbackName;
+                       
+                       var script = elCreate('script');
+                       script.async = true;
+                       elAttr(script, 'src', url);
+                       
+                       document.head.appendChild(script);
+               }
+       };
+       
+       return AjaxJsonp;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js
new file mode 100644 (file)
index 0000000..91c4380
--- /dev/null
@@ -0,0 +1,324 @@
+/**
+ * Versatile AJAX request handling.
+ * 
+ * In case you want to issue JSONP requests, please use `AjaxJsonp` instead.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ajax/Request
+ */
+define(['Core', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Dialog', 'WoltLabSuite/Core/Ajax/Status'], function(Core, Language, DomChangeListener, DomUtil, UiDialog, AjaxStatus) {
+       "use strict";
+       
+       var _didInit = false;
+       var _ignoreAllErrors = false;
+       
+       /**
+        * @constructor
+        */
+       function AjaxRequest(options) {
+               this._data = null;
+               this._options = {};
+               this._previousXhr = null;
+               this._xhr = null;
+               
+               this._init(options);
+       }
+       AjaxRequest.prototype = {
+               /**
+                * Initializes the request options.
+                * 
+                * @param       {Object}        options         request options
+                */
+               _init: function(options) {
+                       this._options = Core.extend({
+                               // request data
+                               data: {},
+                               contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
+                               responseType: 'application/json',
+                               type: 'POST',
+                               url: '',
+                               
+                               // behavior
+                               autoAbort: false,
+                               ignoreError: false,
+                               pinData: false,
+                               silent: false,
+                               
+                               // callbacks
+                               failure: null,
+                               finalize: null,
+                               success: null,
+                               progress: null,
+                               uploadProgress: null,
+                               
+                               callbackObject: null
+                       }, options);
+                       
+                       if (typeof options.callbackObject === 'object') {
+                               this._options.callbackObject = options.callbackObject;
+                       }
+                       
+                       this._options.url = Core.convertLegacyUrl(this._options.url);
+                       
+                       if (this._options.pinData) {
+                               this._data = Core.extend({}, this._options.data);
+                       }
+                       
+                       if (this._options.callbackObject !== null) {
+                               if (typeof this._options.callbackObject._ajaxFailure === 'function') this._options.failure = this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject);
+                               if (typeof this._options.callbackObject._ajaxFinalize === 'function') this._options.finalize = this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject);
+                               if (typeof this._options.callbackObject._ajaxSuccess === 'function') this._options.success = this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject);
+                               if (typeof this._options.callbackObject._ajaxProgress === 'function') this._options.progress = this._options.callbackObject._ajaxProgress.bind(this._options.callbackObject);
+                               if (typeof this._options.callbackObject._ajaxUploadProgress === 'function') this._options.uploadProgress = this._options.callbackObject._ajaxUploadProgress.bind(this._options.callbackObject);
+                       }
+                       
+                       if (_didInit === false) {
+                               _didInit = true;
+                               
+                               window.addEventListener('beforeunload', function() { _ignoreAllErrors = true; });
+                       }
+               },
+               
+               /**
+                * Dispatches a request, optionally aborting a currently active request.
+                * 
+                * @param       {boolean}       abortPrevious   abort currently active request
+                */
+               sendRequest: function(abortPrevious) {
+                       if (abortPrevious === true || this._options.autoAbort) {
+                               this.abortPrevious();
+                       }
+                       
+                       if (!this._options.silent) {
+                               AjaxStatus.show();
+                       }
+                       
+                       if (this._xhr instanceof XMLHttpRequest) {
+                               this._previousXhr = this._xhr;
+                       }
+                       
+                       this._xhr = new XMLHttpRequest();
+                       this._xhr.open(this._options.type, this._options.url, true);
+                       if (this._options.contentType) {
+                               this._xhr.setRequestHeader('Content-Type', this._options.contentType);
+                       }
+                       this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+                       
+                       var self = this;
+                       var options = Core.clone(this._options);
+                       this._xhr.onload = function() {
+                               if (this.readyState === XMLHttpRequest.DONE) {
+                                       if (this.status >= 200 && this.status < 300 || this.status === 304) {
+                                               if (options.responseType && this.getResponseHeader('Content-Type').indexOf(options.responseType) !== 0) {
+                                                       // request succeeded but invalid response type
+                                                       self._failure(this, options);
+                                               }
+                                               else {
+                                                       self._success(this, options);
+                                               }
+                                       }
+                                       else {
+                                               self._failure(this, options);
+                                       }
+                               }
+                       };
+                       this._xhr.onerror = function() {
+                               self._failure(this, options);
+                       };
+                       
+                       if (this._options.progress) {
+                               this._xhr.onprogress = this._options.progress;
+                       }
+                       if (this._options.uploadProgress) {
+                               this._xhr.upload.onprogress = this._options.uploadProgress;
+                       }
+                       
+                       if (this._options.type === 'POST') {
+                               var data = this._options.data;
+                               if (typeof data === 'object' && Core.getType(data) !== 'FormData') {
+                                       data = Core.serialize(data);
+                               }
+                               
+                               this._xhr.send(data);
+                       }
+                       else {
+                               this._xhr.send();
+                       }
+               },
+               
+               /**
+                * Aborts a previous request.
+                */
+               abortPrevious: function() {
+                       if (this._previousXhr === null) {
+                               return;
+                       }
+                       
+                       this._previousXhr.abort();
+                       this._previousXhr = null;
+                       
+                       if (!this._options.silent) {
+                               AjaxStatus.hide();
+                       }
+               },
+               
+               /**
+                * Sets a specific option.
+                * 
+                * @param       {string}        key     option name
+                * @param       {?}             value   option value
+                */
+               setOption: function(key, value) {
+                       this._options[key] = value;
+               },
+               
+               /**
+                * Returns an option by key or undefined.
+                * 
+                * @param       {string}        key     option name
+                * @return      {(*|null)}      option value or null
+                */
+               getOption: function(key) {
+                       if (objOwns(this._options, key)) {
+                               return this._options[key];
+                       }
+                       
+                       return null;
+               },
+               
+               /**
+                * Sets request data while honoring pinned data from setup callback.
+                * 
+                * @param       {Object}        data    request data
+                */
+               setData: function(data) {
+                       if (this._data !== null && Core.getType(data) !== 'FormData') {
+                               data = Core.extend(this._data, data);
+                       }
+                       
+                       this._options.data = data;
+               },
+               
+               /**
+                * Handles a successful request.
+                * 
+                * @param       {XMLHttpRequest}        xhr             request object
+                * @param       {Object}                options         request options
+                */
+               _success: function(xhr, options) {
+                       if (!options.silent) {
+                               AjaxStatus.hide();
+                       }
+                       
+                       if (typeof options.success === 'function') {
+                               var data = null;
+                               if (xhr.getResponseHeader('Content-Type') === 'application/json') {
+                                       try {
+                                               data = JSON.parse(xhr.responseText);
+                                       }
+                                       catch (e) {
+                                               // invalid JSON
+                                               this._failure(xhr, options);
+                                               
+                                               return;
+                                       }
+                                       
+                                       // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring
+                                       if (data && data.returnValues && data.returnValues.template !== undefined) {
+                                               data.returnValues.template = data.returnValues.template.trim();
+                                       }
+                               }
+                               
+                               options.success(data, xhr.responseText, xhr, options.data);
+                       }
+                       
+                       this._finalize(options);
+               },
+               
+               /**
+                * Handles failed requests, this can be both a successful request with
+                * a non-success status code or an entirely failed request.
+                * 
+                * @param       {XMLHttpRequest}        xhr             request object
+                * @param       {Object}                options         request options
+                */
+               _failure: function (xhr, options) {
+                       if (_ignoreAllErrors) {
+                               return;
+                       }
+                       
+                       if (!options.silent) {
+                               AjaxStatus.hide();
+                       }
+                       
+                       var data = null;
+                       try {
+                               data = JSON.parse(xhr.responseText);
+                       }
+                       catch (e) {}
+                       
+                       var showError = true;
+                       if (data !== null && typeof options.failure === 'function') {
+                               showError = options.failure(data, xhr.responseText, xhr, options.data);
+                       }
+                       
+                       if (options.ignoreError !== true && showError !== false) {
+                               var details = '';
+                               var message = '';
+                               
+                               if (data !== null) {
+                                       if (data.stacktrace) details = '<br /><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
+                                       else if (data.exceptionID) details = '<br /><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
+                                       
+                                       message = data.message;
+                               }
+                               else {
+                                       message = xhr.responseText;
+                               }
+                               
+                               if (!message || message === 'undefined') {
+                                       return;
+                               }
+                               
+                               var html = '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
+                               
+                               if (UiDialog === undefined) UiDialog = require('Ui/Dialog');
+                               UiDialog.openStatic(DomUtil.getUniqueId(), html, {
+                                       title: Language.get('wcf.global.error.title')
+                               });
+                       }
+                       
+                       this._finalize(options);
+               },
+               
+               /**
+                * Finalizes a request.
+                * 
+                * @param       {Object}        options         request options
+                */
+               _finalize: function(options) {
+                       if (typeof options.finalize === 'function') {
+                               options.finalize(this._xhr);
+                       }
+                       
+                       this._previousXhr = null;
+                       
+                       DomChangeListener.trigger();
+                       
+                       // fix anchor tags generated through WCF::getAnchor()
+                       var links = elBySelAll('a[href*="#"]');
+                       for (var i = 0, length = links.length; i < length; i++) {
+                               var link = links[i];
+                               var href = elAttr(link, 'href');
+                               if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) {
+                                       href = href.substr(href.indexOf('#'));
+                                       elAttr(link, 'href', document.location.toString().replace(/#.*/, '') + href);
+                               }
+                       }
+               }
+       };
+       
+       return AjaxRequest;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Status.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Status.js
new file mode 100644 (file)
index 0000000..5a0b4c2
--- /dev/null
@@ -0,0 +1,76 @@
+/**
+ * Provides the AJAX status overlay.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ajax/Status
+ */
+define(['Language'], function(Language) {
+       "use strict";
+       
+       var _activeRequests = 0;
+       var _overlay = null;
+       var _timeoutShow = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ajax/Status
+        */
+       var AjaxStatus = {
+               /**
+                * Initializes the status overlay on first usage.
+                */
+               _init: function() {
+                       _overlay = elCreate('div');
+                       _overlay.classList.add('spinner');
+                       
+                       var icon = elCreate('span');
+                       icon.className = 'icon icon48 fa-spinner';
+                       _overlay.appendChild(icon);
+                       
+                       var title = elCreate('span');
+                       title.textContent = Language.get('wcf.global.loading');
+                       _overlay.appendChild(title);
+                       
+                       document.body.appendChild(_overlay);
+               },
+               
+               /**
+                * Shows the loading overlay.
+                */
+               show: function() {
+                       if (_overlay === null) {
+                               this._init();
+                       }
+                       
+                       _activeRequests++;
+                       
+                       if (_timeoutShow === null) {
+                               _timeoutShow = window.setTimeout(function() {
+                                       if (_activeRequests) {
+                                               _overlay.classList.add('active');
+                                       }
+                                       
+                                       _timeoutShow = null;
+                               }, 250);
+                       }
+               },
+               
+               /**
+                * Hides the loading overlay.
+                */
+               hide: function() {
+                       _activeRequests--;
+                       
+                       if (_activeRequests === 0) {
+                               if (_timeoutShow !== null) {
+                                       window.clearTimeout(_timeoutShow);
+                               }
+                               
+                               _overlay.classList.remove('active');
+                       }
+               }
+       };
+       
+       return AjaxStatus;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/Collapsible.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/Collapsible.js
new file mode 100644 (file)
index 0000000..392deac
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Generic handler for collapsible bbcode boxes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Bbcode/Collapsible
+ */
+define([], function() {
+       "use strict";
+       
+       var _containers = elByClass('jsCollapsibleBbcode');
+       
+       /**
+        * @exports     WoltLabSuite/Core/Bbcode/Collapsible
+        */
+       var BbcodeCollapsible = {
+               observe: function() {
+                       var container, toggleButton;
+                       while (_containers.length) {
+                               container = _containers[0];
+                               container.classList.remove('jsCollapsibleBbcode');
+                               
+                               toggleButton = elBySel('.toggleButton');
+                               if (toggleButton === null) {
+                                       continue;
+                               }
+                               
+                               (function(container, toggleButton) {
+                                       var toggle = function() {
+                                               var expand = container.classList.contains('collapsed');
+                                               container.classList[expand ? 'remove' : 'add']('collapsed');
+                                               toggleButton.textContent = elData(toggleButton, 'title-' + (expand ? 'collapse' : 'expand'));
+                                       };
+                                       
+                                       toggleButton.addEventListener(WCF_CLICK_EVENT, toggle);
+                                       
+                                       // searching in a page causes Google Chrome to scroll
+                                       // the box if something inside it matches
+                                       // 
+                                       // expand the box in this case, to:
+                                       // a) Improve UX
+                                       // b) Hide an ugly misplaced "show all" button
+                                       container.addEventListener('scroll', toggle);
+                                       
+                                       // expand boxes that are initially scrolled
+                                       if (container.scrollTop !== 0) {
+                                               toggle();
+                                       }
+                               })(container, toggleButton);
+                       }
+               }
+       };
+       
+       return BbcodeCollapsible;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/FromHtml.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/FromHtml.js
new file mode 100644 (file)
index 0000000..e3386aa
--- /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     WoltLabSuite/Core/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      WoltLabSuite/Core/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 = elCreate('div');
+                       container.innerHTML = message;
+                       
+                       // convert line breaks
+                       var elements = elByTag('P', container);
+                       while (elements.length) elements[0].outerHTML = elements[0].innerHTML;
+                       
+                       elements = elByTag('BR', container);
+                       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 = elByTag('*', container);
+                       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 = elBySelAll(_sourceConverter[i].selector, container);
+                               
+                               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 = elCreate('var');
+                       elData(placeholder, '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);
+                                       
+                                       elRemove(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 = elByTag(converter.tagName, container);
+                       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 = elData(element, 'author');
+                       var link = elAttr(element, '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) ? ' ' : '') + elAttr(element, '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 = elData(element, '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 = elByTag('TD', element);
+                       while (elements.length) {
+                               elements[0].outerHTML = '[td]' + elements[0].innerHTML + '[/td]\n';
+                       }
+                       
+                       elements = elByTag('TR', element);
+                       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 = elData(element, 'filename').trim() || '';
+                       var highlighter = elData(element, 'highlighter');
+                       window.dtdesign = element;
+                       var list = DomTraverse.childByTag(element.children[0], 'OL');
+                       var lineNumber = ~~elAttr(list, '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/WoltLabSuite/Core/Bbcode/Parser.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/Parser.js
new file mode 100644 (file)
index 0000000..3490616
--- /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     WoltLabSuite/Core/Bbcode/Parser
+ */
+define([], function() {
+       "use strict";
+       
+       /**
+        * @module      WoltLabSuite/Core/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/WoltLabSuite/Core/Bbcode/ToHtml.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Bbcode/ToHtml.js
new file mode 100644 (file)
index 0000000..e2bb156
--- /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     WoltLabSuite/Core/Bbcode/ToHtml
+ */
+define(['Core', 'EventHandler', 'Language', 'StringUtil', 'WoltLabSuite/Core/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 objOwns(__REDACTOR_CODE_HIGHLIGHTERS, value);
+       }
+       
+       /**
+        * @module      WoltLabSuite/Core/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       {int}                   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       {int}           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       {int}           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 (!objOwns(_options.attachments.images, 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 width, 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       {int}           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       {int}           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       {int}           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       {int}           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       {int}           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 = (item.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       {int}           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 (objOwns(__REDACTOR_SMILIES, 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       {int}           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       {int}           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/WoltLabSuite/Core/Bootstrap.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
new file mode 100644 (file)
index 0000000..d0a5173
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Bootstraps WCF's JavaScript.
+ * It defines globals needed for backwards compatibility
+ * and runs modules that are needed on page load.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Bootstrap
+ */
+define(
+       [
+               'favico',                  'enquire',                'perfect-scrollbar',      'WoltLabSuite/Core/Date/Time/Relative',
+               'Ui/SimpleDropdown',       'WoltLabSuite/Core/Ui/Mobile',  'WoltLabSuite/Core/Ui/TabMenu', 'WoltLabSuite/Core/Ui/FlexibleMenu',
+               'Ui/Dialog',               'WoltLabSuite/Core/Ui/Tooltip', 'WoltLabSuite/Core/Language',   'WoltLabSuite/Core/Environment',
+               'WoltLabSuite/Core/Date/Picker', 'EventHandler',           'Core',                   'WoltLabSuite/Core/Ui/Page/JumpToTop'
+       ], 
+       function(
+                favico,                   enquire,                  perfectScrollbar,         DateTimeRelative,
+                UiSimpleDropdown,         UiMobile,                 UiTabMenu,                UiFlexibleMenu,
+                UiDialog,                 UiTooltip,                Language,                 Environment,
+                DatePicker,               EventHandler,             Core,                     UiPageJumpToTop
+       )
+{
+       "use strict";
+       
+       // perfectScrollbar does not need to be bound anywhere, it just has to be loaded for WCF.js
+       window.Favico = favico;
+       window.enquire = enquire;
+       // non strict equals by intent
+       if (window.WCF == null) window.WCF = { };
+       if (window.WCF.Language == null) window.WCF.Language = { };
+       window.WCF.Language.get = Language.get;
+       window.WCF.Language.add = Language.add;
+       window.WCF.Language.addObject = Language.addObject;
+       
+       // WCF.System.Event compatibility
+       window.__wcf_bc_eventHandler = EventHandler;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Bootstrap
+        */
+       return {
+               /**
+                * Initializes the core UI modifications and unblocks jQuery's ready event.
+                * 
+                * @param       {Object=}       options         initialization options
+                */
+               setup: function(options) {
+                       options = Core.extend({
+                               enableMobileMenu: true
+                       }, options);
+                       
+                       Environment.setup();
+                       
+                       DateTimeRelative.setup();
+                       DatePicker.init();
+                       
+                       UiSimpleDropdown.setup();
+                       UiMobile.setup({
+                               enableMobileMenu: options.enableMobileMenu
+                       });
+                       UiTabMenu.setup();
+                       //UiFlexibleMenu.setup();
+                       UiDialog.setup();
+                       UiTooltip.setup();
+                       
+                       new UiPageJumpToTop();
+                       
+                       // convert method=get into method=post
+                       var forms = elBySelAll('form[method=get]');
+                       for (var i = 0, length = forms.length; i < length; i++) {
+                               forms[i].setAttribute('method', 'post');
+                       }
+                       
+                       if (Environment.browser() === 'microsoft') {
+                               window.onbeforeunload = function() {
+                                       /* Prevent "Back navigation caching" (http://msdn.microsoft.com/en-us/library/ie/dn265017%28v=vs.85%29.aspx) */
+                               };
+                       }
+                       
+                       // DEBUG ONLY
+                       var interval = 0;
+                       interval = window.setInterval(function() {
+                               if (typeof window.jQuery === 'function') {
+                                       window.clearInterval(interval);
+                                       
+                                       window.jQuery.holdReady(false);
+                               }
+                       }, 20);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js
new file mode 100644 (file)
index 0000000..696fb98
--- /dev/null
@@ -0,0 +1,92 @@
+/**
+ * Bootstraps WCF's JavaScript with additions for the frontend usage.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/BootstrapFrontend
+ */
+define(
+       [
+               'Ajax',                           'WoltLabSuite/Core/Bootstrap',      'WoltLabSuite/Core/Controller/Style/Changer',
+               'WoltLabSuite/Core/Controller/Popover', 'WoltLabSuite/Core/Ui/User/Ignore'
+       ],
+       function(
+               Ajax,                              Bootstrap,                    ControllerStyleChanger,
+               ControllerPopover,                 UiUserIgnore
+       )
+{
+       "use strict";
+       
+       var queueInvocations = 0;
+       
+       /**
+        * @exports     WoltLabSuite/Core/BootstrapFrontend
+        */
+       return {
+               /**
+                * Bootstraps general modules and frontend exclusive ones.
+                * 
+                * @param       {object<string, *>}     options         bootstrap options
+                */
+               setup: function(options) {
+                       Bootstrap.setup();
+                       
+                       if (options.styleChanger) {
+                               ControllerStyleChanger.setup();
+                       }
+                       
+                       this._initUserPopover();
+                       this._invokeBackgroundQueue(options.backgroundQueue.url, options.backgroundQueue.force);
+                       
+                       UiUserIgnore.init();
+               },
+               
+               /**
+                * Initializes user profile popover.
+                */
+               _initUserPopover: function() {
+                       ControllerPopover.init({
+                               attributeName: 'data-user-id',
+                               className: 'userLink',
+                               identifier: 'com.woltlab.wcf.user',
+                               loadCallback: function(objectId, popover) {
+                                       var callback = function(data) {
+                                               popover.setContent('com.woltlab.wcf.user', objectId, data.returnValues.template);
+                                       };
+                                       
+                                       popover.ajaxApi({
+                                               actionName: 'getUserProfile',
+                                               className: 'wcf\\data\\user\\UserProfileAction',
+                                               objectIDs: [ objectId ]
+                                       }, callback, callback);
+                               }
+                       });
+               },
+               
+               /**
+                * Invokes the background queue roughly every 10th request.
+                * 
+                * @param       {string}        url     background queue url
+                * @param       {boolean}       force   whether execution should be forced
+                */
+               _invokeBackgroundQueue: function(url, force) {
+                       var again = this._invokeBackgroundQueue.bind(this, url, true);
+                       
+                       if (Math.random() < 0.1 || force) {
+                               // 'fire and forget' background queue perform task
+                               Ajax.apiOnce({
+                                       url: url,
+                                       ignoreError: true,
+                                       silent: true,
+                                       success: (function(data) {
+                                               queueInvocations++;
+                                               
+                                               // process up to 5 queue items per page load
+                                               if (data > 0 && queueInvocations < 5) setTimeout(again, 1000);
+                                       }).bind(this)
+                               });
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/CallbackList.js b/wcfsetup/install/files/js/WoltLabSuite/Core/CallbackList.js
new file mode 100644 (file)
index 0000000..eff0b9a
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Simple API to store and invoke multiple callbacks per identifier.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/CallbackList
+ */
+define(['Dictionary'], function(Dictionary) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function CallbackList() {
+               this._dictionary = new Dictionary();
+       }
+       CallbackList.prototype = {
+               /**
+                * Adds a callback for given identifier.
+                * 
+                * @param       {string}        identifier      arbitrary string to group and identify callbacks
+                * @param       {function}      callback        callback function
+                */
+               add: function(identifier, callback) {
+                       if (typeof callback !== 'function') {
+                               throw new TypeError("Expected a valid callback as second argument for identifier '" + identifier + "'.");
+                       }
+                       
+                       if (!this._dictionary.has(identifier)) {
+                               this._dictionary.set(identifier, []);
+                       }
+                       
+                       this._dictionary.get(identifier).push(callback);
+               },
+               
+               /**
+                * Removes all callbacks registered for given identifier
+                * 
+                * @param       {string}        identifier      arbitrary string to group and identify callbacks
+                */
+               remove: function(identifier) {
+                       this._dictionary['delete'](identifier);
+               },
+               
+               /**
+                * Invokes callback function on each registered callback.
+                * 
+                * @param       {string|null}           identifier      arbitrary string to group and identify callbacks.
+                *                                                      null is a wildcard to match every identifier
+                * @param       {function(function)}    callback        function called with the individual callback as parameter
+                */
+               forEach: function(identifier, callback) {
+                       if (identifier === null) {
+                               this._dictionary.forEach(function(callbacks, identifier) {
+                                       callbacks.forEach(callback);
+                               });
+                       }
+                       else {
+                               var callbacks = this._dictionary.get(identifier);
+                               if (callbacks !== undefined) {
+                                       callbacks.forEach(callback);
+                               }
+                       }
+               }
+       };
+       
+       return CallbackList;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/ColorUtil.js b/wcfsetup/install/files/js/WoltLabSuite/Core/ColorUtil.js
new file mode 100644 (file)
index 0000000..a6d5f8f
--- /dev/null
@@ -0,0 +1,63 @@
+define([], function() {
+       "use strict";
+       
+       var ColorUtil = {
+               /**
+                * Converts HEX into RGB.
+                *
+                * @param       string          hex     hex value as #ccc or #abc123
+                * @return      object          r-g-b values
+                */
+               hexToRgb: function(hex) {
+                       hex = hex.replace(/^#/, '');
+                       if (/^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)) {
+                               // only convert abc and abcdef
+                               hex = hex.split('');
+                               
+                               // parse shorthand #xyz
+                               if (hex.length === 3) {
+                                       return {
+                                               r: parseInt(hex[0] + '' + hex[0], 16),
+                                               g: parseInt(hex[1] + '' + hex[1], 16),
+                                               b: parseInt(hex[2] + '' + hex[2], 16)
+                                       };
+                               }
+                               else {
+                                       return {
+                                               r: parseInt(hex[0] + '' + hex[1], 16),
+                                               g: parseInt(hex[2] + '' + hex[3], 16),
+                                               b: parseInt(hex[4] + '' + hex[5], 16)
+                                       };
+                               }
+                       }
+                       
+                       return Number.NaN;
+               },
+               
+               /**
+                * Converts RGB into HEX.
+                *
+                * @see http://www.linuxtopia.org/online_books/javascript_guides/javascript_faq/rgbtohex.htm
+                * 
+                * @param       {(int|string)}  r       red or rgb(1, 2, 3) or rgba(1, 2, 3, .4)
+                * @param       {int}           g       green
+                * @param       {int}           b       blue
+                * @return      {string}        hex value #abc123
+                */
+               rgbToHex: function(r, g, b) {
+                       var charList = "0123456789ABCDEF";
+                       
+                       if (g === undefined) {
+                               if (r.match(/^rgba?\((\d+), ?(\d+), ?(d\+)(?:, ?[0-9.]+)?\)$/)) {
+                                       r = RegExp.$1;
+                                       g = RegExp.$2;
+                                       b = RegExp.$3;
+                               }
+                       }
+                       
+                       return (charList.charAt((r - r % 16) / 16) + '' + charList.charAt(r % 16)) + '' + (charList.charAt((g - g % 16) / 16) + '' + charList.charAt(g % 16)) + '' + (charList.charAt((b - b % 16) / 16) + '' + charList.charAt(b % 16));
+               }
+       };
+       
+       return ColorUtil;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Captcha.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Captcha.js
new file mode 100644 (file)
index 0000000..9793047
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Provides data of the active user.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Controller/Captcha
+ */
+define(['Dictionary'], function(Dictionary) {
+       "use strict";
+       
+       var _captchas = new Dictionary();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Controller/Captcha
+        */
+       return {
+               /**
+                * Registers a captcha with the given identifier and callback used to get captcha data.
+                * 
+                * @param       {string}        captchaId       captcha identifier
+                * @param       {function}      callback        callback to get captcha data
+                */
+               add: function(captchaId, callback) {
+                       if (_captchas.has(captchaId)) {
+                               throw new Error("Captcha with id '" + captchaId + "' is already registered.");
+                       }
+                       
+                       if (typeof callback !== 'function') {
+                               throw new TypeError("Expected a valid callback for parameter 'callback'.");
+                       }
+                       
+                       _captchas.set(captchaId, callback);
+               },
+               
+               /**
+                * Deletes the captcha with the given identifier.
+                * 
+                * @param       {string}        captchaId       identifier of the captcha to be deleted
+                */
+               'delete': function(captchaId) {
+                       if (!_captchas.has(captchaId)) {
+                               throw new Error("Unknown captcha with id '" + captchaId + "'.");
+                       }
+                       
+                       _captchas.delete(captchaId)();
+               },
+               
+               /**
+                * Returns true if a captcha with the given identifier exists.
+                * 
+                * @param       {string}        captchaId       captcha identifier
+                * @return      {boolean}
+                */
+               has: function(captchaId) {
+                       return _captchas.has(captchaId);
+               },
+               
+               /**
+                * Returns the data of the captcha with the given identifier.
+                * 
+                * @param       {string}        captchaId       captcha identifier
+                * @return      {Object}        captcha data
+                */
+               getData: function(captchaId) {
+                       if (!_captchas.has(captchaId)) {
+                               throw new Error("Unknown captcha with id '" + captchaId + "'.");
+                       }
+                       
+                       return _captchas.get(captchaId)();
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js
new file mode 100644 (file)
index 0000000..de7b951
--- /dev/null
@@ -0,0 +1,633 @@
+/**
+ * Clipboard API Handler.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Controller/Clipboard
+ */
+define(
+       [
+               'Ajax',         'Core',     'Dictionary',      'EventHandler',
+               'Language',     'List',     'ObjectMap',       'Dom/ChangeListener',
+               'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown',
+               'WoltLabSuite/Core/Ui/Page/Action'
+       ],
+       function(
+               Ajax,            Core,       Dictionary,        EventHandler,
+               Language,        List,       ObjectMap,         DomChangeListener,
+               DomTraverse,     DomUtil,    UiConfirmation,    UiSimpleDropdown,
+               UiPageAction
+       )
+{
+       "use strict";
+       
+       var _containers = new Dictionary();
+       var _editors = new Dictionary();
+       var _editorDropdowns = new Dictionary();
+       var _elements = elByClass('jsClipboardContainer');
+       var _itemData = new ObjectMap();
+       var _knownCheckboxes = new List();
+       var _options = {};
+       
+       var _callbackCheckbox = null;
+       var _callbackItem = null;
+       var _callbackUnmarkAll = null;
+       
+       var _addPageOverlayActiveClass = false;
+       
+       /**
+        * Clipboard API
+        * 
+        * @exports     WoltLabSuite/Core/Controller/Clipboard
+        */
+       return {
+               /**
+                * Initializes the clipboard API handler.
+                * 
+                * @param       {Object}        options         initialization options
+                */
+               setup: function(options) {
+                       if (!options.pageClassName) {
+                               throw new Error("Expected a non-empty string for parameter 'pageClassName'.");
+                       }
+                       
+                       if (_callbackCheckbox === null) {
+                               _callbackCheckbox = this._mark.bind(this);
+                               _callbackItem = this._executeAction.bind(this);
+                               _callbackUnmarkAll = this._unmarkAll.bind(this);
+                               
+                               _options = Core.extend({
+                                       hasMarkedItems: false,
+                                       pageClassNames: [options.pageClassName],
+                                       pageObjectId: 0
+                               }, options);
+                               
+                               delete _options.pageClassName;
+                       }
+                       else {
+                               if (options.pageObjectId) {
+                                       throw new Error("Cannot load secondary clipboard with page object id set.");
+                               }
+                               
+                               _options.pageClassNames.push(options.pageClassName);
+                       }
+                       
+                       this._initContainers();
+                       
+                       if (_options.hasMarkedItems && _elements.length) {
+                               this._loadMarkedItems();
+                       }
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Controller/Clipboard', this._initContainers.bind(this));
+               },
+               
+               /**
+                * Reloads the clipboard data.
+                */
+               reload: function() {
+                       if (_containers.size) {
+                               this._loadMarkedItems();
+                       }
+               },
+               
+               /**
+                * Initializes clipboard containers.
+                */
+               _initContainers: function() {
+                       for (var i = 0, length = _elements.length; i < length; i++) {
+                               var container = _elements[i];
+                               var containerId = DomUtil.identify(container);
+                               var containerData = _containers.get(containerId);
+                               
+                               if (containerData === undefined) {
+                                       var markAll = elBySel('.jsClipboardMarkAll', container);
+                                       if (markAll !== null) {
+                                               elData(markAll, 'container-id', containerId);
+                                               markAll.addEventListener(WCF_CLICK_EVENT, this._markAll.bind(this));
+                                       }
+                                       
+                                       containerData = {
+                                               checkboxes: elByClass('jsClipboardItem', container),
+                                               element: container,
+                                               markAll: markAll,
+                                               markedObjectIds: new List()
+                                       };
+                                       _containers.set(containerId, containerData);
+                               }
+                               
+                               for (var j = 0, innerLength = containerData.checkboxes.length; j < innerLength; j++) {
+                                       var checkbox = containerData.checkboxes[j];
+                                       
+                                       if (!_knownCheckboxes.has(checkbox)) {
+                                               elData(checkbox, 'container-id', containerId);
+                                               checkbox.addEventListener(WCF_CLICK_EVENT, _callbackCheckbox);
+                                               
+                                               _knownCheckboxes.add(checkbox);
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Loads marked items from clipboard.
+                */
+               _loadMarkedItems: function() {
+                       Ajax.api(this, {
+                               actionName: 'getMarkedItems',
+                               parameters: {
+                                       pageClassNames: _options.pageClassNames,
+                                       pageObjectID: _options.pageObjectId
+                               }
+                       });
+               },
+               
+               /**
+                * Marks or unmarks all visible items at once.
+                * 
+                * @param       {object}        event   event object
+                */
+               _markAll: function(event) {
+                       var checkbox = event.currentTarget;
+                       var isMarked = (checkbox.nodeName !== 'INPUT' || checkbox.checked);
+                       var objectIds = [];
+                       
+                       var containerId = elData(checkbox, 'container-id');
+                       var data = _containers.get(containerId);
+                       var type = elData(data.element, 'type');
+                       
+                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
+                               var item = data.checkboxes[i];
+                               var objectId = ~~elData(item, 'object-id');
+                               
+                               if (isMarked) {
+                                       if (!item.checked) {
+                                               item.checked = true;
+                                               
+                                               data.markedObjectIds.add(objectId);
+                                               objectIds.push(objectId);
+                                       }
+                               }
+                               else {
+                                       if (item.checked) {
+                                               item.checked = false;
+                                               
+                                               data.markedObjectIds['delete'](objectId);
+                                               objectIds.push(objectId);
+                                       }
+                               }
+                               
+                               var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
+                               if (clipboardObject !== null) {
+                                       clipboardObject.classList[(isMarked ? 'addClass' : 'removeClass')]('jsMarked');
+                               }
+                       }
+                       
+                       this._saveState(type, objectIds, isMarked);
+               },
+               
+               /**
+                * Marks or unmarks an individual item.
+                * 
+                * @param       {object}        event           event object
+                */
+               _mark: function(event) {
+                       var checkbox = event.currentTarget;
+                       var objectId = ~~elData(checkbox, 'object-id');
+                       var isMarked = checkbox.checked;
+                       var containerId = elData(checkbox, 'container-id');
+                       var data = _containers.get(containerId);
+                       var type = elData(data.element, 'type');
+                       
+                       var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
+                       data.markedObjectIds[(isMarked ? 'add' : 'delete')](objectId);
+                       clipboardObject.classList[(isMarked) ? 'add' : 'remove']('jsMarked');
+                       
+                       if (data.markAll !== null) {
+                               var markedAll = true;
+                               for (var i = 0, length = data.checkboxes.length; i < length; i++) {
+                                       if (!data.checkboxes[i].checked) {
+                                               markedAll = false;
+                                               
+                                               break;
+                                       }
+                               }
+                               
+                               data.markAll.checked = markedAll;
+                       }
+                       
+                       this._saveState(type, [ objectId ], isMarked);
+               },
+               
+               /**
+                * Saves the state for given item object ids.
+                * 
+                * @param       {string}        type            object type
+                * @param       {int[]}         objectIds       item object ids
+                * @param       {boolean}       isMarked        true if marked
+                */
+               _saveState: function(type, objectIds, isMarked) {
+                       Ajax.api(this, {
+                               actionName: (isMarked ? 'mark' : 'unmark'),
+                               parameters: {
+                                       pageClassNames: _options.pageClassNames,
+                                       pageObjectID: _options.pageObjectId,
+                                       objectIDs: objectIds,
+                                       objectType: type
+                               }
+                       });
+               },
+               
+               /**
+                * Executes an editor action.
+                * 
+                * @param       {object}        event           event object
+                */
+               _executeAction: function(event) {
+                       var listItem = event.currentTarget;
+                       var data = _itemData.get(listItem);
+                       
+                       if (data.url) {
+                               window.location.href = data.url;
+                               return;
+                       }
+                       
+                       var triggerEvent = function() {
+                               var type = elData(listItem, 'type');
+                               
+                               EventHandler.fire('com.woltlab.wcf.clipboard', type, {
+                                       data: data,
+                                       listItem: listItem,
+                                       responseData: null
+                               });
+                       };
+                       
+                       //noinspection JSUnresolvedVariable
+                       var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : '';
+                       var fireEvent = true;
+                       
+                       if (typeof data.parameters === 'object' && data.parameters.actionName && data.parameters.className) {
+                               if (data.parameters.actionName === 'unmarkAll' || Array.isArray(data.parameters.objectIDs)) {
+                                       if (confirmMessage.length) {
+                                               //noinspection JSUnresolvedVariable
+                                               var template = (typeof data.internalData.template === 'string') ? data.internalData.template : '';
+                                               
+                                               UiConfirmation.show({
+                                                       confirm: (function() {
+                                                               var formData = {};
+                                                               
+                                                               if (template.length) {
+                                                                       var items = elBySelAll('input, select, textarea', UiConfirmation.getContentElement());
+                                                                       for (var i = 0, length = items.length; i < length; i++) {
+                                                                               var item = items[i];
+                                                                               var name = elAttr(item, 'name');
+                                                                               
+                                                                               switch (item.nodeName) {
+                                                                                       case 'INPUT':
+                                                                                               if (item.checked) {
+                                                                                                       formData[name] = elAttr(item, 'value');
+                                                                                               }
+                                                                                               break;
+                                                                                       
+                                                                                       case 'SELECT':
+                                                                                               formData[name] = item.value;
+                                                                                               break;
+                                                                                       
+                                                                                       case 'TEXTAREA':
+                                                                                               formData[name] = item.value.trim();
+                                                                                               break;
+                                                                               }
+                                                                       }
+                                                               }
+                                                               
+                                                               //noinspection JSUnresolvedFunction
+                                                               this._executeProxyAction(listItem, data, formData);
+                                                       }).bind(this),
+                                                       message: confirmMessage,
+                                                       template: template
+                                               });
+                                       }
+                                       else {
+                                               this._executeProxyAction(listItem, data);
+                                       }
+                               }
+                       }
+                       else if (confirmMessage.length) {
+                               fireEvent = false;
+                               
+                               UiConfirmation.show({
+                                       confirm: triggerEvent,
+                                       message: confirmMessage
+                               });
+                       }
+                       
+                       if (fireEvent) {
+                               triggerEvent();
+                       }
+               },
+               
+               /**
+                * Forwards clipboard actions to an individual handler.
+                * 
+                * @param       {Element}       listItem        dropdown item element
+                * @param       {Object}        data            action data
+                * @param       {Object?}       formData        form data
+                */
+               _executeProxyAction: function(listItem, data, formData) {
+                       formData = formData || {};
+                       
+                       var objectIds = (data.parameters.actionName !== 'unmarkAll') ? data.parameters.objectIDs : [];
+                       var parameters = { data: formData };
+                       
+                       //noinspection JSUnresolvedVariable
+                       if (typeof data.internalData.parameters === 'object') {
+                               //noinspection JSUnresolvedVariable
+                               for (var key in data.internalData.parameters) {
+                                       //noinspection JSUnresolvedVariable
+                                       if (data.internalData.parameters.hasOwnProperty(key)) {
+                                               //noinspection JSUnresolvedVariable
+                                               parameters[key] = data.internalData.parameters[key];
+                                       }
+                               }
+                       }
+                       
+                       Ajax.api(this, {
+                               actionName: data.parameters.actionName,
+                               className: data.parameters.className,
+                               objectIDs: objectIds,
+                               parameters: parameters
+                       }, (function(responseData) {
+                               if (data.actionName !== 'unmarkAll') {
+                                       var type = elData(listItem, 'type');
+                                       
+                                       EventHandler.fire('com.woltlab.wcf.clipboard', type, {
+                                               data: data,
+                                               listItem: listItem,
+                                               responseData: responseData
+                                       });
+                               }
+                               
+                               this._loadMarkedItems();
+                       }).bind(this));
+               },
+               
+               /**
+                * Unmarks all clipboard items for an object type.
+                * 
+                * @param       {object}        event           event object
+                */
+               _unmarkAll: function(event) {
+                       var type = elData(event.currentTarget, 'type');
+                       
+                       Ajax.api(this, {
+                               actionName: 'unmarkAll',
+                               parameters: {
+                                       objectType: type
+                               }
+                       });
+               },
+               
+               /**
+                * Sets up ajax request object.
+                * 
+                * @return      {object}        request options
+                */
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       className: 'wcf\\data\\clipboard\\item\\ClipboardItemAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                * 
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       if (data.actionName === 'unmarkAll') {
+                               _containers.forEach((function(containerData) {
+                                       //noinspection JSUnresolvedVariable
+                                       if (elData(containerData.element, 'type') === data.returnValues.objectType) {
+                                               var clipboardObjects = elByClass('jsMarked', containerData.element);
+                                               while (clipboardObjects.length) {
+                                                       clipboardObjects[0].classList.remove('jsMarked');
+                                               }
+                                               
+                                               if (containerData.markAll !== null) {
+                                                       containerData.markAll.checked = false;
+                                               }
+                                               for (var i = 0, length = containerData.checkboxes.length; i < length; i++) {
+                                                       containerData.checkboxes[i].checked = false;
+                                               }
+                                               
+                                               //noinspection JSUnresolvedVariable
+                                               UiPageAction.remove('wcfClipboard-' + data.returnValues.objectType);
+                                       }
+                               }).bind(this));
+                               
+                               return;
+                       }
+                       
+                       _itemData = new ObjectMap();
+                       
+                       // rebuild markings
+                       _containers.forEach((function(containerData) {
+                               var typeName = elData(containerData.element, 'type');
+                               
+                               //noinspection JSUnresolvedVariable
+                               var objectIds = (data.returnValues.markedItems && data.returnValues.markedItems.hasOwnProperty(typeName)) ? data.returnValues.markedItems[typeName] : [];
+                               this._rebuildMarkings(containerData, objectIds);
+                       }).bind(this));
+                       
+                       var keepEditors = [], typeName;
+                       //noinspection JSUnresolvedVariable
+                       if (data.returnValues && data.returnValues.items) {
+                               //noinspection JSUnresolvedVariable
+                               for (typeName in data.returnValues.items) {
+                                       //noinspection JSUnresolvedVariable
+                                       if (data.returnValues.items.hasOwnProperty(typeName)) {
+                                               keepEditors.push(typeName);
+                                       }
+                               }
+                       }
+                       
+                       // clear editors
+                       _editors.forEach(function(editor, typeName) {
+                               if (keepEditors.indexOf(typeName) === -1) {
+                                       UiPageAction.remove('wcfClipboard-' + typeName);
+                                       
+                                       _editorDropdowns.get(typeName).innerHTML = '';
+                               }
+                       });
+                       
+                       // no items
+                       //noinspection JSUnresolvedVariable
+                       if (!data.returnValues || !data.returnValues.items) {
+                               return;
+                       }
+                       
+                       // rebuild editors
+                       var actionName, created, dropdown, editor, typeData;
+                       var divider, item, itemData, itemIndex, label, unmarkAll;
+                       //noinspection JSUnresolvedVariable
+                       for (typeName in data.returnValues.items) {
+                               //noinspection JSUnresolvedVariable
+                               if (!data.returnValues.items.hasOwnProperty(typeName)) {
+                                       continue;
+                               }
+                               
+                               //noinspection JSUnresolvedVariable
+                               typeData = data.returnValues.items[typeName];
+                               created = false;
+                               
+                               editor = _editors.get(typeName);
+                               dropdown = _editorDropdowns.get(typeName);
+                               if (editor === undefined) {
+                                       created = true;
+                                       
+                                       editor = elCreate('a');
+                                       editor.className = 'dropdownToggle';
+                                       editor.textContent = typeData.label;
+                                       
+                                       _editors.set(typeName, editor);
+                                       
+                                       dropdown = elCreate('ol');
+                                       dropdown.className = 'dropdownMenu';
+                                       
+                                       _editorDropdowns.set(typeName, dropdown);
+                               }
+                               else {
+                                       editor.textContent = typeData.label;
+                                       dropdown.innerHTML = '';
+                               }
+                               
+                               // create editor items
+                               for (itemIndex in typeData.items) {
+                                       if (!typeData.items.hasOwnProperty(itemIndex)) {
+                                               continue;
+                                       }
+                                       
+                                       itemData = typeData.items[itemIndex];
+                                       
+                                       item = elCreate('li');
+                                       label = elCreate('span');
+                                       label.textContent = itemData.label;
+                                       item.appendChild(label);
+                                       dropdown.appendChild(item);
+                                       
+                                       elData(item, 'type', typeName);
+                                       item.addEventListener(WCF_CLICK_EVENT, _callbackItem);
+                                       
+                                       _itemData.set(item, itemData);
+                               }
+                               
+                               divider = elCreate('li');
+                               divider.classList.add('dropdownDivider');
+                               dropdown.appendChild(divider);
+                               
+                               // add 'unmark all'
+                               unmarkAll = elCreate('li');
+                               elData(unmarkAll, 'type', typeName);
+                               label = elCreate('span');
+                               label.textContent = Language.get('wcf.clipboard.item.unmarkAll');
+                               unmarkAll.appendChild(label);
+                               unmarkAll.addEventListener(WCF_CLICK_EVENT, _callbackUnmarkAll);
+                               dropdown.appendChild(unmarkAll);
+                               
+                               if (keepEditors.indexOf(typeName) !== -1) {
+                                       actionName = 'wcfClipboard-' + typeName;
+                                       
+                                       if (UiPageAction.has(actionName)) {
+                                               UiPageAction.show(actionName);
+                                       }
+                                       else {
+                                               UiPageAction.add(actionName, editor);
+                                       }
+                               }
+                               
+                               if (created) {
+                                       editor.parentNode.classList.add('dropdown');
+                                       editor.parentNode.appendChild(dropdown);
+                                       UiSimpleDropdown.init(editor);
+                               }
+                       }
+               },
+               
+               /**
+                * Rebuilds the mark state for each item.
+                * 
+                * @param       {Object}        data            container data
+                * @param       {int[]}         objectIds       item object ids
+                */
+               _rebuildMarkings: function(data, objectIds) {
+                       var markAll = true;
+                       
+                       for (var i = 0, length = data.checkboxes.length; i < length; i++) {
+                               var checkbox = data.checkboxes[i];
+                               var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject');
+                               
+                               var isMarked = (objectIds.indexOf(~~elData(checkbox, 'object-id')) !== -1);
+                               if (!isMarked) markAll = false;
+                               
+                               checkbox.checked = isMarked;
+                               clipboardObject.classList[(isMarked ? 'add' : 'remove')]('jsMarked');
+                       }
+                       
+                       if (data.markAll !== null) {
+                               data.markAll.checked = markAll;
+                               
+                               var parent = data.markAll;
+                               while (parent = parent.parentNode) {
+                                       if (parent instanceof Element && parent.classList.contains('columnMark')) {
+                                               parent = parent.parentNode;
+                                               break;
+                                       }
+                               }
+                               
+                               if (parent) {
+                                       parent.classList[(markAll ? 'add' : 'remove')]('jsMarked');
+                               }
+                       }
+               },
+               
+               /**
+                * Hides the clipboard editor for the given object type.
+                * 
+                * @param       {string}        objectType
+                */
+               hideEditor: function(objectType) {
+                       UiPageAction.remove('wcfClipboard-' + objectType);
+                       
+                       if (_addPageOverlayActiveClass) {
+                               _addPageOverlayActiveClass = false;
+                               
+                               document.documentElement.classList.add('pageOverlayActive');
+                       }
+               },
+               
+               /**
+                * Shows the clipboard editor.
+                */
+               showEditor: function() {
+                       this._loadMarkedItems();
+                       
+                       if (document.documentElement.classList.contains('pageOverlayActive')) {
+                               document.documentElement.classList.remove('pageOverlayActive');
+                               
+                               _addPageOverlayActiveClass = true;
+                       }
+               },
+               
+               /**
+                * Unmarks the objects with given clipboard object type and ids.
+                * 
+                * @param       {string}        objectType
+                * @param       {int[]}         objectIds
+                */
+               unmark: function(objectType, objectIds) {
+                       this._saveState(objectType, objectIds, false);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Condition/Page/Dependence.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Condition/Page/Dependence.js
new file mode 100644 (file)
index 0000000..893c0aa
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Shows and hides an element that depends on certain selected pages when setting up conditions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Controller/Condition/Page/Dependence
+ */
+define(['Dom/ChangeListener', 'Dom/Traverse', 'EventHandler', 'ObjectMap'], function(DomChangeListener, DomTraverse, EventHandler, ObjectMap) {
+       "use strict";
+       
+       var _pages = elBySelAll('input[name="pageIDs[]"]');
+       var _dependentElements = [];
+       var _pageIds = new ObjectMap();
+       var _hiddenElements = new ObjectMap();
+       
+       var _didInit = false;
+       
+       return {
+               register: function(dependentElement, pageIds) {
+                       _dependentElements.push(dependentElement);
+                       _pageIds.set(dependentElement, pageIds);
+                       _hiddenElements.set(dependentElement, []);
+                       
+                       if (!_didInit) {
+                               for (var i = 0, length = _pages.length; i < length; i++) {
+                                       _pages[i].addEventListener('change', this._checkVisibility.bind(this));
+                               }
+                               
+                               _didInit = true;
+                       }
+                       
+                       // remove the dependent element before submit if it is hidden
+                       DomTraverse.parentByTag(dependentElement, 'FORM').addEventListener('submit', function() {
+                               if (dependentElement.style.getPropertyValue('display') === 'none') {
+                                       dependentElement.remove();
+                               }
+                       });
+                       
+                       this._checkVisibility();
+               },
+               
+               /**
+                * Checks if any of the relevant pages is selected. If that is the case, the dependent
+                * element is shown, otherwise it is hidden.
+                * 
+                * @private
+                */
+               _checkVisibility: function() {
+                       var dependentElement, page, pageIds;
+                       
+                       depenentElementLoop: for (var i = 0, length = _dependentElements.length; i < length; i++) {
+                               dependentElement = _dependentElements[i];
+                               pageIds = _pageIds.get(dependentElement);
+                               
+                               for (var j = 0, length2 = _pages.length; j < length2; j++) {
+                                       page = _pages[j];
+                                       
+                                       if (page.checked && pageIds.indexOf(~~page.value) !== -1) {
+                                               this._showDependentElement(dependentElement);
+                                               
+                                               continue depenentElementLoop;
+                                       }
+                               }
+                               
+                               this._hideDependentElement(dependentElement);
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.pageConditionDependence', 'checkVisivility');
+               },
+               
+               _hideDependentElement: function(dependentElement) {
+                       elHide(dependentElement);
+                       
+                       var hiddenElements = _hiddenElements.get(dependentElement);
+                       for (var i = 0, length = hiddenElements.length; i < length; i++) {
+                               elHide(hiddenElements[i]);
+                       }
+                       
+                       _hiddenElements.set(dependentElement, []);
+               },
+               
+               _showDependentElement: function(dependentElement) {
+                       elShow(dependentElement);
+                       
+                       // make sure that all parent elements are also visible
+                       var parentNode = dependentElement;
+                       while ((parentNode = parentNode.parentNode) && parentNode instanceof Element) {
+                               if (parentNode.style.getPropertyValue('display') === 'none') {
+                                       _hiddenElements.get(dependentElement).push(parentNode);
+                               }
+                               
+                               elShow(parentNode);
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Notice/Dismiss.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Notice/Dismiss.js
new file mode 100644 (file)
index 0000000..cef21fb
--- /dev/null
@@ -0,0 +1,56 @@
+/**
+ * Handles dismissible user notices.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Controller/Notice/Dismiss
+ */
+define(['Ajax'], function(Ajax) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Controller/Notice/Dismiss
+        */
+       var ControllerNoticeDismiss = {
+               /**
+                * Initializes dismiss buttons.
+                */
+               setup: function() {
+                       var buttons = elByClass('jsDismissNoticeButton');
+                       
+                       if (buttons.length) {
+                               var clickCallback = this._click.bind(this);
+                               for (var i = 0, length = buttons.length; i < length; i++) {
+                                       buttons[i].addEventListener(WCF_CLICK_EVENT, clickCallback);
+                               }
+                       }
+               },
+               
+               /**
+                * Sends a request to dismiss a notice and removes it afterwards.
+                */
+               _click: function(event) {
+                       var button = event.currentTarget;
+                       
+                       Ajax.apiOnce({
+                               data: {
+                                       actionName: 'dismiss',
+                                       className: 'wcf\\data\\notice\\NoticeAction',
+                                       objectIDs: [ elData(button, 'object-id') ]
+                               },
+                               success: function() {
+                                       var parent = button.parentNode;
+                                       
+                                       parent.addEventListener('transitionend', function() {
+                                               elRemove(parent);
+                                       });
+                                       
+                                       parent.classList.remove('active');
+                               }
+                       });
+               }
+       };
+       
+       return ControllerNoticeDismiss;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Popover.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Popover.js
new file mode 100644 (file)
index 0000000..18853c6
--- /dev/null
@@ -0,0 +1,386 @@
+/**
+ * Versatile popover manager.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Controller/Popover
+ */
+define(['Ajax', 'Dictionary', 'Environment', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Alignment'], function(Ajax, Dictionary, Environment, DomChangeListener, DomUtil, UiAlignment) {
+       "use strict";
+       
+       var _activeId = null;
+       var _cache = new Dictionary();
+       var _elements = new Dictionary();
+       var _handlers = new Dictionary();
+       var _hoverId = null;
+       var _suspended = false;
+       var _timeoutEnter = null;
+       var _timeoutLeave = null;
+       
+       var _popover = null;
+       var _popoverContent = null;
+       
+       var _callbackClick = null;
+       var _callbackHide = null;
+       var _callbackMouseEnter = null;
+       var _callbackMouseLeave = null;
+       
+       /** @const */ var STATE_NONE = 0;
+       /** @const */ var STATE_LOADING = 1;
+       /** @const */ var STATE_READY = 2;
+       
+       /** @const */ var DELAY_HIDE = 500;
+       /** @const */ var DELAY_SHOW = 300;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Controller/Popover
+        */
+       return {
+               /**
+                * Builds popover DOM elements and binds event listeners.
+                */
+               _setup: function() {
+                       if (_popover !== null) {
+                               return;
+                       }
+                       
+                       _popover = elCreate('div');
+                       _popover.className = 'popover forceHide';
+                       
+                       _popoverContent = elCreate('div');
+                       _popoverContent.className = 'popoverContent';
+                       _popover.appendChild(_popoverContent);
+                       
+                       var pointer = elCreate('span');
+                       pointer.className = 'elementPointer';
+                       pointer.appendChild(elCreate('span'));
+                       _popover.appendChild(pointer);
+                       
+                       document.body.appendChild(_popover);
+                       
+                       // static binding for callbacks (they don't change anyway and binding each time is expensive)
+                       _callbackClick = this._hide.bind(this);
+                       _callbackMouseEnter = this._mouseEnter.bind(this);
+                       _callbackMouseLeave = this._mouseLeave.bind(this);
+                       
+                       // event listener
+                       _popover.addEventListener('mouseenter', this._popoverMouseEnter.bind(this));
+                       _popover.addEventListener('mouseleave', _callbackMouseLeave);
+                       
+                       _popover.addEventListener('animationend', this._clearContent.bind(this));
+                       
+                       window.addEventListener('beforeunload', (function() {
+                               _suspended = true;
+                               
+                               if (_timeoutEnter !== null) {
+                                       window.clearTimeout(_timeoutEnter);
+                               }
+                               
+                               this._hide(true);
+                       }).bind(this));
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Controller/Popover', this._init.bind(this));
+               },
+               
+               /**
+                * Initializes a popover handler.
+                * 
+                * Usage:
+                * 
+                * ControllerPopover.init({
+                *      attributeName: 'data-object-id',
+                *      className: 'fooLink',
+                *      identifier: 'com.example.bar.foo',
+                *      loadCallback: function(objectId, popover) {
+                *              // request data for object id (e.g. via WoltLabSuite/Core/Ajax)
+                *              
+                *              // then call this to set the content
+                *              popover.setContent('com.example.bar.foo', objectId, htmlTemplateString);
+                *      }
+                * });
+                * 
+                * @param       {Object}        options         handler options
+                */
+               init: function(options) {
+                       if (Environment.platform() !== 'desktop') {
+                               return;
+                       }
+                       
+                       options.attributeName = options.attributeName || 'data-object-id';
+                       options.legacy = (options.legacy === true);
+                       
+                       this._setup();
+                       
+                       if (_handlers.has(options.identifier)) {
+                               return;
+                       }
+                       
+                       _handlers.set(options.identifier, {
+                               attributeName: options.attributeName,
+                               elements: options.legacy ? options.className : elByClass(options.className),
+                               legacy: options.legacy,
+                               loadCallback: options.loadCallback
+                       });
+                       
+                       this._init(options.identifier);
+               },
+               
+               /**
+                * Initializes a popover handler.
+                * 
+                * @param       {string}        identifier      handler identifier
+                */
+               _init: function(identifier) {
+                       if (typeof identifier === 'string' && identifier.length) {
+                               this._initElements(_handlers.get(identifier), identifier);
+                       }
+                       else {
+                               _handlers.forEach(this._initElements.bind(this));
+                       }
+               },
+               
+               /**
+                * Binds event listeners for popover-enabled elements.
+                * 
+                * @param       {Object}        options         handler options
+                * @param       {string}        identifier      handler identifier
+                */
+               _initElements: function(options, identifier) {
+                       var elements = options.legacy ? elBySelAll(options.elements) : options.elements;
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               var element = elements[i];
+                               
+                               var id = DomUtil.identify(element);
+                               if (_cache.has(id)) {
+                                       return;
+                               }
+                               
+                               var objectId = (options.legacy) ? id : ~~element.getAttribute(options.attributeName);
+                               if (objectId === 0) {
+                                       continue;
+                               }
+                               
+                               element.addEventListener('mouseenter', _callbackMouseEnter);
+                               element.addEventListener('mouseleave', _callbackMouseLeave);
+                               
+                               if (element.nodeName === 'A' && elAttr(element, 'href')) {
+                                       element.addEventListener(WCF_CLICK_EVENT, _callbackClick);
+                               }
+                               
+                               var cacheId = identifier + "-" + objectId;
+                               elData(element, 'cache-id', cacheId);
+                               
+                               _elements.set(id, {
+                                       element: element,
+                                       identifier: identifier,
+                                       objectId: objectId
+                               });
+                               
+                               if (!_cache.has(cacheId)) {
+                                       _cache.set(identifier + "-" + objectId, {
+                                               content: null,
+                                               state: STATE_NONE
+                                       });
+                               }
+                       }
+               },
+               
+               /**
+                * Sets the content for given identifier and object id.
+                * 
+                * @param       {string}        identifier      handler identifier
+                * @param       {int}           objectId        object id
+                * @param       {string}        content         HTML string
+                */
+               setContent: function(identifier, objectId, content) {
+                       var cacheId = identifier + "-" + objectId;
+                       var data = _cache.get(cacheId);
+                       if (data === undefined) {
+                               throw new Error("Unable to find element for object id '" + objectId + "' (identifier: '" + identifier + "').");
+                       }
+                       
+                       data.content = DomUtil.createFragmentFromHtml(content);
+                       data.state = STATE_READY;
+                       
+                       if (_activeId) {
+                               var activeElement = _elements.get(_activeId).element;
+                               
+                               if (elData(activeElement, 'cache-id') === cacheId) {
+                                       this._show();
+                               }
+                       }
+               },
+               
+               /**
+                * Handles the mouse start hovering the popover-enabled element.
+                * 
+                * @param       {object}        event   event object
+                */
+               _mouseEnter: function(event) {
+                       if (_suspended) {
+                               return;
+                       }
+                       
+                       if (_timeoutEnter !== null) {
+                               window.clearTimeout(_timeoutEnter);
+                               _timeoutEnter = null;
+                       }
+                       
+                       var id = DomUtil.identify(event.currentTarget);
+                       if (_activeId === id && _timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
+                       }
+                       
+                       _hoverId = id;
+                       
+                       _timeoutEnter = window.setTimeout((function() {
+                               _timeoutEnter = null;
+                               
+                               if (_hoverId === id) {
+                                       this._show();
+                               }
+                       }).bind(this), DELAY_SHOW);
+               },
+               
+               /**
+                * Handles the mouse leaving the popover-enabled element or the popover itself.
+                */
+               _mouseLeave: function() {
+                       _hoverId = null;
+                       
+                       if (_timeoutLeave !== null) {
+                               return;
+                       }
+                       
+                       if (_callbackHide === null) {
+                               _callbackHide = this._hide.bind(this);
+                       }
+                       
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                       }
+                       
+                       _timeoutLeave = window.setTimeout(_callbackHide, DELAY_HIDE);
+               },
+               
+               /**
+                * Handles the mouse start hovering the popover element.
+                */
+               _popoverMouseEnter: function() {
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
+                       }
+               },
+               
+               /**
+                * Shows the popover and loads content on-the-fly.
+                */
+               _show: function() {
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
+                       }
+                       
+                       var forceHide = false;
+                       if (_popover.classList.contains('active')) {
+                               this._hide();
+                               
+                               forceHide = true;
+                       }
+                       else if (_popoverContent.childElementCount) {
+                               forceHide = true;
+                       }
+                       
+                       if (forceHide) {
+                               _popover.classList.add('forceHide');
+                               
+                               // force layout
+                               _popover.offsetTop;
+                               
+                               this._clearContent();
+                               
+                               _popover.classList.remove('forceHide');
+                       }
+                       
+                       _activeId = _hoverId;
+                       
+                       var elementData = _elements.get(_activeId);
+                       var data = _cache.get(elData(elementData.element, 'cache-id'));
+                       
+                       if (data.state === STATE_READY) {
+                               _popoverContent.appendChild(data.content);
+                               
+                               this._rebuild(_activeId);
+                       }
+                       else if (data.state === STATE_NONE) {
+                               data.state = STATE_LOADING;
+                               
+                               _handlers.get(elementData.identifier).loadCallback(elementData.objectId, this);
+                       }
+               },
+               
+               /**
+                * Hides the popover element.
+                */
+               _hide: function() {
+                       if (_timeoutLeave !== null) {
+                               window.clearTimeout(_timeoutLeave);
+                               _timeoutLeave = null;
+                       }
+                       
+                       _popover.classList.remove('active');
+               },
+               
+               /**
+                * Clears popover content by moving it back into the cache.
+                */
+               _clearContent: function() {
+                       if (_activeId && _popoverContent.childElementCount && !_popover.classList.contains('active')) {
+                               var activeElData = _cache.get(elData(_elements.get(_activeId).element, 'cache-id'));
+                               while (_popoverContent.childNodes.length) {
+                                       activeElData.content.appendChild(_popoverContent.childNodes[0]);
+                               }
+                       }
+               },
+               
+               /**
+                * Rebuilds the popover.
+                */
+               _rebuild: function() {
+                       if (_popover.classList.contains('active')) {
+                               return;
+                       }
+                       
+                       _popover.classList.remove('forceHide');
+                       _popover.classList.add('active');
+                       
+                       UiAlignment.set(_popover, _elements.get(_activeId).element, {
+                               pointer: true,
+                               vertical: 'top'
+                       });
+               },
+               
+               _ajaxSetup: function() {
+                       // does nothing
+                       return {};
+               },
+               
+               /**
+                * Sends an AJAX requests to the server, simple wrapper to reuse the request object.
+                * 
+                * @param       {Object}        data            request data
+                * @param       {function}      success         success callback
+                * @param       {function=}     failure         error callback
+                */
+               ajaxApi: function(data, success, failure) {
+                       if (typeof success !== 'function') {
+                               throw new TypeError("Expected a valid callback for parameter 'success'.");
+                       }
+                       
+                       Ajax.api(this, data, success, failure);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Style/Changer.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Style/Changer.js
new file mode 100644 (file)
index 0000000..c578b6f
--- /dev/null
@@ -0,0 +1,80 @@
+/**
+ * Dialog based style changer.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Controller/Style/Changer
+ */
+define(['Ajax', 'Language', 'Ui/Dialog'], function(Ajax, Language, UiDialog) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Controller/Style/Changer
+        */
+       return {
+               /**
+                * Adds the style changer to the bottom navigation.
+                */
+               setup: function() {
+                       var link = elBySel('.jsButtonStyleChanger');
+                       if (link) {
+                               link.addEventListener(WCF_CLICK_EVENT, this.showDialog.bind(this));
+                       }
+               },
+               
+               /**
+                * Loads and displays the style change dialog.
+                * 
+                * @param       {object}        event   event object
+                */
+               showDialog: function(event) {
+                       event.preventDefault();
+                       
+                       UiDialog.open(this);
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'styleChanger',
+                               options: {
+                                       disableContentPadding: true,
+                                       title: Language.get('wcf.style.changeStyle')
+                               },
+                               source: {
+                                       data: {
+                                               actionName: 'getStyleChooser',
+                                               className: 'wcf\\data\\style\\StyleAction'
+                                       },
+                                       after: (function(content) {
+                                               var styles = elBySelAll('.styleList > li', content);
+                                               for (var i = 0, length = styles.length; i < length; i++) {
+                                                       var style = styles[i];
+                                                       
+                                                       style.classList.add('pointer');
+                                                       style.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+                                               }
+                                       }).bind(this)
+                               }
+                       };
+               },
+               
+               /**
+                * Changes the style and reloads current page.
+                * 
+                * @param       {object}        event   event object
+                */
+               _click: function(event) {
+                       event.preventDefault();
+                       
+                       Ajax.apiOnce({
+                               data: {
+                                       actionName: 'changeStyle',
+                                       className: 'wcf\\data\\style\\StyleAction',
+                                       objectIDs: [ elData(event.currentTarget, 'style-id') ]
+                               },
+                               success: function() { window.location.reload(); }
+                       });
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/User/Notification/Settings.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/User/Notification/Settings.js
new file mode 100644 (file)
index 0000000..69b0570
--- /dev/null
@@ -0,0 +1,153 @@
+/**
+ * Handles email notification type for user notification settings.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Controller/User/Notification/Settings
+ */
+define(['Dictionary', 'Language', 'Dom/Traverse', 'Ui/SimpleDropdown'], function(Dictionary, Language, DomTraverse, UiSimpleDropdown) {
+       "use strict";
+       
+       var _data = new Dictionary();
+       
+       var _callbackClick = null;
+       var _callbackSelectType = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Controller/User/Notification/Settings
+        */
+       var ControllerUserNotificationSettings = {
+               /**
+                * Binds event listeners for all notifications supporting emails.
+                */
+               setup: function() {
+                       _callbackClick = this._click.bind(this);
+                       _callbackSelectType = this._selectType.bind(this);
+                       
+                       var group, mailSetting, groups = elBySelAll('#notificationSettings .flexibleButtonGroup');
+                       for (var i = 0, length = groups.length; i < length; i++) {
+                               group = groups[i];
+                               
+                               mailSetting = elBySel('.notificationSettingsEmail', group);
+                               if (mailSetting === null) {
+                                       continue;
+                               }
+                               
+                               this._initGroup(group, mailSetting);
+                       }
+               },
+               
+               /**
+                * Initializes a setting.
+                * 
+                * @param       {Element}       group           button group element
+                * @param       {Element}       mailSetting     mail settings element
+                */
+               _initGroup: function(group, mailSetting) {
+                       var groupId = ~~elData(group, 'object-id');
+                       
+                       var disabledNotification = elById('settings_' + groupId + '_disabled');
+                       disabledNotification.addEventListener(WCF_CLICK_EVENT, function() { mailSetting.classList.remove('active'); });
+                       var enabledNotification = elById('settings_' + groupId + '_enabled');
+                       enabledNotification.addEventListener(WCF_CLICK_EVENT, function() { mailSetting.classList.add('active'); });
+                       
+                       var mailValue = DomTraverse.childByTag(mailSetting, 'INPUT');
+                       
+                       var button = DomTraverse.childByTag(mailSetting, 'A');
+                       elData(button, 'object-id', groupId);
+                       button.addEventListener(WCF_CLICK_EVENT, _callbackClick);
+                       
+                       _data.set(groupId, {
+                               button: button,
+                               dropdownMenu: null,
+                               mailSetting: mailSetting,
+                               mailValue: mailValue
+                       });
+               },
+               
+               /**
+                * Creates and displays the email type dropdown.
+                * 
+                * @param       {Object}        event           event object
+                */
+               _click: function(event) {
+                       event.preventDefault();
+                       
+                       var button = event.currentTarget;
+                       var objectId = ~~elData(button, 'object-id');
+                       var data = _data.get(objectId);
+                       if (data.dropdownMenu === null) {
+                               data.dropdownMenu = this._createDropdown(objectId, data.mailValue.value);
+                               
+                               button.parentNode.classList.add('dropdown');
+                               button.parentNode.appendChild(data.dropdownMenu);
+                               
+                               UiSimpleDropdown.init(button, true);
+                       }
+                       else {
+                               var items = DomTraverse.childrenByTag(data.dropdownMenu, 'LI'), value = data.mailValue.value;
+                               for (var i = 0; i < 4; i++) {
+                                       items[i].classList[(elData(items[i], 'value') === value) ? 'add' : 'remove']('active');
+                               }
+                       }
+               },
+               
+               /**
+                * Creates the email type dropdown.
+                * 
+                * @param       {int}           objectId        notification event id
+                * @param       {string}        initialValue    initial email type
+                * @returns     {Element}       dropdown menu object
+                */
+               _createDropdown: function(objectId, initialValue) {
+                       var dropdownMenu = elCreate('ul');
+                       dropdownMenu.className = 'dropdownMenu';
+                       elData(dropdownMenu, 'object-id', objectId);
+                       
+                       var link, listItem, value, items = ['instant', 'daily', 'divider', 'none'];
+                       for (var i = 0; i < 4; i++) {
+                               value = items[i];
+                               
+                               listItem = elCreate('li');
+                               if (value === 'divider') {
+                                       listItem.className = 'dropdownDivider';
+                               }
+                               else {
+                                       link = elCreate('a');
+                                       link.textContent = Language.get('wcf.user.notification.mailNotificationType.' + value);
+                                       listItem.appendChild(link);
+                                       elData(listItem, 'value', value);
+                                       listItem.addEventListener(WCF_CLICK_EVENT, _callbackSelectType);
+                                       
+                                       if (initialValue === value) {
+                                               listItem.className = 'active';
+                                       }
+                               }
+                               
+                               dropdownMenu.appendChild(listItem);
+                       }
+                       
+                       return dropdownMenu;
+               },
+               
+               /**
+                * Sets the selected email notification type.
+                * 
+                * @param       {Object}        event           event object
+                */
+               _selectType: function(event) {
+                       var value = elData(event.currentTarget, 'value');
+                       var groupId = ~~elData(event.currentTarget.parentNode, 'object-id');
+                       
+                       var data = _data.get(groupId);
+                       data.mailValue.value = value;
+                       elBySel('span.title', data.mailSetting).textContent = Language.get('wcf.user.notification.mailNotificationType.' + value);
+                       
+                       data.button.classList[(value === 'none') ? 'remove' : 'add']('yellow');
+                       data.button.classList[(value === 'none') ? 'remove' : 'add']('active');
+               }
+       };
+       
+       return ControllerUserNotificationSettings;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Core.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Core.js
new file mode 100644 (file)
index 0000000..af69149
--- /dev/null
@@ -0,0 +1,250 @@
+/**
+ * Provides the basic core functionality.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Core
+ */
+define([], function() {
+       "use strict";
+       
+       var _clone = function(variable) {
+               if (typeof variable === 'object' && (Array.isArray(variable) || Core.isPlainObject(variable))) {
+                       return _cloneObject(variable);
+               }
+               
+               return variable;
+       };
+       
+       var _cloneObject = function(obj) {
+               if (!obj) {
+                       return null;
+               }
+               
+               if (Array.isArray(obj)) {
+                       return obj.slice();
+               }
+               
+               var newObj = {};
+               for (var key in obj) {
+                       if (objOwns(obj, key) && typeof obj[key] !== 'undefined') {
+                               newObj[key] = _clone(obj[key]);
+                       }
+               }
+               
+               return newObj;
+       };
+       
+       /**
+        * @exports     WoltLabSuite/Core/Core
+        */
+       var Core = {
+               /**
+                * Deep clones an object.
+                * 
+                * @param       {object}        obj     source object
+                * @return      {object}        cloned object
+                */
+               clone: function(obj) {
+                       return _clone(obj);
+               },
+               
+               /**
+                * Converts WCF 2.0-style URLs into the default URL layout.
+                * 
+                * @param       string  url     target url
+                * @return      rewritten url
+                */
+               convertLegacyUrl: function(url) {
+                       if (URL_LEGACY_MODE) {
+                               return url;
+                       }
+                       
+                       return url.replace(/^index\.php\/(.*?)\/\?/, function(match, controller) {
+                               var parts = controller.split(/([A-Z][a-z0-9]+)/);
+                               controller = '';
+                               for (var i = 0, length = parts.length; i < length; i++) {
+                                       var part = parts[i].trim();
+                                       if (part.length) {
+                                               if (controller.length) controller += '-';
+                                               controller += part.toLowerCase();
+                                       }
+                               }
+                               
+                               return 'index.php?' + controller + '/&';
+                       });
+               },
+               
+               /**
+                * Merges objects with the first argument.
+                * 
+                * @param       {object}        out             destination object
+                * @param       {...object}     arguments       variable number of objects to be merged into the destination object
+                * @return      {object}        destination object with all provided objects merged into
+                */
+               extend: function(out) {
+                       out = out || {};
+                       var newObj = this.clone(out);
+                       
+                       for (var i = 1, length = arguments.length; i < length; i++) {
+                               var obj = arguments[i];
+                               
+                               if (!obj) continue;
+                               
+                               for (var key in obj) {
+                                       if (objOwns(obj, key)) {
+                                               if (!Array.isArray(obj[key]) && typeof obj[key] === 'object') {
+                                                       if (this.isPlainObject(obj[key])) {
+                                                               // object literals have the prototype of Object which in return has no parent prototype
+                                                               newObj[key] = this.extend(out[key], obj[key]);
+                                                       }
+                                                       else {
+                                                               newObj[key] = obj[key];
+                                                       }
+                                               }
+                                               else {
+                                                       newObj[key] = obj[key];
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       return newObj;
+               },
+               
+               /**
+                * Inherits the prototype methods from one constructor to another
+                * constructor.
+                * 
+                * Usage:
+                * 
+                * function MyDerivedClass() {}
+                * Core.inherit(MyDerivedClass, TheAwesomeBaseClass, {
+                *      // regular prototype for `MyDerivedClass`
+                *      
+                *      overwrittenMethodFromBaseClass: function(foo, bar) {
+                *              // do stuff
+                *              
+                *              // invoke parent
+                *              MyDerivedClass._super.prototype.overwrittenMethodFromBaseClass.call(this, foo, bar);
+                *      }
+                * });
+                * 
+                * @see https://github.com/nodejs/node/blob/7d14dd9b5e78faabb95d454a79faa513d0bbc2a5/lib/util.js#L697-L735
+                * @param       {function}      constructor             inheriting constructor function
+                * @param       {function}      superConstructor        inherited constructor function
+                * @param       {object=}       propertiesObject        additional prototype properties
+                */
+               inherit: function(constructor, superConstructor, propertiesObject) {
+                       if (constructor === undefined || constructor === null) {
+                               throw new TypeError("The constructor must not be undefined or null.");
+                       }
+                       if (superConstructor === undefined || superConstructor === null) {
+                               throw new TypeError("The super constructor must not be undefined or null.");
+                       }
+                       if (superConstructor.prototype === undefined) {
+                               throw new TypeError("The super constructor must have a prototype.");
+                       }
+                       
+                       constructor._super = superConstructor;
+                       constructor.prototype = Core.extend(Object.create(superConstructor.prototype, {
+                               constructor: {
+                                       configurable: true,
+                                       enumerable: false,
+                                       value: constructor,
+                                       writable: true
+                               }
+                       }), propertiesObject || {});
+               },
+               
+               /**
+                * Returns true if `obj` is an object literal.
+                * 
+                * @param       {*}     obj     target object
+                * @returns     {boolean}       true if target is an object literal
+                */
+               isPlainObject: function(obj) {
+                       if (typeof obj !== 'object' || obj === null || obj.nodeType) {
+                               return false;
+                       }
+                       
+                       return (Object.getPrototypeOf(obj) === Object.prototype);
+               },
+               
+               /**
+                * Returns the object's class name.
+                * 
+                * @param       {object}        obj     target object
+                * @return      {string}        object class name
+                */
+               getType: function(obj) {
+                       return Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1');
+               },
+               
+               /**
+                * Returns a RFC4122 version 4 compilant UUID.
+                * 
+                * @see         http://stackoverflow.com/a/2117523
+                * @return      {string}
+                */
+               getUuid: function() {
+                       return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+                               var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
+                               return v.toString(16);
+                       });
+               },
+               
+               /**
+                * Recursively serializes an object into an encoded URI parameter string.
+                *  
+                * @param       {object}        obj     target object
+                * @param       {string=}       prefix  parameter prefix
+                * @return      encoded parameter string
+                */
+               serialize: function(obj, prefix) {
+                       var parameters = [];
+                       
+                       for (var key in obj) {
+                               if (objOwns(obj, key)) {
+                                       var parameterKey = (prefix) ? prefix + '[' + key + ']' : key;
+                                       var value = obj[key];
+                                       
+                                       if (typeof value === 'object') {
+                                               parameters.push(this.serialize(value, parameterKey));
+                                       }
+                                       else {
+                                               parameters.push(encodeURIComponent(parameterKey) + '=' + encodeURIComponent(value));
+                                       }
+                               }
+                       }
+                       
+                       return parameters.join('&');
+               },
+               
+               /**
+                * Triggers a custom or built-in event.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {string}        eventName       event name
+                */
+               triggerEvent: function(element, eventName) {
+                       var event;
+                       
+                       try {
+                               event = new Event(eventName, {
+                                       bubbles: true,
+                                       cancelable: true
+                               });
+                       }
+                       catch (e) {
+                               event = document.createEvent('Event');
+                               event.initEvent(eventName, true, true);
+                       }
+                       
+                       element.dispatchEvent(event);
+               }
+       };
+       
+       return Core;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Date/Picker.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Date/Picker.js
new file mode 100644 (file)
index 0000000..1bb2f71
--- /dev/null
@@ -0,0 +1,783 @@
+/**
+ * Date picker with time support.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Date/Picker
+ */
+define(['DateUtil', 'Language', 'ObjectMap', 'Dom/ChangeListener', 'Ui/Alignment', 'WoltLabSuite/Core/Ui/CloseOverlay'], function(DateUtil, Language, ObjectMap, DomChangeListener, UiAlignment, UiCloseOverlay) {
+       "use strict";
+       
+       var _didInit = false;
+       var _firstDayOfWeek = 0;
+       
+       var _data = new ObjectMap();
+       var _input = null;
+       var _maxDate = 0;
+       var _minDate = 0;
+       
+       var _dateCells = [];
+       var _dateGrid = null;
+       var _dateHour = null;
+       var _dateMinute = null;
+       var _dateMonth = null;
+       var _dateMonthNext = null;
+       var _dateMonthPrevious = null;
+       var _dateTime = null;
+       var _dateYear = null;
+       var _datePicker = null;
+       
+       var _callbackOpen = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Date/Picker
+        */
+       var DatePicker = {
+               /**
+                * Initializes all date and datetime input fields.
+                */
+               init: function() {
+                       this._setup();
+                       
+                       var elements = elBySelAll('input[type="date"]:not(.inputDatePicker), input[type="datetime"]:not(.inputDatePicker)');
+                       var now = new Date();
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               var element = elements[i];
+                               element.classList.add('inputDatePicker');
+                               element.readOnly = true;
+                               
+                               var isDateTime = (elAttr(element, 'type') === 'datetime');
+                               var isTimeOnly = (isDateTime && elDataBool(element, 'time-only'));
+                               
+                               elData(element, 'is-date-time', isDateTime);
+                               elData(element, 'is-time-only', isTimeOnly);
+                               
+                               // convert value
+                               var date = null, value = elAttr(element, 'value');
+                               if (elAttr(element, 'value')) {
+                                       if (isTimeOnly) {
+                                               date = new Date();
+                                               var tmp = value.split(':');
+                                               date.setHours(tmp[0], tmp[1]);
+                                       }
+                                       else {
+                                               date = new Date(value);
+                                       }
+                                       
+                                       elData(element, 'value', date.getTime());
+                                       var format = (isTimeOnly) ? 'formatTime' : ('formatDate' + (isDateTime ? 'Time' : '')); 
+                                       value = DateUtil[format](date);
+                               }
+                               
+                               var isEmpty = (value.length === 0);
+                               
+                               // handle birthday input
+                               if (element.classList.contains('birthday')) {
+                                       elData(element, 'min-date', '100');
+                                       elData(element, 'max-date', 'now');
+                               }
+                               else {
+                                       if (element.min) elData(element, 'min-date', element.min);
+                                       if (element.max) elData(element, 'max-date', element.max);
+                               }
+                               
+                               this._initDateRange(element, now, true);
+                               this._initDateRange(element, now, false);
+                               
+                               if (elData(element, 'min-date') === elData(element, 'max-date')) {
+                                       throw new Error("Minimum and maximum date cannot be the same (element id '" + element.id + "').");
+                               }
+                               
+                               // change type to prevent browser's datepicker to trigger
+                               element.type = 'text';
+                               element.value = value;
+                               elData(element, 'empty', isEmpty);
+                               
+                               if (elData(element, 'placeholder')) {
+                                       elAttr(element, 'placeholder', elData(element, 'placeholder'));
+                               }
+                               
+                               // add a hidden element to hold the actual date
+                               var shadowElement = elCreate('input');
+                               shadowElement.id = element.id + 'DatePicker';
+                               shadowElement.name = element.name;
+                               shadowElement.type = 'hidden';
+                               
+                               if (date !== null) {
+                                       if (isTimeOnly) {
+                                               shadowElement.value = DateUtil.format(date, 'H:i');
+                                       }
+                                       else {
+                                               shadowElement.value = DateUtil.format(date, (isDateTime) ? 'c' : 'Y-m-d');
+                                       }
+                               }
+                               
+                               element.parentNode.insertBefore(shadowElement, element);
+                               element.removeAttribute('name');
+                               
+                               element.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
+                               
+                               // create input addon
+                               var container = elCreate('div');
+                               container.className = 'inputAddon';
+                               
+                               var button = elCreate('a');
+                               button.className = 'inputSuffix button';
+                               button.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
+                               container.appendChild(button);
+                               
+                               var icon = elCreate('span');
+                               icon.className = 'icon icon16 fa-calendar';
+                               button.appendChild(icon);
+                               
+                               element.parentNode.insertBefore(container, element);
+                               container.insertBefore(element, button);
+                               
+                               button = elCreate('a');
+                               button.className = 'inputSuffix button';
+                               button.addEventListener(WCF_CLICK_EVENT, this.clear.bind(this, element));
+                               if (isEmpty) button.style.setProperty('visibility', 'hidden', '');
+                               
+                               container.appendChild(button);
+                               
+                               icon = elCreate('span');
+                               icon.className = 'icon icon16 fa-times';
+                               button.appendChild(icon);
+                               
+                               // check if the date input has one of the following classes set otherwise default to 'short'
+                               var hasClass = false, knownClasses = ['tiny', 'short', 'medium', 'long'];
+                               for (var j = 0; j < 4; j++) {
+                                       if (element.classList.contains(knownClasses[j])) {
+                                               hasClass = true;
+                                       }
+                               }
+                               
+                               if (!hasClass) {
+                                       element.classList.add('short');
+                               }
+                               
+                               _data.set(element, {
+                                       clearButton: button,
+                                       shadow: shadowElement,
+                                       
+                                       isDateTime: isDateTime,
+                                       isEmpty: isEmpty,
+                                       isTimeOnly: isTimeOnly,
+                                       
+                                       onClose: null
+                               });
+                       }
+               },
+               
+               /**
+                * Initializes the minimum/maximum date range.
+                * 
+                * @param       {Element}       element         input element
+                * @param       {Date}          now             current date
+                * @param       {boolean}       isMinDate       true for the minimum date
+                */
+               _initDateRange: function(element, now, isMinDate) {
+                       var attribute = 'data-' + (isMinDate ? 'min' : 'max') + '-date';
+                       var value = (element.hasAttribute(attribute)) ? elAttr(element, attribute).trim() : '';
+                       
+                       if (value.match(/^(\d{4})-(\d{2})-(\d{2})$/)) {
+                               // YYYY-mm-dd
+                               value = new Date(value).getTime();
+                       }
+                       else if (value === 'now') {
+                               value = now.getTime();
+                       }
+                       else if (value.match(/^\d{1,3}$/)) {
+                               // relative time span in years
+                               var date = new Date(now.getTime());
+                               date.setFullYear(date.getFullYear() + ~~value * (isMinDate ? -1 : 1));
+                               
+                               value = date.getTime();
+                       }
+                       else if (value.match(/^datePicker-(.+)$/)) {
+                               // element id, e.g. `datePicker-someOtherElement`
+                               value = RegExp.$1;
+                               
+                               if (elById(value) === null) {
+                                       throw new Error("Reference date picker identified by '" + value + "' does not exists (element id: '" + element.id + "').");
+                               }
+                       }
+                       else if (/^\d{4}\-\d{2}\-\d{2}T/.test(value)) {
+                               value = new Date(value).getTime();
+                       }
+                       else {
+                               value = new Date((isMinDate ? 1970 : 2038), 0, 1).getTime();
+                       }
+                       
+                       elAttr(element, attribute, value);
+               },
+               
+               /**
+                * Sets up callbacks and event listeners.
+                */
+               _setup: function() {
+                       if (_didInit) return;
+                       _didInit = true;
+                       
+                       _firstDayOfWeek = ~~Language.get('wcf.date.firstDayOfTheWeek');
+                       _callbackOpen = this._open.bind(this);
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Date/Picker', this.init.bind(this));
+                       UiCloseOverlay.add('WoltLabSuite/Core/Date/Picker', this._close.bind(this));
+               },
+               
+               /**
+                * Opens the date picker.
+                * 
+                * @param       {object}        event           event object
+                */
+               _open: function(event) {
+                       event.preventDefault();
+                       event.stopPropagation();
+                       
+                       this._createPicker();
+                       
+                       var input = (event.currentTarget.nodeName === 'INPUT') ? event.currentTarget : event.currentTarget.previousElementSibling;
+                       if (input === _input) {
+                               return;
+                       }
+                       
+                       _input = input;
+                       var data = _data.get(_input), date, value = elData(_input, 'value');
+                       if (value) {
+                               date = new Date(+value);
+                               
+                               if (date.toString() === 'Invalid Date') {
+                                       date = new Date();
+                               }
+                       }
+                       else {
+                               date = new Date();
+                       }
+                       
+                       // set min/max date
+                       _minDate = elData(_input, 'min-date');
+                       if (_minDate.match(/^datePicker-(.+)$/)) _minDate = elData(elById(RegExp.$1), 'value');
+                       _minDate = new Date(+_minDate);
+                       
+                       _maxDate = elData(_input, 'max-date');
+                       if (_maxDate.match(/^datePicker-(.+)$/)) _maxDate = elData(elById(RegExp.$1), 'value');
+                       _maxDate = new Date(+_maxDate);
+                       
+                       if (data.isDateTime) {
+                               _dateHour.value = date.getHours();
+                               _dateMinute.value = date.getMinutes();
+                               
+                               _datePicker.classList.add('datePickerTime');
+                       }
+                       else {
+                               _datePicker.classList.remove('datePickerTime');
+                       }
+                       
+                       _datePicker.classList[(data.isTimeOnly) ? 'add' : 'remove']('datePickerTimeOnly');
+                       
+                       this._renderPicker(date.getDate(), date.getMonth(), date.getFullYear());
+                       
+                       UiAlignment.set(_datePicker, _input);
+               },
+               
+               /**
+                * Closes the date picker.
+                */
+               _close: function() {
+                       if (_datePicker !== null && _datePicker.classList.contains('active')) {
+                               _datePicker.classList.remove('active');
+                               
+                               var data = _data.get(_input);
+                               if (typeof data.onClose === 'function') {
+                                       data.onClose();
+                               }
+                               
+                               _input = null;
+                               _minDate = 0;
+                               _maxDate = 0;
+                       }
+               },
+               
+               /**
+                * Renders the full picker on init.
+                * 
+                * @param       {int}           day
+                * @param       {int}           month
+                * @param       {int}           year
+                */
+               _renderPicker: function(day, month, year) {
+                       this._renderGrid(day, month, year);
+                       
+                       // create options for month and year
+                       var years = '';
+                       for (var i = _minDate.getFullYear(), last = _maxDate.getFullYear(); i <= last; i++) {
+                               years += '<option value="' + i + '">' + i + '</option>';
+                       }
+                       _dateYear.innerHTML = years;
+                       _dateYear.value = year;
+                       
+                       _dateMonth.value = month;
+                       
+                       _datePicker.classList.add('active');
+               },
+               
+               /**
+                * Updates the date grid.
+                * 
+                * @param       {int}           day
+                * @param       {int}           month
+                * @param       {int}           year
+                */
+               _renderGrid: function(day, month, year) {
+                       var cell, hasDay = (day !== undefined), hasMonth = (month !== undefined), i;
+                       
+                       day = ~~day || ~~elData(_dateGrid, 'day');
+                       month = ~~month;
+                       year = ~~year;
+                       
+                       // rebuild cells
+                       if (hasMonth || year) {
+                               var rebuildMonths = (year !== 0);
+                               
+                               // rebuild grid
+                               var fragment = document.createDocumentFragment();
+                               fragment.appendChild(_dateGrid);
+                               
+                               if (!hasMonth) month = ~~elData(_dateGrid, 'month');
+                               year = year || ~~elData(_dateGrid, 'year');
+                               
+                               // check if current selection exceeds min/max date
+                               var date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-' + ('0' + day.toString()).slice(-2));
+                               if (date < _minDate) {
+                                       year = _minDate.getFullYear();
+                                       month = _minDate.getMonth();
+                                       day = _minDate.getDate();
+                                       
+                                       _dateMonth.value = month;
+                                       _dateYear.value = year;
+                                       
+                                       rebuildMonths = true;
+                               }
+                               else if (date > _maxDate) {
+                                       year = _maxDate.getFullYear();
+                                       month = _maxDate.getMonth();
+                                       day = _maxDate.getDate();
+                                       
+                                       _dateMonth.value = month;
+                                       _dateYear.value = year;
+                                       
+                                       rebuildMonths = true;
+                               }
+                               
+                               date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+                               
+                               // shift until first displayed day equals first day of week
+                               while (date.getDay() !== _firstDayOfWeek) {
+                                       date.setDate(date.getDate() - 1);
+                               }
+                               
+                               var selectable;
+                               for (i = 0; i < 35; i++) {
+                                       cell = _dateCells[i];
+                                       
+                                       cell.textContent = date.getDate();
+                                       selectable = (date.getMonth() === month);
+                                       if (selectable) {
+                                               if (date < _minDate) selectable = false;
+                                               else if (date > _maxDate) selectable = false;
+                                       }
+                                       
+                                       cell.classList[selectable ? 'remove' : 'add']('otherMonth');
+                                       date.setDate(date.getDate() + 1); 
+                               }
+                               
+                               elData(_dateGrid, 'month', month);
+                               elData(_dateGrid, 'year', year);
+                               
+                               _datePicker.insertBefore(fragment, _dateTime);
+                               
+                               if (!hasDay) {
+                                       // check if date is valid
+                                       date = new Date(year, month, day);
+                                       if (date.getDate() !== day) {
+                                               while (date.getMonth() !== month) {
+                                                       date.setDate(date.getDate() - 1);
+                                               }
+                                               
+                                               day = date.getDate();
+                                       }
+                               }
+                               
+                               if (rebuildMonths) {
+                                       for (i = 0; i < 12; i++) {
+                                               var currentMonth = _dateMonth.children[i];
+                                               
+                                               currentMonth.disabled = (year === _minDate.getFullYear() && currentMonth.value < _minDate.getMonth()) || (year === _maxDate.getFullYear() && currentMonth.value > _maxDate.getMonth());
+                                       }
+                                       
+                                       var nextMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+                                       nextMonth.setMonth(nextMonth.getMonth() + 1);
+                                       
+                                       _dateMonthNext.classList[(nextMonth < _maxDate) ? 'add' : 'remove']('active');
+                                       
+                                       var previousMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+                                       previousMonth.setDate(previousMonth.getDate() - 1);
+                                       
+                                       _dateMonthPrevious.classList[(previousMonth > _minDate) ? 'add' : 'remove']('active');
+                               }
+                       }
+                       
+                       // update active day
+                       if (day) {
+                               for (i = 0; i < 35; i++) {
+                                       cell = _dateCells[i];
+                                       
+                                       cell.classList[(!cell.classList.contains('otherMonth') && ~~cell.textContent === day) ? 'add' : 'remove']('active');
+                               }
+                               
+                               elData(_dateGrid, 'day', day);
+                       }
+                       
+                       this._formatValue();
+               },
+               
+               /**
+                * Sets the visible and shadow value
+                */
+               _formatValue: function() {
+                       var data = _data.get(_input), date, value, shadowValue;
+                       
+                       if (elData(_input, 'empty') === 'true') {
+                               return;
+                       }
+                       
+                       if (data.isDateTime) {
+                               date = new Date(
+                                       elData(_dateGrid, 'year'),
+                                       elData(_dateGrid, 'month'),
+                                       elData(_dateGrid, 'day'),
+                                       _dateHour.value,
+                                       _dateMinute.value
+                               );
+                               
+                               if (data.isTimeOnly) {
+                                       value = DateUtil.formatTime(date);
+                                       shadowValue = DateUtil.format(date, 'H:i');
+                               }
+                               else {
+                                       value = DateUtil.formatDateTime(date);
+                                       shadowValue = DateUtil.format(date, 'c');
+                               }
+                       }
+                       else {
+                               date = new Date(
+                                       elData(_dateGrid, 'year'),
+                                       elData(_dateGrid, 'month'),
+                                       elData(_dateGrid, 'day')
+                               );
+                               
+                               value = DateUtil.formatDate(date);
+                               shadowValue = DateUtil.format(date, 'Y-m-d');
+                       }
+                       
+                       _input.value = value;
+                       elData(_input, 'value', date.getTime());
+                       data.clearButton.style.removeProperty('visibility');
+                       data.shadow.value = shadowValue;
+               },
+               
+               /**
+                * Creates the date picker DOM.
+                */
+               _createPicker: function() {
+                       if (_datePicker !== null) {
+                               return;
+                       }
+                       
+                       _datePicker = elCreate('div');
+                       _datePicker.className = 'datePicker';
+                       _datePicker.addEventListener(WCF_CLICK_EVENT, function(event) { event.stopPropagation(); });
+                       
+                       var header = elCreate('header');
+                       _datePicker.appendChild(header);
+                       
+                       _dateMonthPrevious = elCreate('a');
+                       _dateMonthPrevious.className = 'previous';
+                       _dateMonthPrevious.innerHTML = '<span class="icon icon16 fa-arrow-left"></span>';
+                       _dateMonthPrevious.addEventListener(WCF_CLICK_EVENT, this.previousMonth.bind(this));
+                       header.appendChild(_dateMonthPrevious);
+                       
+                       var monthYearContainer = elCreate('span');
+                       header.appendChild(monthYearContainer);
+                       
+                       _dateMonth = elCreate('select');
+                       _dateMonth.className = 'month';
+                       _dateMonth.addEventListener('change', this._changeMonth.bind(this));
+                       
+                       var selectWrapper = elCreate('label');
+                       selectWrapper.className = 'selectDropdown';
+                       selectWrapper.appendChild(_dateMonth);
+                       monthYearContainer.appendChild(selectWrapper);
+                       
+                       var i, months = '', monthNames = Language.get('__monthsShort');
+                       for (i = 0; i < 12; i++) {
+                               months += '<option value="' + i + '">' + monthNames[i] + '</option>';
+                       }
+                       _dateMonth.innerHTML = months;
+                       
+                       _dateYear = elCreate('select');
+                       _dateYear.className = 'year';
+                       _dateYear.addEventListener('change', this._changeYear.bind(this));
+                       
+                       selectWrapper = elCreate('label');
+                       selectWrapper.className = 'selectDropdown';
+                       selectWrapper.appendChild(_dateYear);
+                       monthYearContainer.appendChild(selectWrapper);
+                       
+                       _dateMonthNext = elCreate('a');
+                       _dateMonthNext.className = 'next';
+                       _dateMonthNext.innerHTML = '<span class="icon icon16 fa-arrow-right"></span>';
+                       _dateMonthNext.addEventListener(WCF_CLICK_EVENT, this.nextMonth.bind(this));
+                       header.appendChild(_dateMonthNext);
+                       
+                       _dateGrid = elCreate('ul');
+                       _datePicker.appendChild(_dateGrid);
+                       
+                       var item = elCreate('li');
+                       item.className = 'weekdays';
+                       _dateGrid.appendChild(item);
+                       
+                       var span, weekdays = Language.get('__daysShort');
+                       for (i = 0; i < 7; i++) {
+                               var day = i + _firstDayOfWeek;
+                               if (day > 6) day -= 7;
+                               
+                               span = elCreate('span');
+                               span.textContent = weekdays[day];
+                               item.appendChild(span);
+                       }
+                       
+                       // create date grid
+                       var callbackClick = this._click.bind(this), cell, row;
+                       for (i = 0; i < 5; i++) {
+                               row = elCreate('li');
+                               _dateGrid.appendChild(row);
+                               
+                               for (var j = 0; j < 7; j++) {
+                                       cell = elCreate('a');
+                                       cell.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                                       _dateCells.push(cell);
+                                       
+                                       row.appendChild(cell);
+                               }
+                       }
+                       
+                       _dateTime = elCreate('footer');
+                       _datePicker.appendChild(_dateTime);
+                       
+                       _dateHour = elCreate('select');
+                       _dateHour.className = 'hour';
+                       _dateHour.addEventListener('change', this._formatValue.bind(this));
+                       
+                       var tmp = '';
+                       var date = new Date(2000, 0, 1);
+                       var timeFormat = Language.get('wcf.date.timeFormat').replace(/:/, '').replace(/[isu]/g, '');
+                       for (i = 0; i < 24; i++) {
+                               date.setHours(i);
+                               tmp += '<option value="' + i + '">' + DateUtil.format(date, timeFormat) + "</option>";
+                       }
+                       _dateHour.innerHTML = tmp;
+                       
+                       _dateTime.appendChild(_dateHour);
+                       
+                       _dateTime.appendChild(document.createTextNode('\u00A0:\u00A0'));
+                       
+                       _dateMinute = elCreate('select');
+                       _dateMinute.className = 'minute';
+                       _dateMinute.addEventListener('change', this._formatValue.bind(this));
+                       
+                       tmp = '';
+                       for (i = 0; i < 60; i++) {
+                               tmp += '<option value="' + i + '">' + (i < 10 ? '0' + i.toString() : i) + '</option>';
+                       }
+                       _dateMinute.innerHTML = tmp;
+                       
+                       _dateTime.appendChild(_dateMinute);
+                       
+                       document.body.appendChild(_datePicker);
+               },
+               
+               /**
+                * Shows the previous month.
+                */
+               previousMonth: function() {
+                       if (_dateMonth.value === '0') {
+                               _dateMonth.value = 11;
+                               _dateYear.value = ~~_dateYear.value - 1;
+                       }
+                       else {
+                               _dateMonth.value = ~~_dateMonth.value - 1;
+                       }
+                       
+                       this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
+               },
+               
+               /**
+                * Shows the next month.
+                */
+               nextMonth: function() {
+                       if (_dateMonth.value === '11') {
+                               _dateMonth.value = 0;
+                               _dateYear.value = ~~_dateYear.value + 1;
+                       }
+                       else {
+                               _dateMonth.value = ~~_dateMonth.value + 1;
+                       }
+                       
+                       this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
+               },
+               
+               /**
+                * Handles changes to the month select element.
+                * 
+                * @param       {object}        event           event object
+                */
+               _changeMonth: function(event) {
+                       this._renderGrid(undefined, event.currentTarget.value);
+               },
+               
+               /**
+                * Handles changes to the year select element.
+                * 
+                * @param       {object}        event           event object
+                */
+               _changeYear: function(event) {
+                       this._renderGrid(undefined, undefined, event.currentTarget.value);
+               },
+               
+               /**
+                * Handles clicks on an individual day.
+                * 
+                * @param       {object}        event           event object
+                */
+               _click: function(event) {
+                       if (event.currentTarget.classList.contains('otherMonth')) {
+                               return;
+                       }
+                       
+                       elData(_input, 'empty', false);
+                       
+                       this._renderGrid(event.currentTarget.textContent);
+                       
+                       this._close();
+               },
+               
+               /**
+                * Returns the current Date object or null.
+                * 
+                * @param       {(Element|string)}      element         input element or id
+                * @return      {?Date}                 Date object or null
+                */
+               getDate: function(element) {
+                       element = this._getElement(element);
+                       
+                       if (element.hasAttribute('data-value')) {
+                               return new Date(+elData(element, 'value'));
+                       }
+                       
+                       return null;
+               },
+               
+               /**
+                * Sets the date of given element.
+                * 
+                * @param       {(HTMLInputElement|string)}     element         input element or id
+                * @param       {Date}                          date            Date object
+                */
+               setDate: function(element, date) {
+                       element = this._getElement(element);
+                       var data = _data.get(element);
+                       
+                       elData(element, 'value', date.getTime());
+                       element.value = DateUtil['formatDate' + (data.isDateTime ? 'Time' : '')](date);
+                       
+                       data.shadow.value = DateUtil.format(date, (data.isDateTime ? 'c' : 'Y-m-d'));
+               },
+               
+               /**
+                * Clears the date value of given element.
+                * 
+                * @param       {(HTMLInputElement|string)}     element         input element or id
+                */
+               clear: function(element) {
+                       element = this._getElement(element);
+                       var data = _data.get(element);
+                       
+                       element.removeAttribute('data-value');
+                       element.value = '';
+                       
+                       data.clearButton.style.setProperty('visibility', 'hidden', '');
+                       data.isEmpty = true;
+                       data.shadow.value = '';
+               },
+               
+               /**
+                * Reverts the date picker into a normal input field.
+                * 
+                * @param       {(HTMLInputElement|string)}     element         input element or id
+                */
+               destroy: function(element) {
+                       element = this._getElement(element);
+                       var data = _data.get(element);
+                       
+                       var container = element.parentNode;
+                       container.parentNode.insertBefore(element, container);
+                       elRemove(container);
+                       
+                       elAttr(element, 'type', 'date' + (data.isDateTime ? 'time' : ''));
+                       element.value = data.shadow.value;
+                       
+                       element.removeAttribute('data-value');
+                       element.removeEventListener(WCF_CLICK_EVENT, _callbackOpen);
+                       elRemove(data.shadow);
+                       
+                       element.classList.remove('inputDatePicker');
+                       element.readOnly = false;
+                       _data['delete'](element);
+               },
+               
+               /**
+                * Sets the callback invoked on picker close.
+                * 
+                * @param       {(Element|string)}      element         input element or id
+                * @param       {function}              callback        callback function
+                */
+               setCloseCallback: function(element, callback) {
+                       element = this._getElement(element);
+                       _data.get(element).onClose = callback;
+               },
+               
+               /**
+                * Validates given element or id if it represents an active date picker.
+                * 
+                * @param       {(Element|string)}      element         input element or id
+                * @return      {Element}               input element
+                */
+               _getElement: function(element) {
+                       if (typeof element === 'string') element = elById(element);
+                       
+                       if (!(element instanceof Element) || !element.classList.contains('inputDatePicker') || !_data.has(element)) {
+                               throw new Error("Expected a valid date picker input element or id.");
+                       }
+                       
+                       return element;
+               }
+       };
+       
+       // backward-compatibility for `$.ui.datepicker` shim
+       window.__wcf_bc_datePicker = DatePicker;
+       
+       return DatePicker;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Date/Time/Relative.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Date/Time/Relative.js
new file mode 100644 (file)
index 0000000..6678213
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * Transforms <time> elements to display the elapsed time relative to the current time.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Date/Time/Relative
+ */
+define(['Dom/ChangeListener', 'Language', 'WoltLabSuite/Core/Date/Util', 'WoltLabSuite/Core/Timer/Repeating'], function(DomChangeListener, Language, DateUtil, Repeating) {
+       "use strict";
+       
+       var _elements = elByTag('time');
+       var _offset = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Date/Time/Relative
+        */
+       return {
+               /**
+                * Transforms <time> elements on init and binds event listeners.
+                */
+               setup: function() {
+                       this._refresh();
+                       
+                       new Repeating(this._refresh.bind(this), 60000);
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Date/Time/Relative', this._refresh.bind(this));
+               },
+               
+               _refresh: function() {
+                       var date = new Date();
+                       var timestamp = (date.getTime() - date.getMilliseconds()) / 1000;
+                       if (_offset === null) _offset = timestamp - TIME_NOW;
+                       
+                       for (var i = 0, length = _elements.length; i < length; i++) {
+                               var element = _elements[i];
+                               
+                               if (!element.classList.contains('datetime') || elData(element, 'is-future-date')) continue;
+                               
+                               var elTimestamp = ~~elData(element, 'timestamp') + _offset;
+                               var elDate = elData(element, 'date');
+                               var elTime = elData(element, 'time');
+                               var elOffset = elData(element, 'offset');
+                               
+                               if (!elAttr(element, 'title')) {
+                                       elAttr(element, 'title', Language.get('wcf.date.dateTimeFormat').replace(/%date%/, elDate).replace(/%time%/, elTime));
+                               }
+                               
+                               // timestamp is less than 60 seconds ago
+                               if (elTimestamp >= timestamp || timestamp < (elTimestamp + 60)) {
+                                       element.textContent = Language.get('wcf.date.relative.now');
+                               }
+                               // timestamp is less than 60 minutes ago (display 1 hour ago rather than 60 minutes ago)
+                               else if (timestamp < (elTimestamp + 3540)) {
+                                       var minutes = Math.max(Math.round((timestamp - elTimestamp) / 60), 1);
+                                       element.textContent = Language.get('wcf.date.relative.minutes', { minutes: minutes });
+                               }
+                               // timestamp is less than 24 hours ago
+                               else if (timestamp < (elTimestamp + 86400)) {
+                                       var hours = Math.round((timestamp - elTimestamp) / 3600);
+                                       element.textContent = Language.get('wcf.date.relative.hours', { hours: hours });
+                               }
+                               // timestamp is less than 6 days ago
+                               else if (timestamp < (elTimestamp + 518400)) {
+                                       var midnight = new Date(date.getFullYear(), date.getMonth(), date.getDate());
+                                       var days = Math.ceil((midnight / 1000 - elTimestamp) / 86400);
+                                       
+                                       // get day of week
+                                       var dateObj = DateUtil.getTimezoneDate((elTimestamp * 1000), elOffset * 1000);
+                                       var dow = dateObj.getDay();
+                                       var day = Language.get('__days')[dow];
+                                       
+                                       element.textContent = Language.get('wcf.date.relative.pastDays', { days: days, day: day, time: elTime });
+                               }
+                               // timestamp is between ~700 million years BC and last week
+                               else {
+                                       element.textContent = Language.get('wcf.date.shortDateTimeFormat').replace(/%date%/, elDate).replace(/%time%/, elTime);
+                               }
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Date/Util.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Date/Util.js
new file mode 100644 (file)
index 0000000..df84cd6
--- /dev/null
@@ -0,0 +1,231 @@
+/**
+ * Provides utility functions for date operations.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Date/Util
+ */
+define(['Language'], function(Language) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Date/Util
+        */
+       var DateUtil = {
+               /**
+                * Returns the formatted date.
+                * 
+                * @param       {Date}          date            date object
+                * @returns     {string}        formatted date
+                */
+               formatDate: function(date) {
+                       return this.format(date, Language.get('wcf.date.dateFormat'));
+               },
+               
+               /**
+                * Returns the formatted time.
+                * 
+                * @param       {Date}          date            date object
+                * @returns     {string}        formatted time
+                */
+               formatTime: function(date) {
+                       return this.format(date, Language.get('wcf.date.timeFormat'));
+               },
+               
+               /**
+                * Returns the formatted date time.
+                * 
+                * @param       {Date}          date            date object
+                * @returns     {string}        formatted date time
+                */
+               formatDateTime: function(date) {
+                       return this.format(date, Language.get('wcf.date.dateTimeFormat').replace(/%date%/, Language.get('wcf.date.dateFormat')).replace(/%time%/, Language.get('wcf.date.timeFormat')));
+               },
+               
+               /**
+                * Formats a date using PHP's `date()` modifiers.
+                * 
+                * @param       {Date}          date            date object
+                * @param       {string}        format          output format
+                * @returns     {string}        formatted date
+                */
+               format: function(date, format) {
+                       var char;
+                       var out = '';
+                       
+                       // ISO 8601 date, best recognition by PHP's strtotime()
+                       if (format === 'c') {
+                               format = 'Y-m-dTH:i:sP';
+                       }
+                       
+                       for (var i = 0, length = format.length; i < length; i++) {
+                               switch (format[i]) {
+                                       // seconds
+                                       case 's':
+                                               // `00` through `59`
+                                               char = ('0' + date.getSeconds().toString()).slice(-2);
+                                               break;
+                                       
+                                       // minutes
+                                       case 'i':
+                                               // `00` through `59`
+                                               char = date.getMinutes();
+                                               if (char < 10) char = "0" + char;
+                                               break;
+                                       
+                                       // hours
+                                       case 'a':
+                                               // `am` or `pm`
+                                               char = (date.getHours() > 11) ? 'pm' : 'am';
+                                               break;
+                                       case 'g':
+                                               // `1` through `12`
+                                               char = date.getHours();
+                                               if (char === 0) char = 12;
+                                               else if (char > 12) char -= 12;
+                                               break;
+                                       case 'h':
+                                               // `01` through `12`
+                                               char = date.getHours();
+                                               if (char === 0) char = 12;
+                                               else if (char > 12) char -= 12;
+                                               
+                                               char = ('0' + char.toString()).slice(-2);
+                                               break;
+                                       case 'A':
+                                               // `AM` or `PM`
+                                               char = (date.getHours() > 11) ? 'PM' : 'AM';
+                                               break;
+                                       case 'G':
+                                               // `0` through `23`
+                                               char = date.getHours();
+                                               break;
+                                       case 'H':
+                                               // `00` through `23`
+                                               char = date.getHours();
+                                               char = ('0' + char.toString()).slice(-2);
+                                               break;
+                                       
+                                       // day
+                                       case 'd':
+                                               // `01` through `31`
+                                               char = date.getDate();
+                                               char = ('0' + char.toString()).slice(-2);
+                                               break;
+                                       case 'j':
+                                               // `1` through `31`
+                                               char = date.getDate();
+                                               break;
+                                       case 'l':
+                                               // `Monday` through `Sunday` (localized)
+                                               char = Language.get('__days')[date.getDay()];
+                                               break;
+                                       case 'D':
+                                               // `Mon` through `Sun` (localized)
+                                               char = Language.get('__daysShort')[date.getDay()];
+                                               break;
+                                       case 'S':
+                                               // ignore english ordinal suffix
+                                               char = '';
+                                               break;
+                                       
+                                       // month
+                                       case 'm':
+                                               // `01` through `12`
+                                               char = date.getMonth() + 1;
+                                               char = ('0' + char.toString()).slice(-2);
+                                               break;
+                                       case 'n':
+                                               // `1` through `12`
+                                               char = date.getMonth() + 1;
+                                               break;
+                                       case 'F':
+                                               // `January` through `December` (localized)
+                                               char = Language.get('__months')[date.getMonth()];
+                                               break;
+                                       case 'M':
+                                               // `Jan` through `Dec` (localized)
+                                               char = Language.get('__monthsShort')[date.getMonth()];
+                                               break;
+                                       
+                                       // year
+                                       case 'y':
+                                               // `00` through `99`
+                                               char = date.getYear().toString().replace(/^\d{2}/, '');
+                                               break;
+                                       case 'Y':
+                                               // Examples: `1988` or `2015`
+                                               char = date.getFullYear();
+                                               break;
+                                       
+                                       // timezone
+                                       case 'P':
+                                               var offset = date.getTimezoneOffset();
+                                               char = (offset > 0) ? '-' : '+';
+                                               
+                                               offset = Math.abs(offset);
+                                               
+                                               char += ('0' + (~~(offset / 60)).toString()).slice(-2);
+                                               char += ':';
+                                               char += ('0' + (offset % 60).toString()).slice(-2);
+                                               
+                                               break;
+                                               
+                                       // specials
+                                       case 'r':
+                                               char = date.toString();
+                                               break;
+                                       case 'U':
+                                               char = Math.round(date.getTime() / 1000);
+                                               break;
+                                       
+                                       default:
+                                               char = format[i];
+                                               break;
+                               }
+                               
+                               out += char;
+                       }
+                       
+                       return out;
+               },
+               
+               /**
+                * Returns UTC timestamp, if date is not given, current time will be used.
+                * 
+                * @param       {Date}          date    target date
+                * @return      {int}           UTC timestamp in seconds
+                */
+               gmdate: function(date) {
+                       if (!(date instanceof Date)) {
+                               date = new Date();
+                       }
+                       
+                       return Math.round(Date.UTC(
+                               date.getUTCFullYear(),
+                               date.getUTCMonth(),
+                               date.getUTCDay(),
+                               date.getUTCHours(),
+                               date.getUTCMinutes(),
+                               date.getUTCSeconds()
+                       ) / 1000);
+               },
+               
+               /**
+                * Returns a Date object with precise offset (including timezone and local timezone).
+                * 
+                * @param       {int}           timestamp       timestamp in milliseconds
+                * @param       {int}           offset          timezone offset in milliseconds
+                * @return      {Date}          localized date
+                */
+               getTimezoneDate: function(timestamp, offset) {
+                       var date = new Date(timestamp);
+                       var localOffset = date.getTimezoneOffset() * 60000;
+                       
+                       return new Date((timestamp + localOffset + offset));
+               }
+       };
+       
+       return DateUtil;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Dictionary.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Dictionary.js
new file mode 100644 (file)
index 0000000..ebf1032
--- /dev/null
@@ -0,0 +1,175 @@
+/**
+ * Dictionary implementation relying on an object or if supported on a Map to hold key => value data.
+ * 
+ * If you're looking for a dictionary with object keys, please see `WoltLabSuite/Core/ObjectMap`.
+ * 
+ * @author     Tim Duesterhus, Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Dictionary
+ */
+define(['Core'], function(Core) {
+       "use strict";
+       
+       var _hasMap = objOwns(window, 'Map') && typeof window.Map === 'function';
+       
+       /**
+        * @constructor
+        */
+       function Dictionary() {
+               this._dictionary = (_hasMap) ? new Map() : {};
+       }
+       Dictionary.prototype = {
+               /**
+                * Sets a new key with given value, will overwrite an existing key.
+                * 
+                * @param       {(number|string)}       key     key
+                * @param       {?}                     value   value
+                */
+               set: function(key, value) {
+                       if (typeof key === 'number') key = key.toString();
+                       
+                       if (typeof key !== "string") {
+                               throw new TypeError("Only strings can be used as keys, rejected '" + key + "' (" + typeof key + ").");
+                       }
+                       
+                       if (_hasMap) this._dictionary.set(key, value);
+                       else this._dictionary[key] = value;
+               },
+               
+               /**
+                * Removes a key from the dictionary.
+                * 
+                * @param       {(number|string)}       key     key
+                */
+               'delete': function(key) {
+                       if (typeof key === 'number') key = key.toString();
+                       
+                       if (_hasMap) this._dictionary['delete'](key);
+                       else this._dictionary[key] = undefined;
+               },
+               
+               /**
+                * Returns true if dictionary contains a value for given key and is not undefined.
+                * 
+                * @param       {(number|string)}       key     key
+                * @return      {boolean}       true if key exists and value is not undefined
+                */
+               has: function(key) {
+                       if (typeof key === 'number') key = key.toString();
+                       
+                       if (_hasMap) return this._dictionary.has(key);
+                       else {
+                               return (objOwns(this._dictionary, key) && typeof this._dictionary[key] !== "undefined");
+                       }
+               },
+               
+               /**
+                * Retrieves a value by key, returns undefined if there is no match.
+                * 
+                * @param       {(number|string)}       key     key
+                * @return      {*}
+                */
+               get: function(key) {
+                       if (typeof key === 'number') key = key.toString();
+                       
+                       if (this.has(key)) {
+                               if (_hasMap) return this._dictionary.get(key);
+                               else return this._dictionary[key];
+                       }
+                       
+                       return undefined;
+               },
+               
+               /**
+                * Iterates over the dictionary keys and values, callback function should expect the
+                * value as first parameter and the key name second.
+                * 
+                * @param       {function<*, string>}   callback        callback for each iteration
+                */
+               forEach: function(callback) {
+                       if (typeof callback !== "function") {
+                               throw new TypeError("forEach() expects a callback as first parameter.");
+                       }
+                       
+                       if (_hasMap) {
+                               this._dictionary.forEach(callback);
+                       }
+                       else {
+                               var keys = Object.keys(this._dictionary);
+                               for (var i = 0, length = keys.length; i < length; i++) {
+                                       callback(this._dictionary[keys[i]], keys[i]);
+                               }
+                       }
+               },
+               
+               /**
+                * Merges one or more Dictionary instances into this one.
+                * 
+                * @param       {...Dictionary}         var_args        one or more Dictionary instances
+                */
+               merge: function() {
+                       for (var i = 0, length = arguments.length; i < length; i++) {
+                               var dictionary = arguments[i];
+                               if (!(dictionary instanceof Dictionary)) {
+                                       throw new TypeError("Expected an object of type Dictionary, but argument " + i + " is not.");
+                               }
+                               
+                               dictionary.forEach((function(value, key) {
+                                       this.set(key, value);
+                               }).bind(this));
+                       }
+               },
+               
+               /**
+                * Returns the object representation of the dictionary.
+                * 
+                * @return      {object}        dictionary's object representation
+                */
+               toObject: function() {
+                       if (!_hasMap) return Core.clone(this._dictionary);
+                       
+                       var object = { };
+                       this._dictionary.forEach(function(value, key) {
+                               object[key] = value;
+                       });
+                       
+                       return object;
+               }
+       };
+       
+       /**
+        * Creates a new Dictionary based on the given object.
+        * All properties that are owned by the object will be added
+        * as keys to the resulting Dictionary.
+        * 
+        * @param       {object}        object
+        * @return      {Dictionary}
+        */
+       Dictionary.fromObject = function(object) {
+               var result = new Dictionary();
+               
+               for (var key in object) {
+                       if (objOwns(object, key)) {
+                               result.set(key, object[key]);
+                       }
+               }
+               
+               return result;
+       };
+       
+       Object.defineProperty(Dictionary.prototype, 'size', {
+               enumerable: false,
+               configurable: true,
+               get: function() {
+                       if (_hasMap) {
+                               return this._dictionary.size;
+                       }
+                       else {
+                               return Object.keys(this._dictionary).length;
+                       }
+               }
+       });
+       
+       return Dictionary;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Change/Listener.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Change/Listener.js
new file mode 100644 (file)
index 0000000..7862980
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * 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     WoltLabSuite/Core/Dom/Change/Listener
+ */
+define(['CallbackList'], function(CallbackList) {
+       "use strict";
+       
+       var _callbackList = new CallbackList();
+       var _hot = false;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Dom/Change/Listener
+        */
+       return {
+               /**
+                * @see WoltLabSuite/Core/CallbackList#add
+                */
+               add: _callbackList.add.bind(_callbackList),
+               
+               /**
+                * @see WoltLabSuite/Core/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;
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Traverse.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Traverse.js
new file mode 100644 (file)
index 0000000..4228b49
--- /dev/null
@@ -0,0 +1,268 @@
+/**
+ * Provides helper functions to traverse the DOM.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Dom/Traverse
+ */
+define([], function() {
+       "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 el.matches(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     WoltLabSuite/Core/Dom/Traverse
+        */
+       return {
+               /**
+                * 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}        tagName         element tag 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', TAG_NAME, tagName);
+               },
+               
+               /**
+                * 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}        tagName         element tag 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', TAG_NAME, tagName);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Util.js
new file mode 100644 (file)
index 0000000..d8d2d0c
--- /dev/null
@@ -0,0 +1,431 @@
+/**
+ * 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     WoltLabSuite/Core/Dom/Util
+ */
+define(['Environment', 'StringUtil'], function(Environment, StringUtil) {
+       "use strict";
+       
+       function _isBoundaryNode(element, ancestor, position) {
+               if (!ancestor.contains(element)) {
+                       throw new Error("Ancestor element does not contain target element.");
+               }
+               
+               var node, whichSibling = position + 'Sibling';
+               while (element !== null && element !== ancestor) {
+                       if (element[position + 'ElementSibling'] !== null) {
+                               return false;
+                       }
+                       else if (element[whichSibling]) {
+                               node = element[whichSibling];
+                               while (node) {
+                                       if (node.textContent.trim() !== '') {
+                                               return false;
+                                       }
+                                       
+                                       node = node[whichSibling];
+                               }
+                       }
+                       
+                       element = element.parentNode;
+               }
+               
+               return true;
+       }
+       
+       var _idCounter = 0;
+       
+       /**
+        * @exports     WoltLabSuite/Core/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 = elCreate('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 (elById(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 instanceof Element)) {
+                               throw new TypeError("Expected a valid DOM element as argument.");
+                       }
+                       
+                       var id = elAttr(el, 'id');
+                       if (!id) {
+                               id = this.getUniqueId();
+                               elAttr(el, 'id', id);
+                       }
+                       
+                       return id;
+               },
+               
+               /**
+                * Returns the outer height of an element including margins.
+                * 
+                * @param       {Element}               el              element
+                * @param       {CSSStyleDeclaration=}  styles          result of window.getComputedStyle()
+                * @return      {int}                   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      {int}   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: int, width: int}}     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: int, top: int}}         offset relative to top left corner
+                */
+               offset: function(el) {
+                       var rect = el.getBoundingClientRect();
+                       
+                       return {
+                               top: Math.round(rect.top + (window.scrollY || window.pageYOffset)),
+                               left: Math.round(rect.left + (window.scrollX || window.pageXOffset))
+                       };
+               },
+               
+               /**
+                * 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.childNodes.length === 0) {
+                               parentEl.appendChild(el);
+                       }
+                       else {
+                               parentEl.insertBefore(el, parentEl.childNodes[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, *>}     styles  list of CSS styles
+                */
+               setStyles: function(el, styles) {
+                       var important = false;
+                       for (var property in styles) {
+                               if (styles.hasOwnProperty(property)) {
+                                       if (/ !important$/.test(styles[property])) {
+                                               important = true;
+                                               
+                                               styles[property] = styles[property].replace(/ !important$/, '');
+                                       }
+                                       else {
+                                               important = false;
+                                       }
+                                       
+                                       // for a set style property with priority = important, some browsers are
+                                       // not able to overwrite it with a property != important; removing the
+                                       // property first solves this issue
+                                       if (el.style.getPropertyPriority(property) === 'important' && !important) {
+                                               el.style.removeProperty(property);
+                                       }
+                                       
+                                       el.style.setProperty(property, styles[property], (important ? 'important' : ''));
+                               }
+                       }
+               },
+               
+               /**
+                * 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      {int}                   property value as integer
+                */
+               styleAsInt: function(styles, propertyName) {
+                       var value = styles.getPropertyValue(propertyName);
+                       if (value === null) {
+                               return 0;
+                       }
+                       
+                       return parseInt(value);
+               },
+               
+               /**
+                * Sets the inner HTML of given element and reinjects <script> elements to be properly executed.
+                * 
+                * @see         http://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0
+                * @param       {Element}       element         target element
+                * @param       {string}        innerHtml       HTML string
+                */
+               setInnerHtml: function(element, innerHtml) {
+                       element.innerHTML = innerHtml;
+                       
+                       var newScript, script, scripts = elBySelAll('script', element);
+                       for (var i = 0, length = scripts.length; i < length; i++) {
+                               script = scripts[i];
+                               newScript = elCreate('script');
+                               if (script.src) {
+                                       newScript.src = script.src;
+                               }
+                               else {
+                                       newScript.textContent = script.textContent;
+                               }
+                               
+                               element.appendChild(newScript);
+                               elRemove(script);
+                       }
+               },
+               
+               /**
+                * 
+                * @param html
+                * @param {Element} referenceElement
+                * @param insertMethod
+                */
+               insertHtml: function(html, referenceElement, insertMethod) {
+                       var element = elCreate('div');
+                       this.setInnerHtml(element, html);
+                       
+                       if (insertMethod === 'append' || insertMethod === 'after') {
+                               while (element.childNodes.length) {
+                                       if (insertMethod === 'append') {
+                                               referenceElement.appendChild(element.childNodes[0]);
+                                       }
+                                       else {
+                                               this.insertAfter(element.childNodes[0], referenceElement);
+                                       }
+                               }
+                       }
+                       else if (insertMethod === 'prepend' || insertMethod === 'before') {
+                               for (var i = element.childNodes.length - 1; i >= 0; i--) {
+                                       if (insertMethod === 'prepend') {
+                                               this.prepend(element.childNodes[i], referenceElement);
+                                       }
+                                       else {
+                                               referenceElement.parentNode.insertBefore(element.childNodes[i], referenceElement);
+                                       }
+                               }
+                       }
+                       else {
+                               throw new Error("Unknown insert method '" + insertMethod + "'.");
+                       }
+               },
+               
+               /**
+                * Returns true if `element` contains the `child` element.
+                * 
+                * @param       {Element}       element         container element
+                * @param       {Element}       child           child element
+                * @returns     {boolean}       true if `child` is a (in-)direct child of `element`
+                */
+               contains: function(element, child) {
+                       while (child !== null) {
+                               child = child.parentNode;
+                               
+                               if (element === child) {
+                                       return true;
+                               }
+                       }
+                       
+                       return false;
+               },
+               
+               /**
+                * Retrieves all data attributes from target element, optionally allowing for
+                * a custom prefix that serves two purposes: First it will restrict the results
+                * for items starting with it and second it will remove that prefix.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {string=}       prefix          attribute prefix
+                * @param       {boolean=}      camelCaseName  transform attribute names into camel case using dashes as separators
+                * @param       {boolean=}      idToUpperCase   transform '-id' into 'ID'
+                * @returns     {object<string, string>}        list of data attributes
+                */
+               getDataAttributes: function(element, prefix, camelCaseName, idToUpperCase) {
+                       prefix = prefix || '';
+                       if (!/^data-/.test(prefix)) prefix = 'data-' + prefix;
+                       camelCaseName = (camelCaseName === true);
+                       idToUpperCase = (idToUpperCase === true);
+                       
+                       var attribute, attributes = {}, name, tmp;
+                       for (var i = 0, length = element.attributes.length; i < length; i++) {
+                               attribute = element.attributes[i];
+                               
+                               if (attribute.name.indexOf(prefix) === 0) {
+                                       name = attribute.name.replace(new RegExp('^' + prefix), '');
+                                       if (camelCaseName) {
+                                               tmp = name.split('-');
+                                               name = '';
+                                               for (var j = 0, innerLength = tmp.length; j < innerLength; j++) {
+                                                       if (name.length) {
+                                                               if (idToUpperCase && tmp[j] === 'id') {
+                                                                       tmp[j] = 'ID';
+                                                               }
+                                                               else {
+                                                                       tmp[j] = StringUtil.ucfirst(tmp[j]);
+                                                               }
+                                                       }
+                                                       
+                                                       name += tmp[j];
+                                               }
+                                       }
+                                       
+                                       attributes[name] = attribute.value;
+                               }
+                       }
+                       
+                       return attributes;
+               },
+               
+               /**
+                * Unwraps contained nodes by moving them out of `element` while
+                * preserving their previous order. Target element will be removed
+                * at the end of the operation.
+                * 
+                * @param       {Element}       element         target element
+                */
+               unwrapChildNodes: function(element) {
+                       var parent = element.parentNode;
+                       while (element.childNodes.length) {
+                               parent.insertBefore(element.childNodes[0], element);
+                       }
+                       
+                       elRemove(element);
+               },
+               
+               /**
+                * Replaces an element by moving all child nodes into the new element
+                * while preserving their previous order. The old element will be removed
+                * at the end of the operation.
+                * 
+                * @param       {Element}       oldElement      old element
+                * @param       {Element}       newElement      old element
+                */
+               replaceElement: function(oldElement, newElement) {
+                       while (oldElement.childNodes.length) {
+                               newElement.appendChild(oldElement.childNodes[0]);
+                       }
+                       
+                       oldElement.parentNode.insertBefore(newElement, oldElement);
+                       elRemove(oldElement);
+               },
+               
+               /**
+                * Returns true if given element is the most left node of the ancestor, that is
+                * a node without any content nor elements before it or its parent nodes.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {Element}       ancestor        ancestor element, must contain the target element
+                * @returns     {boolean}       true if target element is the most left node
+                */
+               isAtNodeStart: function(element, ancestor) {
+                       return _isBoundaryNode(element, ancestor, 'previous');
+               },
+               
+               /**
+                * Returns true if given element is the most right node of the ancestor, that is
+                * a node without any content nor elements after it or its parent nodes.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {Element}       ancestor        ancestor element, must contain the target element
+                * @returns     {boolean}       true if target element is the most right node
+                */
+               isAtNodeEnd: function(element, ancestor) {
+                       return _isBoundaryNode(element, ancestor, 'next');
+               }
+       };
+       
+       // expose on window object for backward compatibility
+       window.bc_wcfDomUtil = DomUtil;
+       
+       return DomUtil;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Environment.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Environment.js
new file mode 100644 (file)
index 0000000..b4b4758
--- /dev/null
@@ -0,0 +1,124 @@
+/**
+ * Provides basic details on the JavaScript environment.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Environment
+ */
+define([], function() {
+       "use strict";
+       
+       var _browser = 'other';
+       var _editor = 'none';
+       var _platform = 'desktop';
+       var _touch = false;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Enviroment
+        */
+       return {
+               /**
+                * Determines environment variables.
+                */
+               setup: function() {
+                       if (typeof window.chrome === 'object') {
+                               // this detects Opera as well, we could check for window.opr if we need to
+                               _browser = 'chrome';
+                       }
+                       else {
+                               var styles = window.getComputedStyle(document.documentElement);
+                               for (var i = 0, length = styles.length; i < length; i++) {
+                                       var property = styles[i];
+                                       
+                                       if (property.indexOf('-ms-') === 0) {
+                                               // it is tempting to use 'msie', but it wouldn't really represent 'Edge'
+                                               _browser = 'microsoft';
+                                       }
+                                       else if (property.indexOf('-moz-') === 0) {
+                                               _browser = 'firefox';
+                                       }
+                                       else if (property.indexOf('-webkit-') === 0) {
+                                               _browser = 'safari';
+                                       }
+                               }
+                       }
+                       
+                       var ua = window.navigator.userAgent.toLowerCase();
+                       if (ua.indexOf('crios') !== -1) {
+                               _browser = 'chrome';
+                               _platform = 'ios';
+                       }
+                       else if (/(?:iphone|ipad|ipod)/.test(ua)) {
+                               _browser = 'safari';
+                               _platform = 'ios';
+                       }
+                       else if (ua.indexOf('android') !== -1) {
+                               _platform = 'android';
+                       }
+                       else if (ua.indexOf('iemobile') !== -1) {
+                               _browser = 'microsoft';
+                               _platform = 'windows';
+                       }
+                       
+                       if (_platform === 'desktop' && (ua.indexOf('mobile') !== -1 || ua.indexOf('tablet') !== -1)) {
+                               _platform = 'mobile';
+                       }
+                       
+                       _editor = 'redactor';
+                       _touch = (!!('ontouchstart' in window) || (!!('msMaxTouchPoints' in window.navigator) && window.navigator.msMaxTouchPoints > 0) || window.DocumentTouch && document instanceof DocumentTouch);
+               },
+               
+               /**
+                * Returns the lower-case browser identifier.
+                * 
+                * Possible values:
+                *  - chrome: Chrome and Opera
+                *  - firefox
+                *  - microsoft: Internet Explorer and Microsoft Edge
+                *  - safari
+                * 
+                * @return      {string}        browser identifier
+                */
+               browser: function() {
+                       return _browser;
+               },
+               
+               /**
+                * Returns the available editor's name or an empty string.
+                * 
+                * @return      {string}        editor name
+                */
+               editor: function() {
+                       return _editor;
+               },
+               
+               /**
+                * Returns the browser platform.
+                * 
+                * Possible values:
+                *  - desktop
+                *  - android
+                *  - ios: iPhone, iPad and iPod
+                *  - windows: Windows on phones/tablets
+                * 
+                * @return      {string}        browser platform
+                */
+               platform: function() {
+                       return _platform;
+               },
+               
+               /**
+                * Returns true if browser is potentially used with a touchscreen.
+                * 
+                * Warning: Detecting touch is unreliable and should be avoided at all cost.
+                * 
+                * @deprecated  3.0 - exists for backward-compatibility only, will be removed in the future
+                * 
+                * @return      {boolean}       true if a touchscreen is present
+                */
+               touch: function() {
+                       return _touch;
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Handler.js
new file mode 100644 (file)
index 0000000..63794a8
--- /dev/null
@@ -0,0 +1,114 @@
+/**
+ * Versatile event system similar to the WCF-PHP counter part.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Event/Handler
+ */
+define(['Core', 'Dictionary'], function(Core, Dictionary) {
+       "use strict";
+       
+       var _listeners = new Dictionary();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Event/Handler
+        */
+       return {
+               /**
+                * Adds an event listener.
+                * 
+                * @param       {string}                identifier      event identifier
+                * @param       {string}                action          action name
+                * @param       {function(object)}      callback        callback function
+                * @return      {string}        uuid required for listener removal
+                */
+               add: function(identifier, action, callback) {
+                       if (typeof callback !== 'function') {
+                               throw new TypeError("[WoltLabSuite/Core/Event/Handler] Expected a valid callback for '" + action + "@" + identifier + "'.");
+                       }
+                       
+                       var actions = _listeners.get(identifier);
+                       if (actions === undefined) {
+                               actions = new Dictionary();
+                               _listeners.set(identifier, actions);
+                       }
+                       
+                       var callbacks = actions.get(action);
+                       if (callbacks === undefined) {
+                               callbacks = new Dictionary();
+                               actions.set(action, callbacks);
+                       }
+                       
+                       var uuid = Core.getUuid();
+                       callbacks.set(uuid, callback);
+                       
+                       return uuid;
+               },
+               
+               /**
+                * Fires an event and notifies all listeners.
+                * 
+                * @param       {string}        identifier      event identifier
+                * @param       {string}        action          action name
+                * @param       {object=}       data            event data
+                */
+               fire: function(identifier, action, data) {
+                       data = data || {};
+                       
+                       var actions = _listeners.get(identifier);
+                       if (actions !== undefined) {
+                               var callbacks = actions.get(action);
+                               if (callbacks !== undefined) {
+                                       callbacks.forEach(function(callback) {
+                                               callback(data);
+                                       });
+                               }
+                       }
+               },
+               
+               /**
+                * Removes an event listener, requires the uuid returned by add().
+                * 
+                * @param       {string}        identifier      event identifier
+                * @param       {string}        action          action name
+                * @param       {string}        uuid            listener uuid
+                */
+               remove: function(identifier, action, uuid) {
+                       var actions = _listeners.get(identifier);
+                       if (actions === undefined) {
+                               return;
+                       }
+                       
+                       var callbacks = actions.get(action);
+                       if (callbacks === undefined) {
+                               return;
+                       }
+                       
+                       callbacks['delete'](uuid);
+               },
+               
+               /**
+                * Removes all event listeners for given action. Omitting the second parameter will
+                * remove all listeners for this identifier.
+                * 
+                * @param       {string}        identifier      event identifier
+                * @param       {string=}       action          action name
+                */
+               removeAll: function(identifier, action) {
+                       if (typeof action !== 'string') action = undefined;
+                       
+                       var actions = _listeners.get(identifier);
+                       if (actions === undefined) {
+                               return;
+                       }
+                       
+                       if (typeof action === 'undefined') {
+                               _listeners['delete'](identifier);
+                       }
+                       else {
+                               actions['delete'](action);
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Event/Key.js
new file mode 100644 (file)
index 0000000..4912826
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * Provides reliable checks for common key presses, uses `Event.key` on supported browsers
+ * or the deprecated `Event.which`.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Event/Key
+ */
+define([], function() {
+       "use strict";
+       
+       function _isKey(event, key, which) {
+               if (!(event instanceof Event)) {
+                       throw new TypeError("Expected a valid event when testing for key '" + key + "'.");
+               }
+               
+               return event.key === key || event.which === which;
+       }
+       
+       /**
+        * @exports     WoltLabSuite/Core/Event/Key
+        */
+       return {
+               /**
+                * Returns true if pressed key equals 'ArrowDown'.
+                * 
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               ArrowDown: function(event) {
+                       return _isKey(event, 'ArrowDown', 40);
+               },
+               
+               /**
+                * Returns true if pressed key equals 'ArrowLeft'.
+                * 
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               ArrowLeft: function(event) {
+                       return _isKey(event, 'ArrowLeft', 37);
+               },
+               
+               /**
+                * Returns true if pressed key equals 'ArrowRight'.
+                * 
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               ArrowRight: function(event) {
+                       return _isKey(event, 'ArrowRight', 39);
+               },
+               
+               /**
+                * Returns true if pressed key equals 'ArrowUp'.
+                * 
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               ArrowUp: function(event) {
+                       return _isKey(event, 'ArrowUp', 38);
+               },
+               
+               /**
+                * Returns true if pressed key equals 'Enter'.
+                * 
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               Enter: function(event) {
+                       return _isKey(event, 'Enter', 13);
+               },
+               
+               /**
+                * Returns true if pressed key equals 'Escape'.
+                * 
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               Escape: function(event) {
+                       return _isKey(event, 'Escape', 27);
+               },
+               
+               /**
+                * Returns true if pressed key equals 'Tab'.
+                * 
+                * @param       {Event}         event           event object
+                * @return      {boolean}
+                */
+               Tab: function(event) {
+                       return _isKey(event, 'Tab', 9);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/File/Util.js b/wcfsetup/install/files/js/WoltLabSuite/Core/File/Util.js
new file mode 100644 (file)
index 0000000..2728ec5
--- /dev/null
@@ -0,0 +1,73 @@
+/**
+ * Provides helper functions to work with files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/File/Util
+ */
+define([], function() {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/File/Util
+        */
+       var FileUtil = {
+               /**
+                * Returns the FontAwesome icon CSS class name for a mime type.
+                * 
+                * @param       {string}        mimeType        mime type of the relevant file
+                * @return      {string}        FontAwesome icon CSS class name for the mime type
+                */
+               getIconClassByMimeType: function(mimeType) {
+                       if (mimeType.substr(0, 6) == 'image/') {
+                               return 'fa-file-image-o';
+                       }
+                       else if (mimeType.substr(0, 6) == 'video/') {
+                               return 'fa-file-video-o';
+                       }
+                       else if (mimeType.substr(0, 6) == 'audio/') {
+                               return 'fa-file-sound-o';
+                       }
+                       else if (mimeType.substr(0, 5) == 'text/') {
+                               return 'fa-file-text-o';
+                       }
+                       else {
+                               switch (mimeType) {
+                                       case 'application/msword':
+                                       case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+                                               return 'fa-file-word-o';
+                                       break;
+                                       
+                                       case 'application/pdf':
+                                               return 'fa-file-pdf-o';
+                                       break;
+                                       
+                                       case 'application/vnd.ms-powerpoint':
+                                       case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
+                                               return 'fa-file-powerpoint-o';
+                                       break;
+                                       
+                                       case 'application/vnd.ms-excel':
+                                       case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+                                               return 'fa-file-excel-o';
+                                       break;
+                                       
+                                       case 'application/zip':
+                                       case 'application/x-tar':
+                                       case 'application/x-gzip':
+                                               return 'fa-file-archive-o';
+                                       break;
+                                       
+                                       case 'application/xml':
+                                               return 'fa-file-text-o';
+                                       break;
+                               }
+                       }
+                       
+                       return 'fa-file-o';
+               }
+       };
+       
+       return FileUtil;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Language.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Language.js
new file mode 100644 (file)
index 0000000..17b7cec
--- /dev/null
@@ -0,0 +1,78 @@
+/**
+ * Manages language items.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Language
+ */
+define(['Dictionary', './Template'], function(Dictionary, Template) {
+       "use strict";
+       
+       var _languageItems = new Dictionary();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Language
+        */
+       var Language = {
+               /**
+                * Adds all the language items in the given object to the store.
+                * 
+                * @param       {Object.<string, string>}       object
+                */
+               addObject: function(object) {
+                       _languageItems.merge(Dictionary.fromObject(object));
+               },
+               
+               /**
+                * Adds a single language item to the store.
+                * 
+                * @param       {string}        key
+                * @param       {string}        value
+                */
+               add: function(key, value) {
+                       _languageItems.set(key, value);
+               },
+               
+               /**
+                * Fetches the language item specified by the given key.
+                * If the language item is a string it will be evaluated as
+                * WoltLabSuite/Core/Template with the given parameters.
+                * 
+                * @param       {string}        key             Language item to return.
+                * @param       {Object=}       parameters      Parameters to provide to WoltLabSuite/Core/Template.
+                * @return      {string}
+                */
+               get: function(key, parameters) {
+                       if (!parameters) parameters = { };
+                       
+                       var value = _languageItems.get(key);
+                       
+                       if (value === undefined) {
+                               // TODO
+                               //console.warn("Attempt to retrieve unknown phrase '" + key + "'.");
+                               //console.warn(new Error().stack);
+                               return key;
+                       }
+                       
+                       if (typeof value === 'string') {
+                               // lazily convert to WCF.Template
+                               try {
+                                       _languageItems.set(key, new Template(value));
+                               }
+                               catch (e) {
+                                       _languageItems.set(key, new Template('{literal}' + value.replace(/\{\/literal\}/g, '{/literal}{ldelim}/literal}{literal}') + '{/literal}'));
+                               }
+                               value = _languageItems.get(key);
+                       }
+                       
+                       if (value instanceof Template) {
+                               value = value.fetch(parameters);
+                       }
+                       
+                       return value;
+               }
+       };
+       
+       return Language;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Chooser.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Chooser.js
new file mode 100644 (file)
index 0000000..8752bbc
--- /dev/null
@@ -0,0 +1,298 @@
+/**
+ * Dropdown language chooser.
+ * 
+ * @author     Alexander Ebert, Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Language/Chooser
+ */
+define(['Dictionary', 'Language', 'Dom/Traverse', 'Dom/Util', 'ObjectMap', 'Ui/SimpleDropdown'], function(Dictionary, Language, DomTraverse, DomUtil, ObjectMap, UiSimpleDropdown) {
+       "use strict";
+       
+       var _choosers = new Dictionary();
+       var _didInit = false;
+       var _forms = new ObjectMap();
+       
+       var _callbackSubmit = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Language/Chooser
+        */
+       return {
+               /**
+                * Initializes a language chooser.
+                * 
+                * @param       {string}                                containerId             input element conainer id
+                * @param       {string}                                chooserId               input element id
+                * @param       {int}                                   languageId              selected language id
+                * @param       {object<int, object<string, string>>}   languages               data of available languages
+                * @param       {function}                              callback                function called after a language is selected
+                * @param       {boolean}                               allowEmptyValue         true if no language may be selected
+                */
+               init: function(containerId, chooserId, languageId, languages, callback, allowEmptyValue) {
+                       if (_choosers.has(chooserId)) {
+                               return;
+                       }
+                       
+                       var container = elById(containerId);
+                       if (container === null) {
+                               throw new Error("Expected a valid container id, cannot find '" + chooserId + "'.");
+                       }
+                       
+                       var element = elById(chooserId);
+                       if (element === null) {
+                               element = elCreate('input');
+                               elAttr(element, 'type', 'hidden');
+                               elAttr(element, 'id', chooserId);
+                               elAttr(element, 'name', chooserId);
+                               elAttr(element, 'value', languageId);
+                               
+                               container.appendChild(element);
+                       }
+                       
+                       this._initElement(chooserId, element, languageId, languages, callback, allowEmptyValue);
+               },
+               
+               /**
+                * Caches common event listener callbacks.
+                */
+               _setup: function() {
+                       if (_didInit) return;
+                       _didInit = true;
+                       
+                       _callbackSubmit = this._submit.bind(this);
+               },
+               
+               /**
+                * Sets up DOM and event listeners for a language chooser.
+                *
+                * @param       {string}                                chooserId               chooser id
+                * @param       {Element}                               element                 chooser element
+                * @param       {int}                                   languageId              selected language id
+                * @param       {object<int, object<string, string>>}   languages               data of available languages
+                * @param       {function}                              callback                callback function invoked on selection change
+                * @param       {boolean}                               allowEmptyValue         true if no language may be selected
+                */
+               _initElement: function(chooserId, element, languageId, languages, callback, allowEmptyValue) {
+                       var container;
+                       
+                       if (element.parentNode.nodeName === 'DD') {
+                               container = elCreate('div');
+                               container.className = 'dropdown';
+                               element.parentNode.insertBefore(container, element);
+                       }
+                       else {
+                               container = element.parentNode;
+                               container.classList.add('dropdown');
+                       }
+                       
+                       elHide(element);
+                       
+                       var dropdownToggle = elCreate('a');
+                       dropdownToggle.className = 'dropdownToggle dropdownIndicator boxFlag box24 inputPrefix' + (element.parentNode.nodeName === 'DD' ? ' button' : '');
+                       container.appendChild(dropdownToggle);
+                       
+                       var dropdownMenu = elCreate('ul');
+                       dropdownMenu.className = 'dropdownMenu';
+                       container.appendChild(dropdownMenu);
+                       
+                       var callbackClick = (function(event) {
+                               var languageId = ~~elData(event.currentTarget, 'language-id');
+                               
+                               var activeItem = DomTraverse.childByClass(dropdownMenu, 'active');
+                               if (activeItem !== null) activeItem.classList.remove('active');
+                               
+                               if (languageId) event.currentTarget.classList.add('active');
+                               
+                               this._select(chooserId, languageId, event.currentTarget);
+                       }).bind(this);
+                       
+                       // add language dropdown items
+                       var link, img, listItem, span;
+                       for (var availableLanguageId in languages) {
+                               if (languages.hasOwnProperty(availableLanguageId)) {
+                                       var language = languages[availableLanguageId];
+                                       
+                                       listItem = elCreate('li');
+                                       listItem.className = 'boxFlag';
+                                       listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                                       elData(listItem, 'language-id', availableLanguageId);
+                                       if (language.languageCode !== undefined) elData(listItem, 'language-code', language.languageCode);
+                                       dropdownMenu.appendChild(listItem);
+                                       
+                                       link = elCreate('a');
+                                       link.className = 'box24';
+                                       listItem.appendChild(link);
+                                       
+                                       img = elCreate('img');
+                                       elAttr(img, 'src', language.iconPath);
+                                       elAttr(img, 'alt', '');
+                                       img.className = 'iconFlag';
+                                       link.appendChild(img);
+                                       
+                                       span = elCreate('span');
+                                       span.textContent = language.languageName;
+                                       link.appendChild(span);
+                                       
+                                       if (availableLanguageId == languageId) {
+                                               dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
+                                       }
+                               }
+                       }
+                       
+                       // add dropdown item for "no selection"
+                       if (allowEmptyValue) {
+                               listItem = elCreate('li');
+                               listItem.className = 'dropdownDivider';
+                               dropdownMenu.appendChild(listItem);
+                               
+                               listItem = elCreate('li');
+                               elData(listItem, 'language-id', 0);
+                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                               dropdownMenu.appendChild(listItem);
+                               
+                               link = elCreate('a');
+                               link.textContent = Language.get('wcf.global.language.noSelection');
+                               listItem.appendChild(link);
+                               
+                               if (languageId === 0) {
+                                       dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
+                               }
+                               
+                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                       }
+                       else if (languageId === 0) {
+                               dropdownToggle.innerHTML = null;
+                               
+                               var div = elCreate('div');
+                               dropdownToggle.appendChild(div);
+                               
+                               span = elCreate('span');
+                               span.className = 'icon icon24 fa-question';
+                               div.appendChild(span);
+                               
+                               span = elCreate('span');
+                               span.textContent = Language.get('wcf.global.language.noSelection');
+                               div.appendChild(span);
+                       }
+                       
+                       UiSimpleDropdown.init(dropdownToggle);
+                       
+                       _choosers.set(chooserId, {
+                               callback: callback,
+                               dropdownMenu: dropdownMenu,
+                               dropdownToggle: dropdownToggle,
+                               element: element
+                       });
+                       
+                       // bind to submit event
+                       var form = DomTraverse.parentByTag(element, 'FORM');
+                       if (form !== null) {
+                               form.addEventListener('submit', _callbackSubmit);
+                               
+                               var chooserIds = _forms.get(form);
+                               if (chooserIds === undefined) {
+                                       chooserIds = [];
+                                       _forms.set(form, chooserIds);
+                               }
+                               
+                               chooserIds.push(chooserId);
+                       }
+               },
+               
+               /**
+                * Selects a language from the dropdown list.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @param       {int}           languageId      language id or `0` to disable i18n
+                * @param       {Element=}      listItem        selected list item
+                */
+               _select: function(chooserId, languageId, listItem) {
+                       var chooser = _choosers.get(chooserId);
+                       
+                       if (listItem === undefined) {
+                               var listItems = chooser.dropdownMenu.childNodes;
+                               for (var i = 0, length = listItems.length; i < length; i++) {
+                                       var _listItem = listItems[i];
+                                       if (~~elData(_listItem, 'language-id') === languageId) {
+                                               listItem = _listItem;
+                                               break;
+                                       }
+                               }
+                               
+                               if (listItem === undefined) {
+                                       throw new Error("Cannot select unknown language id '" + languageId + "'");
+                               }
+                       }
+                       
+                       chooser.element.value = languageId;
+                       
+                       chooser.dropdownToggle.innerHTML = listItem.firstChild.innerHTML;
+                       
+                       _choosers.set(chooserId, chooser);
+                       
+                       // execute callback
+                       if (typeof chooser.callback === 'function') {
+                               chooser.callback(listItem);
+                       }
+               },
+               
+               /**
+                * Inserts hidden fields for the language chooser value on submit.
+                *
+                * @param       {object}        event           event object
+                */
+               _submit: function(event) {
+                       var elementIds = _forms.get(event.currentTarget);
+                       
+                       var input;
+                       for (var i = 0, length = elementIds.length; i < length; i++) {
+                               input = elCreate('input');
+                               input.type = 'hidden';
+                               input.name = elementIds[i];
+                               input.value = this.getLanguageId(elementIds[i]);
+                               
+                               event.currentTarget.appendChild(input);
+                       }
+               },
+               
+               /**
+                * Returns the chooser for an input field.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @return      {Dictionary}    data of the chooser
+                */
+               getChooser: function(chooserId) {
+                       var chooser = _choosers.get(chooserId);
+                       if (chooser === undefined) {
+                               throw new Error("Expected a valid language chooser input element, '" + chooserId + "' is not i18n input field.");
+                       }
+                       
+                       return chooser;
+               },
+               
+               /**
+                * Returns the selected language for a certain chooser.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @return      {int}           chosen language id
+                */
+               getLanguageId: function(chooserId) {
+                       return ~~this.getChooser(chooserId).element.value;
+               },
+               
+               /**
+                * Sets the language for a certain chooser.
+                * 
+                * @param       {string}        chooserId       input element id
+                * @param       {int}           languageId      language id to be set
+                */
+               setLanguageId: function(chooserId, languageId) {
+                       if (_choosers.get(chooserId) === undefined) {
+                               throw new Error("Expected a valid  input element, '" + chooserId + "' is not i18n input field.");
+                       }
+                       
+                       this._select(chooserId, languageId);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Input.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Input.js
new file mode 100644 (file)
index 0000000..864c655
--- /dev/null
@@ -0,0 +1,460 @@
+/**
+ * I18n interface for input and textarea fields.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Language/Input
+ */
+define(['Core', 'Dictionary', 'Language', 'ObjectMap', 'StringUtil', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function(Core, Dictionary, Language, ObjectMap, StringUtil, DomTraverse, DomUtil, UiSimpleDropdown) {
+       "use strict";
+       
+       var _elements = new Dictionary();
+       var _didInit = false;
+       var _forms = new ObjectMap();
+       var _values = new Dictionary();
+       
+       var _callbackDropdownToggle = null;
+       var _callbackSubmit = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Language/Input
+        */
+       var LanguageInput = {
+               /**
+                * Initializes an input field.
+                * 
+                * @param       {string}                        elementId               input element id
+                * @param       {object<int, string>}           values                  preset values per language id
+                * @param       {object<int, string>}           availableLanguages      language names per language id
+                * @param       {boolean}                       forceSelection          require i18n input
+                */
+               init: function(elementId, values, availableLanguages, forceSelection) {
+                       if (_values.has(elementId)) {
+                               return;
+                       }
+                       
+                       var element = elById(elementId);
+                       if (element === null) {
+                               throw new Error("Expected a valid element id, cannot find '" + elementId + "'.");
+                       }
+                       
+                       this._setup();
+                       
+                       // unescape values
+                       var unescapedValues = new Dictionary();
+                       for (var key in values) {
+                               if (objOwns(values, key)) {
+                                       unescapedValues.set(~~key, StringUtil.unescapeHTML(values[key]));
+                               }
+                       }
+                       
+                       _values.set(elementId, unescapedValues);
+                       
+                       this._initElement(elementId, element, unescapedValues, availableLanguages, forceSelection);
+               },
+               
+               /**
+                * Caches common event listener callbacks.
+                */
+               _setup: function() {
+                       if (_didInit) return;
+                       _didInit = true;
+                       
+                       _callbackDropdownToggle = this._dropdownToggle.bind(this);
+                       _callbackSubmit = this._submit.bind(this);
+               },
+               
+               /**
+                * Sets up DOM and event listeners for an input field.
+                * 
+                * @param       {string}                        elementId               input element id
+                * @param       {Element}                       element                 input or textarea element
+                * @param       {Dictionary}                    values                  preset values per language id
+                * @param       {object<int, string>}           availableLanguages      language names per language id
+                * @param       {boolean}                       forceSelection          require i18n input
+                */
+               _initElement: function(elementId, element, values, availableLanguages, forceSelection) {
+                       var container = element.parentNode;
+                       if (!container.classList.contains('inputAddon')) {
+                               container = elCreate('div');
+                               container.className = 'inputAddon' + (element.nodeName === 'TEXTAREA' ? ' inputAddonTextarea' : '');
+                               elData(container, 'input-id', elementId);
+                               
+                               element.parentNode.insertBefore(container, element);
+                               container.appendChild(element);
+                       }
+                       
+                       container.classList.add('dropdown');
+                       var button = elCreate('span');
+                       button.className = 'button dropdownToggle inputPrefix';
+                       
+                       var span = elCreate('span');
+                       span.textContent = Language.get('wcf.global.button.disabledI18n');
+                       
+                       button.appendChild(span);
+                       container.insertBefore(button, element);
+                       
+                       var dropdownMenu = elCreate('ul');
+                       dropdownMenu.className = 'dropdownMenu';
+                       DomUtil.insertAfter(dropdownMenu, button);
+                       
+                       var callbackClick = (function(event, isInit) {
+                               var languageId = ~~elData(event.currentTarget, 'language-id');
+                               
+                               var activeItem = DomTraverse.childByClass(dropdownMenu, 'active');
+                               if (activeItem !== null) activeItem.classList.remove('active');
+                               
+                               if (languageId) event.currentTarget.classList.add('active');
+                               
+                               this._select(elementId, languageId, isInit || false);
+                       }).bind(this);
+                       
+                       // build language dropdown
+                       for (var languageId in availableLanguages) {
+                               if (objOwns(availableLanguages, languageId)) {
+                                       var listItem = elCreate('li');
+                                       elData(listItem, 'language-id', languageId);
+                                       
+                                       span = elCreate('span');
+                                       span.textContent = availableLanguages[languageId];
+                                       
+                                       listItem.appendChild(span);
+                                       listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                                       dropdownMenu.appendChild(listItem);
+                               }
+                       }
+                       
+                       if (forceSelection !== true) {
+                               var listItem = elCreate('li');
+                               listItem.className = 'dropdownDivider';
+                               dropdownMenu.appendChild(listItem);
+                               
+                               listItem = elCreate('li');
+                               elData(listItem, 'language-id', 0);
+                               span = elCreate('span');
+                               span.textContent = Language.get('wcf.global.button.disabledI18n');
+                               listItem.appendChild(span);
+                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                               dropdownMenu.appendChild(listItem);
+                       }
+                       
+                       var activeItem = null;
+                       if (forceSelection === true || values.size) {
+                               for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
+                                       if (~~elData(dropdownMenu.children[i], 'language-id') === LANGUAGE_ID) {
+                                               activeItem = dropdownMenu.children[i];
+                                               break;
+                                       }
+                               }
+                       }
+                       
+                       UiSimpleDropdown.init(button);
+                       UiSimpleDropdown.registerCallback(container.id, _callbackDropdownToggle);
+                       
+                       _elements.set(elementId, {
+                               buttonLabel: button.children[0],
+                               element: element,
+                               languageId: 0,
+                               isEnabled: true,
+                               forceSelection: forceSelection
+                       });
+                       
+                       // bind to submit event
+                       var submit = DomTraverse.parentByTag(element, 'FORM');
+                       if (submit !== null) {
+                               submit.addEventListener('submit', _callbackSubmit);
+                               
+                               var elementIds = _forms.get(submit);
+                               if (elementIds === undefined) {
+                                       elementIds = [];
+                                       _forms.set(submit, elementIds);
+                               }
+                               
+                               elementIds.push(elementId);
+                       }
+                       
+                       if (activeItem !== null) {
+                               callbackClick({ currentTarget: activeItem }, true);
+                       }
+               },
+               
+               /**
+                * Selects a language or non-i18n from the dropdown list.
+                * 
+                * @param       {string}        elementId       input element id
+                * @param       {int}           languageId      language id or `0` to disable i18n
+                * @param       {boolean}       isInit          triggers pre-selection on init
+                */
+               _select: function(elementId, languageId, isInit) {
+                       var data = _elements.get(elementId);
+                       
+                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(data.element.parentNode.id);
+                       var item, label = '';
+                       for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
+                               item = dropdownMenu.children[i];
+                               
+                               var itemLanguageId = elData(item, 'language-id');
+                               if (itemLanguageId.length && languageId === ~~itemLanguageId) {
+                                       label = item.children[0].textContent;
+                               }
+                       }
+                       
+                       // save current value
+                       if (data.languageId !== languageId) {
+                               var values = _values.get(elementId);
+                               
+                               if (data.languageId) {
+                                       values.set(data.languageId, data.element.value);
+                               }
+                               
+                               if (languageId === 0) {
+                                       _values.set(elementId, new Dictionary());
+                               }
+                               else if (data.buttonLabel.classList.contains('active') || isInit === true) {
+                                       data.element.value = (values.has(languageId)) ? values.get(languageId) : '';
+                               }
+                               
+                               // update label
+                               data.buttonLabel.textContent = label;
+                               data.buttonLabel.classList[(languageId ? 'add' : 'remove')]('active');
+                               
+                               data.languageId = languageId;
+                       }
+                       
+                       data.element.blur();
+                       data.element.focus();
+               },
+               
+               /**
+                * Callback for dropdowns being opened, flags items with a missing value for one or more languages.
+                * 
+                * @param       {string}        containerId     dropdown container id
+                * @param       {string}        action          toggle action, can be `open` or `close`
+                */
+               _dropdownToggle: function(containerId, action) {
+                       if (action !== 'open') {
+                               return;
+                       }
+                       
+                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(containerId);
+                       var elementId = elData(elById(containerId), 'input-id');
+                       var values = _values.get(elementId);
+                       
+                       var item, languageId;
+                       for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
+                               item = dropdownMenu.children[i];
+                               languageId = ~~elData(item, 'language-id');
+                               
+                               if (languageId) {
+                                       item.classList[(values.has(languageId) || !values.size ? 'remove' : 'add')]('missingValue');
+                               }
+                       }
+               },
+               
+               /**
+                * Inserts hidden fields for i18n input on submit.
+                * 
+                * @param       {object}        event           event object
+                */
+               _submit: function(event) {
+                       var elementIds = _forms.get(event.currentTarget);
+                       
+                       var data, elementId, input, values;
+                       for (var i = 0, length = elementIds.length; i < length; i++) {
+                               elementId = elementIds[i];
+                               data = _elements.get(elementId);
+                               if (data.isEnabled) {
+                                       values = _values.get(elementId);
+                                       
+                                       // update with current value
+                                       if (data.languageId) {
+                                               values.set(data.languageId, data.element.value);
+                                       }
+                                       
+                                       if (values.size) {
+                                               values.forEach(function(value, languageId) {
+                                                       input = elCreate('input');
+                                                       input.type = 'hidden';
+                                                       input.name = elementId + '_i18n[' + languageId + ']';
+                                                       input.value = value;
+                                                       
+                                                       event.currentTarget.appendChild(input);
+                                               });
+                                               
+                                               // remove name attribute to enforce i18n values
+                                               data.element.removeAttribute('name');
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Returns the values of an input field.
+                * 
+                * @param       {string}        elementId       input element id
+                * @return      {Dictionary}    values stored for the different languages
+                */
+               getValues: function(elementId) {
+                       var element = _elements.get(elementId);
+                       if (element === undefined) {
+                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
+                       }
+                       
+                       var values = _values.get(elementId);
+                       
+                       // update with current value
+                       values.set(element.languageId, element.element.value);
+                       
+                       return values;
+               },
+               
+               /**
+                * Sets the values of an input field.
+                * 
+                * @param       {string}        elementId       input element id
+                * @param       {Dictionary}    values          values for the different languages
+                */
+               setValues: function(elementId, values) {
+                       var element = _elements.get(elementId);
+                       if (element === undefined) {
+                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
+                       }
+                       
+                       if (Core.isPlainObject(values)) {
+                               values = Dictionary.fromObject(values);
+                       }
+                       
+                       element.element.value = '';
+                       
+                       if (values.has(0)) {
+                               element.element.value = values.get(0);
+                               values['delete'](0);
+                       }
+                       
+                       _values.set(elementId, values);
+                       
+                       element.languageId = 0;
+                       this._select(elementId, LANGUAGE_ID, true);
+               },
+               
+               /**
+                * Disables the i18n interface for an input field.
+                * 
+                * @param       {string}        elementId       input element id
+                */
+               disable: function(elementId) {
+                       var element = _elements.get(elementId);
+                       if (element === undefined) {
+                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
+                       }
+                       
+                       if (!element.isEnabled) return;
+                       
+                       element.isEnabled = false;
+                       
+                       // hide language dropdown
+                       elHide(element.buttonLabel.parentNode);
+                       var dropdownContainer = element.buttonLabel.parentNode.parentNode;
+                       dropdownContainer.classList.remove('inputAddon');
+                       dropdownContainer.classList.remove('dropdown');
+               },
+               
+               /**
+                * Enables the i18n interface for an input field.
+                * 
+                * @param       {string}        elementId       input element id
+                */
+               enable: function(elementId) {
+                       var element = _elements.get(elementId);
+                       if (element === undefined) {
+                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
+                       }
+                       
+                       if (element.isEnabled) return;
+                       
+                       element.isEnabled = true;
+                       
+                       // show language dropdown
+                       elShow(element.buttonLabel.parentNode);
+                       var dropdownContainer = element.buttonLabel.parentNode.parentNode;
+                       dropdownContainer.classList.add('inputAddon');
+                       dropdownContainer.classList.add('dropdown');
+               },
+               
+               /**
+                * Returns true if i18n input is enabled for an input field.
+                * 
+                * @param       {string}        elementId       input element id
+                * @return      {boolean}
+                */
+               isEnabled: function(elementId) {
+                       var element = _elements.get(elementId);
+                       if (element === undefined) {
+                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
+                       }
+                       
+                       return element.isEnabled;
+               },
+               
+               /**
+                * Returns true if the value of an i18n input field is valid.
+                * 
+                * If the element is disabled, true is returned.
+                * 
+                * @param       {string}        elementId               input element id
+                * @param       {boolean}       permitEmptyValue        if true, input may be empty for all languages
+                * @return      {boolean}       true if input is valid
+                */
+               validate: function(elementId, permitEmptyValue) {
+                       var element = _elements.get(elementId);
+                       if (element === undefined) {
+                               throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field.");
+                       }
+                       
+                       if (!element.isEnabled) return true;
+                       
+                       var values = _values.get(elementId);
+                       
+                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(element.element.parentNode.id);
+                       
+                       if (element.languageId) {
+                               values.set(element.languageId, element.element.value);
+                       }
+                       
+                       var item, languageId;
+                       var hasEmptyValue = false, hasNonEmptyValue = false;
+                       for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) {
+                               item = dropdownMenu.children[i];
+                               languageId = ~~elData(item, 'language-id');
+                               
+                               if (languageId) {
+                                       if (!values.has(languageId) || values.get(languageId).length === 0) {
+                                               // input has non-empty value for previously checked language
+                                               if (hasNonEmptyValue) {
+                                                       return false;
+                                               }
+                                               
+                                               hasEmptyValue = true;
+                                       }
+                                       else {
+                                               // input has empty value for previously checked language
+                                               if (hasEmptyValue) {
+                                                       return false;
+                                               }
+                                               
+                                               hasNonEmptyValue = true;
+                                       }
+                               }
+                       }
+                       
+                       if (hasEmptyValue && !permitEmptyValue) {
+                               return false;
+                       }
+                       
+                       return true;
+               }
+       };
+       
+       return LanguageInput;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/List.js
new file mode 100644 (file)
index 0000000..56ba9c9
--- /dev/null
@@ -0,0 +1,112 @@
+/**
+ * List implementation relying on an array or if supported on a Set to hold values.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/List
+ */
+define([], function() {
+       "use strict";
+       
+       var _hasSet = objOwns(window, 'Set') && typeof window.Set === 'function';
+       
+       /**
+        * @constructor
+        */
+       function List() {
+               this._set = (_hasSet) ? new Set() : [];
+       }
+       List.prototype = {
+               /**
+                * Appends an element to the list, silently rejects adding an already existing value.
+                * 
+                * @param       {?}     value   unique element
+                */
+               add: function(value) {
+                       if (_hasSet) {
+                               this._set.add(value);
+                       }
+                       else if (!this.has(value)) {
+                               this._set.push(value);
+                       }
+               },
+               
+               /**
+                * Removes all elements from the list.
+                */
+               clear: function() {
+                       if (_hasSet) {
+                               this._set.clear();
+                       }
+                       else {
+                               this._set = [];
+                       }
+               },
+               
+               /**
+                * Removes an element from the list, returns true if the element was in the list.
+                * 
+                * @param       {?}             value   element
+                * @return      {boolean}       true if element was in the list
+                */
+               'delete': function(value) {
+                       if (_hasSet) {
+                               return this._set['delete'](value);
+                       }
+                       else {
+                               var index = this._set.indexOf(value);
+                               if (index === -1) {
+                                       return false;
+                               }
+                               
+                               this._set.splice(index, 1);
+                               return true;
+                       }
+               },
+               
+               /**
+                * Calls `callback` for each element in the list.
+                */
+               forEach: function(callback) {
+                       if (_hasSet) {
+                               this._set.forEach(callback);
+                       }
+                       else {
+                               for (var i = 0, length = this._set.length; i < length; i++) {
+                                       callback(this._set[i]);
+                               }
+                       }
+               },
+               
+               /**
+                * Returns true if the list contains the element.
+                * 
+                * @param       {?}             value   element
+                * @return      {boolean}       true if element is in the list
+                */
+               has: function(value) {
+                       if (_hasSet) {
+                               return this._set.has(value);
+                       }
+                       else {
+                               return (this._set.indexOf(value) !== -1);
+                       }
+               }
+       };
+       
+       Object.defineProperty(List.prototype, 'size', {
+               enumerable: false,
+               configurable: true,
+               get: function() {
+                       if (_hasSet) {
+                               return this._set.size;
+                       }
+                       else {
+                               return this._set.length;
+                       }
+               }
+       });
+       
+       return List;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js
new file mode 100644 (file)
index 0000000..26dea28
--- /dev/null
@@ -0,0 +1,286 @@
+/**
+ * Handles editing media files via dialog.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Media/Editor
+ */
+define(
+       [
+               'Ajax',                         'Core',                       'Dictionary',          'Dom/ChangeListener',
+               'Dom/Traverse',                 'Language',                   'Ui/Dialog',           'Ui/Notification',
+               'WoltLabSuite/Core/Language/Chooser', 'WoltLabSuite/Core/Language/Input', 'WoltLabSuite/Core/File/Util'
+       ],
+       function(
+               Ajax,                            Core,                         Dictionary,            DomChangeListener,
+               DomTraverse,                     Language,                     UiDialog,              UiNotification,
+               LanguageChooser,                 LanguageInput,                FileUtil
+       )
+{
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaEditor(callbackObject) {
+               if (typeof callbackObject !== 'object') {
+                       throw new TypeError("Parameter 'callbackObject' has to be an object, " + typeof callbackObject + " given.");
+               }
+               if (typeof callbackObject._editorClose !== 'function') {
+                       throw new TypeError("Callback object has no function '_editorClose'.");
+               }
+               if (typeof callbackObject._editorSuccess !== 'function') {
+                       throw new TypeError("Callback object has no function '_editorSuccess'.");
+               }
+               
+               this._callbackObject = callbackObject;
+               this._media = null;
+               
+               this._dialogs = new Dictionary();
+       }
+       MediaEditor.prototype = {
+               /**
+                * Returns the data for Ajax to setup the Ajax/Request object.
+                * 
+                * @return      {object}        setup data for Ajax/Request object
+                */
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'update',
+                                       className: 'wcf\\data\\media\\MediaAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                * 
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       UiNotification.show();
+                       
+                       this._callbackObject._editorSuccess(this._media);
+                       
+                       UiDialog.close('mediaEditor_' + this._media.mediaID);
+                       
+                       this._media = null;
+               },
+               
+               /**
+                * Is called if an editor is manually closed by the user.
+                */
+               _close: function() {
+                       this._media = null;
+                       
+                       this._callbackObject._editorClose();
+               },
+               
+               /**
+                * Handles the `[ENTER]` key to submit the form.
+                * 
+                * @param       {object}        event           event object
+                */
+               _keyPress: function(event) {
+                       // 13 = [ENTER]
+                       if (event.charCode === 13) {
+                               event.preventDefault();
+                               
+                               this._saveData();
+                       }
+               },
+               
+               /**
+                * Saves the data of the currently edited media.
+                */
+               _saveData: function() {
+                       var content = UiDialog.getDialog('mediaEditor_' + this._media.mediaID).content;
+                       
+                       var altText = elBySel('input[name=altText]', content);
+                       var caption = elBySel('textarea[name=caption]', content);
+                       var title = elBySel('input[name=title]', content);
+                       
+                       var hasError = false;
+                       var altTextError = DomTraverse.childByClass(altText.parentNode.parentNode, 'innerError');
+                       var captionError = DomTraverse.childByClass(caption.parentNode.parentNode, 'innerError');
+                       var titleError = DomTraverse.childByClass(title.parentNode.parentNode, 'innerError');
+                       
+                       this._media.isMultilingual = ~~elBySel('input[name=isMultilingual]', content).checked;
+                       this._media.languageID = this._media.isMultilingual ? null : LanguageChooser.getLanguageId('languageID');
+                       
+                       this._media.altText = {};
+                       this._media.caption = {};
+                       this._media.title = {};
+                       if (this._media.isMultilingual) {
+                               if (!LanguageInput.validate('altText_' + this._media.mediaID, true)) {
+                                       hasError = true;
+                                       if (!altTextError) {
+                                               var error = elCreate('small');
+                                               error.className = 'innerError';
+                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
+                                               altText.parentNode.parentNode.appendChild(error);
+                                       }
+                               }
+                               if (!LanguageInput.validate('caption_' + this._media.mediaID, true)) {
+                                       hasError = true;
+                                       if (!captionError) {
+                                               var error = elCreate('small');
+                                               error.className = 'innerError';
+                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
+                                               caption.parentNode.parentNode.appendChild(error);
+                                       }
+                               }
+                               if (!LanguageInput.validate('title_' + this._media.mediaID, true)) {
+                                       hasError = true;
+                                       if (!titleError) {
+                                               var error = elCreate('small');
+                                               error.className = 'innerError';
+                                               error.textContent = Language.get('wcf.global.form.error.multilingual');
+                                               thistitle.parentNode.parentNode.appendChild(error);
+                                       }
+                               }
+                               
+                               this._media.altText = LanguageInput.getValues('altText_' + this._media.mediaID).toObject();
+                               this._media.caption = LanguageInput.getValues('caption_' + this._media.mediaID).toObject();
+                               this._media.title = LanguageInput.getValues('title_' + this._media.mediaID).toObject();
+                       }
+                       else {
+                               this._media.altText[this._media.languageID] = altText.value;
+                               this._media.caption[this._media.languageID] = caption.value;
+                               this._media.title[this._media.languageID] = title.value;
+                       }
+                       
+                       var aclValues = {
+                               allowAll: ~~elById('mediaEditor_' + this._media.mediaID + '_aclAllowAll').checked,
+                               group: [],
+                               user: []
+                       };
+                       
+                       var aclGroups = elBySelAll('input[name="aclValues[group][]"]', content);
+                       for (var i = 0, length = aclGroups.length; i < length; i++) {
+                               aclValues.group.push(~~aclGroups[i].value);
+                       }
+                       
+                       var aclUsers = elBySelAll('input[name="aclValues[user][]"]', content);
+                       for (var i = 0, length = aclUsers.length; i < length; i++) {
+                               aclValues.user.push(~~aclUsers[i].value);
+                       }
+                       
+                       if (!hasError) {
+                               if (altTextError) elRemove(altTextError);
+                               if (captionError) elRemove(captionError);
+                               if (titleError) elRemove(titleError);
+                               
+                               Ajax.api(this, {
+                                       actionName: 'update',
+                                       objectIDs: [ this._media.mediaID ],
+                                       parameters: {
+                                               aclValues: aclValues,
+                                               altText: this._media.altText,
+                                               caption: this._media.caption,
+                                               data: {
+                                                       isMultilingual: this._media.isMultilingual,
+                                                       languageID: this._media.languageID
+                                               },
+                                               title: this._media.title
+                                       }
+                               });
+                       }
+               },
+               
+               /**
+                * Updates language-related input fields depending on whether multilingualism
+                * is enabled.
+                */
+               _updateLanguageFields: function(event, element) {
+                       if (event) element = event.currentTarget;
+                       
+                       var languageChooserContainer = elById('mediaEditor_' + this._media.mediaID + '_languageIDContainer').parentNode;
+                       
+                       if (element.checked) {
+                               LanguageInput.enable('title_' + this._media.mediaID);
+                               LanguageInput.enable('caption_' + this._media.mediaID);
+                               LanguageInput.enable('altText_' + this._media.mediaID);
+                               
+                               elHide(languageChooserContainer);
+                       }
+                       else {
+                               LanguageInput.disable('title_' + this._media.mediaID);
+                               LanguageInput.disable('caption_' + this._media.mediaID);
+                               LanguageInput.disable('altText_' + this._media.mediaID);
+                               
+                               elShow(languageChooserContainer);
+                       }
+               },
+               
+               /**
+                * Edits the media with the given data.
+                * 
+                * @param       {object}        media           data of the edited media
+                */
+               edit: function(media) {
+                       if (this._media !== null) {
+                               throw new Error("Cannot edit media with id '" + media.mediaID + "' while editing media with id '" + this._media.mediaID + "'");
+                       }
+                       
+                       this._media = media;
+                       
+                       if (!this._dialogs.has('mediaEditor_' + media.mediaID)) {
+                               this._dialogs.set('mediaEditor_' + media.mediaID, {
+                                       _dialogSetup: function() {
+                                               return {
+                                                       id: 'mediaEditor_' + media.mediaID,
+                                                       options: {
+                                                               backdropCloseOnClick: false,
+                                                               onClose: this._close.bind(this),
+                                                               title: Language.get('wcf.media.edit')
+                                                       },
+                                                       source: {
+                                                               after: (function(content, data) {
+                                                                       // make sure that the language chooser is initialized first
+                                                                       setTimeout(function() {
+                                                                               LanguageChooser.setLanguageId('languageID', this._media.languageID || LANGUAGE_ID);
+                                                                               
+                                                                               if (this._media.isMultilingual) {
+                                                                                       LanguageInput.setValues('altText_' + this._media.mediaID, Dictionary.fromObject(this._media.altText || { }));
+                                                                                       LanguageInput.setValues('caption_' + this._media.mediaID, Dictionary.fromObject(this._media.caption || { }));
+                                                                                       LanguageInput.setValues('title_' + this._media.mediaID, Dictionary.fromObject(this._media.title || { }));
+                                                                               }
+                                                                               
+                                                                               var isMultilingual = elBySel('input[name=isMultilingual]', content);
+                                                                               isMultilingual.addEventListener('change', this._updateLanguageFields.bind(this));
+                                                                               
+                                                                               this._updateLanguageFields(null, isMultilingual);
+                                                                               
+                                                                               var keyPress = this._keyPress.bind(this);
+                                                                               elBySel('input[name=altText]', content).addEventListener('keypress', keyPress);
+                                                                               elBySel('input[name=title]', content).addEventListener('keypress', keyPress);
+                                                                               
+                                                                               elBySel('button[data-type=submit]', content).addEventListener(WCF_CLICK_EVENT, this._saveData.bind(this));
+                                                                               
+                                                                               // remove focus from input elements and scroll dialog to top
+                                                                               document.activeElement.blur();
+                                                                               elById('mediaEditor_' + this._media.mediaID).parentNode.scrollTop = 0;
+                                                                               
+                                                                               DomChangeListener.trigger();
+                                                                       }.bind(this), 0);
+                                                               }).bind(this),
+                                                               data: {
+                                                                       actionName: 'getEditorDialog',
+                                                                       className: 'wcf\\data\\media\\MediaAction',
+                                                                       objectIDs: [media.mediaID]
+                                                               }
+                                                       }
+                                               };
+                                       }.bind(this)
+                               });
+                       }
+                       
+                       UiDialog.open(this._dialogs.get('mediaEditor_' + media.mediaID));
+               }
+       };
+       
+       return MediaEditor;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js
new file mode 100644 (file)
index 0000000..ee4036e
--- /dev/null
@@ -0,0 +1,444 @@
+/**
+ * Provides the media manager dialog.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Media/Manager/Base
+ */
+define(
+       [
+               'Core',                     'Dictionary',               'Dom/ChangeListener',              'Dom/Traverse',
+               'Dom/Util',                 'EventHandler',             'Language',                        'List',
+               'Permission',               'Ui/Dialog',                'Ui/Notification',                 'WoltLabSuite/Core/Controller/Clipboard',
+               'WoltLabSuite/Core/Media/Editor', 'WoltLabSuite/Core/Media/Upload', 'WoltLabSuite/Core/Media/Manager/Search'
+       ],
+       function(
+               Core,                        Dictionary,                 DomChangeListener,                 DomTraverse,
+               DomUtil,                     EventHandler,               Language,                          List,
+               Permission,                  UiDialog,                   UiNotification,                    Clipboard,
+               MediaEditor,                 MediaUpload,                MediaManagerSearch
+       )
+{
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaManagerBase(options) {
+               this._options = Core.extend({
+                       dialogTitle: Language.get('wcf.media.manager'),
+                       fileTypeFilters: {},
+                       minSearchLength: 3
+               }, options);
+               
+               this._media = new Dictionary();
+               this._mediaData = new Dictionary();
+               this._mediaCache = null;
+               this._mediaManagerMediaList = null;
+               this._search = null;
+               
+               if (Permission.get('admin.content.cms.canManageMedia')) {
+                       this._mediaEditor = new MediaEditor(this);
+               }
+               
+               DomChangeListener.add('WoltLabSuite/Core/Media/Manager', this._addButtonEventListeners.bind(this));
+       }
+       MediaManagerBase.prototype = {
+               /**
+                * Adds click event listeners to media buttons.
+                */
+               _addButtonEventListeners: function() {
+                       if (!this._mediaManagerMediaList) return;
+                       
+                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = listItems.length; i < length; i++) {
+                               var listItem = listItems[i];
+                               
+                               if (Permission.get('admin.content.cms.canManageMedia')) {
+                                       var editIcon = elByClass('jsMediaEditIcon', listItem)[0];
+                                       if (editIcon) {
+                                               editIcon.classList.remove('jsMediaEditIcon');
+                                               editIcon.addEventListener(WCF_CLICK_EVENT, this._editMedia.bind(this));
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Handles clicks on the media manager button.
+                * 
+                * @param       {object}        event   event object
+                */
+               _click: function(event) {
+                       event.preventDefault();
+                       
+                       UiDialog.open(this);
+               },
+               
+               /**
+                * Reacts to executed clipboard actions.
+                * 
+                * @param       {object<string, *>}     actionData      data of the executed clipboard action
+                */
+               _clipboardAction: function(actionData) {
+                       // only consider events if the action has been executed
+                       if (actionData.data.actionName === 'com.woltlab.wcf.media.delete' && actionData.responseData === null) {
+                               var mediaIds = actionData.responseData.objectIDs;
+                               for (var i = 0, length = mediaIds.length; i < length; i++) {
+                                       this.removeMedia(~~mediaIds[i], true);
+                               }
+                               
+                               UiNotification.show();
+                       }
+               },
+               
+               /**
+                * Is called if the media manager dialog is closed.
+                */
+               _dialogClose: function() {
+                       // only show media clipboard if editor is open
+                       Clipboard.hideEditor('com.woltlab.wcf.media');
+               },
+               
+               /**
+                * Initializes the dialog when first loaded.
+                *
+                * @param       {string}        content         dialog content
+                * @param       {object}        data            AJAX request's response data
+                */
+               _dialogInit: function(content, data) {
+                       // store media data locally
+                       var media = data.returnValues.media || { };
+                       for (var mediaId in media) {
+                               if (objOwns(media, mediaId)) {
+                                       this._mediaData.set(~~mediaId, media[mediaId]);
+                               }
+                       }
+                       
+                       this._mediaManagerMediaList = elById('mediaManagerMediaList');
+                       
+                       // store list items locally
+                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = listItems.length; i < length; i++) {
+                               var listItem = listItems[i];
+                               
+                               this._media.set(~~elData(listItem, 'object-id'), listItem);
+                       }
+                       
+                       if (Permission.get('admin.content.cms.canManageMedia')) {
+                               new MediaUpload('mediaManagerMediaUploadButton', 'mediaManagerMediaList', {
+                                       mediaManager: this
+                               });
+                               
+                               Clipboard.setup({
+                                       hasMarkedItems: data.returnValues.hasMarkedItems ? true : false,
+                                       pageClassName: 'menuManagerDialog-' + this.getMode()
+                               });
+                               
+                               EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.media', this._clipboardAction.bind(this));
+                       }
+                       
+                       this._search = new MediaManagerSearch(this);
+                       
+                       if (!listItems.length) {
+                               this._search.hideSearch();
+                       }
+                       
+                       this._dialogShow();
+               },
+               
+               /**
+                * Returns all data to setup the media manager dialog.
+                * 
+                * @return      {object}        dialog setup data
+                */
+               _dialogSetup: function() {
+                       return {
+                               id: 'mediaManager',
+                               options: {
+                                       onClose: this._dialogClose.bind(this),
+                                       onShow: this._dialogShow.bind(this),
+                                       title: this._options.dialogTitle
+                               },
+                               source: {
+                                       after: this._dialogInit.bind(this),
+                                       data: {
+                                               actionName: 'getManagementDialog',
+                                               className: 'wcf\\data\\media\\MediaAction',
+                                               parameters: {
+                                                       mode: this.getMode(),
+                                                       fileTypeFilters: this._options.fileTypeFilters
+                                               }
+                                       }
+                               }
+                       };
+               },
+               
+               /**
+                * Is called if the media manager dialog is shown.
+                */
+               _dialogShow: function() {
+                       if (!this._mediaManagerMediaList) return;
+                       
+                       // only show media clipboard if editor is open
+                       Clipboard.showEditor('com.woltlab.wcf.media');
+               },
+               
+               /**
+                * Opens the media editor for a media file.
+                * 
+                * @param       {Event}         event           event object for clicks on edit icons
+                */
+               _editMedia: function(event) {
+                       if (!Permission.get('admin.content.cms.canManageMedia')) {
+                               throw new Error("You are not allowed to edit media files.");
+                       }
+                       
+                       UiDialog.close('mediaManager');
+                       
+                       this._mediaEditor.edit(this._mediaData.get(~~elData(event.currentTarget, 'object-id')));
+               },
+               
+               /**
+                * Re-opens the manager dialog after closing the editor dialog.
+                */
+               _editorClose: function() {
+                       UiDialog.open(this);
+               },
+               
+               /**
+                * Re-opens the manager dialog and updates the media data after
+                * successfully editing a media file.
+                * 
+                * @param       {object}        media           updated media file data
+                */
+               _editorSuccess: function(media) {
+                       UiDialog.open(this);
+                       
+                       this._mediaData.set(~~media.mediaID, media);
+                       
+                       var listItem = this._media.get(~~media.mediaID);
+                       var p = elByClass('mediaTitle', listItem)[0];
+                       if (media.isMultilingual) {
+                               p.textContent = media.title[LANGUAGE_ID] || media.filename;
+                       }
+                       else {
+                               p.textContent = media.title[media.languageID] || media.filename;
+                       }
+               },
+               
+               /**
+                * Sets the displayed media (after a search).
+                * 
+                * @param       {Dictionary}    media           media to be set as active
+                */
+               _setMedia: function(media) {
+                       if (Core.isPlainObject(media)) {
+                               this._media = Dictionary.fromObject(media);
+                       }
+                       else {
+                               this._media = media;
+                       }
+                       
+                       var info = DomTraverse.nextByClass(this._mediaManagerMediaList, 'info');
+                       
+                       if (this._media.size) {
+                               if (info) {
+                                       elHide(info);
+                               }
+                       }
+                       else {
+                               if (info === null) {
+                                       info = elCreate('p');
+                                       info.className = 'info';
+                                       info.textContent = Language.get('wcf.media.search.noResults');
+                               }
+                               
+                               elShow(info);
+                               DomUtil.insertAfter(info, this._mediaManagerMediaList);
+                       }
+                       
+                       var mediaListItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = mediaListItems.length; i < length; i++) {
+                               var listItem = mediaListItems[i];
+                               
+                               if (!this._media.has(elData(listItem, 'object-id'))) {
+                                       elHide(listItem);
+                               }
+                               else {
+                                       elShow(listItem);
+                               }
+                       }
+                       
+                       DomChangeListener.trigger();
+                       
+                       Clipboard.reload();
+               },
+               
+               /**
+                * Adds a media file to the manager.
+                * 
+                * @param       {object}        media           data of the media file
+                * @param       {Element}       listItem        list item representing the file
+                */
+               addMedia: function(media, listItem) {
+                       if (!media.languageID) media.isMultilingual = 1;
+                       
+                       this._mediaData.set(~~media.mediaID, media);
+                       this._media.set(~~media.mediaID, listItem);
+                       
+                       if (this._media.size === 1) {
+                               this._search.showSearch();
+                       }
+               },
+               
+               /**
+                * Returns the mode of the media manager.
+                *
+                * @return      {string}
+                */
+               getMode: function() {
+                       return '';
+               },
+               
+               /**
+                * Returns the media manager option with the given name.
+                * 
+                * @param       {string}        name            option name
+                * @return      {mixed}         option value or null
+                */
+               getOption: function(name) {
+                       if (this._options[name]) {
+                               return this._options[name];
+                       }
+                       
+                       return null;
+               },
+               
+               /**
+                * Removes a media file.
+                *
+                * @param       {int}                   mediaId         id of the removed media file
+                * @param       {boolean|undefined}     checkCache      media file will also be removed from the local cache if true
+                */
+               removeMedia: function(mediaId, checkCache) {
+                       if (this._media.has(mediaId)) {
+                               // remove list item
+                               elRemove(this._media.get(mediaId));
+                               
+                               this._media.delete(mediaId);
+                               this._mediaData.delete(mediaId);
+                       }
+                       
+                       if (checkCache && this._mediaCache && this._mediaCache.has(mediaId)) {
+                               this._mediaCache.delete(mediaId);
+                       }
+               },
+               
+               /**
+                * Changes the displayed media to the previously displayed media.
+                */
+               resetMedia: function() {
+                       if (this._mediaCache !== null) {
+                               this._setMedia(this._mediaCache);
+                               
+                               this._mediaCache = null;
+                               
+                               this._search.resetSearch();
+                       }
+               },
+               
+               /**
+                * Sets the media files currently displayed.
+                * 
+                * @param       {object}        media           media data
+                * @param       {string}        template        
+                */
+               setMedia: function(media, template) {
+                       if (!this._mediaCache) {
+                               this._mediaCache = this._media;
+                       }
+                       
+                       var hasMedia = false;
+                       for (var mediaId in media) {
+                               if (objOwns(media, mediaId)) {
+                                       hasMedia = true;
+                               }
+                       }
+                       
+                       var newListItems = [];
+                       if (hasMedia) {
+                               var ul = elCreate('ul');
+                               ul.innerHTML = template;
+                               
+                               var listItems = DomTraverse.childrenByTag(ul, 'LI');
+                               for (var i = 0, length = listItems.length; i < length; i++) {
+                                       var listItem = listItems[i];
+                                       if (!this._mediaData.has(~~elData(listItem, 'object-id'))) {
+                                               this._mediaData.set(elData(listItem, 'object-id'), listItem);
+                                               
+                                               this._mediaManagerMediaList.appendChild(listItem);
+                                       }
+                               }
+                       }
+                       
+                       this._setMedia(media);
+               },
+               
+               /**
+                * Sets up a new media element.
+                * 
+                * @param       {object}        media           data of the media file
+                * @param       {HTMLElement}   mediaElement    element representing the media file
+                */
+               setupMediaElement: function(media, mediaElement) {
+                       var mediaInformation = DomTraverse.childByClass(mediaElement, 'mediaInformation');
+                       
+                       var buttonGroupNavigation = elCreate('nav');
+                       buttonGroupNavigation.className = 'buttonGroupNavigation';
+                       mediaInformation.parentNode.appendChild(buttonGroupNavigation);
+                       
+                       var smallButtons = elCreate('ul');
+                       smallButtons.className = 'smallButtons buttonGroup';
+                       buttonGroupNavigation.appendChild(smallButtons);
+                       
+                       var listItem = elCreate('li');
+                       smallButtons.appendChild(listItem);
+                       
+                       var checkbox = elCreate('input');
+                       checkbox.className = 'jsClipboardItem jsMediaCheckbox';
+                       elAttr(checkbox, 'type', 'checkbox');
+                       elData(checkbox, 'object-id', media.mediaID);
+                       listItem.appendChild(checkbox);
+                       
+                       if (Permission.get('admin.content.cms.canManageMedia')) {
+                               listItem = elCreate('li');
+                               smallButtons.appendChild(listItem);
+                               
+                               var a = elCreate('a');
+                               listItem.appendChild(a);
+                               
+                               var icon = elCreate('span');
+                               icon.className = 'icon icon16 fa-pencil jsTooltip jsMediaEditIcon';
+                               elData(icon, 'object-id', media.mediaID);
+                               elAttr(icon, 'title', Language.get('wcf.global.button.edit'));
+                               a.appendChild(icon);
+                               
+                               listItem = elCreate('li');
+                               smallButtons.appendChild(listItem);
+                               
+                               a = elCreate('a');
+                               listItem.appendChild(a);
+                               
+                               icon = elCreate('span');
+                               icon.className = 'icon icon16 fa-times jsTooltip jsMediaDeleteIcon';
+                               elData(icon, 'object-id', media.mediaID);
+                               elAttr(icon, 'title', Language.get('wcf.global.button.delete'));
+                               a.appendChild(icon);
+                       }
+               }
+       };
+       
+       return MediaManagerBase;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Editor.js
new file mode 100644 (file)
index 0000000..8ead779
--- /dev/null
@@ -0,0 +1,340 @@
+/**
+ * Provides the media manager dialog for selecting media for Redactor editors.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Media/Manager/Editor
+ */
+define(['Core', 'Dictionary', 'Dom/Traverse', 'Language', 'Ui/Dialog', 'WoltLabSuite/Core/Controller/Clipboard', 'WoltLabSuite/Core/Media/Manager/Base'],
+       function(Core, Dictionary, DomTraverse, Language, UiDialog, ControllerClipboard, MediaManagerBase) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaManagerEditor(options) {
+               options = Core.extend({
+                       callbackInsert: null
+               }, options);
+               
+               MediaManagerBase.call(this, options);
+               
+               this._activeButton = null;
+               this._buttons = elByClass(this._options.buttonClass || 'jsMediaEditorButton');
+               for (var i = 0, length = this._buttons.length; i < length; i++) {
+                       this._buttons[i].addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+               }
+               this._mediaToInsert = new Dictionary();
+               this._mediaToInsertByClipboard = false;
+       }
+       Core.inherit(MediaManagerEditor, MediaManagerBase, {
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#_addButtonEventListeners
+                */
+               _addButtonEventListeners: function() {
+                       MediaManagerEditor._super.prototype._addButtonEventListeners.call(this);
+                       
+                       if (!this._mediaManagerMediaList) return;
+                       
+                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = listItems.length; i < length; i++) {
+                               var listItem = listItems[i];
+                               
+                               var insertIcon = elByClass('jsMediaInsertIcon', listItem)[0];
+                               if (insertIcon) {
+                                       insertIcon.classList.remove('jsMediaInsertIcon');
+                                       insertIcon.addEventListener(WCF_CLICK_EVENT, this._openInsertDialog.bind(this));
+                               }
+                       }
+               },
+               
+               /**
+                * Builds the dialog to setup inserting media files.
+                */
+               _buildInsertDialog: function() {
+                       var thumbnailOptions = '';
+                       
+                       var sizes = ['small', 'medium', 'large'];
+                       var size, option;
+                       lengthLoop: for (var i = 0, length = sizes.length; i < length; i++) {
+                               size = sizes[i];
+                               
+                               // make sure that all thumbnails support the thumbnail size
+                               for (var j = 0, mediaLength = this._mediaToInsert.length; j < mediaLength; j++) {
+                                       if (!this._mediaToInsert[i][size + 'ThumbnailType']) {
+                                               continue lengthLoop;
+                                       }
+                               }
+                               
+                               thumbnailOptions += '<option value="' + size + '">' + Language.get('wcf.media.insert.imageSize.' + size) + '</option>';
+                       }
+                       thumbnailOptions += '<option value="original">' + Language.get('wcf.media.insert.imageSize.original') + '</option>';
+                       
+                       var dialog = '<div class="section">'
+                       + (this._mediaToInsert.size > 1 ? '<dl>'
+                               + '<dt>' + Language.get('wcf.media.insert.type') + '</dt>'
+                               + '<dd>'
+                                       + '<select name="insertType">'
+                                               + '<option value="separate">' + Language.get('wcf.media.insert.type.separate') + '</option>'
+                                               + '<option value="gallery">' + Language.get('wcf.media.insert.type.gallery') + '</option>'
+                                       + '</select>'
+                               + '</dd>'
+                       + '</dl>' : '')
+                       + '<dl class="thumbnailSizeSelection">'
+                               + '<dt>' + Language.get('wcf.media.insert.imageSize') + '</dt>'
+                               + '<dd>'
+                                       + '<select name="thumbnailSize">'
+                                               + thumbnailOptions
+                                       + '</select>'
+                               + '</dd>'
+                       + '</dl>'
+                       + '</div>'
+                       + '<div class="formSubmit">'
+                               + '<button class="buttonPrimary">' + Language.get('wcf.global.button.insert') + '</button>'
+                       + '</div>';
+                       
+                       UiDialog.open({
+                               _dialogSetup: (function() {
+                                       return {
+                                               id: this._getInsertDialogId(),
+                                               options: {
+                                                       onClose: this._editorClose.bind(this),
+                                                       onSetup: function(content) {
+                                                               elByClass('buttonPrimary', content)[0].addEventListener(WCF_CLICK_EVENT, this._insertMedia.bind(this));
+                                                               
+                                                               // toggle thumbnail size selection based on selected insert type
+                                                               var insertType = elBySel('select[name=insertType]', content);
+                                                               if (insertType !== null) {
+                                                                       var thumbnailSelection = elByClass('thumbnailSizeSelection', content)[0];
+                                                                       insertType.addEventListener('change', function(event) {
+                                                                               if (event.currentTarget.value === 'gallery') {
+                                                                                       elHide(thumbnailSelection);
+                                                                               }
+                                                                               else {
+                                                                                       elShow(thumbnailSelection);
+                                                                               }
+                                                                       });
+                                                               }
+                                                       }.bind(this),
+                                                       title: Language.get('wcf.media.insert')
+                                               },
+                                               source: dialog
+                                       };
+                               }).bind(this)
+                       });
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#_click
+                */
+               _click: function(event) {
+                       this._activeButton = event.currentTarget;
+                       
+                       MediaManagerEditor._super.prototype._click.call(this, event);
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#_clipboardAction
+                */
+               _clipboardAction: function(actionData) {
+                       MediaManagerEditor._super.prototype._clipboardAction.call(this, actionData);
+                       
+                       if (actionData.data.actionName === 'com.woltlab.wcf.media.insert') {
+                               this.insertMedia(actionData.data.parameters.objectIDs, true);
+                       }
+               },
+               
+               /**
+                * Returns the id of the insert dialog based on the media files to be inserted.
+                * 
+                * @return      {string}        insert dialog id
+                */
+               _getInsertDialogId: function() {
+                       var dialogId = 'mediaInsert';
+                       
+                       this._mediaToInsert.forEach(function(media, mediaId) {
+                               dialogId += '-' + mediaId;
+                       });
+                       
+                       return dialogId;
+               },
+               
+               /**
+                * Inserts media files into redactor.
+                * 
+                * @param       {Event?}        event
+                */
+               _insertMedia: function(event) {
+                       var insertType = 'separate';
+                       var thumbnailSize;
+                       
+                       // update insert options with selected values if method is called by clicking on 'insert' button
+                       // in dialog
+                       if (event) {
+                               UiDialog.close(this._getInsertDialogId());
+                               
+                               var dialogContent = event.currentTarget.closest('.dialogContent');
+                               
+                               if (this._mediaToInsert.size > 1) {
+                                       insertType = elBySel('select[name=insertType]', dialogContent).value;
+                               }
+                               thumbnailSize = elBySel('select[name=thumbnailSize]', dialogContent).value;
+                       }
+                       
+                       if (this._options.callbackInsert !== null) {
+                               this._options.callbackInsert(this._mediaToInsert, insertType, thumbnailSize);
+                       }
+                       else {
+                               if (insertType === 'separate') {
+                                       this._options.editor.buffer.set();
+                                       
+                                       this._mediaToInsert.forEach(this._insertMediaItem.bind(this, thumbnailSize));
+                               }
+                               else {
+                                       this._insertMediaGallery();
+                               }
+                       }
+                       
+                       if (this._mediaToInsertByClipboard) {
+                               var mediaIds = [];
+                               this._mediaToInsert.forEach(function(media) {
+                                       mediaIds.push(media.mediaID);
+                               })
+                               
+                               ControllerClipboard.unmark('com.woltlab.wcf.media', mediaIds);
+                       }
+                       
+                       this._mediaToInsert = new Dictionary();
+                       this._mediaToInsertByClipboard = false;
+                       
+                       // close manager dialog
+                       UiDialog.close(this);
+               },
+               
+               /**
+                * Inserts a series of uploaded images using a slider.
+                * 
+                * @protected
+                */
+               _insertMediaGallery: function() {
+                       var mediaIds = [];
+                       this._mediaToInsert.forEach(function(item) {
+                               mediaIds.push(item.mediaID);
+                       });
+                       
+                       this._options.editor.buffer.set();
+                       this._options.editor.insert.text("[wsmg='" + mediaIds.join(',') + "'][/wsmg]");
+               },
+               
+               /**
+                * Inserts a single media item.
+                * 
+                * @param       {string}        thumbnailSize   preferred image dimension, is ignored for non-images
+                * @param       {Object}        item            media item data
+                * @protected
+                */
+               _insertMediaItem: function(thumbnailSize, item) {
+                       if (item.isImage) {
+                               var sizes = ['small', 'medium', 'large', 'original'];
+                               
+                               // check if size is actually available
+                               var available = '', size;
+                               for (var i = 0; i < 4; i++) {
+                                       size = sizes[i];
+                                       
+                                       if (item[size + 'ThumbnailHeight']) {
+                                               available = size;
+                                               
+                                               if (thumbnailSize == size) {
+                                                       break;
+                                               }
+                                       }
+                               }
+                               
+                               thumbnailSize = available;
+                               
+                               this._options.editor.insert.html('<img src="' + item[thumbnailSize + 'ThumbnailLink'] + '" class="woltlabSuiteMedia" data-media-id="' + item.mediaID + '" data-media-size="' + thumbnailSize + '">');
+                       }
+                       else {
+                               this._options.editor.insert.text("[wsm='" + item.mediaID + "'][/wsm]");
+                       }
+               },
+               
+               /**
+                * Handles clicking on the insert button.
+                * 
+                * @param       {Event}         event           insert button click event
+                */
+               _openInsertDialog: function(event) {
+                       this.insertMedia([~~elData(event.currentTarget, 'object-id')]);
+               },
+               
+               /**
+                * Prepares insertion of the media files with the given ids.
+                * 
+                * @param       {array<int>}    mediaIds                ids of the media files to be inserted
+                * @param       {boolean?}      insertedByClipboard     is true if the media files are inserted by clipboard
+                */
+               insertMedia: function(mediaIds, insertedByClipboard) {
+                       this._mediaToInsert = new Dictionary();
+                       this._mediaToInsertByClipboard = insertedByClipboard || false;
+                       
+                       // open the insert dialog if all media files are images
+                       var imagesOnly = true, media;
+                       for (var i = 0, length = mediaIds.length; i < length; i++) {
+                               media = this._mediaData.get(mediaIds[i]);
+                               this._mediaToInsert.set(media.mediaID, media);
+                               
+                               if (!media.isImage) {
+                                       imagesOnly = false;
+                               }
+                       }
+                       
+                       if (imagesOnly) {
+                               UiDialog.close(this);
+                               var dialogId = this._getInsertDialogId();
+                               if (UiDialog.getDialog(dialogId)) {
+                                       UiDialog.openStatic(dialogId);
+                               }
+                               else {
+                                       this._buildInsertDialog();
+                               }
+                       }
+                       else {
+                               this._insertMedia();
+                       }
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#getMode
+                */
+               getMode: function() {
+                       return 'editor';
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#setupMediaElement
+                */
+               setupMediaElement: function(media, mediaElement) {
+                       MediaManagerEditor._super.prototype.setupMediaElement.call(this, media, mediaElement);
+                       
+                       // add media insertion icon
+                       var smallButtons = elBySel('nav.buttonGroupNavigation > ul.smallButtons', mediaElement);
+                       
+                       var listItem = elCreate('li');
+                       smallButtons.appendChild(listItem);
+                       
+                       var a = elCreate('a');
+                       listItem.appendChild(a);
+                       
+                       var icon = elCreate('span');
+                       icon.className = 'icon icon16 fa-plus jsTooltip jsMediaInsertIcon';
+                       elData(icon, 'object-id', media.mediaID);
+                       elAttr(icon, 'title', Language.get('wcf.media.button.insert'));
+                       a.appendChild(icon);
+               }
+       });
+       
+       return MediaManagerEditor;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Search.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Search.js
new file mode 100644 (file)
index 0000000..ba5aae4
--- /dev/null
@@ -0,0 +1,156 @@
+/**
+ * Provides the media search for the media manager.
+ *
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Media/Manager/Search
+ */
+define(['Ajax', 'Core', 'Dom/Traverse', 'Dom/Util', 'Language', 'WoltLabSuite/Core/Media/Search', 'Ui/SimpleDropdown'], function(Ajax, Core, DomTraverse, DomUtil, Language, MediaSearch, UiSimpleDropdown) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaManagerSearch(mediaManager) {
+               MediaSearch.call(this);
+               
+               this._mediaManager = mediaManager;
+               this._searchMode = false;
+               
+               this._input = elById(this._getIdPrefix() + 'SearchField');
+               this._input.addEventListener('keypress', this._keyPress.bind(this));
+               
+               this._cancelButton = elById(this._getIdPrefix() + 'SearchCancelButton');
+               this._cancelButton.addEventListener(WCF_CLICK_EVENT, this._cancelSearch.bind(this));
+       }
+       Core.inherit(MediaManagerSearch, MediaSearch, {
+               /**
+                * Returns the data for Ajax to setup the Ajax/Request object.
+                *
+                * @return      {object}        setup data for Ajax/Request object
+                */
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'getSearchResultList',
+                                       className: 'wcf\\data\\media\\MediaAction',
+                                       interfaceName: 'wcf\\data\\ISearchAction'
+                               }
+                       };
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                *
+                * @param       {object}        data    response data
+                */
+               _ajaxSuccess: function(data) {
+                       this._mediaManager.setMedia(data.returnValues.media || { }, data.returnValues.template || '');
+               },
+               
+               /**
+                * Cancels the search after clicking on the cancel search button.
+                */
+               _cancelSearch: function() {
+                       if (this._searchMode) {
+                               this._searchMode = false;
+                               
+                               this._mediaManager.resetMedia();
+                               this.resetSearch();
+                       }
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Search#_getIdPrefix
+                */
+               _getIdPrefix: function() {
+                       return 'mediaManager';
+               },
+               
+               /**
+                * Handles the `[ENTER]` key to submit the form.
+                *
+                * @param       {Event}         event           event object
+                */
+               _keyPress: function(event) {
+                       // 13 = [ENTER]
+                       if (event.charCode === 13) {
+                               event.preventDefault();
+                               
+                               var innerInfo = DomTraverse.childByClass(this._input.parentNode.parentNode, 'innerInfo');
+                               
+                               if (this._input.value.length >= this._mediaManager.getOption('minSearchLength')) {
+                                       if (innerInfo) {
+                                               elHide(innerInfo);
+                                       }
+                                       
+                                       this._search();
+                               }
+                               else {
+                                       if (innerInfo) {
+                                               elShow(innerInfo);
+                                       }
+                                       else {
+                                               innerInfo = elCreate('p');
+                                               innerInfo.className = 'innerInfo';
+                                               innerInfo.textContent = Language.get('wcf.media.search.info.searchStringThreshold');
+                                               
+                                               DomUtil.insertAfter(innerInfo, this._input.parentNode);
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Sends an AJAX request to fetch search results.
+                */
+               _search: function() {
+                       this._searchMode = true;
+                       
+                       Ajax.api(this, {
+                               parameters: {
+                                       fileType: this._fileType,
+                                       fileTypeFilters: this._mediaManager.getOption('fileTypeFilters'),
+                                       mode: this._mediaManager.getMode(),
+                                       searchString: this._input.value
+                               }
+                       });
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Search#_selectFileType
+                */
+               _selectFileType: function(event) {
+                       MediaManagerSearch._super.prototype._selectFileType.call(this, event);
+                       
+                       this._search();
+               },
+               
+               /**
+                * Hides the media search.
+                */
+               hideSearch: function() {
+                       elHide(elById(this._getIdPrefix() + 'Search'));
+               },
+               
+               /**
+                * Resets the media search.
+                */
+               resetSearch: function() {
+                       this._input.value = '';
+                       this._fileType = 'all';
+                       
+                       this._updateDropdownButtonLabel();
+               },
+               
+               /**
+                * Shows the media search.
+                */
+               showSearch: function() {
+                       elShow(elById(this._getIdPrefix() + 'Search'));
+               }
+       });
+       
+       return MediaManagerSearch;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Select.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Select.js
new file mode 100644 (file)
index 0000000..570647c
--- /dev/null
@@ -0,0 +1,189 @@
+/**
+ * Provides the media manager dialog for selecting media for input elements.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Media/Manager/Select
+ */
+define(['Core', 'Dom/Traverse', 'Dom/Util', 'Language', 'ObjectMap', 'Ui/Dialog', 'WoltLabSuite/Core/File/Util', 'WoltLabSuite/Core/Media/Manager/Base'],
+       function(Core, DomTraverse, DomUtil, Language, ObjectMap, UiDialog, FileUtil, MediaManagerBase) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaManagerSelect(options) {
+               MediaManagerBase.call(this, options);
+               
+               this._activeButton = null;
+               this._buttons = elByClass(this._options.buttonClass || 'jsMediaSelectButton');
+               this._storeElements = new ObjectMap();
+               
+               for (var i = 0, length = this._buttons.length; i < length; i++) {
+                       var button = this._buttons[i];
+                       
+                       // only consider buttons with a proper store specified
+                       var store = elData(button, 'store');
+                       if (store) {
+                               var storeElement = elById(store);
+                               if (storeElement && storeElement.tagName === 'INPUT') {
+                                       this._buttons[i].addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+                                       
+                                       this._storeElements.set(button, storeElement);
+                                       
+                                       // add remove button
+                                       var removeButton = elCreate('p');
+                                       removeButton.className = 'button';
+                                       DomUtil.insertAfter(removeButton, button);
+                                       
+                                       var icon = elCreate('span');
+                                       icon.className = 'icon icon16 fa-times';
+                                       removeButton.appendChild(icon);
+                                       
+                                       if (!storeElement.value) elHide(removeButton);
+                                       removeButton.addEventListener(WCF_CLICK_EVENT, this._removeMedia.bind(this));
+                               }
+                       }
+               }
+       }
+       Core.inherit(MediaManagerSelect, MediaManagerBase, {
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#_addButtonEventListeners
+                */
+               _addButtonEventListeners: function() {
+                       MediaManagerSelect._super.prototype._addButtonEventListeners.call(this);
+                       
+                       if (!this._mediaManagerMediaList) return;
+                       
+                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
+                       for (var i = 0, length = listItems.length; i < length; i++) {
+                               var listItem = listItems[i];
+                               
+                               var chooseIcon = elByClass('jsMediaSelectIcon', listItem)[0];
+                               if (chooseIcon) {
+                                       chooseIcon.classList.remove('jsMediaSelectIcon');
+                                       chooseIcon.addEventListener(WCF_CLICK_EVENT, this._chooseMedia.bind(this));
+                               }
+                       }
+               },
+               
+               /**
+                * Handles clicking on a media choose icon.
+                * 
+                * @param       {Event}         event           click event
+                */
+               _chooseMedia: function(event) {
+                       if (this._activeButton === null) {
+                               throw new Error("Media cannot be chosen if no button is active.");
+                       }
+                       
+                       var media = this._mediaData.get(~~elData(event.currentTarget, 'object-id'));
+                       
+                       // save selected media in store element
+                       elById(elData(this._activeButton, 'store')).value = media.mediaID;
+                       
+                       // display selected media
+                       var display = elData(this._activeButton, 'display');
+                       if (display) {
+                               var displayElement = elById(display);
+                               if (displayElement) {
+                                       if (media.isImage) {
+                                               displayElement.innerHTML = '<img src="' + media.smallThumbnailLink + '" alt="' + media.altText + '" />';
+                                       }
+                                       else {
+                                               displayElement.innerHTML = '<div class="box48" style="margin-bottom: 10px;">'
+                                                       + '<span class="icon icon48 ' + FileUtil.getIconClassByMimeType(media.fileType) + '"></span>'
+                                                       + '<div class="containerHeadline">'
+                                                               + '<h3>' + media.filename + '</h3>'
+                                                               + '<p>' + media.formattedFilesize + '</p>'
+                                                       + '</div>'
+                                               + '</div>';
+                                       }
+                               }
+                       }
+                       
+                       // show remove button
+                       elShow(this._activeButton.nextElementSibling);
+                       
+                       UiDialog.close('mediaManager');
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#_click
+                */
+               _click: function(event) {
+                       event.preventDefault();
+                       this._activeButton = event.currentTarget;
+                       
+                       MediaManagerSelect._super.prototype._click.call(this, event);
+                       
+                       if (!this._mediaManagerMediaList) return;
+                       
+                       var storeElement = this._storeElements.get(this._activeButton);
+                       var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI'), listItem;
+                       for (var i = 0, length = listItems.length; i < length; i++) {
+                               listItem = listItems[i];
+                               if (storeElement.value && storeElement.value == elData(listItem, 'object-id')) {
+                                       listItem.classList.add('jsSelected');
+                               }
+                               else {
+                                       listItem.classList.remove('jsSelected');
+                               }
+                       }
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#getMode
+                */
+               getMode: function() {
+                       return 'select';
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Media/Manager/Base#setupMediaElement
+                */
+               setupMediaElement: function(media, mediaElement) {
+                       MediaManagerSelect._super.prototype.setupMediaElement.call(this, media, mediaElement);
+                       
+                       // add media insertion icon
+                       var smallButtons = elBySel('nav.buttonGroupNavigation > ul.smallButtons', mediaElement);
+                       
+                       var listItem = elCreate('li');
+                       smallButtons.appendChild(listItem);
+                       
+                       var a = elCreate('a');
+                       listItem.appendChild(a);
+                       
+                       var icon = elCreate('span');
+                       icon.className = 'icon icon16 fa-check jsTooltip jsMediaSelectIcon';
+                       elData(icon, 'object-id', media.mediaID);
+                       elAttr(icon, 'title', Language.get('wcf.media.button.choose'));
+                       a.appendChild(icon);
+               },
+               
+               /**
+                * Handles clicking on the remove button.
+                *
+                * @param       {Event}         event           click event
+                */
+               _removeMedia: function(event) {
+                       event.preventDefault();
+                       
+                       var removeButton = event.currentTarget;
+                       elHide(removeButton);
+                       
+                       var button = removeButton.previousElementSibling;
+                       elById(elData(button, 'store')).value = 0;
+                       var display = elData(button, 'display');
+                       if (display) {
+                               var displayElement = elById(display);
+                               if (displayElement) {
+                                       displayElement.innerHTML = '';
+                               }
+                       }
+               }
+       });
+       
+       return MediaManagerSelect;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Search.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Search.js
new file mode 100644 (file)
index 0000000..47fe818
--- /dev/null
@@ -0,0 +1,119 @@
+/**
+ * Provides the media search for the media manager.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Media/Search
+ */
+define(['Ajax', 'Dom/Traverse', 'Dom/Util', 'Language', 'Ui/SimpleDropdown'], function(Ajax, DomTraverse, DomUtil, Language, UiSimpleDropdown) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaSearch(initialFileType) {
+               this._fileType = 'all';
+               
+               var dropdown = UiSimpleDropdown.getDropdownMenu(this._getIdPrefix() + 'Search');
+               if (dropdown) {
+                       this._fileTypes = DomTraverse.childrenBySel(dropdown, 'li:not(.dropdownDivider)');
+                       
+                       var selectFileType = this._selectFileType.bind(this);
+                       for (var i = 0, length = this._fileTypes.length; i < length; i++) {
+                               var listItem = this._fileTypes[i];
+                               
+                               if (initialFileType && elData(listItem, 'file-type') == initialFileType) {
+                                       this._fileType = initialFileType;
+                               }
+                               
+                               this._fileTypes[i].addEventListener(WCF_CLICK_EVENT, selectFileType);
+                       }
+                       
+                       if (initialFileType && initialFileType.length) {
+                               this._updateDropdownButtonLabel();
+                       }
+                       
+                       UiSimpleDropdown.registerCallback(this._getIdPrefix() + 'Search', this._updateFileTypeDropdown.bind(this));
+                       
+                       var form = DomTraverse.parentByTag(elById(this._getIdPrefix() + 'Search'), 'FORM');
+                       if (form) {
+                               form.addEventListener('submit', function() {
+                                       var fileTypeInput = elCreate('input');
+                                       elAttr(fileTypeInput, 'type', 'hidden');
+                                       elAttr(fileTypeInput, 'name', 'fileType');
+                                       elAttr(fileTypeInput, 'value', this._fileType);
+                                       
+                                       form.appendChild(fileTypeInput);
+                               }.bind(this));
+                       }
+               }
+               else {
+                       this._fileType = null;
+               }
+       }
+       MediaSearch.prototype = {
+               /**
+                * Returns the prefix to identify search-related elements.
+                * 
+                * @return      {string}
+                */
+               _getIdPrefix: function() {
+                       return 'media';
+               },
+               
+               /**
+                * Selects a certain file type after clicking on it in the dropdown menu.
+                *
+                * @param       {Event}         event
+                */
+               _selectFileType: function(event) {
+                       this._fileType = elData(event.currentTarget, 'file-type');
+                       
+                       this._updateDropdownButtonLabel(event);
+               },
+               
+               /**
+                * Updates the label of the dropdown button based on the currently selected file type.
+                */
+               _updateDropdownButtonLabel: function(event) {
+                       var dropdown = UiSimpleDropdown.getDropdown(this._getIdPrefix() + 'Search');
+                       var buttonLabel = DomTraverse.childBySel(DomTraverse.childByClass(dropdown, 'dropdownToggle'), 'SPAN');
+                       
+                       if (this._fileType !== 'all') {
+                               var listItem;
+                               if (event) {
+                                       listItem = event.currentTarget;
+                               }
+                               else {
+                                       for (var i = 0, length = this._fileTypes.length; i < length; i++) {
+                                               var _listItem = this._fileTypes[i];
+                                               
+                                               if (elData(_listItem, 'file-type') == this._fileType) {
+                                                       listItem = _listItem;
+                                                       break;
+                                               }
+                                       }
+                               }
+                               
+                               buttonLabel.textContent = DomTraverse.childBySel(listItem, 'SPAN').textContent;
+                       }
+                       else {
+                               buttonLabel.textContent = Language.get('wcf.media.search.filetype');
+                       }
+               },
+               
+               /**
+                * Updates the file type dropdown by correctly marking the currently selected file type.
+                */
+               _updateFileTypeDropdown: function() {
+                       for (var i = 0, length = this._fileTypes.length; i < length; i++) {
+                               var listItem = this._fileTypes[i];
+                               
+                               listItem.classList[elData(listItem, 'file-type') === this._fileType ? 'add' : 'remove']('active');
+                       }
+               }
+       };
+       
+       return MediaSearch;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Upload.js
new file mode 100644 (file)
index 0000000..e26f682
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * Uploads media files.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Media/Upload
+ */
+define(
+       [
+               'Core',                'Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util',
+               'EventHandler',        'Language',           'Permission',   'Upload',
+               'WoltLabSuite/Core/File/Util'
+       ],
+       function(
+               Core,                   DomChangeListener,    DomTraverse,    DomUtil,
+               EventHandler,           Language,             Permission,     Upload,
+               FileUtil
+       )
+{
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function MediaUpload(buttonContainerId, targetId, options) {
+               options = options || {};
+               
+               this._mediaManager = null;
+               if (options.mediaManager) {
+                       this._mediaManager = options.mediaManager;
+                       delete options.mediaManager;
+               }
+               
+               Upload.call(this, buttonContainerId, targetId, Core.extend({
+                       className: 'wcf\\data\\media\\MediaAction',
+                       multiple: this._mediaManager ? true : false,
+                       singleFileRequests: true
+               }, options));
+       }
+       Core.inherit(MediaUpload, Upload, {
+               /**
+                * @see WoltLabSuite/Core/Upload#_createFileElement
+                */
+               _createFileElement: function(file) {
+                       var fileElement;
+                       if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
+                               fileElement = elCreate('li');
+                       }
+                       else {
+                               fileElement = elCreate('p');
+                       }
+                       
+                       var thumbnail = elCreate('div');
+                       thumbnail.className = 'mediaThumbnail';
+                       fileElement.appendChild(thumbnail);
+                       
+                       var fileIcon = elCreate('span');
+                       fileIcon.className = 'icon icon144 fa-spinner';
+                       thumbnail.appendChild(fileIcon);
+                       
+                       var mediaInformation = elCreate('div');
+                       mediaInformation.className = 'mediaInformation';
+                       fileElement.appendChild(mediaInformation);
+                       
+                       var p = elCreate('p');
+                       p.className = 'mediaTitle';
+                       p.textContent = file.name;
+                       mediaInformation.appendChild(p);
+                       
+                       var progress = elCreate('progress');
+                       elAttr(progress, 'max', 100);
+                       mediaInformation.appendChild(progress);
+                       
+                       DomUtil.prepend(fileElement, this._target);
+                       
+                       DomChangeListener.trigger();
+                       
+                       return fileElement;
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_getParameters
+                */
+               _getParameters: function() {
+                       if (this._mediaManager) {
+                               return Core.extend(MediaUpload._super.prototype._getParameters.call(this), {
+                                       fileTypeFilters: this._mediaManager.getOption('fileTypeFilters')
+                               });
+                       }
+                       
+                       return MediaUpload._super.prototype._getParameters.call(this);
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Upload#_success
+                */
+               _success: function(uploadId, data) {
+                       var files = this._fileElements[uploadId];
+                       
+                       for (var i = 0, length = files.length; i < length; i++) {
+                               var file = files[i];
+                               var internalFileId = elData(file, 'internal-file-id');
+                               var media = data.returnValues.media[internalFileId];
+                               
+                               elRemove(DomTraverse.childByTag(DomTraverse.childByClass(file, 'mediaInformation'), 'PROGRESS'));
+                               
+                               if (media) {
+                                       var fileIcon = DomTraverse.childByTag(DomTraverse.childByClass(file, 'mediaThumbnail'), 'SPAN');
+                                       if (media.tinyThumbnailType) {
+                                               var parentNode = fileIcon.parentNode;
+                                               elRemove(fileIcon);
+                                               
+                                               var img = elCreate('img');
+                                               elAttr(img, 'src', media.tinyThumbnailLink);
+                                               elAttr(img, 'alt', '');
+                                               img.style.setProperty('width', '144px');
+                                               img.style.setProperty('height', '144px');
+                                               parentNode.appendChild(img);
+                                       }
+                                       else {
+                                               fileIcon.classList.remove('fa-spinner');
+                                               fileIcon.classList.add(FileUtil.getIconClassByMimeType(media.fileType));
+                                       }
+                                       
+                                       file.className = 'jsClipboardObject';
+                                       elData(file, 'object-id', media.mediaID);
+                                       
+                                       if (this._mediaManager) {
+                                               this._mediaManager.setupMediaElement(media, file);
+                                               this._mediaManager.resetMedia();
+                                               this._mediaManager.addMedia(media, file);
+                                       }
+                               }
+                               else {
+                                       var error = data.returnValues.errors[internalFileId];
+                                       if (!error) {
+                                               error = {
+                                                       errorType: 'uploadFailed',
+                                                       filename: elData(file, 'filename')
+                                               };
+                                       }
+                                       
+                                       var fileIcon = DomTraverse.childByTag(DomTraverse.childByClass(file, 'mediaThumbnail'), 'SPAN');
+                                       fileIcon.classList.remove('fa-spinner');
+                                       fileIcon.classList.add('fa-remove');
+                                       fileIcon.classList.add('pointer');
+                                       
+                                       file.classList.add('uploadFailed');
+                                       file.addEventListener(WCF_CLICK_EVENT, function() {
+                                               elRemove(this);
+                                       });
+                                       
+                                       var title = DomTraverse.childByClass(DomTraverse.childByClass(file, 'mediaInformation'), 'mediaTitle');
+                                       title.innerText = Language.get('wcf.media.upload.error.' + error.errorType, {
+                                               filename: error.filename
+                                       });
+                               }
+                               
+                               DomChangeListener.trigger();
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.media.upload', 'success', {
+                               files: files,
+                               media: data.returnValues.media,
+                               upload: this
+                       });
+               }
+       });
+       
+       return MediaUpload;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/NumberUtil.js b/wcfsetup/install/files/js/WoltLabSuite/Core/NumberUtil.js
new file mode 100644 (file)
index 0000000..4debae7
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Provides helper functions for Number handling.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/NumberUtil
+ */
+define([], function() {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/NumberUtil
+        */
+       var NumberUtil = {
+               /**
+                * Decimal adjustment of a number.
+                *
+                * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
+                * @param       {Number}        value   The number.
+                * @param       {Integer}       exp     The exponent (the 10 logarithm of the adjustment base).
+                * @returns     {Number}        The adjusted value.
+                */
+               round: function (value, exp) {
+                       // If the exp is undefined or zero...
+                       if (typeof exp === 'undefined' || +exp === 0) {
+                               return Math.round(value);
+                       }
+                       value = +value;
+                       exp = +exp;
+                       
+                       // If the value is not a number or the exp is not an integer...
+                       if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) {
+                               return NaN;
+                       }
+                       
+                       // Shift
+                       value = value.toString().split('e');
+                       value = Math.round(+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
+                       
+                       // Shift back
+                       value = value.toString().split('e');
+                       return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
+               }
+       };
+       
+       return NumberUtil;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/ObjectMap.js b/wcfsetup/install/files/js/WoltLabSuite/Core/ObjectMap.js
new file mode 100644 (file)
index 0000000..7051d62
--- /dev/null
@@ -0,0 +1,100 @@
+/**
+ * Simple `object` to `object` map using a native WeakMap on supported browsers, otherwise a set of two arrays.
+ * 
+ * If you're looking for a dictionary with string keys, please see `WoltLabSuite/Core/Dictionary`.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/ObjectMap
+ */
+define([], function() {
+       "use strict";
+       
+       var _hasMap = objOwns(window, 'WeakMap') && typeof window.WeakMap === 'function';
+       
+       /**
+        * @constructor
+        */
+       function ObjectMap() {
+               this._map = (_hasMap) ? new WeakMap() : { key: [], value: [] };
+       }
+       ObjectMap.prototype = {
+               /**
+                * Sets a new key with given value, will overwrite an existing key.
+                * 
+                * @param       {object}        key     key
+                * @param       {object}        value   value
+                */
+               set: function(key, value) {
+                       if (typeof key !== 'object' || key === null) {
+                               throw new TypeError("Only objects can be used as key");
+                       }
+                       
+                       if (typeof value !== 'object' || value === null) {
+                               throw new TypeError("Only objects can be used as value");
+                       }
+                       
+                       if (_hasMap) {
+                               this._map.set(key, value);
+                       }
+                       else {
+                               this._map.key.push(key);
+                               this._map.value.push(value);
+                       }
+               },
+               
+               /**
+                * Removes a key from the map.
+                * 
+                * @param       {object}        key     key
+                */
+               'delete': function(key) {
+                       if (_hasMap) {
+                               this._map['delete'](key);
+                       }
+                       else {
+                               var index = this._map.key.indexOf(key);
+                               this._map.key.splice(index);
+                               this._map.value.splice(index);
+                       }
+               },
+               
+               /**
+                * Returns true if dictionary contains a value for given key.
+                * 
+                * @param       {object}        key     key
+                * @return      {boolean}       true if key exists
+                */
+               has: function(key) {
+                       if (_hasMap) {
+                               return this._map.has(key);
+                       }
+                       else {
+                               return (this._map.key.indexOf(key) !== -1);
+                       }
+               },
+               
+               /**
+                * Retrieves a value by key, returns undefined if there is no match.
+                * 
+                * @param       {object}        key     key
+                * @return      {*}
+                */
+               get: function(key) {
+                       if (_hasMap) {
+                               return this._map.get(key);
+                       }
+                       else {
+                               var index = this._map.key.indexOf(key);
+                               if (index !== -1) {
+                                       return this._map.value[index];
+                               }
+                               
+                               return undefined;
+                       }
+               }
+       };
+       
+       return ObjectMap;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Permission.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Permission.js
new file mode 100644 (file)
index 0000000..1ad04af
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Manages user permissions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Permission
+ */
+define(['Dictionary'], function(Dictionary) {
+       "use strict";
+       
+       var _permissions = new Dictionary();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Permission
+        */
+       return {
+               /**
+                * Adds a single permission to the store.
+                * 
+                * @param       {string}        permission      permission name
+                * @param       {boolean}       value           permission value
+                */
+               add: function(permission, value) {
+                       if (typeof value !== "boolean") {
+                               throw new TypeError("Permission value has to be boolean.");
+                       }
+                       
+                       _permissions.set(permission, value);
+               },
+               
+               /**
+                * Adds all the permissions in the given object to the store.
+                * 
+                * @param       {Object.<string, boolean>}      object          permission list
+                */
+               addObject: function(object) {
+                       for (var key in object) {
+                               if (objOwns(object, key)) {
+                                       this.add(key, object[key]);
+                               }
+                       }
+               },
+               
+               /**
+                * Returns the value of a permission.
+                * 
+                * If the permission is unknown, false is returned.
+                * 
+                * @param       {string}        permission      permission name
+                * @return      {boolean}       permission value
+                */
+               get: function(permission) {
+                       if (_permissions.has(permission)) {
+                               return _permissions.get(permission);
+                       }
+                       
+                       return false;
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/StringUtil.js b/wcfsetup/install/files/js/WoltLabSuite/Core/StringUtil.js
new file mode 100644 (file)
index 0000000..37884bc
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * Provides helper functions for String handling.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/StringUtil
+ */
+define(['Language', './NumberUtil'], function(Language, NumberUtil) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/StringUtil
+        */
+       return {
+               /**
+                * Adds thousands separators to a given number.
+                * 
+                * @see         http://stackoverflow.com/a/6502556/782822
+                * @param       {?}     number
+                * @return      {String}
+                */
+               addThousandsSeparator: function(number) {
+                       // Fetch Language, as it cannot be provided because of a circular dependency
+                       if (Language === undefined) Language = require('Language');
+                       
+                       return String(number).replace(/(^-?\d{1,3}|\d{3})(?=(?:\d{3})+(?:$|\.))/g, '$1' + Language.get('wcf.global.thousandsSeparator'));
+               },
+               
+               /**
+                * Escapes special HTML-characters within a string
+                * 
+                * @param       {?}     string
+                * @return      {String}
+                */
+               escapeHTML: function (string) {
+                       return String(string).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
+               },
+               
+               /**
+                * Escapes a String to work with RegExp.
+                * 
+                * @see         https://github.com/sstephenson/prototype/blob/master/src/prototype/lang/regexp.js#L25
+                * @param       {?}     string
+                * @return      {String}
+                */
+               escapeRegExp: function(string) {
+                       return String(string).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+               },
+               
+               /**
+                * Rounds number to given count of floating point digits, localizes decimal-point and inserts thousands separators.
+                * 
+                * @param       {?}             number
+                * @param       {int}           decimalPlaces   The number of decimal places to leave after rounding.
+                * @return      {String}
+                */
+               formatNumeric: function(number, decimalPlaces) {
+                       // Fetch Language, as it cannot be provided because of a circular dependency
+                       if (Language === undefined) Language = require('Language');
+                       
+                       number = String(NumberUtil.round(number, decimalPlaces || -2));
+                       var numberParts = number.split('.');
+                       
+                       number = this.addThousandsSeparator(numberParts[0]);
+                       if (numberParts.length > 1) number += Language.get('wcf.global.decimalPoint') + numberParts[1];
+                       
+                       number = number.replace('-', '\u2212');
+                       
+                       return number;
+               },
+               
+               /**
+                * Makes a string's first character lowercase.
+                * 
+                * @param       {?}             string
+                * @return      {String}
+                */
+               lcfirst: function(string) {
+                       return String(string).substring(0, 1).toLowerCase() + string.substring(1);
+               },
+               
+               /**
+                * Makes a string's first character uppercase.
+                * 
+                * @param       {?}             string
+                * @return      {String}
+                */
+               ucfirst: function(string) {
+                       return String(string).substring(0, 1).toUpperCase() + string.substring(1);
+               },
+               
+               /**
+                * Unescapes special HTML-characters within a string.
+                * 
+                * @param       {?}             string
+                * @return      {String}
+                */
+               unescapeHTML: function (string) {
+                       return String(string).replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>');
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Template.grammar.jison b/wcfsetup/install/files/js/WoltLabSuite/Core/Template.grammar.jison
new file mode 100644 (file)
index 0000000..7499d05
--- /dev/null
@@ -0,0 +1,164 @@
+/**
+ * Grammar for WoltLabSuite/Core/Template.
+ * 
+ * Recompile using:
+ *    jison -m amd -o Template.grammar.js Template.grammar.jison
+ * after making changes to the grammar.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Template.grammar
+ */
+
+%lex
+%s command
+%%
+
+\{\*.*\*\} /* comment */
+\{literal\}.*?\{\/literal\} { yytext = yytext.substring(9, yytext.length - 10); return 'T_LITERAL'; }
+<command>\"([^"]|\\\.)*\" return 'T_QUOTED_STRING';
+<command>\'([^']|\\\.)*\' return 'T_QUOTED_STRING';
+\$ return 'T_VARIABLE';
+[_a-zA-Z][_a-zA-Z0-9]* { return 'T_VARIABLE_NAME'; }
+"."     return '.';
+"["     return '[';
+"]"     return ']';
+"("     return '(';
+")"     return ')';
+"="     return '=';
+"{ldelim}"  return '{ldelim}';
+"{rdelim}"  return '{rdelim}';
+"{#"   return '{#';
+"{@"   return '{@';
+"{if " { this.begin('command'); return '{if'; }
+"{else if " { this.begin('command'); return '{elseif'; }
+"{elseif "  { this.begin('command'); return '{elseif'; }
+"{else}"    return '{else}';
+"{/if}"     return '{/if}';
+"{lang}"    return '{lang}';
+"{/lang}"   return '{/lang}';
+"{include " { this.begin('command'); return '{include'; }
+"{implode " { this.begin('command'); return '{implode'; }
+"{/implode}" return '{/implode}';
+"{foreach "  { this.begin('command'); return '{foreach'; }
+"{foreachelse}"  return '{foreachelse}';
+"{/foreach}"  return '{/foreach}';
+"{"     return '{';
+<command>"}" { this.popState(); return '}';}
+"}"     return '}';
+\s+     return 'T_WS';
+<<EOF>>            return 'EOF';
+[^{]   return 'T_ANY';
+
+/lex
+
+%start TEMPLATE
+%ebnf
+
+%%
+
+// A valid template is any number of CHUNKs.
+TEMPLATE: CHUNK_STAR EOF { return $1 + ";"; };
+
+CHUNK_STAR: CHUNK* {
+       var result = $1.reduce(function (carry, item) {
+               if (item.encode && !carry[1]) carry[0] += " + '" + item.value;
+               else if (item.encode && carry[1]) carry[0] += item.value;
+               else if (!item.encode && carry[1]) carry[0] += "' + " + item.value;
+               else if (!item.encode && !carry[1]) carry[0] += " + " + item.value;
+               
+               carry[1] = item.encode;
+               return carry;
+       }, [ "''", false ]);
+       if (result[1]) result[0] += "'";
+       
+       $$ = result[0];
+};
+
+CHUNK:
+       PLAIN_ANY -> { encode: true, value: $1.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n') }
+|      T_LITERAL -> { encode: true, value: $1.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n') }
+|      COMMAND -> { encode: false, value: $1 }
+;
+
+PLAIN_ANY: T_ANY | '}' | '{' T_WS -> $1 + $2
+| ']' | '[' | ')' | '(' | '.' | '=' | T_VARIABLE | T_VARIABLE_NAME | T_QUOTED_STRING | T_WS;
+
+COMMAND:
+       '{if' COMMAND_PARAMETERS '}' CHUNK_STAR (ELSE_IF)* ELSE? '{/if}' {
+               $$ = "(function() { if (" + $2 + ") { return " + $4 + "; } " + $5.join(' ') + " " + ($6 || '') + " return ''; })()";
+       }
+|      '{include' COMMAND_PARAMETER_LIST '}' {
+               if (!$2['file']) throw new Error('Missing parameter file');
+               
+               $$ = $2['file'] + ".fetch(v)";
+       }
+|      '{implode' COMMAND_PARAMETER_LIST '}' CHUNK_STAR '{/implode}' {
+               if (!$2['from']) throw new Error('Missing parameter from');
+               if (!$2['item']) throw new Error('Missing parameter item');
+               if (!$2['glue']) $2['glue'] = "', '";
+               
+               $$ = "(function() { return " + $2['from'] + ".map(function(item) { v[" + $2['item'] + "] = item; return " + $4 + "; }).join(" + $2['glue'] + "); })()";
+       }
+|      '{foreach' COMMAND_PARAMETER_LIST '}' CHUNK_STAR FOREACH_ELSE? '{/foreach}' {
+               if (!$2['from']) throw new Error('Missing parameter from');
+               if (!$2['item']) throw new Error('Missing parameter item');
+               
+               $$ = "(function() {"
+               + "var looped = false, result = '';"
+               + "if (" + $2['from'] + " instanceof Array) {"
+                       + "for (var i = 0; i < " + $2['from'] + ".length; i++) { looped = true;"
+                               + "v[" + $2['key'] + "] = i;"
+                               + "v[" + $2['item'] + "] = " + $2['from'] + "[i];"
+                               + "result += " + $4 + ";"
+                       + "}"
+               + "} else {"
+                       + "for (var key in " + $2['from'] + ") {"
+                               + "if (!" + $2['from'] + ".hasOwnProperty(key)) continue;"
+                               + "looped = true;"
+                               + "v[" + $2['key'] + "] = key;"
+                               + "v[" + $2['item'] + "] = " + $2['from'] + "[key];"
+                               + "result += " + $4 + ";"
+                       + "}"
+               + "}"
+               + "return (looped ? result : " + ($5 || "''") + "); })()"
+       }
+|      '{lang}' CHUNK_STAR '{/lang}' -> "Language.get(" + $2 + ")"
+|      '{' VARIABLE '}'  -> "StringUtil.escapeHTML(" + $2 + ")"
+|      '{#' VARIABLE '}' -> "StringUtil.formatNumeric(" + $2 + ")"
+|      '{@' VARIABLE '}' -> $2
+|      '{ldelim}' -> "'{'"
+|      '{rdelim}' -> "'}'"
+;
+
+ELSE: '{else}' CHUNK_STAR -> "else { return " + $2 + "; }"
+;
+
+ELSE_IF: '{elseif' COMMAND_PARAMETERS '}' CHUNK_STAR -> "else if (" + $2 + ") { return " + $4 + "; }"
+;
+
+FOREACH_ELSE: '{foreachelse}' CHUNK_STAR -> $2
+;
+
+// VARIABLE parses a valid variable access (with optional property access)
+VARIABLE: T_VARIABLE T_VARIABLE_NAME VARIABLE_SUFFIX* -> "v['" + $2 + "']" + $3.join('');
+;
+
+VARIABLE_SUFFIX:
+       '[' COMMAND_PARAMETERS ']' -> $1 + $2 + $3
+|      '.' T_VARIABLE_NAME -> "['" + $2 + "']"
+|      '(' COMMAND_PARAMETERS? ')' -> $1 + ($2 || '') + $3
+;
+
+COMMAND_PARAMETER_LIST:
+       T_VARIABLE_NAME '=' COMMAND_PARAMETER_VALUE T_WS COMMAND_PARAMETER_LIST { $$ = $5; $$[$1] = $3; }
+|      T_VARIABLE_NAME '=' COMMAND_PARAMETER_VALUE { $$ = {}; $$[$1] = $3; }
+;
+
+COMMAND_PARAMETER_VALUE: T_QUOTED_STRING | VARIABLE;
+
+// COMMAND_PARAMETERS parses anything that is valid between a command name and the closing brace
+COMMAND_PARAMETERS: COMMAND_PARAMETER+ -> $1.join('')
+;
+COMMAND_PARAMETER: T_ANY | T_WS | '=' | T_QUOTED_STRING | VARIABLE | T_VARIABLE_NAME;
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Template.grammar.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Template.grammar.js
new file mode 100644 (file)
index 0000000..4dad8f3
--- /dev/null
@@ -0,0 +1,702 @@
+
+
+define(function(require){
+var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[2,47],$V1=[5,9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,28,29,31,32,33,35,36,37,39,40,41,42,44,46,48],$V2=[1,33],$V3=[1,37],$V4=[1,38],$V5=[1,39],$V6=[1,42],$V7=[1,40],$V8=[1,44],$V9=[11,12,14,15,17,20,21,22,23],$Va=[11,12,14,15,16,17,18,19,20,21,22,23],$Vb=[9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,28,29,31,33,36,39,40,41,42,44,46],$Vc=[28,44,46],$Vd=[12,14];
+var parser = {trace: function trace() { },
+yy: {},
+symbols_: {"error":2,"TEMPLATE":3,"CHUNK_STAR":4,"EOF":5,"CHUNK_STAR_repetition0":6,"CHUNK":7,"PLAIN_ANY":8,"T_LITERAL":9,"COMMAND":10,"T_ANY":11,"}":12,"{":13,"T_WS":14,"]":15,"[":16,")":17,"(":18,".":19,"=":20,"T_VARIABLE":21,"T_VARIABLE_NAME":22,"T_QUOTED_STRING":23,"{if":24,"COMMAND_PARAMETERS":25,"COMMAND_repetition0":26,"COMMAND_option0":27,"{/if}":28,"{include":29,"COMMAND_PARAMETER_LIST":30,"{implode":31,"{/implode}":32,"{foreach":33,"COMMAND_option1":34,"{/foreach}":35,"{lang}":36,"{/lang}":37,"VARIABLE":38,"{#":39,"{@":40,"{ldelim}":41,"{rdelim}":42,"ELSE":43,"{else}":44,"ELSE_IF":45,"{elseif":46,"FOREACH_ELSE":47,"{foreachelse}":48,"VARIABLE_repetition0":49,"VARIABLE_SUFFIX":50,"VARIABLE_SUFFIX_option0":51,"COMMAND_PARAMETER_VALUE":52,"COMMAND_PARAMETERS_repetition_plus0":53,"COMMAND_PARAMETER":54,"$accept":0,"$end":1},
+terminals_: {2:"error",5:"EOF",9:"T_LITERAL",11:"T_ANY",12:"}",13:"{",14:"T_WS",15:"]",16:"[",17:")",18:"(",19:".",20:"=",21:"T_VARIABLE",22:"T_VARIABLE_NAME",23:"T_QUOTED_STRING",24:"{if",28:"{/if}",29:"{include",31:"{implode",32:"{/implode}",33:"{foreach",35:"{/foreach}",36:"{lang}",37:"{/lang}",39:"{#",40:"{@",41:"{ldelim}",42:"{rdelim}",44:"{else}",46:"{elseif",48:"{foreachelse}"},
+productions_: [0,[3,2],[4,1],[7,1],[7,1],[7,1],[8,1],[8,1],[8,2],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[10,7],[10,3],[10,5],[10,6],[10,3],[10,3],[10,3],[10,3],[10,1],[10,1],[43,2],[45,4],[47,2],[38,3],[50,3],[50,2],[50,3],[30,5],[30,3],[52,1],[52,1],[25,1],[54,1],[54,1],[54,1],[54,1],[54,1],[54,1],[6,0],[6,2],[26,0],[26,2],[27,0],[27,1],[34,0],[34,1],[49,0],[49,2],[51,0],[51,1],[53,1],[53,2]],
+performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) {
+/* this == yyval */
+
+var $0 = $$.length - 1;
+switch (yystate) {
+case 1:
+ return $$[$0-1] + ";"; 
+break;
+case 2:
+
+       var result = $$[$0].reduce(function (carry, item) {
+               if (item.encode && !carry[1]) carry[0] += " + '" + item.value;
+               else if (item.encode && carry[1]) carry[0] += item.value;
+               else if (!item.encode && carry[1]) carry[0] += "' + " + item.value;
+               else if (!item.encode && !carry[1]) carry[0] += " + " + item.value;
+               
+               carry[1] = item.encode;
+               return carry;
+       }, [ "''", false ]);
+       if (result[1]) result[0] += "'";
+       
+       this.$ = result[0];
+
+break;
+case 3: case 4:
+this.$ = { encode: true, value: $$[$0].replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/(\r\n|\n|\r)/g, '\\n') };
+break;
+case 5:
+this.$ = { encode: false, value: $$[$0] };
+break;
+case 8:
+this.$ = $$[$0-1] + $$[$0];
+break;
+case 19:
+
+               this.$ = "(function() { if (" + $$[$0-5] + ") { return " + $$[$0-3] + "; } " + $$[$0-2].join(' ') + " " + ($$[$0-1] || '') + " return ''; })()";
+       
+break;
+case 20:
+
+               if (!$$[$0-1]['file']) throw new Error('Missing parameter file');
+               
+               this.$ = $$[$0-1]['file'] + ".fetch(v)";
+       
+break;
+case 21:
+
+               if (!$$[$0-3]['from']) throw new Error('Missing parameter from');
+               if (!$$[$0-3]['item']) throw new Error('Missing parameter item');
+               if (!$$[$0-3]['glue']) $$[$0-3]['glue'] = "', '";
+               
+               this.$ = "(function() { return " + $$[$0-3]['from'] + ".map(function(item) { v[" + $$[$0-3]['item'] + "] = item; return " + $$[$0-1] + "; }).join(" + $$[$0-3]['glue'] + "); })()";
+       
+break;
+case 22:
+
+               if (!$$[$0-4]['from']) throw new Error('Missing parameter from');
+               if (!$$[$0-4]['item']) throw new Error('Missing parameter item');
+               
+               this.$ = "(function() {"
+               + "var looped = false, result = '';"
+               + "if (" + $$[$0-4]['from'] + " instanceof Array) {"
+                       + "for (var i = 0; i < " + $$[$0-4]['from'] + ".length; i++) { looped = true;"
+                               + "v[" + $$[$0-4]['key'] + "] = i;"
+                               + "v[" + $$[$0-4]['item'] + "] = " + $$[$0-4]['from'] + "[i];"
+                               + "result += " + $$[$0-2] + ";"
+                       + "}"
+               + "} else {"
+                       + "for (var key in " + $$[$0-4]['from'] + ") {"
+                               + "if (!" + $$[$0-4]['from'] + ".hasOwnProperty(key)) continue;"
+                               + "looped = true;"
+                               + "v[" + $$[$0-4]['key'] + "] = key;"
+                               + "v[" + $$[$0-4]['item'] + "] = " + $$[$0-4]['from'] + "[key];"
+                               + "result += " + $$[$0-2] + ";"
+                       + "}"
+               + "}"
+               + "return (looped ? result : " + ($$[$0-1] || "''") + "); })()"
+       
+break;
+case 23:
+this.$ = "Language.get(" + $$[$0-1] + ")";
+break;
+case 24:
+this.$ = "StringUtil.escapeHTML(" + $$[$0-1] + ")";
+break;
+case 25:
+this.$ = "StringUtil.formatNumeric(" + $$[$0-1] + ")";
+break;
+case 26:
+this.$ = $$[$0-1];
+break;
+case 27:
+this.$ = "'{'";
+break;
+case 28:
+this.$ = "'}'";
+break;
+case 29:
+this.$ = "else { return " + $$[$0] + "; }";
+break;
+case 30:
+this.$ = "else if (" + $$[$0-2] + ") { return " + $$[$0] + "; }";
+break;
+case 31:
+this.$ = $$[$0];
+break;
+case 32:
+this.$ = "v['" + $$[$0-1] + "']" + $$[$0].join('');;
+break;
+case 33:
+this.$ = $$[$0-2] + $$[$0-1] + $$[$0];
+break;
+case 34:
+this.$ = "['" + $$[$0] + "']";
+break;
+case 35:
+this.$ = $$[$0-2] + ($$[$0-1] || '') + $$[$0];
+break;
+case 36:
+ this.$ = $$[$0]; this.$[$$[$0-4]] = $$[$0-2]; 
+break;
+case 37:
+ this.$ = {}; this.$[$$[$0-2]] = $$[$0]; 
+break;
+case 40:
+this.$ = $$[$0].join('');
+break;
+case 47: case 49: case 55:
+this.$ = [];
+break;
+case 48: case 50: case 56: case 60:
+$$[$0-1].push($$[$0]);
+break;
+case 59:
+this.$ = [$$[$0]];
+break;
+}
+},
+table: [o([5,9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,36,39,40,41,42],$V0,{3:1,4:2,6:3}),{1:[3]},{5:[1,4]},o([5,28,32,35,37,44,46,48],[2,2],{7:5,8:6,10:8,9:[1,7],11:[1,9],12:[1,10],13:[1,11],14:[1,21],15:[1,12],16:[1,13],17:[1,14],18:[1,15],19:[1,16],20:[1,17],21:[1,18],22:[1,19],23:[1,20],24:[1,22],29:[1,23],31:[1,24],33:[1,25],36:[1,26],39:[1,27],40:[1,28],41:[1,29],42:[1,30]}),{1:[2,1]},o($V1,[2,48]),o($V1,[2,3]),o($V1,[2,4]),o($V1,[2,5]),o($V1,[2,6]),o($V1,[2,7]),{14:[1,31],21:$V2,38:32},o($V1,[2,9]),o($V1,[2,10]),o($V1,[2,11]),o($V1,[2,12]),o($V1,[2,13]),o($V1,[2,14]),o($V1,[2,15]),o($V1,[2,16]),o($V1,[2,17]),o($V1,[2,18]),{11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7,25:34,38:41,53:35,54:36},{22:$V8,30:43},{22:$V8,30:45},{22:$V8,30:46},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,36,37,39,40,41,42],$V0,{6:3,4:47}),{21:$V2,38:48},{21:$V2,38:49},o($V1,[2,27]),o($V1,[2,28]),o($V1,[2,8]),{12:[1,50]},{22:[1,51]},{12:[1,52]},o([12,15,17],[2,40],{38:41,54:53,11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7}),o($V9,[2,59]),o($V9,[2,41]),o($V9,[2,42]),o($V9,[2,43]),o($V9,[2,44]),o($V9,[2,45]),o($V9,[2,46]),{12:[1,54]},{20:[1,55]},{12:[1,56]},{12:[1,57]},{37:[1,58]},{12:[1,59]},{12:[1,60]},o($V1,[2,24]),o($Va,[2,55],{49:61}),o($Vb,$V0,{6:3,4:62}),o($V9,[2,60]),o($V1,[2,20]),{21:$V2,23:[1,64],38:65,52:63},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,32,33,36,39,40,41,42],$V0,{6:3,4:66}),o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,35,36,39,40,41,42,48],$V0,{6:3,4:67}),o($V1,[2,23]),o($V1,[2,25]),o($V1,[2,26]),o($V9,[2,32],{50:68,16:[1,69],18:[1,71],19:[1,70]}),o($Vc,[2,49],{26:72}),{12:[2,37],14:[1,73]},o($Vd,[2,38]),o($Vd,[2,39]),{32:[1,74]},{34:75,35:[2,53],47:76,48:[1,77]},o($Va,[2,56]),{11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7,25:78,38:41,53:35,54:36},{22:[1,79]},{11:$V3,14:$V4,17:[2,57],20:$V5,21:$V2,22:$V6,23:$V7,25:81,38:41,51:80,53:35,54:36},{27:82,28:[2,51],43:84,44:[1,86],45:83,46:[1,85]},{22:$V8,30:87},o($V1,[2,21]),{35:[1,88]},{35:[2,54]},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,29,31,33,35,36,39,40,41,42],$V0,{6:3,4:89}),{15:[1,90]},o($Va,[2,34]),{17:[1,91]},{17:[2,58]},{28:[1,92]},o($Vc,[2,50]),{28:[2,52]},{11:$V3,14:$V4,20:$V5,21:$V2,22:$V6,23:$V7,25:93,38:41,53:35,54:36},o([9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,28,29,31,33,36,39,40,41,42],$V0,{6:3,4:94}),{12:[2,36]},o($V1,[2,22]),{35:[2,31]},o($Va,[2,33]),o($Va,[2,35]),o($V1,[2,19]),{12:[1,95]},{28:[2,29]},o($Vb,$V0,{6:3,4:96}),o($Vc,[2,30])],
+defaultActions: {4:[2,1],76:[2,54],81:[2,58],84:[2,52],87:[2,36],89:[2,31],94:[2,29]},
+parseError: function parseError(str, hash) {
+    if (hash.recoverable) {
+        this.trace(str);
+    } else {
+        throw new Error(str);
+    }
+},
+parse: function parse(input) {
+    var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1;
+    var args = lstack.slice.call(arguments, 1);
+    var lexer = Object.create(this.lexer);
+    var sharedState = { yy: {} };
+    for (var k in this.yy) {
+        if (Object.prototype.hasOwnProperty.call(this.yy, k)) {
+            sharedState.yy[k] = this.yy[k];
+        }
+    }
+    lexer.setInput(input, sharedState.yy);
+    sharedState.yy.lexer = lexer;
+    sharedState.yy.parser = this;
+    if (typeof lexer.yylloc == 'undefined') {
+        lexer.yylloc = {};
+    }
+    var yyloc = lexer.yylloc;
+    lstack.push(yyloc);
+    var ranges = lexer.options && lexer.options.ranges;
+    if (typeof sharedState.yy.parseError === 'function') {
+        this.parseError = sharedState.yy.parseError;
+    } else {
+        this.parseError = Object.getPrototypeOf(this).parseError;
+    }
+    function popStack(n) {
+        stack.length = stack.length - 2 * n;
+        vstack.length = vstack.length - n;
+        lstack.length = lstack.length - n;
+    }
+    _token_stack:
+        function lex() {
+            var token;
+            token = lexer.lex() || EOF;
+            if (typeof token !== 'number') {
+                token = self.symbols_[token] || token;
+            }
+            return token;
+        }
+    var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected;
+    while (true) {
+        state = stack[stack.length - 1];
+        if (this.defaultActions[state]) {
+            action = this.defaultActions[state];
+        } else {
+            if (symbol === null || typeof symbol == 'undefined') {
+                symbol = lex();
+            }
+            action = table[state] && table[state][symbol];
+        }
+                    if (typeof action === 'undefined' || !action.length || !action[0]) {
+                var errStr = '';
+                expected = [];
+                for (p in table[state]) {
+                    if (this.terminals_[p] && p > TERROR) {
+                        expected.push('\'' + this.terminals_[p] + '\'');
+                    }
+                }
+                if (lexer.showPosition) {
+                    errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\'';
+                } else {
+                    errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\'');
+                }
+                this.parseError(errStr, {
+                    text: lexer.match,
+                    token: this.terminals_[symbol] || symbol,
+                    line: lexer.yylineno,
+                    loc: yyloc,
+                    expected: expected
+                });
+            }
+        if (action[0] instanceof Array && action.length > 1) {
+            throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol);
+        }
+        switch (action[0]) {
+        case 1:
+            stack.push(symbol);
+            vstack.push(lexer.yytext);
+            lstack.push(lexer.yylloc);
+            stack.push(action[1]);
+            symbol = null;
+            if (!preErrorSymbol) {
+                yyleng = lexer.yyleng;
+                yytext = lexer.yytext;
+                yylineno = lexer.yylineno;
+                yyloc = lexer.yylloc;
+                if (recovering > 0) {
+                    recovering--;
+                }
+            } else {
+                symbol = preErrorSymbol;
+                preErrorSymbol = null;
+            }
+            break;
+        case 2:
+            len = this.productions_[action[1]][1];
+            yyval.$ = vstack[vstack.length - len];
+            yyval._$ = {
+                first_line: lstack[lstack.length - (len || 1)].first_line,
+                last_line: lstack[lstack.length - 1].last_line,
+                first_column: lstack[lstack.length - (len || 1)].first_column,
+                last_column: lstack[lstack.length - 1].last_column
+            };
+            if (ranges) {
+                yyval._$.range = [
+                    lstack[lstack.length - (len || 1)].range[0],
+                    lstack[lstack.length - 1].range[1]
+                ];
+            }
+            r = this.performAction.apply(yyval, [
+                yytext,
+                yyleng,
+                yylineno,
+                sharedState.yy,
+                action[1],
+                vstack,
+                lstack
+            ].concat(args));
+            if (typeof r !== 'undefined') {
+                return r;
+            }
+            if (len) {
+                stack = stack.slice(0, -1 * len * 2);
+                vstack = vstack.slice(0, -1 * len);
+                lstack = lstack.slice(0, -1 * len);
+            }
+            stack.push(this.productions_[action[1]][0]);
+            vstack.push(yyval.$);
+            lstack.push(yyval._$);
+            newState = table[stack[stack.length - 2]][stack[stack.length - 1]];
+            stack.push(newState);
+            break;
+        case 3:
+            return true;
+        }
+    }
+    return true;
+}};
+
+/* generated by jison-lex 0.3.4 */
+var lexer = (function(){
+var lexer = ({
+
+EOF:1,
+
+parseError:function parseError(str, hash) {
+        if (this.yy.parser) {
+            this.yy.parser.parseError(str, hash);
+        } else {
+            throw new Error(str);
+        }
+    },
+
+// resets the lexer, sets new input
+setInput:function (input, yy) {
+        this.yy = yy || this.yy || {};
+        this._input = input;
+        this._more = this._backtrack = this.done = false;
+        this.yylineno = this.yyleng = 0;
+        this.yytext = this.matched = this.match = '';
+        this.conditionStack = ['INITIAL'];
+        this.yylloc = {
+            first_line: 1,
+            first_column: 0,
+            last_line: 1,
+            last_column: 0
+        };
+        if (this.options.ranges) {
+            this.yylloc.range = [0,0];
+        }
+        this.offset = 0;
+        return this;
+    },
+
+// consumes and returns one char from the input
+input:function () {
+        var ch = this._input[0];
+        this.yytext += ch;
+        this.yyleng++;
+        this.offset++;
+        this.match += ch;
+        this.matched += ch;
+        var lines = ch.match(/(?:\r\n?|\n).*/g);
+        if (lines) {
+            this.yylineno++;
+            this.yylloc.last_line++;
+        } else {
+            this.yylloc.last_column++;
+        }
+        if (this.options.ranges) {
+            this.yylloc.range[1]++;
+        }
+
+        this._input = this._input.slice(1);
+        return ch;
+    },
+
+// unshifts one char (or a string) into the input
+unput:function (ch) {
+        var len = ch.length;
+        var lines = ch.split(/(?:\r\n?|\n)/g);
+
+        this._input = ch + this._input;
+        this.yytext = this.yytext.substr(0, this.yytext.length - len);
+        //this.yyleng -= len;
+        this.offset -= len;
+        var oldLines = this.match.split(/(?:\r\n?|\n)/g);
+        this.match = this.match.substr(0, this.match.length - 1);
+        this.matched = this.matched.substr(0, this.matched.length - 1);
+
+        if (lines.length - 1) {
+            this.yylineno -= lines.length - 1;
+        }
+        var r = this.yylloc.range;
+
+        this.yylloc = {
+            first_line: this.yylloc.first_line,
+            last_line: this.yylineno + 1,
+            first_column: this.yylloc.first_column,
+            last_column: lines ?
+                (lines.length === oldLines.length ? this.yylloc.first_column : 0)
+                 + oldLines[oldLines.length - lines.length].length - lines[0].length :
+              this.yylloc.first_column - len
+        };
+
+        if (this.options.ranges) {
+            this.yylloc.range = [r[0], r[0] + this.yyleng - len];
+        }
+        this.yyleng = this.yytext.length;
+        return this;
+    },
+
+// When called from action, caches matched text and appends it on next action
+more:function () {
+        this._more = true;
+        return this;
+    },
+
+// When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead.
+reject:function () {
+        if (this.options.backtrack_lexer) {
+            this._backtrack = true;
+        } else {
+            return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), {
+                text: "",
+                token: null,
+                line: this.yylineno
+            });
+
+        }
+        return this;
+    },
+
+// retain first n characters of the match
+less:function (n) {
+        this.unput(this.match.slice(n));
+    },
+
+// displays already matched input, i.e. for error messages
+pastInput:function () {
+        var past = this.matched.substr(0, this.matched.length - this.match.length);
+        return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
+    },
+
+// displays upcoming input, i.e. for error messages
+upcomingInput:function () {
+        var next = this.match;
+        if (next.length < 20) {
+            next += this._input.substr(0, 20-next.length);
+        }
+        return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, "");
+    },
+
+// displays the character position where the lexing error occurred, i.e. for error messages
+showPosition:function () {
+        var pre = this.pastInput();
+        var c = new Array(pre.length + 1).join("-");
+        return pre + this.upcomingInput() + "\n" + c + "^";
+    },
+
+// test the lexed token: return FALSE when not a match, otherwise return token
+test_match:function (match, indexed_rule) {
+        var token,
+            lines,
+            backup;
+
+        if (this.options.backtrack_lexer) {
+            // save context
+            backup = {
+                yylineno: this.yylineno,
+                yylloc: {
+                    first_line: this.yylloc.first_line,
+                    last_line: this.last_line,
+                    first_column: this.yylloc.first_column,
+                    last_column: this.yylloc.last_column
+                },
+                yytext: this.yytext,
+                match: this.match,
+                matches: this.matches,
+                matched: this.matched,
+                yyleng: this.yyleng,
+                offset: this.offset,
+                _more: this._more,
+                _input: this._input,
+                yy: this.yy,
+                conditionStack: this.conditionStack.slice(0),
+                done: this.done
+            };
+            if (this.options.ranges) {
+                backup.yylloc.range = this.yylloc.range.slice(0);
+            }
+        }
+
+        lines = match[0].match(/(?:\r\n?|\n).*/g);
+        if (lines) {
+            this.yylineno += lines.length;
+        }
+        this.yylloc = {
+            first_line: this.yylloc.last_line,
+            last_line: this.yylineno + 1,
+            first_column: this.yylloc.last_column,
+            last_column: lines ?
+                         lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length :
+                         this.yylloc.last_column + match[0].length
+        };
+        this.yytext += match[0];
+        this.match += match[0];
+        this.matches = match;
+        this.yyleng = this.yytext.length;
+        if (this.options.ranges) {
+            this.yylloc.range = [this.offset, this.offset += this.yyleng];
+        }
+        this._more = false;
+        this._backtrack = false;
+        this._input = this._input.slice(match[0].length);
+        this.matched += match[0];
+        token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]);
+        if (this.done && this._input) {
+            this.done = false;
+        }
+        if (token) {
+            return token;
+        } else if (this._backtrack) {
+            // recover context
+            for (var k in backup) {
+                this[k] = backup[k];
+            }
+            return false; // rule action called reject() implying the next rule should be tested instead.
+        }
+        return false;
+    },
+
+// return next match in input
+next:function () {
+        if (this.done) {
+            return this.EOF;
+        }
+        if (!this._input) {
+            this.done = true;
+        }
+
+        var token,
+            match,
+            tempMatch,
+            index;
+        if (!this._more) {
+            this.yytext = '';
+            this.match = '';
+        }
+        var rules = this._currentRules();
+        for (var i = 0; i < rules.length; i++) {
+            tempMatch = this._input.match(this.rules[rules[i]]);
+            if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
+                match = tempMatch;
+                index = i;
+                if (this.options.backtrack_lexer) {
+                    token = this.test_match(tempMatch, rules[i]);
+                    if (token !== false) {
+                        return token;
+                    } else if (this._backtrack) {
+                        match = false;
+                        continue; // rule action called reject() implying a rule MISmatch.
+                    } else {
+                        // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
+                        return false;
+                    }
+                } else if (!this.options.flex) {
+                    break;
+                }
+            }
+        }
+        if (match) {
+            token = this.test_match(match, rules[index]);
+            if (token !== false) {
+                return token;
+            }
+            // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace)
+            return false;
+        }
+        if (this._input === "") {
+            return this.EOF;
+        } else {
+            return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), {
+                text: "",
+                token: null,
+                line: this.yylineno
+            });
+        }
+    },
+
+// return next match that has a token
+lex:function lex() {
+        var r = this.next();
+        if (r) {
+            return r;
+        } else {
+            return this.lex();
+        }
+    },
+
+// activates a new lexer condition state (pushes the new lexer condition state onto the condition stack)
+begin:function begin(condition) {
+        this.conditionStack.push(condition);
+    },
+
+// pop the previously active lexer condition state off the condition stack
+popState:function popState() {
+        var n = this.conditionStack.length - 1;
+        if (n > 0) {
+            return this.conditionStack.pop();
+        } else {
+            return this.conditionStack[0];
+        }
+    },
+
+// produce the lexer rule set which is active for the currently active lexer condition state
+_currentRules:function _currentRules() {
+        if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) {
+            return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules;
+        } else {
+            return this.conditions["INITIAL"].rules;
+        }
+    },
+
+// return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available
+topState:function topState(n) {
+        n = this.conditionStack.length - 1 - Math.abs(n || 0);
+        if (n >= 0) {
+            return this.conditionStack[n];
+        } else {
+            return "INITIAL";
+        }
+    },
+
+// alias for begin(condition)
+pushState:function pushState(condition) {
+        this.begin(condition);
+    },
+
+// return the number of states currently on the stack
+stateStackSize:function stateStackSize() {
+        return this.conditionStack.length;
+    },
+options: {},
+performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
+var YYSTATE=YY_START;
+switch($avoiding_name_collisions) {
+case 0:/* comment */
+break;
+case 1: yy_.yytext = yy_.yytext.substring(9, yy_.yytext.length - 10); return 9; 
+break;
+case 2:return 23;
+break;
+case 3:return 23;
+break;
+case 4:return 21;
+break;
+case 5: return 22; 
+break;
+case 6:return 19;
+break;
+case 7:return 16;
+break;
+case 8:return 15;
+break;
+case 9:return 18;
+break;
+case 10:return 17;
+break;
+case 11:return 20;
+break;
+case 12:return 41;
+break;
+case 13:return 42;
+break;
+case 14:return 39;
+break;
+case 15:return 40;
+break;
+case 16: this.begin('command'); return 24; 
+break;
+case 17: this.begin('command'); return 46; 
+break;
+case 18: this.begin('command'); return 46; 
+break;
+case 19:return 44;
+break;
+case 20:return 28;
+break;
+case 21:return 36;
+break;
+case 22:return 37;
+break;
+case 23: this.begin('command'); return 29; 
+break;
+case 24: this.begin('command'); return 31; 
+break;
+case 25:return 32;
+break;
+case 26: this.begin('command'); return 33; 
+break;
+case 27:return 48;
+break;
+case 28:return 35;
+break;
+case 29:return 13;
+break;
+case 30: this.popState(); return 12;
+break;
+case 31:return 12;
+break;
+case 32:return 14;
+break;
+case 33:return 5;
+break;
+case 34:return 11;
+break;
+}
+},
+rules: [/^(?:\{\*.*\*\})/,/^(?:\{literal\}.*?\{\/literal\})/,/^(?:"([^"]|\\\.)*")/,/^(?:'([^']|\\\.)*')/,/^(?:\$)/,/^(?:[_a-zA-Z][_a-zA-Z0-9]*)/,/^(?:\.)/,/^(?:\[)/,/^(?:\])/,/^(?:\()/,/^(?:\))/,/^(?:=)/,/^(?:\{ldelim\})/,/^(?:\{rdelim\})/,/^(?:\{#)/,/^(?:\{@)/,/^(?:\{if )/,/^(?:\{else if )/,/^(?:\{elseif )/,/^(?:\{else\})/,/^(?:\{\/if\})/,/^(?:\{lang\})/,/^(?:\{\/lang\})/,/^(?:\{include )/,/^(?:\{implode )/,/^(?:\{\/implode\})/,/^(?:\{foreach )/,/^(?:\{foreachelse\})/,/^(?:\{\/foreach\})/,/^(?:\{)/,/^(?:\})/,/^(?:\})/,/^(?:\s+)/,/^(?:$)/,/^(?:[^{])/],
+conditions: {"command":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34],"inclusive":true},"INITIAL":{"rules":[0,1,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,31,32,33,34],"inclusive":true}}
+});
+return lexer;
+})();
+parser.lexer = lexer;
+return parser;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Template.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Template.js
new file mode 100644 (file)
index 0000000..69afbdc
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * WoltLabSuite/Core/Template provides a template scripting compiler similar
+ * to the PHP one of WoltLab Suite Core. It supports a limited
+ * set of useful commands and compiles templates down to a pure
+ * JavaScript Function.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Template
+ */
+define(['./Template.grammar', './StringUtil', 'Language'], function(parser, StringUtil, Language) {
+       "use strict";
+       
+       // work around bug in AMD module generation of Jison
+       function Parser() {
+               this.yy = {};
+       }
+       Parser.prototype = parser;
+       parser.Parser = Parser;
+       parser = new Parser();
+
+       /**
+        * Compiles the given template.
+        * 
+        * @param       {string}        template        Template to compile.
+        * @constructor
+        */
+       function Template(template) {
+               // Fetch Language/StringUtil, as it cannot be provided because of a circular dependency
+               if (Language === undefined) Language = require('Language');
+               if (StringUtil === undefined) StringUtil = require('StringUtil');
+               
+               try {
+                       template = parser.parse(template);
+                       template = "var tmp = {};\n"
+                       + "for (var key in v) tmp[key] = v[key];\n"
+                       + "v = tmp;\n"
+                       + "v.__wcf = window.WCF; v.__window = window;\n"
+                       + "return " + template;
+                       
+                       this.fetch = new Function("StringUtil", "Language", "v", template).bind(undefined, StringUtil, Language);
+               }
+               catch (e) {
+                       console.debug(e.message);
+                       throw e;
+               }
+       }
+       
+       Object.defineProperty(Template, 'callbacks', {
+               enumerable: false,
+               configurable: false,
+               get: function() {
+                       throw new Error('WCF.Template.callbacks is no longer supported');
+               },
+               set: function(value) {
+                       throw new Error('WCF.Template.callbacks is no longer supported');
+               }
+       });
+       
+       Template.prototype = {
+               /**
+                * Evaluates the Template using the given parameters.
+                * 
+                * @param       {object}        v       Parameters to pass to the template.
+                */
+               fetch: function(v) {
+                       // this will be replaced in the init function
+                       throw new Error('This Template is not initialized.');
+               }
+       };
+       
+       return Template;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Timer/Repeating.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Timer/Repeating.js
new file mode 100644 (file)
index 0000000..becc538
--- /dev/null
@@ -0,0 +1,70 @@
+/**
+ * Provides an object oriented API on top of `setInterval`.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Timer/Repeating
+ */
+define([], function() {
+       "use strict";
+       
+       /**
+        * Creates a new timer that executes the given `callback` every `delta` milliseconds.
+        * It will be created in started mode. Call `stop()` if necessary.
+        * The `callback` will be passed the owning instance of `Repeating`.
+        * 
+        * @constructor
+        * @param       {function(Repeating)}   callback
+        * @param       {int}                   delta
+        */
+       function Repeating(callback, delta) {
+               if (typeof callback !== 'function') {
+                       throw new TypeError("Expected a valid callback as first argument.");
+               }
+               if (delta < 0 || delta > 86400 * 1000) {
+                       throw new RangeError("Invalid delta " + delta + ". Delta must be in the interval [0, 86400000].");
+               }
+               
+               // curry callback with `this` as the first parameter
+               this._callback = callback.bind(undefined, this);
+               
+               this._delta = delta;
+               this._timer = undefined;
+               
+               this.restart();
+       }
+       Repeating.prototype = {
+               /**
+                * Stops the timer and restarts it. The next call will occur in `delta` milliseconds.
+                */
+               restart: function() {
+                       this.stop();
+                       
+                       this._timer = setInterval(this._callback, this._delta);
+               },
+               
+               /**
+                * Stops the timer. It will no longer be called until you call `restart`.
+                */
+               stop: function() {
+                       if (this._timer !== undefined) {
+                               clearInterval(this._timer);
+                               this._timer = undefined;
+                       }
+               },
+               
+               /**
+                * Changes the `delta` of the timer and `restart`s it.
+                * 
+                * @param       {int}   delta   New delta of the timer.
+                */
+               setDelta: function(delta) {
+                       this._delta = delta;
+                       
+                       this.restart();
+               }
+       };
+       
+       return Repeating;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Acl/Simple.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Acl/Simple.js
new file mode 100644 (file)
index 0000000..906136e
--- /dev/null
@@ -0,0 +1,74 @@
+define(['Language', 'Dom/ChangeListener', 'WoltLabSuite/Core/Ui/User/Search/Input'], function(Language, DomChangeListener, UiUserSearchInput) {
+       "use strict";
+       
+       function UiAclSimple(prefix) { this.init(prefix); }
+       UiAclSimple.prototype = {
+               init: function(prefix) {
+                       this._prefix = prefix || '';
+                       
+                       this._build();
+               },
+               
+               _build: function () {
+                       var container = elById(this._prefix + 'aclInputContainer');
+                       
+                       elById(this._prefix + 'aclAllowAll').addEventListener('change', (function() {
+                               elHide(container);
+                       }));
+                       elById(this._prefix + 'aclAllowAll_no').addEventListener('change', (function() {
+                               elShow(container);
+                       }));
+                       
+                       new UiUserSearchInput(elById(this._prefix + 'aclSearchInput'), {
+                               callbackSelect: this._select.bind(this),
+                               includeUserGroups: true,
+                               preventSubmit: true
+                       });
+                       
+                       this._aclListContainer = elById(this._prefix + 'aclListContainer');
+                       
+                       this._list = elById(this._prefix + 'aclAccessList');
+                       this._list.addEventListener(WCF_CLICK_EVENT, this._removeItem.bind(this));
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               _select: function(listItem) {
+                       var type = elData(listItem, 'type');
+                       
+                       var html = '<span class="icon icon16 fa-' + (type === 'group' ? 'users' : 'user') + '"></span>';
+                       html += '<span class="aclLabel">' + elData(listItem, 'label') + '</span>';
+                       html += '<span class="icon icon16 fa-times pointer jsTooltip" title="' + Language.get('wcf.global.button.delete') + '"></span>';
+                       html += '<input type="hidden" name="aclValues[' + type + '][]" value="' + elData(listItem, 'object-id') + '">';
+                       
+                       var item = elCreate('li');
+                       item.innerHTML = html;
+                       
+                       var firstUser = elBySel('.fa-user', this._list);
+                       if (firstUser === null) {
+                               this._list.appendChild(item);
+                       }
+                       else {
+                               this._list.insertBefore(item, firstUser.parentNode);
+                       }
+                       
+                       elShow(this._aclListContainer);
+                       
+                       DomChangeListener.trigger();
+                       
+                       return false;
+               },
+               
+               _removeItem: function (event) {
+                       if (event.target.classList.contains('fa-times')) {
+                               elRemove(event.target.parentNode);
+                               
+                               if (this._list.childElementCount === 0) {
+                                       elHide(this._aclListContainer);
+                               }
+                       }
+               }
+       };
+       
+       return UiAclSimple;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Alignment.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Alignment.js
new file mode 100644 (file)
index 0000000..53543d9
--- /dev/null
@@ -0,0 +1,253 @@
+/**
+ * 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     WoltLabSuite/Core/Ui/Alignment
+ */
+define(['Core', 'Language', 'Dom/Traverse', 'Dom/Util'], function(Core, Language, DomTraverse, DomUtil) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Alignment
+        */
+       return {
+               /**
+                * 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: 0,
+                               
+                               // 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',
+                               visibility: 'hidden !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 pointerBottom = 0;
+                               var pointerRight = 1;
+                               
+                               el.classList[(top === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerBottom]);
+                               el.classList[(left === 'auto' ? 'add' : 'remove')](options.pointerClassNames[pointerRight]);
+                       }
+                       
+                       if (bottom !== 'auto') bottom = Math.round(bottom) + 'px';
+                       if (left !== 'auto') left = Math.ceil(left) + 'px';
+                       if (right !== 'auto') right = Math.floor(right) + 'px';
+                       if (top !== 'auto') top = Math.round(top) + 'px';
+                       
+                       DomUtil.setStyles(el, {
+                               bottom: bottom,
+                               left: left,
+                               right: right,
+                               top: top
+                       });
+                       
+                       elShow(el);
+                       el.style.removeProperty('visibility');
+               },
+               
+               /**
+                * Calculates left/right position and verifies 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, int>}   elDimensions    element dimensions
+                * @param       {Object<string, int>}   refDimensions   reference element dimensions
+                * @param       {Object<string, int>}   refOffsets      position of reference element relative to the document
+                * @param       {int}                   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, int>}   elDimensions    element dimensions
+                * @param       {Object<string, int>}   refDimensions   reference element dimensions
+                * @param       {Object<string, int>}   refOffsets      position of reference element relative to the document
+                * @param       {int}                   windowHeight    window height
+                * @param       {int}                   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) < window.scrollY) {
+                                       result = false;
+                               }
+                       }
+                       else {
+                               top = refOffsets.top + refDimensions.height + verticalOffset;
+                               if (top + elDimensions.height - window.scrollY > windowHeight) {
+                                       result = false;
+                               }
+                       }
+                       
+                       return {
+                               align: align,
+                               bottom: bottom,
+                               top: top,
+                               result: result
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/CloseOverlay.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/CloseOverlay.js
new file mode 100644 (file)
index 0000000..8dc952f
--- /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     WoltLabSuite/Core/Ui/CloseOverlay
+ */
+define(['CallbackList'], function(CallbackList) {
+       "use strict";
+       
+       var _callbackList = new CallbackList();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/CloseOverlay
+        */
+       var UiCloseOverlay = {
+               /**
+                * Sets up global event listener for bubbled clicks events.
+                */
+               setup: function() {
+                       document.body.addEventListener(WCF_CLICK_EVENT, this.execute.bind(this));
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/CallbackList#add
+                */
+               add: _callbackList.add.bind(_callbackList),
+               
+               /**
+                * @see WoltLabSuite/Core/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/WoltLabSuite/Core/Ui/Confirmation.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Confirmation.js
new file mode 100644 (file)
index 0000000..8141f29
--- /dev/null
@@ -0,0 +1,171 @@
+/**
+ * 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     WoltLabSuite/Core/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     WoltLabSuite/Core/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: '',
+                               messageIsHtml: false,
+                               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() : '';
+                       if (_options.messageIsHtml) _text.innerHTML = _options.message;
+                       else _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 = elCreate('div');
+                       elAttr(dialog, 'id', 'wcfSystemConfirmation');
+                       dialog.classList.add('systemConfirmation');
+                       
+                       _text = elCreate('p');
+                       dialog.appendChild(_text);
+                       
+                       _content = elCreate('div');
+                       elAttr(_content, 'id', 'wcfSystemConfirmationContent');
+                       dialog.appendChild(_content);
+                       
+                       var formSubmit = elCreate('div');
+                       formSubmit.classList.add('formSubmit');
+                       dialog.appendChild(formSubmit);
+                       
+                       _confirmButton = elCreate('button');
+                       _confirmButton.classList.add('buttonPrimary');
+                       _confirmButton.textContent = Language.get('wcf.global.confirmation.confirm');
+                       _confirmButton.addEventListener(WCF_CLICK_EVENT, this._confirm.bind(this));
+                       formSubmit.appendChild(_confirmButton);
+                       
+                       var cancelButton = elCreate('button');
+                       cancelButton.textContent = Language.get('wcf.global.confirmation.cancel');
+                       cancelButton.addEventListener(WCF_CLICK_EVENT, 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/WoltLabSuite/Core/Ui/Dialog.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dialog.js
new file mode 100644 (file)
index 0000000..ed40787
--- /dev/null
@@ -0,0 +1,548 @@
+/**
+ * Modal dialog handler.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Dialog
+ */
+define(
+       [
+               'enquire',      'Ajax',       'Core',      'Dictionary',
+               'Environment',  'Language',   'ObjectMap', 'Dom/ChangeListener',
+               'Dom/Traverse', 'Dom/Util',   'Ui/Confirmation'
+       ],
+       function(
+               enquire,        Ajax,         Core,        Dictionary,
+               Environment,    Language,     ObjectMap,   DomChangeListener,
+               DomTraverse,    DomUtil,      UiConfirmation
+       )
+{
+       "use strict";
+       
+       var _activeDialog = null;
+       var _container = null;
+       var _dialogs = new Dictionary();
+       var _dialogObjects = new ObjectMap();
+       var _dialogFullHeight = false;
+       var _keyupListener = null;
+       var _staticDialogs = elByClass('jsStaticDialog');
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Dialog
+        */
+       return {
+               /**
+                * 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 = elCreate('div');
+                       _container.classList.add('dialogOverlay');
+                       elAttr(_container, 'aria-hidden', 'true');
+                       _container.addEventListener(WCF_CLICK_EVENT, this._closeOnBackdrop.bind(this));
+                       
+                       elById('content').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('(max-width: 767px)', {
+                               match: function() { _dialogFullHeight = true; },
+                               unmatch: function() { _dialogFullHeight = false; },
+                               setup: function() { _dialogFullHeight = true; },
+                               deferSetup: true
+                       });
+                       
+                       this._initStaticDialogs();
+                       DomChangeListener.add('Ui/Dialog', this._initStaticDialogs.bind(this));
+               },
+               
+               _initStaticDialogs: function() {
+                       var button, container, id;
+                       while (_staticDialogs.length) {
+                               button = _staticDialogs[0];
+                               button.classList.remove('jsStaticDialog');
+                               
+                               id = elData(button, 'dialog-id');
+                               if (id && (container = elById(id))) {
+                                       ((function(button, container) {
+                                               container.classList.remove('jsStaticDialogContent');
+                                               elHide(container);
+                                               button.addEventListener(WCF_CLICK_EVENT, this.openStatic.bind(this, container.id, null, { title: elData(container, 'title') }));
+                                       }).bind(this))(button, container);
+                               }
+                       }
+               },
+               
+               /**
+                * 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 = elById(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);
+                               
+                               // remove id and `display: none` from dialog element
+                               dialogElement.removeAttribute('id');
+                               elShow(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)) {
+                               if (typeof html === 'string' && html.trim() !== '') {
+                                       setupData.source = html;
+                               }
+                               else {
+                                       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));
+                                       
+                                       // deferred initialization
+                                       return {};
+                               }
+                       }
+                       else {
+                               if (typeof setupData.source === 'string') {
+                                       var dialogElement = elCreate('div');
+                                       elAttr(dialogElement, 'id', setupData.id);
+                                       DomUtil.setInnerHtml(dialogElement, 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) {
+                       document.documentElement.classList.add('pageOverlayActive');
+                       
+                       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,
+                                       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|object)}       id              element id
+                * @param       {string}                title           dialog title
+                */
+               setTitle: function(id, title) {
+                       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.");
+                       }
+                       
+                       var dialogTitle = elByClass('dialogTitle', data.dialog);
+                       if (dialogTitle.length) {
+                               dialogTitle[0].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 = elById(id);
+                               if (element === null) {
+                                       throw new Error("Expected either a HTML string or an existing element id.");
+                               }
+                       }
+                       
+                       var dialog = elCreate('div');
+                       dialog.classList.add('dialogContainer');
+                       elAttr(dialog, 'aria-hidden', 'true');
+                       elAttr(dialog, 'role', 'dialog');
+                       elData(dialog, 'id', id);
+                       
+                       var header = elCreate('header');
+                       dialog.appendChild(header);
+                       
+                       var titleId = DomUtil.getUniqueId();
+                       elAttr(dialog, 'aria-labelledby', titleId);
+                       
+                       var title = elCreate('span');
+                       title.classList.add('dialogTitle');
+                       title.textContent = options.title;
+                       elAttr(title, 'id', titleId);
+                       header.appendChild(title);
+                       
+                       if (options.closable) {
+                               var closeButton = elCreate('a');
+                               closeButton.className = 'dialogCloseButton jsTooltip';
+                               elAttr(closeButton, 'title', options.closeButtonLabel);
+                               elAttr(closeButton, 'aria-label', options.closeButtonLabel);
+                               closeButton.addEventListener(WCF_CLICK_EVENT, this._close.bind(this));
+                               header.appendChild(closeButton);
+                               
+                               var span = elCreate('span');
+                               span.className = 'icon icon24 fa-times';
+                               closeButton.appendChild(span);
+                       }
+                       
+                       var contentContainer = elCreate('div');
+                       contentContainer.classList.add('dialogContent');
+                       if (options.disableContentPadding) contentContainer.classList.add('dialogContentNoPadding');
+                       dialog.appendChild(contentContainer);
+                       
+                       var content;
+                       if (element === null) {
+                               if (typeof html === 'string') {
+                                       content = elCreate('div');
+                                       content.id = id;
+                                       DomUtil.setInnerHtml(content, html);
+                               }
+                               else if (html instanceof DocumentFragment) {
+                                       if (html.children[0].nodeName !== 'div' || html.childElementCount > 1) {
+                                               content = elCreate('div');
+                                               content.id = id;
+                                               content.appendChild(html);
+                                       }
+                                       else {
+                                               content = html;
+                                       }
+                               }
+                       }
+                       else {
+                               content = element;
+                       }
+                       
+                       contentContainer.appendChild(content);
+                       
+                       if (content.style.getPropertyValue('display') === 'none') {
+                               elShow(content);
+                       }
+                       
+                       _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 (typeof options.onSetup === 'function') {
+                               options.onSetup(content);
+                       }
+                       
+                       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 = elCreate('div');
+                               DomUtil.setInnerHtml(content, html);
+                               
+                               data.content.appendChild(content);
+                       }
+                       
+                       if (elAttr(data.dialog, 'aria-hidden') === 'true') {
+                               if (elAttr(_container, 'aria-hidden') === 'true') {
+                                       window.addEventListener('keyup', _keyupListener);
+                               }
+                               
+                               elAttr(data.dialog, 'aria-hidden', 'false');
+                               elAttr(_container, 'aria-hidden', 'false');
+                               elData(_container, 'close-on-click', (data.backdropCloseOnClick ? 'true' : 'false'));
+                               _activeDialog = id;
+                               
+                               // set focus on first applicable element
+                               var focusElement = elBySel('.jsDialogAutoFocus', data.dialog);
+                               if (focusElement !== null && focusElement.offsetParent !== null) {
+                                       focusElement.focus();
+                               }
+                               
+                               if (typeof data.onShow === 'function') {
+                                       data.onShow(data.content);
+                               }
+                       }
+                       
+                       this.rebuild(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 (elAttr(data.dialog, 'aria-hidden') === 'true') {
+                               return;
+                       }
+                       
+                       var contentContainer = data.content.parentNode;
+                       
+                       var formSubmit = elBySel('.formSubmit', data.content);
+                       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');
+                               contentContainer.style.removeProperty('margin-bottom');
+                       }
+                       
+                       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 (elData(_container, '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);
+                       }
+                       
+                       elAttr(data.dialog, 'aria-hidden', 'true');
+                       
+                       // get next active dialog
+                       _activeDialog = null;
+                       for (var i = 0; i < _container.childElementCount; i++) {
+                               var child = _container.children[i];
+                               if (elAttr(child, 'aria-hidden') === 'false') {
+                                       _activeDialog = elData(child, 'id');
+                                       break;
+                               }
+                       }
+                       
+                       if (_activeDialog === null) {
+                               elAttr(_container, 'aria-hidden', 'true');
+                               elData(_container, 'close-on-click', 'false');
+                               
+                               window.removeEventListener('keyup', _keyupListener);
+                               document.documentElement.classList.remove('pageOverlayActive');
+                       }
+                       else {
+                               data = _dialogs.get(_activeDialog);
+                               elData(_container, '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 {};
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Reusable.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Reusable.js
new file mode 100644 (file)
index 0000000..24b1e55
--- /dev/null
@@ -0,0 +1,82 @@
+/**
+ * Simple interface to work with reusable dropdowns that are not bound to a specific item.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Dropdown/Reusable
+ */
+define(['Dictionary', 'Ui/SimpleDropdown'], function(Dictionary, UiSimpleDropdown) {
+       "use strict";
+       
+       var _dropdowns = new Dictionary();
+       var _ghostElementId = 0;
+       
+       /**
+        * Returns dropdown name by internal identifier.
+        *
+        * @param       {string}        identifier      internal identifier
+        * @returns     {string}        dropdown name
+        */
+       function _getDropdownName(identifier) {
+               if (!_dropdowns.has(identifier)) {
+                       throw new Error("Unknown dropdown identifier '" + identifier + "'");
+               }
+               
+               return _dropdowns.get(identifier);
+       }
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Dropdown/Reusable
+        */
+       return {
+               /**
+                * Initializes a new reusable dropdown.
+                * 
+                * @param       {string}        identifier      internal identifier
+                * @param       {Element}       menu            dropdown menu element
+                */
+               init: function(identifier, menu) {
+                       if (_dropdowns.has(identifier)) {
+                               return;
+                       }
+                       
+                       var ghostElement = elCreate('div');
+                       ghostElement.id = 'reusableDropdownGhost' + _ghostElementId++;
+                       
+                       UiSimpleDropdown.initFragment(ghostElement, menu);
+                       
+                       _dropdowns.set(identifier, ghostElement.id);
+               },
+               
+               /**
+                * Returns the dropdown menu element.
+                * 
+                * @param       {string}        identifier      internal identifier
+                * @returns     {Element}       dropdown menu element
+                */
+               getDropdownMenu: function(identifier) {
+                       return UiSimpleDropdown.getDropdownMenu(_getDropdownName(identifier));
+               },
+               
+               /**
+                * Registers a callback invoked upon open and close.
+                * 
+                * @param       {string}        identifier      internal identifier
+                * @param       {function}      callback        callback function
+                */
+               registerCallback: function(identifier, callback) {
+                       UiSimpleDropdown.registerCallback(_getDropdownName(identifier), callback);
+               },
+               
+               /**
+                * Toggles a dropdown.
+                * 
+                * @param       {string}        identifier              internal identifier
+                * @param       {Element}       referenceElement        reference element used for alignment
+                */
+               toggleDropdown: function(identifier, referenceElement) {
+                       UiSimpleDropdown.toggleDropdown(_getDropdownName(identifier), referenceElement);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Dropdown/Simple.js
new file mode 100644 (file)
index 0000000..2bd4c4f
--- /dev/null
@@ -0,0 +1,462 @@
+/**
+ * Simple dropdown implementation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/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     WoltLabSuite/Core/Ui/Dropdown/Simple
+        */
+       return {
+               /**
+                * Performs initial setup such as setting up dropdowns and binding listeners.
+                */
+               setup: function() {
+                       if (_didInit) return;
+                       _didInit = true;
+                       
+                       _menuContainer = elCreate('div');
+                       _menuContainer.className = 'dropdownMenuContainer';
+                       document.body.appendChild(_menuContainer);
+                       
+                       _availableDropdowns = elByClass('dropdownToggle');
+                       
+                       this.initAll();
+                       
+                       UiCloseOverlay.add('WoltLabSuite/Core/Ui/Dropdown/Simple', this.closeAll.bind(this));
+                       DomChangeListener.add('WoltLabSuite/Core/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') || elData(button, '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(WCF_CLICK_EVENT, this._toggle.bind(this));
+                               
+                               _dropdowns.set(containerId, dropdown);
+                               _menus.set(containerId, menu);
+                               
+                               if (!containerId.match(/^wcf\d+$/)) {
+                                       elData(menu, 'source', containerId);
+                               }
+                               
+                               // prevent page scrolling
+                               if (menu.childElementCount && menu.children[0].classList.contains('scrollableDropdownMenu')) {
+                                       menu = menu.children[0];
+                                       elData(menu, 'scroll-to-active', true);
+                                       
+                                       var menuHeight = null, menuRealHeight = null;
+                                       menu.addEventListener('wheel', function (event) {
+                                               if (menuHeight === null) menuHeight = menu.clientHeight;
+                                               if (menuRealHeight === null) menuRealHeight = menu.scrollHeight;
+                                               
+                                               // positive value: scrolling up
+                                               if (event.wheelDelta > 0 && menu.scrollTop === 0) {
+                                                       event.preventDefault();
+                                               }
+                                               else if (event.wheelDelta < 0 && (menu.scrollTop + menuHeight === menuRealHeight)) {
+                                                       event.preventDefault();
+                                               }
+                                       });
+                               }
+                       }
+                       
+                       elData(button, 'target', containerId);
+                       
+                       if (isLazyInitialization) {
+                               setTimeout(function() { Core.triggerEvent(button, WCF_CLICK_EVENT); }, 10);
+                       }
+               },
+               
+               /**
+                * Initializes a remote-controlled dropdown.
+                * 
+                * @param       {Element}       dropdown        dropdown wrapper element
+                * @param       {Element}       menu            menu list element
+                */
+               initFragment: function(dropdown, menu) {
+                       this.setup();
+                       
+                       var containerId = DomUtil.identify(dropdown);
+                       if (_dropdowns.has(containerId)) {
+                               return;
+                       }
+                       
+                       _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
+                * @param       {Element=}      referenceElement        alternative reference element, used for reusable dropdown menus
+                */
+               toggleDropdown: function(containerId, referenceElement) {
+                       this._toggle(null, containerId, referenceElement);
+               },
+               
+               /**
+                * Calculates and sets the alignment of given dropdown.
+                * 
+                * @param       {Element}       dropdown                dropdown wrapper element
+                * @param       {Element}       dropdownMenu            menu list element
+                * @param       {Element=}      alternateElement        alternative reference element for alignment
+                */
+               setAlignment: function(dropdown, dropdownMenu, alternateElement) {
+                       // check if button belongs to an i18n textarea
+                       var button = elBySel('.dropdownToggle', dropdown), refDimensionsElement;
+                       if (button !== null && button.parentNode.classList.contains('inputAddonTextarea')) {
+                               refDimensionsElement = button;
+                       }
+                       
+                       UiAlignment.set(dropdownMenu, alternateElement || dropdown, {
+                               pointerClassNames: ['dropdownArrowBottom', 'dropdownArrowRight'],
+                               refDimensionsElement: refDimensionsElement || null,
+                               
+                               // alignment
+                               horizontal: (elData(dropdownMenu, 'dropdown-alignment-horizontal') === 'right') ? 'right' : 'left',
+                               vertical: (elData(dropdownMenu, 'dropdown-alignment-vertical') === 'top') ? 'top' : 'bottom'
+                       });
+               },
+               
+               /**
+                * 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);
+                       return (menu !== undefined && menu.classList.contains('dropdownOpen'));
+               },
+               
+               /**
+                * 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;
+                       //noinspection JSCheckFunctionSignatures
+                       var dropdowns = elBySelAll('.dropdown.dropdownOpen', dialogContent);
+                       
+                       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.classList.contains('dropdownOpen')) {
+                                       if (elDataBool(dropdown, 'is-overlay-dropdown-button')) {
+                                               this.setAlignment(dropdown, _menus.get(containerId));
+                                       }
+                                       else {
+                                               this.close(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
+                * @param       {Element=}      alternateElement        alternative reference element for alignment
+                * @return      {boolean}       'false' if event is not null
+                */
+               _toggle: function(event, targetId, alternateElement) {
+                       if (event !== null) {
+                               event.preventDefault();
+                               event.stopPropagation();
+                               
+                               //noinspection JSCheckFunctionSignatures
+                               targetId = elData(event.currentTarget, 'target');
+                       }
+                       
+                       var dropdown = _dropdowns.get(targetId), preventToggle = false;
+                       if (dropdown !== undefined) {
+                               // check if the dropdown is still the same, as some components (e.g. page actions)
+                               // re-create the parent of a button
+                               if (event) {
+                                       var button = event.currentTarget, parent = button.parentNode;
+                                       if (parent !== dropdown) {
+                                               parent.classList.add('dropdown');
+                                               parent.id = dropdown.id;
+                                               
+                                               // remove dropdown class and id from old parent
+                                               dropdown.classList.remove('dropdown');
+                                               dropdown.id = '';
+                                               
+                                               dropdown = parent;
+                                               _dropdowns.set(targetId, parent);
+                                       }
+                               }
+                               
+                               // Repeated clicks on the dropdown button will not cause it to close, the only way
+                               // to close it is by clicking somewhere else in the document or on another dropdown
+                               // toggle. This is used with the search bar to prevent the dropdown from closing by
+                               // setting the caret position in the search input field.
+                               if (elDataBool(dropdown, 'dropdown-prevent-toggle') && dropdown.classList.contains('dropdownOpen')) {
+                                       preventToggle = true;
+                               }
+                               
+                               // check if 'isOverlayDropdownButton' is set which indicates that the dropdown toggle is within an overlay
+                               if (elData(dropdown, 'is-overlay-dropdown-button') === null) {
+                                       var dialogContent = DomTraverse.parentByClass(dropdown, 'dialogContent');
+                                       elData(dropdown, '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')) {
+                                       if (preventToggle === false) {
+                                               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');
+                                       
+                                       if (menu.childElementCount && elDataBool(menu.children[0], 'scroll-to-active')) {
+                                               var list = menu.children[0];
+                                               list.removeAttribute('data-scroll-to-active');
+                                               
+                                               var active = null;
+                                               for (var i = 0, length = list.childElementCount; i < length; i++) {
+                                                       if (list.children[i].classList.contains('active')) {
+                                                               active = list.children[i];
+                                                               break;
+                                                       }
+                                               }
+                                               
+                                               if (active) {
+                                                       list.scrollTop = Math.max((active.offsetTop + active.clientHeight) - menu.clientHeight, 0);
+                                               }
+                                       }
+                                       
+                                       this._notifyCallbacks(containerId, 'open');
+                                       
+                                       this.setAlignment(dropdown, menu, alternateElement);
+                               }
+                       }).bind(this));
+                       
+                       //noinspection JSDeprecatedSymbols
+                       window.WCF.Dropdown.Interactive.Handler.closeAll();
+                       
+                       return (event === null);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/FlexibleMenu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/FlexibleMenu.js
new file mode 100644 (file)
index 0000000..e89bc2f
--- /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     WoltLabSuite/Core/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     WoltLabSuite/Core/Ui/FlexibleMenu
+        */
+       var UiFlexibleMenu = {
+               /**
+                * Register default menus and set up event listeners.
+                */
+               setup: function() {
+                       if (elById('mainMenu') !== null) this.register('mainMenu');
+                       var navigationHeader = elBySel('.navigationHeader');
+                       if (navigationHeader !== null) this.register(DomUtil.identify(navigationHeader));
+                       
+                       window.addEventListener('resize', this.rebuildAll.bind(this));
+                       DomChangeListener.add('WoltLabSuite/Core/Ui/FlexibleMenu', this.registerTabMenus.bind(this));
+               },
+               
+               /**
+                * Registers a menu by element id.
+                * 
+                * @param       {string}        containerId     element id
+                */
+               register: function(containerId) {
+                       var container = elById(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 = elBySelAll('.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;
+                                       }
+                                       
+                                       elShow(item);
+                               }
+                               
+                               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);
+                                       elHide(item);
+                                       
+                                       if (list.scrollWidth < availableWidth) {
+                                               break;
+                                       }
+                               }
+                       }
+                       
+                       if (hiddenItems.length) {
+                               var dropdownMenu;
+                               if (dropdown === undefined) {
+                                       dropdown = elCreate('li');
+                                       dropdown.className = 'dropdown jsFlexibleMenuDropdown';
+                                       var icon = elCreate('a');
+                                       icon.className = 'icon icon16 fa-list';
+                                       dropdown.appendChild(icon);
+                                       
+                                       dropdownMenu = elCreate('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 = elCreate('li');
+                                       item.innerHTML = hiddenItem.innerHTML;
+                                       
+                                       item.addEventListener(WCF_CLICK_EVENT, (function(event) {
+                                               event.preventDefault();
+                                               
+                                               Core.triggerEvent(elBySel('a', hiddenItem), WCF_CLICK_EVENT);
+                                               
+                                               // 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) {
+                               elRemove(dropdown);
+                       }
+               }
+       };
+       
+       return UiFlexibleMenu;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList.js
new file mode 100644 (file)
index 0000000..1e2e53d
--- /dev/null
@@ -0,0 +1,445 @@
+/**
+ * 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     WoltLabSuite/Core/Ui/ItemList
+ */
+define(['Core', 'Dictionary', 'Language', 'Dom/Traverse', 'WoltLabSuite/Core/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     WoltLabSuite/Core/Ui/ItemList
+        */
+       return {
+               /**
+                * 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}         values          list of existing values
+                * @param       {Object}        options         option list
+                */
+               init: function(elementId, values, options) {
+                       var element = elById(elementId);
+                       if (element === null) {
+                               throw new Error("Expected a valid element id, '" + elementId + "' is invalid.");
+                       }
+                       
+                       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 = elCreate('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);
+                       //noinspection JSUnresolvedVariable
+                       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}        elementId       input element id
+                * @return      {Array}         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: elData(item, 'object-id'),
+                                       value: DomTraverse.childByTag(item, 'SPAN').textContent
+                               };
+                               
+                               values.push(value);
+                       }
+                       
+                       return values;
+               },
+               
+               /**
+                * Sets the list of current values.
+                * 
+                * @param       {string}        elementId       input element id
+                * @param       {Array}         values          list of objects containing object id and value
+                */
+               setValues: function(elementId, values) {
+                       if (!_data.has(elementId)) {
+                               throw new Error("Element id '" + elementId + "' is unknown.");
+                       }
+                       
+                       var data = _data.get(elementId);
+                       
+                       // remove all existing items first
+                       var i, length;
+                       var items = DomTraverse.childrenByClass(data.list, 'item');
+                       for (i = 0, length = items.length; i < length; i++) {
+                               this._removeItem(null, items[i], true);
+                       }
+                       
+                       // add new items
+                       for (i = 0, length = values.length; i < length; i++) {
+                               this._addItem(elementId, values[i]);
+                       }
+               },
+               
+               /**
+                * 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}        options         option list
+                */
+               _createUI: function(element, options) {
+                       var list = elCreate('ol');
+                       list.className = 'inputItemList';
+                       elData(list, 'element-id', element.id);
+                       list.addEventListener(WCF_CLICK_EVENT, function(event) {
+                               if (event.target === list) {
+                                       //noinspection JSUnresolvedFunction
+                                       element.focus();
+                               }
+                       });
+                       
+                       var listItem = elCreate('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) {
+                               elAttr(element, 'maxLength', options.maxLength);
+                       }
+                       
+                       var shadow = null, values = [];
+                       if (options.isCSV) {
+                               shadow = elCreate('input');
+                               shadow.className = 'itemListInputShadow';
+                               shadow.type = 'hidden';
+                               //noinspection JSUnresolvedVariable
+                               shadow.name = element.name;
+                               element.removeAttribute('name');
+                               
+                               list.parentNode.insertBefore(shadow, list);
+                               
+                               if (element.nodeName === 'TEXTAREA') {
+                                       //noinspection JSUnresolvedVariable
+                                       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 = elCreate('input');
+                                       element.parentNode.insertBefore(inputElement, element);
+                                       inputElement.id = element.id;
+                                       
+                                       elRemove(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;
+                               elAttr(data.element, '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       {object}        value           item value
+                */
+               _addItem: function(elementId, value) {
+                       var data = _data.get(elementId);
+                       
+                       var listItem = elCreate('li');
+                       listItem.className = 'item';
+                       
+                       var content = elCreate('span');
+                       content.className = 'content';
+                       elData(content, 'object-id', value.objectId);
+                       content.textContent = value.value;
+                       
+                       var button = elCreate('a');
+                       button.className = 'icon icon16 fa-times';
+                       button.addEventListener(WCF_CLICK_EVENT, _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
+                * @param       {boolean?}      noFocus         input element will not be focused if true
+                */
+               _removeItem: function(event, item, noFocus) {
+                       item = (event === null) ? item : event.currentTarget.parentNode;
+                       
+                       var parent = item.parentNode;
+                       //noinspection JSCheckFunctionSignatures
+                       var elementId = elData(parent, 'element-id');
+                       var data = _data.get(elementId);
+                       
+                       data.suggestion.removeExcludedValue(item.children[0].textContent);
+                       parent.removeChild(item);
+                       if (!noFocus) 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;
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/Filter.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/Filter.js
new file mode 100644 (file)
index 0000000..7b922df
--- /dev/null
@@ -0,0 +1,184 @@
+/**
+ * Provides a filter input for checkbox lists.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Permission
+ */
+define(['EventKey', 'Language', 'List', 'StringUtil', 'Dom/Util'], function (EventKey, Language, List, StringUtil, DomUtil) {
+       "use strict";
+       
+       /**
+        * Creates a new filter input.
+        * 
+        * @param       {string}        elementId       list element id
+        * @constructor
+        */
+       function UiItemListFilter(elementId) { this.init(elementId); }
+       UiItemListFilter.prototype = {
+               /**
+                * Creates a new filter input.
+                * 
+                * @param       {string}        elementId       list element id
+                */
+               init: function(elementId) {
+                       this._value = '';
+                       
+                       var element = elById(elementId);
+                       if (element === null) {
+                               throw new Error("Expected a valid element id, '" + elementId + "' does not match anything.");
+                       }
+                       else if (!element.classList.contains('scrollableCheckboxList')) {
+                               throw new Error("Filter only works with elements with the CSS class 'scrollableCheckboxList'.");
+                       }
+                       
+                       var container = elCreate('div');
+                       container.className = 'itemListFilter';
+                       
+                       element.parentNode.insertBefore(container, element);
+                       container.appendChild(element);
+                       
+                       var inputAddon = elCreate('div');
+                       inputAddon.className = 'inputAddon';
+                       
+                       var input = elCreate('input');
+                       input.className = 'long';
+                       input.type = 'text';
+                       input.placeholder = Language.get('wcf.global.filter.placeholder');
+                       input.addEventListener('keydown', function (event) {
+                               if (EventKey.Enter(event)) {
+                                       event.preventDefault();
+                               }
+                       });
+                       input.addEventListener('keyup', this._keyup.bind(this));
+                       
+                       var clearButton = elCreate('a');
+                       clearButton.href = '#';
+                       clearButton.className = 'button inputSuffix jsTooltip';
+                       clearButton.title = Language.get('wcf.global.filter.button.clear');
+                       clearButton.innerHTML = '<span class="icon icon16 fa-times"></span>';
+                       clearButton.addEventListener('click', (function(event) {
+                               event.preventDefault();
+                               
+                               this._input.value = '';
+                               this._keyup();
+                       }).bind(this));
+                       
+                       inputAddon.appendChild(input);
+                       inputAddon.appendChild(clearButton);
+                       
+                       container.appendChild(inputAddon);
+                       
+                       this._container = container;
+                       this._element = element;
+                       this._input = input;
+                       this._items = null;
+                       this._fragment = null;
+               },
+               
+               /**
+                * Builds the item list and rebuilds the items' DOM for easier manipulation.
+                * 
+                * @protected
+                */
+               _buildItems: function() {
+                       this._items = new List();
+                       
+                       var item;
+                       for (var i = 0, length = this._element.childElementCount; i < length; i++) {
+                               item = this._element.children[i];
+                               
+                               var label = item.children[0];
+                               var text = label.textContent.trim();
+                               
+                               var checkbox = label.children[0];
+                               while (checkbox.nextSibling) {
+                                       label.removeChild(checkbox.nextSibling);
+                               }
+                               
+                               label.appendChild(document.createTextNode(' '));
+                               
+                               var span = elCreate('span');
+                               span.textContent = text;
+                               label.appendChild(span);
+                               
+                               this._items.add({
+                                       item: item,
+                                       span: span,
+                                       text: text
+                               });
+                       }
+               },
+               
+               /**
+                * Rebuilds the list on keyup, uses case-insensitive matching.
+                * 
+                * @protected
+                */
+               _keyup: function() {
+                       var value = this._input.value.trim();
+                       if (this._value === value) {
+                               return;
+                       }
+                       
+                       if (this._fragment === null) {
+                               this._fragment = document.createDocumentFragment();
+                               
+                               // set fixed height to avoid layout jumps
+                               this._element.style.setProperty('height', this._element.offsetHeight + 'px', '');
+                       }
+                       
+                       // move list into fragment before editing items, increases performance
+                       // by avoiding the browser to perform repaint/layout over and over again
+                       this._fragment.appendChild(this._element);
+                       
+                       if (this._items === null) {
+                               this._buildItems();
+                       }
+                       
+                       var regexp = new RegExp('(' + StringUtil.escapeRegExp(value) + ')', 'i');
+                       var hasVisibleItems = (value === '');
+                       this._items.forEach(function (item) {
+                               if (value === '') {
+                                       item.span.textContent = item.text;
+                                       
+                                       elShow(item.item);
+                               }
+                               else {
+                                       if (regexp.test(item.text)) {
+                                               item.span.innerHTML = item.text.replace(regexp, '<u>$1</u>');
+                                               
+                                               elShow(item.item);
+                                               hasVisibleItems = true;
+                                       }
+                                       else {
+                                               elHide(item.item);
+                                       }
+                               }
+                       });
+                       
+                       this._container.insertBefore(this._fragment.firstChild, this._container.firstChild);
+                       this._value = value;
+                       
+                       var innerError = this._container.nextElementSibling;
+                       if (innerError && !innerError.classList.contains('innerError')) innerError = null;
+                       
+                       if (hasVisibleItems) {
+                               if (innerError) {
+                                       elRemove(innerError);
+                               }
+                       }
+                       else {
+                               if (!innerError) {
+                                       innerError = elCreate('small');
+                                       innerError.className = 'innerError';
+                                       innerError.textContent = Language.get('wcf.global.filter.error.noMatches');
+                                       DomUtil.insertAfter(innerError, this._container);
+                               } 
+                       }
+               }
+       };
+       
+       return UiItemListFilter;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/User.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/ItemList/User.js
new file mode 100644 (file)
index 0000000..bba35ea
--- /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     WoltLabSuite/Core/Ui/ItemList/User
+ */
+define(['WoltLabSuite/Core/Ui/ItemList'], function(UiItemList) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/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 WoltLabSuite/Core/Ui/ItemList::getValues()
+                */
+               getValues: function(elementId) {
+                       return UiItemList.getValues(elementId);
+               }
+       };
+       
+       return UiItemListUser;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Like/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Like/Handler.js
new file mode 100644 (file)
index 0000000..ce7f335
--- /dev/null
@@ -0,0 +1,412 @@
+/**
+ * Provides interface elements to display and review likes.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Like/Handler
+ */
+define(
+       [
+               'Ajax',      'Core',                     'Dictionary',         'Language',
+               'ObjectMap', 'StringUtil',               'Dom/ChangeListener', 'Dom/Util',
+               'Ui/Dialog', 'WoltLabSuite/Core/Ui/User/List', 'User'
+       ],
+       function(
+               Ajax,        Core,                        Dictionary,           Language,
+               ObjectMap,   StringUtil,                  DomChangeListener,    DomUtil,
+               UiDialog,    UiUserList,                  User
+       )
+{
+       "use strict";
+       
+       var _isBusy = false;
+       
+       /**
+        * @constructor
+        */
+       function UiLikeHandler(objectType, options) { this.init(objectType, options); }
+       UiLikeHandler.prototype = {
+               /**
+                * Initializes the like handler.
+                * 
+                * @param       {string}        objectType      object type
+                * @param       {object}        options         initialization options
+                */
+               init: function(objectType, options) {
+                       if (options.containerSelector === '') {
+                               throw new Error("[WoltLabSuite/Core/Ui/Like/Handler] Expected a non-empty string for option 'containerSelector'.");
+                       }
+                       
+                       this._containers = new ObjectMap();
+                       this._details = new ObjectMap();
+                       this._objectType = objectType;
+                       this._options = Core.extend({
+                               // settings
+                               badgeClassNames: '',
+                               isSingleItem: false,
+                               markListItemAsActive: false,
+                               renderAsButton: true,
+                               summaryPrepend: true,
+                               summaryUseIcon: true,
+                               
+                               // permissions
+                               canDislike: false,
+                               canLike: false,
+                               canLikeOwnContent: false,
+                               canViewSummary: false,
+                               
+                               // selectors
+                               badgeContainerSelector: '.messageHeader .messageStatus',
+                               buttonAppendToSelector: '.messageFooter .messageFooterButtons',
+                               buttonBeforeSelector: '',
+                               containerSelector: '',
+                               summarySelector: '.messageFooterGroup'
+                       }, options);
+                       
+                       this.initContainers(options, objectType);
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Ui/Like/Handler-' + objectType, this.initContainers.bind(this));
+               },
+               
+               /**
+                * Initializes all applicable containers.
+                */
+               initContainers: function() {
+                       var element, elements = elBySelAll(this._options.containerSelector), elementData, triggerChange = false;
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               element = elements[i];
+                               if (this._containers.has(element)) {
+                                       continue;
+                               }
+                               
+                               elementData = {
+                                       badge: null,
+                                       dislikeButton: null,
+                                       likeButton: null,
+                                       summary: null,
+                                       
+                                       dislikes: ~~elData(element, 'like-dislikes'),
+                                       liked: ~~elData(element, 'like-liked'),
+                                       likes: ~~elData(element, 'like-likes'),
+                                       objectId: ~~elData(element, 'object-id'),
+                                       users: JSON.parse(elData(element, 'like-users'))
+                               };
+                               
+                               this._containers.set(element, elementData);
+                               this._buildWidget(element, elementData);
+                               
+                               triggerChange = true;
+                       }
+                       
+                       if (triggerChange) {
+                               DomChangeListener.trigger();
+                       }
+               },
+               
+               /**
+                * Creates the interface elements.
+                * 
+                * @param       {Element}       element         container element
+                * @param       {object}        elementData     like data
+                */
+               _buildWidget: function(element, elementData) {
+                       // build summary
+                       if (this._options.canViewSummary) {
+                               var summary, summaryContent, summaryIcon;
+                               var summaryContainer = (this._options.isSingleItem) ? elBySel(this._options.summarySelector) : elBySel(this._options.summarySelector, element);
+                               if (summaryContainer !== null) {
+                                       summary = elCreate('div');
+                                       summary.className = 'likesSummary';
+                                       
+                                       if (this._options.summaryUseIcon) {
+                                               summaryIcon = elCreate('span');
+                                               summaryIcon.className = 'icon icon16 fa-thumbs-o-up';
+                                               summary.appendChild(summaryIcon);
+                                       }
+                                       
+                                       summaryContent = elCreate('span');
+                                       summaryContent.className = 'likesSummaryContent';
+                                       summaryContent.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));
+                                       summary.appendChild(summaryContent);
+                                       
+                                       if (this._options.summaryPrepend) {
+                                               DomUtil.prepend(summary, summaryContainer);
+                                       }
+                                       else {
+                                               summaryContainer.appendChild(summary);
+                                       }
+                                       
+                                       elementData.summary = summaryContent;
+                                       
+                                       this._updateSummary(element);
+                               }
+                       }
+                       
+                       // cumulative likes
+                       var badge, listItem;
+                       var badgeContainer = (this._options.isSingleItem) ? elBySel(this._options.badgeContainerSelector) : elBySel(this._options.badgeContainerSelector, element);
+                       if (badgeContainer !== null) {
+                               badge = elCreate('a');
+                               badge.href = '#';
+                               badge.className = 'wcfLikeCounter jsTooltip' + (this._options.badgeClassNames ? ' ' + this._options.badgeClassNames : '');
+                               badge.addEventListener(WCF_CLICK_EVENT, this._showSummary.bind(this, element));
+                               
+                               if (badgeContainer.nodeName === 'OL' || badgeContainer.nodeName === 'UL') {
+                                       listItem = elCreate('li');
+                                       listItem.appendChild(badge);
+                                       badgeContainer.appendChild(listItem);
+                               }
+                               else {
+                                       badgeContainer.appendChild(badge);
+                               }
+                               
+                               elementData.badge = badge;
+                               
+                               this._updateBadge(element);
+                       }
+                       
+                       if (this._options.canLike && (User.userId != elData(element, 'user-id') || this._options.canLikeOwnContent)) {
+                               var appendTo = (this._options.buttonAppendToSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonAppendToSelector) : elBySel(this._options.buttonAppendToSelector, element)) : null;
+                               var insertPosition = (this._options.buttonBeforeSelector) ? ((this._options.isSingleItem) ? elBySel(this._options.buttonBeforeSelector) : elBySel(this._options.buttonBeforeSelector, element)) : null;
+                               if (insertPosition === null && appendTo === null) {
+                                       throw new Error("Unable to find insert location for like/dislike buttons.");
+                               }
+                               else {
+                                       // like button
+                                       elementData.likeButton = this._createButton(element, true, insertPosition, appendTo);
+                                       
+                                       // dislike button
+                                       if (this._options.canDislike) {
+                                               elementData.dislikeButton = this._createButton(element, false, insertPosition, appendTo);
+                                       }
+                                       
+                                       this._updateActiveState(element);
+                               }
+                       }
+               },
+               
+               /**
+                * Creates a like or dislike button.
+                * 
+                * @param       {Element}       element         container element
+                * @param       {boolean}       isLike          false if this is a dislike button
+                * @param       {Element?}      insertBefore    insert button before given element
+                * @param       {Element?}      appendTo        append button to given element
+                * @return      {Element}       button element 
+                */
+               _createButton: function(element, isLike, insertBefore, appendTo) {
+                       var title = Language.get('wcf.like.button.' + (isLike ? 'like' : 'dislike'));
+                       
+                       var listItem = elCreate('li');
+                       listItem.className = 'wcf' + (isLike ? 'Like' : 'Dislike') + 'Button';
+                       
+                       var button = elCreate('a');
+                       button.className = 'jsTooltip' + (this._options.renderAsButton ? ' button' : '');
+                       button.href = '#';
+                       button.title = title;
+                       button.innerHTML = '<span class="icon icon16 fa-thumbs-o-' + (isLike ? 'up' : 'down') + '"></span> <span class="invisible">' + title + '</span>';
+                       button.addEventListener(WCF_CLICK_EVENT, this._like.bind(this, element));
+                       elData(button, 'type', (isLike ? 'like' : 'dislike'));
+                       
+                       listItem.appendChild(button);
+                       
+                       if (insertBefore) {
+                               insertBefore.parentNode.insertBefore(listItem, insertBefore);
+                       }
+                       else {
+                               appendTo.appendChild(listItem);
+                       }
+                       
+                       return button;
+               },
+               
+               /**
+                * Shows the summary of likes/dislikes.
+                * 
+                * @param       {Element}       element         container element
+                * @param       {object}        event           event object
+                */
+               _showSummary: function(element, event) {
+                       event.preventDefault();
+                       
+                       if (!this._details.has(element)) {
+                               this._details.set(element, new UiUserList({
+                                       className: 'wcf\\data\\like\\LikeAction',
+                                       dialogTitle: Language.get('wcf.like.details'),
+                                       parameters: {
+                                               data: {
+                                                       containerID: DomUtil.identify(element),
+                                                       objectID: this._containers.get(element).objectId,
+                                                       objectType: this._objectType
+                                               }
+                                       }
+                               }));
+                       }
+                       
+                       this._details.get(element).open();
+               },
+               
+               /**
+                * Updates the display of cumulative likes.
+                * 
+                * @param       {Element}       element         container element
+                */
+               _updateBadge: function(element) {
+                       var data = this._containers.get(element);
+                       
+                       if (data.likes === 0 && data.dislikes === 0) {
+                               elHide(data.badge);
+                       }
+                       else {
+                               elShow(data.badge);
+                               
+                               // update like counter
+                               var cumulativeLikes = data.likes - data.dislikes;
+                               var content = '<span class="icon icon16 fa-thumbs-o-' + (cumulativeLikes < 0 ? 'down' : 'up' ) + '"></span><span class="wcfLikeValue">';
+                               if (cumulativeLikes > 0) {
+                                       content += '+' + StringUtil.addThousandsSeparator(cumulativeLikes);
+                                       data.badge.classList.add('likeCounterLiked');
+                               }
+                               else if (cumulativeLikes < 0) {
+                                       // U+2212 = minus sign
+                                       content += '\u2212' + StringUtil.addThousandsSeparator(Math.abs(cumulativeLikes));
+                                       data.badge.classList.add('likeCounterDisliked');
+                               }
+                               else {
+                                       // U+00B1 = plus-minus sign
+                                       content += '\u00B1' + '0';
+                               }
+                               
+                               data.badge.innerHTML = content + '</span>';
+                               data.badge.setAttribute('data-tooltip', Language.get('wcf.like.tooltip', {
+                                       dislikes: data.dislikes,
+                                       likes: data.likes
+                               }));
+                       }
+               },
+               
+               /**
+                * Updates the like summary.
+                * 
+                * @param       {Element}       element         container element
+                */
+               _updateSummary: function(element) {
+                       var data = this._containers.get(element);
+                       
+                       if (data.likes) {
+                               elShow(data.summary.parentNode);
+                               
+                               var usernames = [];
+                               var keys = Object.keys(data.users);
+                               for (var i = 0, length = keys.length; i < length; i++) {
+                                       usernames.push(data.users[keys[i]]);
+                               }
+                               
+                               var others = data.likes - usernames.length;
+                               data.summary.innerHTML = Language.get('wcf.like.summary', { users: usernames, others: others });
+                       }
+                       else {
+                               elHide(data.summary.parentNode);
+                       }
+               },
+               
+               /**
+                * Updates the active like/dislike button state.
+                * 
+                * @param       {Element}       element         container element
+                */
+               _updateActiveState: function(element) {
+                       var data = this._containers.get(element);
+                       
+                       var likeTarget = (this._options.markListItemAsActive) ? data.likeButton.parentNode : data.likeButton;
+                       likeTarget.classList.remove('active');
+                       
+                       if (data.liked === 1) {
+                               likeTarget.classList.add('active');
+                       }
+                       
+                       if (this._options.canDislike) {
+                               var dislikeTarget = (this._options.markListItemAsActive) ? data.dislikeButton.parentNode : data.dislikeButton;
+                               dislikeTarget.classList.remove('active');
+                               
+                               if (data.liked === -1) {
+                                       dislikeTarget.classList.add('active');
+                               }
+                       }
+               },
+               
+               /**
+                * Likes or dislikes an element.
+                * 
+                * @param       {Element}       element         container element
+                * @param       {object}        event           event object
+                */
+               _like: function(element, event) {
+                       event.preventDefault();
+                       
+                       if (_isBusy) {
+                               return;
+                       }
+                       
+                       _isBusy = true;
+                       
+                       Ajax.api(this, {
+                               actionName: elData(event.currentTarget, 'type'),
+                               parameters: {
+                                       data: {
+                                               containerID: DomUtil.identify(element),
+                                               objectID: this._containers.get(element).objectId,
+                                               objectType: this._objectType
+                                       }
+                               }
+                       });
+               },
+               
+               _ajaxSuccess: function(data) {
+                       var element = elById(data.returnValues.containerID);
+                       var elementData = this._containers.get(element);
+                       if (elementData === undefined) {
+                               return;
+                       }
+                       
+                       elementData.dislikes = ~~data.returnValues.dislikes;
+                       elementData.likes = ~~data.returnValues.likes;
+                       
+                       var users = data.returnValues.users;
+                       elementData.users = [];
+                       var keys = Object.keys(users);
+                       for (var i = 0, length = keys.length; i < length; i++) {
+                               elementData.users.push(StringUtil.escapeHTML(users[keys[i]].username));
+                       }
+                       
+                       if (data.returnValues.isLiked == 1) elementData.liked = 1;
+                       else if (data.returnValues.isDisliked == 1) elementData.liked = -1;
+                       else elementData.liked = 0;
+                       
+                       // update label
+                       this._updateBadge(element);
+                       
+                       // update summary
+                       if (this._options.canViewSummary) this._updateSummary(element);
+                       
+                       // mark button as active
+                       this._updateActiveState(element);
+                       
+                       // invalidate cache for like details
+                       this._details['delete'](element);
+                       
+                       _isBusy = false;
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       className: 'wcf\\data\\like\\LikeAction'
+                               }
+                       };
+               }
+       };
+       
+       return UiLikeHandler;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/InlineEditor.js
new file mode 100644 (file)
index 0000000..29761b3
--- /dev/null
@@ -0,0 +1,714 @@
+/**
+ * Flexible message inline editor.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Message/InlineEditor
+ */
+define(
+       [
+               'Ajax',         'Core',            'Dictionary',          'Environment',
+               'EventHandler', 'Language',        'ObjectMap',           'Dom/ChangeListener', 'Dom/Traverse',
+               'Dom/Util',     'Ui/Notification', 'Ui/ReusableDropdown', 'WoltLabSuite/Core/Ui/Scroll'
+       ],
+       function(
+               Ajax,            Core,              Dictionary,            Environment,
+               EventHandler,    Language,          ObjectMap,             DomChangeListener,    DomTraverse,
+               DomUtil,         UiNotification,    UiReusableDropdown,    UiScroll
+       )
+{
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiMessageInlineEditor(options) { this.init(options); }
+       UiMessageInlineEditor.prototype = {
+               /**
+                * Initializes the message inline editor.
+                * 
+                * @param       {Object}        options         list of configuration options
+                */
+               init: function(options) {
+                       this._activeDropdownElement = null;
+                       this._activeElement = null;
+                       this._dropdownMenu = null;
+                       this._elements = new ObjectMap();
+                       this._options = Core.extend({
+                               canEditInline: false,
+                               
+                               className: '',
+                               containerId: 0,
+                               dropdownIdentifier: '',
+                               editorPrefix: 'messageEditor',
+                               
+                               messageSelector: '.jsMessage',
+                               
+                               quoteManager: null
+                       }, options);
+                       
+                       this.rebuild();
+                       
+                       DomChangeListener.add('Ui/Message/InlineEdit_' + this._options.className, this.rebuild.bind(this));
+               },
+               
+               /**
+                * Initializes each applicable message, should be called whenever new
+                * messages are being displayed.
+                */
+               rebuild: function() {
+                       var button, canEdit, element, elements = elBySelAll(this._options.messageSelector);
+                       
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               element = elements[i];
+                               if (this._elements.has(element)) {
+                                       continue;
+                               }
+                               
+                               button = elBySel('.jsMessageEditButton', element);
+                               if (button !== null) {
+                                       canEdit = elDataBool(element, 'can-edit');
+                                       
+                                       if (this._options.canEditInline || elDataBool(element, 'can-edit-inline')) {
+                                               button.addEventListener(WCF_CLICK_EVENT, this._clickDropdown.bind(this, element));
+                                               button.classList.add('jsDropdownEnabled');
+                                               
+                                               if (canEdit) {
+                                                       button.addEventListener('dblclick', this._click.bind(this, element));
+                                               }
+                                       }
+                                       else if (canEdit) {
+                                               button.addEventListener(WCF_CLICK_EVENT, this._click.bind(this, element));
+                                       }
+                               }
+                               
+                               var messageBody = elBySel('.messageBody', element);
+                               var messageFooter = elBySel('.messageFooter', element);
+                               var messageHeader = elBySel('.messageHeader', element);
+                               
+                               this._elements.set(element, {
+                                       button: button,
+                                       messageBody: messageBody,
+                                       messageBodyEditor: null,
+                                       messageFooter: messageFooter,
+                                       messageFooterButtons: elBySel('.messageFooterButtons', messageFooter),
+                                       messageHeader: messageHeader,
+                                       messageText: elBySel('.messageText', messageBody)
+                               });
+                       }
+               },
+               
+               /**
+                * Handles clicks on the edit button or the edit dropdown item.
+                * 
+                * @param       {Element}       element         message element
+                * @param       {?Event}        event           event object
+                * @protected
+                */
+               _click: function(element, event) {
+                       if (element === null) element = this._activeDropdownElement;
+                       if (event) event.preventDefault();
+                       
+                       if (this._activeElement === null) {
+                               this._activeElement = element;
+                               
+                               this._prepare();
+                               
+                               Ajax.api(this, {
+                                       actionName: 'beginEdit',
+                                       parameters: {
+                                               containerID: this._options.containerId,
+                                               objectID: this._getObjectId(element)
+                                       }
+                               });
+                       }
+                       else {
+                               UiNotification.show('wcf.message.error.editorAlreadyInUse', null, 'warning');
+                       }
+               },
+               
+               /**
+                * Creates and opens the dropdown on first usage.
+                * 
+                * @param       {Element}       element         message element
+                * @param       {Object}        event           event object
+                * @protected
+                */
+               _clickDropdown: function(element, event) {
+                       event.preventDefault();
+                       
+                       var button = event.currentTarget;
+                       if (button.classList.contains('dropdownToggle')) {
+                               return;
+                       }
+                       
+                       button.classList.add('dropdownToggle');
+                       button.parentNode.classList.add('dropdown');
+                       (function(button, element) {
+                               button.addEventListener(WCF_CLICK_EVENT, (function(event) {
+                                       event.preventDefault();
+                                       event.stopPropagation();
+                                       
+                                       this._activeDropdownElement = element;
+                                       UiReusableDropdown.toggleDropdown(this._options.dropdownIdentifier, button);
+                               }).bind(this));
+                       }).bind(this)(button, element);
+                       
+                       // build dropdown
+                       if (this._dropdownMenu === null) {
+                               this._dropdownMenu = elCreate('ul');
+                               this._dropdownMenu.className = 'dropdownMenu';
+                               
+                               var items = this._dropdownGetItems();
+                               
+                               EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownInit_' + this._options.dropdownIdentifier, {
+                                       items: items
+                               });
+                               
+                               this._dropdownBuild(items);
+                               
+                               UiReusableDropdown.init(this._options.dropdownIdentifier, this._dropdownMenu);
+                               UiReusableDropdown.registerCallback(this._options.dropdownIdentifier, this._dropdownToggle.bind(this));
+                       }
+                       
+                       setTimeout(function() {
+                               Core.triggerEvent(button, WCF_CLICK_EVENT);
+                       }, 10);
+               },
+               
+               /**
+                * Creates the dropdown menu on first usage.
+                * 
+                * @param       {Object}        items   list of dropdown items
+                * @protected
+                */
+               _dropdownBuild: function(items) {
+                       var item, label, listItem;
+                       var callbackClick = this._clickDropdownItem.bind(this);
+                       
+                       for (var i = 0, length = items.length; i < length; i++) {
+                               item = items[i];
+                               listItem = elCreate('li');
+                               elData(listItem, 'item', item.item);
+                               
+                               if (item.item === 'divider') {
+                                       listItem.className = 'dropdownDivider';
+                               }
+                               else {
+                                       label = elCreate('span');
+                                       label.textContent = Language.get(item.label);
+                                       listItem.appendChild(label);
+                                       
+                                       if (item.item === 'editItem') {
+                                               listItem.addEventListener(WCF_CLICK_EVENT, this._click.bind(this, null));
+                                       }
+                                       else {
+                                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                                       }
+                               }
+                               
+                               this._dropdownMenu.appendChild(listItem);
+                       }
+               },
+               
+               /**
+                * Callback for dropdown toggle.
+                * 
+                * @param       {int}           containerId     container id
+                * @param       {string}        action          toggle action, either 'open' or 'close'
+                * @protected
+                */
+               _dropdownToggle: function(containerId, action) {
+                       var elementData = this._elements.get(this._activeDropdownElement);
+                       elementData.button.parentNode.classList[(action === 'open' ? 'add' : 'remove')]('dropdownOpen');
+                       elementData.messageFooterButtons.classList[(action === 'open' ? 'add' : 'remove')]('forceVisible');
+                       
+                       if (action === 'open') {
+                               var visibility = this._dropdownOpen();
+                               
+                               EventHandler.fire('com.woltlab.wcf.inlineEditor', 'dropdownOpen_' + this._options.dropdownIdentifier, {
+                                       element: this._activeDropdownElement,
+                                       visibility: visibility
+                               });
+                               
+                               var item, listItem, visiblePredecessor = false;
+                               for (var i = 0; i < this._dropdownMenu.childElementCount; i++) {
+                                       listItem = this._dropdownMenu.children[i];
+                                       item = elData(listItem, 'item');
+                                       
+                                       if (item === 'divider') {
+                                               if (visiblePredecessor) {
+                                                       elShow(listItem);
+                                                       
+                                                       visiblePredecessor = false;
+                                               }
+                                               else {
+                                                       elHide(listItem);
+                                               }
+                                       }
+                                       else {
+                                               if (objOwns(visibility, item) && visibility[item] === false) {
+                                                       elHide(listItem);
+                                                       
+                                                       // check if previous item was a divider
+                                                       if (i > 0 && i + 1 === this._dropdownMenu.childElementCount) {
+                                                               if (elData(listItem.previousElementSibling, 'item') === 'divider') {
+                                                                       elHide(listItem.previousElementSibling);
+                                                               }
+                                                       }
+                                               }
+                                               else {
+                                                       elShow(listItem);
+                                                       
+                                                       visiblePredecessor = true;
+                                               }
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Returns the list of dropdown items for this type.
+                * 
+                * @return      {Array<Object>}         list of objects containing the type name and label
+                * @protected
+                */
+               _dropdownGetItems: function() {},
+               
+               /**
+                * Invoked once the dropdown for this type is shown, expects a list of type name and a boolean value
+                * to represent the visibility of each item. Items that do not appear in this list will be considered
+                * visible.
+                * 
+                * @return      {Object<string, boolean>}
+                * @protected
+                */
+               _dropdownOpen: function() {},
+               
+               /**
+                * Invoked whenever the user selects an item from the dropdown menu, the selected item is passed as argument.
+                * 
+                * @param       {string}        item    selected dropdown item
+                * @protected
+                */
+               _dropdownSelect: function(item) {},
+               
+               /**
+                * Handles clicks on a dropdown item.
+                * 
+                * @param       {Event}         event   event object
+                * @protected
+                */
+               _clickDropdownItem: function(event) {
+                       event.preventDefault();
+                       
+                       //noinspection JSCheckFunctionSignatures
+                       this._dropdownSelect(elData(event.currentTarget, 'item'));
+               },
+               
+               /**
+                * Prepares the message for editor display.
+                * 
+                * @protected
+                */
+               _prepare: function() {
+                       var data = this._elements.get(this._activeElement);
+                       
+                       var messageBodyEditor = elCreate('div');
+                       messageBodyEditor.className = 'messageBody editor';
+                       data.messageBodyEditor = messageBodyEditor;
+                       
+                       var icon = elCreate('span');
+                       icon.className = 'icon icon48 fa-spinner';
+                       messageBodyEditor.appendChild(icon);
+                       
+                       DomUtil.insertAfter(messageBodyEditor, data.messageBody);
+                       
+                       elHide(data.messageBody);
+               },
+               
+               /**
+                * Shows the message editor.
+                * 
+                * @param       {Object}        data            ajax response data
+                * @protected
+                */
+               _showEditor: function(data) {
+                       var id = this._getEditorId();
+                       var elementData = this._elements.get(this._activeElement);
+                       
+                       this._activeElement.classList.add('jsInvalidQuoteTarget');
+                       var icon = DomTraverse.childByClass(elementData.messageBodyEditor, 'icon');
+                       elRemove(icon);
+                       
+                       var messageBody = elementData.messageBodyEditor;
+                       var editor = elCreate('div');
+                       editor.className = 'editorContainer';
+                       //noinspection JSUnresolvedVariable
+                       DomUtil.setInnerHtml(editor, data.returnValues.template);
+                       messageBody.appendChild(editor);
+                       
+                       // bind buttons
+                       var formSubmit = elBySel('.formSubmit', editor);
+                       
+                       var buttonSave = elBySel('button[data-type="save"]', formSubmit);
+                       buttonSave.addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
+                       
+                       var buttonCancel = elBySel('button[data-type="cancel"]', formSubmit);
+                       buttonCancel.addEventListener(WCF_CLICK_EVENT, this._restoreMessage.bind(this));
+                       
+                       EventHandler.add('com.woltlab.wcf.redactor', 'submitEditor_' + id, (function(data) {
+                               data.cancel = true;
+                               
+                               this._save();
+                       }).bind(this));
+                       
+                       // hide message header and footer
+                       elHide(elementData.messageHeader);
+                       elHide(elementData.messageFooter);
+                       
+                       var editorElement = elById(id);
+                       if (Environment.editor() === 'redactor') {
+                               window.setTimeout((function() {
+                                       if (this._options.quoteManager) {
+                                               this._options.quoteManager.setAlternativeEditor(id);
+                                       }
+                                       
+                                       UiScroll.element(this._activeElement);
+                               }).bind(this), 250);
+                       }
+                       else {
+                               editorElement.focus();
+                       }
+               },
+               
+               /**
+                * Restores the message view.
+                * 
+                * @protected
+                */
+               _restoreMessage: function() {
+                       var elementData = this._elements.get(this._activeElement);
+                       
+                       this._destroyEditor();
+                       
+                       elRemove(elementData.messageBodyEditor);
+                       elementData.messageBodyEditor = null;
+                       
+                       elShow(elementData.messageBody);
+                       elShow(elementData.messageFooter);
+                       elShow(elementData.messageHeader);
+                       this._activeElement.classList.remove('jsInvalidQuoteTarget');
+                       
+                       this._activeElement = null;
+                       
+                       if (this._options.quoteManager) {
+                               this._options.quoteManager.clearAlternativeEditor();
+                       }
+               },
+               
+               /**
+                * Saves the editor message.
+                * 
+                * @protected
+                */
+               _save: function() {
+                       var parameters = {
+                               containerID: this._options.containerId,
+                               data: {
+                                       message: ''
+                               },
+                               objectID: this._getObjectId(this._activeElement),
+                               removeQuoteIDs: (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : []
+                       };
+                       
+                       var id = this._getEditorId();
+                       
+                       // add any available settings
+                       var settingsContainer = elById('settings_' + id);
+                       if (settingsContainer) {
+                               elBySelAll('input, select, textarea', settingsContainer, function (element) {
+                                       if (element.nodeName === 'INPUT' && (element.type === 'checkbox' || element.type === 'radio')) {
+                                               if (!element.checked) {
+                                                       return;
+                                               }
+                                       }
+                                       
+                                       var name = element.name;
+                                       if (parameters.hasOwnProperty(name)) {
+                                               throw new Error("Variable overshadowing, key '" + name + "' is already present.");
+                                       }
+                                       
+                                       parameters[name] = element.value.trim();
+                               });
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'getText_' + id, parameters.data);
+                       
+                       if (!this._validate(parameters)) {
+                               // validation failed
+                               return;
+                       }
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_' + id, parameters);
+                       
+                       Ajax.api(this, {
+                               actionName: 'save',
+                               parameters: parameters
+                       });
+                       
+                       this._hideEditor();
+               },
+               
+               /**
+                * Validates the message and invokes listeners to perform additional validation.
+                *
+                * @param       {Object}        parameters      request parameters
+                * @return      {boolean}       validation result
+                * @protected
+                */
+               _validate: function(parameters) {
+                       // remove all existing error elements
+                       var errorMessages = elByClass('innerError', this._activeElement);
+                       while (errorMessages.length) {
+                               elRemove(errorMessages[0]);
+                       }
+                       
+                       var data = {
+                               api: this,
+                               parameters: parameters,
+                               valid: true
+                       };
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_' + this._getEditorId(), data);
+                       
+                       return (data.valid !== false);
+               },
+               
+               /**
+                * Throws an error by adding an inline error to target element.
+                *
+                * @param       {Element}       element         erroneous element
+                * @param       {string}        message         error message
+                */
+               throwError: function(element, message) {
+                       var error = elCreate('small');
+                       error.className = 'innerError';
+                       error.textContent = message;
+                       
+                       DomUtil.insertAfter(error, element);
+               },
+               
+               /**
+                * Shows the update message.
+                * 
+                * @param       {Object}        data            ajax response data
+                * @protected
+                */
+               _showMessage: function(data) {
+                       var activeElement = this._activeElement;
+                       var editorId = this._getEditorId();
+                       var elementData = this._elements.get(activeElement);
+                       var attachmentLists = elBySelAll('.attachmentThumbnailList, .attachmentFileList', elementData.messageFooter);
+                       
+                       // set new content
+                       //noinspection JSUnresolvedVariable
+                       DomUtil.setInnerHtml(elementData.messageBody, data.returnValues.message);
+                       
+                       // handle attachment list
+                       //noinspection JSUnresolvedVariable
+                       if (typeof data.returnValues.attachmentList === 'string') {
+                               for (var i = 0, length = attachmentLists.length; i < length; i++) {
+                                       elRemove(attachmentLists[i]);
+                               }
+                               
+                               var element = elCreate('div');
+                               //noinspection JSUnresolvedVariable
+                               DomUtil.setInnerHtml(element, data.returnValues.attachmentList);
+                               
+                               while (element.childNodes.length) {
+                                       elementData.messageFooter.appendChild(element.childNodes[0]);
+                               }
+                       }
+                       
+                       // handle poll
+                       //noinspection JSUnresolvedVariable
+                       if (typeof data.returnValues.poll === 'string') {
+                               // find current poll
+                               var poll = elBySel('.pollContainer', elementData.messageBody);
+                               if (poll !== null) {
+                                       // poll contain is wrapped inside `.jsInlineEditorHideContent`
+                                       elRemove(poll.parentNode);
+                               }
+                               
+                               var pollContainer = elCreate('div');
+                               pollContainer.className = 'jsInlineEditorHideContent';
+                               //noinspection JSUnresolvedVariable
+                               DomUtil.setInnerHtml(pollContainer, data.returnValues.poll);
+                               
+                               DomUtil.prepend(pollContainer, elementData.messageBody);
+                       }
+                       
+                       this._restoreMessage();
+                       
+                       this._updateHistory(this._getHash(this._getObjectId(activeElement)));
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor', 'autosaveDestroy_' + editorId);
+                       
+                       UiNotification.show();
+                       
+                       if (this._options.quoteManager) {
+                               this._options.quoteManager.clearAlternativeEditor();
+                               this._options.quoteManager.countQuotes();
+                       }
+               },
+               
+               /**
+                * Hides the editor from view.
+                * 
+                * @protected
+                */
+               _hideEditor: function() {
+                       var elementData = this._elements.get(this._activeElement);
+                       elHide(DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer'));
+                       
+                       var icon = elCreate('span');
+                       icon.className = 'icon icon48 fa-spinner';
+                       elementData.messageBodyEditor.appendChild(icon);
+               },
+               
+               /**
+                * Restores the previously hidden editor.
+                * 
+                * @protected
+                */
+               _restoreEditor: function() {
+                       var elementData = this._elements.get(this._activeElement);
+                       var icon = elBySel('.fa-spinner', elementData.messageBodyEditor);
+                       elRemove(icon);
+                       
+                       var editorContainer = DomTraverse.childByClass(elementData.messageBodyEditor, 'editorContainer');
+                       if (editorContainer !== null) elShow(editorContainer);
+               },
+               
+               /**
+                * Destroys the editor instance.
+                * 
+                * @protected
+                */
+               _destroyEditor: function() {
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'autosaveDestroy_' + this._getEditorId());
+                       EventHandler.fire('com.woltlab.wcf.redactor', 'destroy_' + this._getEditorId());
+               },
+               
+               /**
+                * Returns the hash added to the url after successfully editing a message.
+                * 
+                * @param       {int}   objectId        message object id
+                * @return      string
+                * @protected
+                */
+               _getHash: function(objectId) {
+                       return '#message' + objectId;
+               },
+               
+               /**
+                * Updates the history to avoid old content when going back in the browser
+                * history.
+                * 
+                * @param       {string}        hash    location hash
+                * @protected
+                */
+               _updateHistory: function(hash) {
+                       window.location.hash = hash;
+               },
+               
+               /**
+                * Returns the unique editor id.
+                * 
+                * @return      {string}        editor id
+                * @protected
+                */
+               _getEditorId: function() {
+                       return this._options.editorPrefix + this._getObjectId(this._activeElement);
+               },
+               
+               /**
+                * Returns the element's `data-object-id` value.
+                * 
+                * @param       {Element}       element         target element
+                * @return      {int}
+                * @protected
+                */
+               _getObjectId: function(element) {
+                       return ~~elData(element, 'object-id');
+               },
+               
+               _ajaxFailure: function(data) {
+                       var elementData = this._elements.get(this._activeElement);
+                       var editor = elBySel('.redactor-editor', elementData.messageBodyEditor);
+                       
+                       // handle errors occurring on editor load
+                       if (editor === null) {
+                               this._restoreMessage();
+                               
+                               return true;
+                       }
+                       
+                       this._restoreEditor();
+                       
+                       //noinspection JSUnresolvedVariable
+                       if (!data || data.returnValues === undefined || data.returnValues.errorType === undefined) {
+                               return true;
+                       }
+                       
+                       var innerError = elBySel('.innerError', elementData.messageBodyEditor);
+                       if (innerError === null) {
+                               innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               
+                               DomUtil.insertAfter(innerError, editor);
+                       }
+                       
+                       //noinspection JSUnresolvedVariable
+                       innerError.textContent = data.returnValues.errorType;
+                       
+                       return false;
+               },
+               
+               _ajaxSuccess: function(data) {
+                       switch (data.actionName) {
+                               case 'beginEdit':
+                                       this._showEditor(data);
+                                       break;
+                                       
+                               case 'save':
+                                       this._showMessage(data);
+                                       break;
+                       }
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       className: this._options.className,
+                                       interfaceName: 'wcf\\data\\IMessageInlineEditorAction'
+                               }
+                       };
+               },
+               
+               /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
+               legacyGetDropdownMenus: function() { return this._dropdownMenus; },
+               
+               /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
+               legacyGetElements: function() { return this._elements; },
+               
+               /** @deprecated 3.0 - used only for backward compatibility with `WCF.Message.InlineEditor` */
+               legacyEdit: function(containerId) {
+                       this._click(elById(containerId), null);
+               }
+       };
+       
+       return UiMessageInlineEditor;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Manager.js
new file mode 100644 (file)
index 0000000..1fd9ec2
--- /dev/null
@@ -0,0 +1,272 @@
+/**
+ * Provides access and editing of message properties.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Message/Manager
+ */
+define(['Ajax', 'Core', 'Dictionary', 'Language', 'Dom/ChangeListener', 'Dom/Util'], function(Ajax, Core, Dictionary, Language, DomChangeListener, DomUtil) {
+       "use strict";
+       
+       /**
+        * @param       {Object}        options         initilization options
+        * @constructor
+        */
+       function UiMessageManager(options) { this.init(options); }
+       UiMessageManager.prototype = {
+               /**
+                * Initializes a new manager instance.
+                * 
+                * @param       {Object}        options         initilization options
+                */
+               init: function(options) {
+                       this._elements = null;
+                       this._options = Core.extend({
+                               className: '',
+                               selector: ''
+                       }, options);
+                       
+                       this.rebuild();
+                       
+                       DomChangeListener.add('Ui/Message/Manager' + this._options.className, this.rebuild.bind(this));
+               },
+               
+               /**
+                * Rebuilds the list of observed messages. You should call this method whenever a
+                * message has been either added or removed from the document.
+                */
+               rebuild: function() {
+                       this._elements = new Dictionary();
+                       
+                       var element, elements = elBySelAll(this._options.selector);
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               element = elements[i];
+                               
+                               this._elements.set(elData(element, 'object-id'), element);
+                       }
+               },
+               
+               /**
+                * Returns a boolean value for the given permission. The permission should not start
+                * with "can" or "can-" as this is automatically assumed by this method.
+                * 
+                * @param       {int}           objectId        message object id 
+                * @param       {string}        permission      permission name without a leading "can" or "can-"
+                * @return      {boolean}       true if permission was set and is either 'true' or '1'
+                */
+               getPermission: function(objectId, permission) {
+                       permission = 'can-' + this._getAttributeName(permission);
+                       var element = this._elements.get(objectId);
+                       if (element === undefined) {
+                               throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
+                       }
+                       
+                       return elDataBool(element, permission);
+               },
+               
+               /**
+                * Returns the given property value from a message, optionally supporting a boolean return value.
+                * 
+                * @param       {int}           objectId        message object id
+                * @param       {string}        propertyName    attribute name
+                * @param       {boolean}       asBool          attempt to interpret property value as boolean
+                * @return      {(boolean|string)}      raw property value or boolean if requested
+                */
+               getPropertyValue: function(objectId, propertyName, asBool) {
+                       var element = this._elements.get(objectId);
+                       if (element === undefined) {
+                               throw new Error("Unknown object id '" + objectId + "' for selector '" + this._options.selector + "'");
+                       }
+                       
+                       return window[(asBool ? 'elDataBool' : 'elData')](element, this._getAttributeName(propertyName));
+               },
+               
+               /**
+                * Invokes a method for given message object id in order to alter its state or properties.
+                * 
+                * @param       {int}           objectId        message object id
+                * @param       {string}        actionName      action name used for the ajax api
+                * @param       {Object=}       parameters      optional list of parameters included with the ajax request
+                */
+               update: function(objectId, actionName, parameters) {
+                       Ajax.api(this, {
+                               actionName: actionName,
+                               parameters: parameters || {},
+                               objectIDs: [objectId]
+                       });
+               },
+               
+               /**
+                * Updates properties and states for given object ids. Keep in mind that this method does
+                * not support setting individual properties per message, instead all property changes
+                * are applied to all matching message objects.
+                * 
+                * @param       {Array<int>}    objectIds       list of message object ids
+                * @param       {Object}        data            list of updated properties
+                */
+               updateItems: function(objectIds, data) {
+                       if (!Array.isArray(objectIds)) {
+                               objectIds = [objectIds];
+                       }
+                       
+                       var element;
+                       for (var i = 0, length = objectIds.length; i < length; i++) {
+                               element = this._elements.get(objectIds[i]);
+                               if (element === undefined) {
+                                       continue;
+                               }
+                               
+                               for (var key in data) {
+                                       if (data.hasOwnProperty(key)) {
+                                               this._update(element, key, data[key]);
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Bulk updates the properties and states for all observed messages at once.
+                * 
+                * @param       {Object}        data            list of updated properties
+                */
+               updateAllItems: function(data) {
+                       var objectIds = [];
+                       this._elements.forEach((function(element, objectId) {
+                               objectIds.push(objectId);
+                       }).bind(this));
+                       
+                       this.updateItems(objectIds, data);
+               },
+               
+               /**
+                * Updates a single property of a message element.
+                * 
+                * @param       {Element}       element         message element
+                * @param       {string}        propertyName    property name
+                * @param       {?}             propertyValue   property value, will be implicitly converted to string
+                * @protected
+                */
+               _update: function(element, propertyName, propertyValue) {
+                       elData(element, this._getAttributeName(propertyName), propertyValue);
+                       
+                       // handle special properties
+                       var propertyValueBoolean = (propertyValue == 1 || propertyValue === true || propertyValue === 'true');
+                       this._updateState(element, propertyName, propertyValue, propertyValueBoolean);
+               },
+               
+               /**
+                * Updates the message element's state based upon a property change.
+                * 
+                * @param       {Element}       element                 message element
+                * @param       {string}        propertyName            property name
+                * @param       {?}             propertyValue           property value
+                * @param       {boolean}       propertyValueBoolean    true if `propertyValue` equals either 'true' or '1'
+                * @protected
+                */
+               _updateState: function(element, propertyName, propertyValue, propertyValueBoolean) {
+                       switch (propertyName) {
+                               case 'isDeleted':
+                                       element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDeleted');
+                                       this._toggleMessageStatus(element, 'jsIconDeleted', 'wcf.message.status.deleted', 'red', propertyValueBoolean);
+                                       
+                                       break;
+                               
+                               case 'isDisabled':
+                                       element.classList[(propertyValueBoolean ? 'add' : 'remove')]('messageDisabled');
+                                       this._toggleMessageStatus(element, 'jsIconDisabled', 'wcf.message.status.disabled', 'green', propertyValueBoolean);
+                                       
+                                       break;
+                       }
+               },
+               
+               /**
+                * Toggles the message status bade for provided element.
+                * 
+                * @param       {Element}       element         message element
+                * @param       {string}        className       badge class name
+                * @param       {string}        phrase          language phrase
+                * @param       {string}        badgeColor      color css class
+                * @param       {boolean}       addBadge        add or remove badge
+                * @protected
+                */
+               _toggleMessageStatus: function(element, className, phrase, badgeColor, addBadge) {
+                       var messageStatus = elBySel('.messageStatus', element);
+                       if (messageStatus === null) {
+                               var messageHeaderMetaData = elBySel('.messageHeaderMetaData', element);
+                               if (messageHeaderMetaData === null) {
+                                       // can't find appropriate location to insert badge
+                                       return;
+                               }
+                               
+                               messageStatus = elCreate('ul');
+                               messageStatus.className = 'messageStatus';
+                               DomUtil.insertAfter(messageStatus, messageHeaderMetaData);
+                       }
+                       
+                       var badge = elBySel('.' + className, messageStatus);
+                       
+                       if (addBadge) {
+                               if (badge !== null) {
+                                       // badge already exists
+                                       return;
+                               }
+                               
+                               badge = elCreate('span');
+                               badge.className = 'badge label ' + badgeColor + ' ' + className;
+                               badge.textContent = Language.get(phrase);
+                               
+                               var listItem = elCreate('li');
+                               listItem.appendChild(badge);
+                               messageStatus.appendChild(listItem);
+                       }
+                       else {
+                               if (badge === null) {
+                                       // badge does not exist
+                                       return;
+                               }
+                               
+                               elRemove(badge.parentNode);
+                       }
+               },
+               
+               /**
+                * Transforms camel-cased property names into their attribute equivalent.
+                * 
+                * @param       {string}        propertyName    camel-cased property name
+                * @return      {string}        equivalent attribute name
+                * @protected
+                */
+               _getAttributeName: function(propertyName) {
+                       if (propertyName.indexOf('-') !== -1) {
+                               return propertyName;
+                       }
+                       
+                       var attributeName = '';
+                       var str, tmp = propertyName.split(/([A-Z][a-z]+)/);
+                       for (var i = 0, length = tmp.length; i < length; i++) {
+                               str = tmp[i];
+                               if (str.length) {
+                                       if (attributeName.length) attributeName += '-';
+                                       attributeName += str.toLowerCase();
+                               }
+                       }
+                       
+                       return attributeName;
+               },
+               
+               _ajaxSuccess: function() {
+                       throw new Error("Method _ajaxSuccess() must be implemented by deriving functions.");
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       className: this._options.className
+                               }
+                       };
+               }
+       };
+       
+       return UiMessageManager;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Reply.js
new file mode 100644 (file)
index 0000000..0e2490f
--- /dev/null
@@ -0,0 +1,363 @@
+/**
+ * Handles user interaction with the quick reply feature.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Message/Reply
+ */
+define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', 'WoltLabSuite/Core/Ui/Scroll', 'EventKey', 'User', 'WoltLabSuite/Core/Controller/Captcha'],
+       function(Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiMessageReply(options) { this.init(options); }
+       UiMessageReply.prototype = {
+               /**
+                * Initializes a new quick reply field.
+                * 
+                * @param       {Object}        options         configuration options
+                */
+               init: function(options) {
+                       this._options = Core.extend({
+                               ajax: {
+                                       className: ''
+                               },
+                               quoteManager: null,
+                               successMessage: 'wcf.global.success.add'
+                       }, options);
+                       
+                       this._container = elById('messageQuickReply');
+                       this._content = elBySel('.messageContent', this._container);
+                       this._textarea = elById('text');
+                       this._editor = null;
+                       this._loadingOverlay = null;
+                       
+                       // prevent marking of text for quoting
+                       elBySel('.message', this._container).classList.add('jsInvalidQuoteTarget');
+                       
+                       // handle submit button
+                       var submitCallback = this._submit.bind(this);
+                       var submitButton = elBySel('button[data-type="save"]');
+                       submitButton.addEventListener(WCF_CLICK_EVENT, submitCallback);
+                       
+                       // bind reply button
+                       var replyButtons = elBySelAll('.jsQuickReply');
+                       for (var i = 0, length = replyButtons.length; i < length; i++) {
+                               replyButtons[i].addEventListener(WCF_CLICK_EVENT, (function(event) {
+                                       event.preventDefault();
+                                       
+                                       UiScroll.element(this._container, (function() {
+                                               this._getEditor().focus.end();
+                                       }).bind(this));
+                               }).bind(this));
+                       }
+               },
+               
+               /**
+                * Submits the guest dialog.
+                * 
+                * @param       {Event}         event
+                * @protected
+                */
+               _submitGuestDialog: function(event) {
+                       // only submit when enter key is pressed
+                       if (event.type === 'keypress' && !EventKey.Enter(event)) {
+                               return;
+                       }
+                       
+                       var usernameInput = elBySel('input[name=username]', event.currentTarget.closest('.dialogContent'));
+                       if (usernameInput.value === '') {
+                               var error = DomTraverse.nextByClass(usernameInput, 'innerError');
+                               if (!error) {
+                                       error = elCreate('small');
+                                       error.className = 'innerError';
+                                       error.innerText = Language.get('wcf.global.form.error.empty');
+                                       
+                                       DomUtil.insertAfter(error, usernameInput);
+                                       
+                                       usernameInput.closest('dl').classList.add('formError');
+                               }
+                               
+                               return;
+                       }
+                       
+                       var parameters = {
+                               parameters: {
+                                       data: {
+                                               username: usernameInput.value
+                                       }
+                               }
+                       };
+                       
+                       //noinspection JSCheckFunctionSignatures
+                       var captchaId = elData(event.currentTarget, 'captcha-id');
+                       if (ControllerCaptcha.has(captchaId)) {
+                               parameters = Core.extend(parameters, ControllerCaptcha.getData(captchaId));
+                       }
+                       
+                       this._submit(undefined, parameters);
+               },
+               
+               /**
+                * Validates the message and submits it to the server.
+                * 
+                * @param       {Event?}        event                   event object
+                * @param       {Object?}       additionalParameters    additional parameters sent to the server
+                * @protected
+                */
+               _submit: function(event, additionalParameters) {
+                       if (event) {
+                               event.preventDefault();
+                       }
+                       
+                       if (!this._validate()) {
+                               // validation failed, bail out
+                               return;
+                       }
+                       
+                       this._showLoadingOverlay();
+                       
+                       // build parameters
+                       var parameters = DomUtil.getDataAttributes(this._container, 'data-', true, true);
+                       parameters.data = { message: this._getEditor().code.get() };
+                       parameters.removeQuoteIDs = (this._options.quoteManager) ? this._options.quoteManager.getQuotesMarkedForRemoval() : [];
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data);
+                       
+                       if (!User.userId && !additionalParameters) {
+                               parameters.requireGuestDialog = true;
+                       }
+                       
+                       Ajax.api(this, Core.extend({
+                               parameters: parameters
+                       }, additionalParameters));
+               },
+               
+               /**
+                * Validates the message and invokes listeners to perform additional validation.
+                * 
+                * @return      {boolean}       validation result
+                * @protected
+                */
+               _validate: function() {
+                       // remove all existing error elements
+                       var errorMessages = elByClass('innerError', this._container);
+                       while (errorMessages.length) {
+                               elRemove(errorMessages[0]);
+                       }
+                       
+                       // check if editor contains actual content
+                       if (this._getEditor().utils.isEmpty()) {
+                               this.throwError(this._textarea, Language.get('wcf.global.form.error.empty'));
+                               return false;
+                       }
+                       
+                       var data = {
+                               api: this,
+                               editor: this._getEditor(),
+                               message: this._getEditor().code.get(),
+                               valid: true
+                       };
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_text', data);
+                       
+                       return (data.valid !== false);
+               },
+               
+               /**
+                * Throws an error by adding an inline error to target element.
+                * 
+                * @param       {Element}       element         erroneous element
+                * @param       {string}        message         error message
+                */
+               throwError: function(element, message) {
+                       var error = elCreate('small');
+                       error.className = 'innerError';
+                       error.textContent = message;
+                       
+                       DomUtil.insertAfter(error, element);
+               },
+               
+               /**
+                * Displays a loading spinner while the request is processed by the server.
+                * 
+                * @protected
+                */
+               _showLoadingOverlay: function() {
+                       if (this._loadingOverlay === null) {
+                               this._loadingOverlay = elCreate('div');
+                               this._loadingOverlay.className = 'messageContentLoadingOverlay';
+                               this._loadingOverlay.innerHTML = '<span class="icon icon96 fa-spinner"></span>';
+                       }
+                       
+                       this._content.classList.add('loading');
+                       this._content.appendChild(this._loadingOverlay);
+               },
+               
+               /**
+                * Hides the loading spinner.
+                * 
+                * @protected
+                */
+               _hideLoadingOverlay: function() {
+                       this._content.classList.remove('loading');
+                       
+                       var loadingOverlay = elBySel('.messageContentLoadingOverlay', this._content);
+                       if (loadingOverlay !== null) {
+                               loadingOverlay.parentNode.removeChild(loadingOverlay);
+                       }
+               },
+               
+               /**
+                * Resets the editor contents and notifies event listeners.
+                * 
+                * @protected
+                */
+               _reset: function() {
+                       this._getEditor().code.set('<p>\u200b</p>');
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'reset_text');
+               },
+               
+               /**
+                * Handles errors occured during server processing.
+                * 
+                * @param       {Object}        data    response data
+                * @protected
+                */
+               _handleError: function(data) {
+                       //noinspection JSUnresolvedVariable
+                       this.throwError(this._textarea, data.returnValues.errorType);
+               },
+               
+               /**
+                * Returns the current editor instance.
+                * 
+                * @return      {Object}       editor instance
+                * @protected
+                */
+               _getEditor: function() {
+                       if (this._editor === null) {
+                               if (typeof window.jQuery === 'function') {
+                                       this._editor = window.jQuery(this._textarea).data('redactor');
+                               }
+                               else {
+                                       throw new Error("Unable to access editor, jQuery has not been loaded yet.");
+                               }
+                       }
+                       
+                       return this._editor;
+               },
+               
+               /**
+                * Inserts the rendered message into the post list, unless the post is on the next
+                * page in which case a redirect will be performed instead.
+                * 
+                * @param       {Object}        data    response data
+                * @protected
+                */
+               _insertMessage: function(data) {
+                       this._getEditor().WoltLabAutosave.reset();
+                       
+                       // redirect to new page
+                       //noinspection JSUnresolvedVariable
+                       if (data.returnValues.url) {
+                               //noinspection JSUnresolvedVariable
+                               window.location = data.returnValues.url;
+                       }
+                       else {
+                               //noinspection JSUnresolvedVariable
+                               if (data.returnValues.template) {
+                                       var elementId;
+                                       
+                                       // insert HTML
+                                       if (elData(this._container, 'sort-order') === 'DESC') {
+                                               //noinspection JSUnresolvedVariable
+                                               DomUtil.insertHtml(data.returnValues.template, this._container, 'after');
+                                               elementId = DomUtil.identify(this._container.nextElementSibling);
+                                       }
+                                       else {
+                                               //noinspection JSUnresolvedVariable
+                                               DomUtil.insertHtml(data.returnValues.template, this._container, 'before');
+                                               elementId = DomUtil.identify(this._container.previousElementSibling);
+                                       }
+                                       
+                                       // update last post time
+                                       //noinspection JSUnresolvedVariable
+                                       elData(this._container, 'last-post-time', data.returnValues.lastPostTime);
+                                       
+                                       window.history.replaceState(undefined, '', '#' + elementId);
+                                       UiScroll.element(elById(elementId));
+                               }
+                               
+                               UiNotification.show(Language.get(this._options.successMessage));
+                               
+                               if (this._options.quoteManager) {
+                                       this._options.quoteManager.countQuotes();
+                               }
+                               
+                               DomChangeListener.trigger();
+                       }
+               },
+               
+               /**
+                * @param {{returnValues:{guestDialog:string,guestDialogID:string}}} data
+                * @protected
+                */
+               _ajaxSuccess: function(data) {
+                       if (!User.userId && !data.returnValues.guestDialogID) {
+                               throw new Error("Missing 'guestDialogID' return value for guest.");
+                       }
+                       
+                       if (!User.userId && data.returnValues.guestDialog) {
+                               UiDialog.openStatic(data.returnValues.guestDialogID, data.returnValues.guestDialog, {
+                                       closable: false,
+                                       title: Language.get('wcf.global.confirmation.title')
+                               });
+                               
+                               var dialog = UiDialog.getDialog(data.returnValues.guestDialogID);
+                               elBySel('input[type=submit]', dialog.content).addEventListener(WCF_CLICK_EVENT, this._submitGuestDialog.bind(this));
+                               elBySel('input[type=text]', dialog.content).addEventListener('keypress', this._submitGuestDialog.bind(this));
+                       }
+                       else {
+                               this._insertMessage(data);
+                               
+                               if (!User.userId) {
+                                       UiDialog.close(data.returnValues.guestDialogID);
+                               }
+                               
+                               this._reset();
+                               
+                               this._hideLoadingOverlay();
+                       }
+               },
+               
+               _ajaxFailure: function(data) {
+                       this._hideLoadingOverlay();
+                       
+                       //noinspection JSUnresolvedVariable
+                       if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
+                               return true;
+                       }
+                       
+                       this._handleError(data);
+                       
+                       return false;
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'quickReply',
+                                       className: this._options.ajax.className,
+                                       interfaceName: 'wcf\\data\\IMessageQuickReplyAction'
+                               }
+                       };
+               }
+       };
+       
+       return UiMessageReply;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Share.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Share.js
new file mode 100644 (file)
index 0000000..98d303f
--- /dev/null
@@ -0,0 +1,83 @@
+/**
+ * Provides buttons to share a page through multiple social community sites.
+ *
+ * @author     Marcel Werk
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Message/Share
+ */
+define(['EventHandler'], function(EventHandler) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Message/Share
+        */
+       return {
+               _pageDescription: '',
+               _pageUrl: '',
+               
+               init: function() {
+                       var container = elBySel('.messageShareButtons');
+                       var providers = {
+                               facebook: {
+                                       link: elBySel('.jsShareFacebook', container),
+                                       share: (function() { this._share('facebook', 'https://www.facebook.com/sharer.php?u={pageURL}&t={text}', true); }).bind(this)
+                               },
+                               google: {
+                                       link: elBySel('.jsShareGoogle', container),
+                                       share: (function() { this._share('google', 'https://plus.google.com/share?url={pageURL}', false); }).bind(this)
+                               },
+                               reddit: {
+                                       link: elBySel('.jsShareReddit', container),
+                                       share: (function() { this._share('reddit', 'https://ssl.reddit.com/submit?url={pageURL}', false); }).bind(this)
+                               },
+                               twitter: {
+                                       link: elBySel('.jsShareTwitter', container),
+                                       share: (function() { this._share('twitter', 'https://twitter.com/share?url={pageURL}&text={text}', false); }).bind(this)
+                               },
+                               linkedIn: {
+                                       link: elBySel('.jsShareLinkedIn', container),
+                                       share: (function() { this._share('linkedIn', 'https://www.linkedin.com/cws/share?url={pageURL}', false); }).bind(this)
+                               },
+                               pinterest: {
+                                       link: elBySel('.jsSharePinterest', container),
+                                       share: (function() { this._share('pinterest', 'https://www.pinterest.com/pin/create/link/?url={pageURL}&description={text}', false); }).bind(this)
+                               },
+                               xing: {
+                                       link: elBySel('.jsShareXing', container),
+                                       share: (function() { this._share('xing', 'https://www.xing.com/social_plugins/share?url={pageURL}', false); }).bind(this)
+                               },
+                               whatsApp: {
+                                       link: elBySel('.jsShareWhatsApp', container),
+                                       share: (function() {
+                                               window.location.href = 'whatsapp://send?text=' + this._pageDescription + '%20' + this._pageUrl;
+                                       }).bind(this)
+                               }
+                       };
+
+                       var title = elBySel('meta[property="og:title"]');
+                       if (title !== null) this._pageDescription = encodeURIComponent(title.content);
+                       var url = elBySel('meta[property="og:url"]');
+                       if (url !== null) this._pageUrl = encodeURIComponent(url.content);
+
+                       EventHandler.fire('com.woltlab.wcf.message.share', 'shareProvider', {
+                               container: container,
+                               providers: providers,
+                               pageDescription: this._pageDescription,
+                               pageUrl: this._pageUrl
+                       });
+                       
+                       for (var provider in providers) {
+                               if (providers.hasOwnProperty(provider)) {
+                                       if (providers[provider].link !== null) {
+                                               providers[provider].link.addEventListener(WCF_CLICK_EVENT, providers[provider].share);
+                                       }
+                               }
+                       }
+               },
+               
+               _share: function(objectName, url, appendURL) {
+                       window.open(url.replace(/\{pageURL}/, this._pageUrl).replace(/\{text}/, this._pageDescription + (appendURL ? "%20" + this._pageUrl : "")), objectName, 'height=600,width=600');
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Mobile.js
new file mode 100644 (file)
index 0000000..b08a4c5
--- /dev/null
@@ -0,0 +1,195 @@
+/**
+ * Modifies the interface to provide a better usability for mobile devices.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Mobile
+ */
+define(
+       [        'Core', 'Environment', 'EventHandler', 'Language', 'List', 'Dom/ChangeListener', 'Ui/CloseOverlay', 'Ui/Screen', './Page/Menu/Main', './Page/Menu/User'],
+       function(Core,    Environment,   EventHandler,   Language,   List,   DomChangeListener,    UiCloseOverlay,    UiScreen,    UiPageMenuMain,     UiPageMenuUser)
+{
+       "use strict";
+       
+       var _buttonGroupNavigations = elByClass('buttonGroupNavigation');
+       var _enabled = false;
+       var _knownMessages = new List();
+       var _main = null;
+       var _messages = elByClass('message');
+       var _options = {};
+       var _pageMenuMain = null;
+       var _pageMenuUser = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Mobile
+        */
+       return {
+               /**
+                * Initializes the mobile UI.
+                * 
+                * @param       {Object=}       options         initialization options
+                */
+               setup: function(options) {
+                       _options = Core.extend({
+                               enableMobileMenu: true
+                       }, options);
+                       
+                       _main = elById('main');
+                       
+                       if (Environment.touch()) {
+                               document.documentElement.classList.add('touch');
+                       }
+                       
+                       if (Environment.platform() !== 'desktop') {
+                               document.documentElement.classList.add('mobile');
+                       }
+                       
+                       UiScreen.on('screen-md-down', {
+                               match: this.enable.bind(this),
+                               unmatch: this.disable.bind(this),
+                               setup: this._init.bind(this)
+                       });
+               },
+               
+               /**
+                * Enables the mobile UI.
+                */
+               enable: function() {
+                       _enabled = true;
+                       
+                       if (_options.enableMobileMenu) {
+                               _pageMenuMain.enable();
+                               _pageMenuUser.enable();
+                       }
+               },
+               
+               /**
+                * Disables the mobile UI.
+                */
+               disable: function() {
+                       _enabled = false;
+                       
+                       if (_options.enableMobileMenu) {
+                               _pageMenuMain.disable();
+                               _pageMenuUser.disable();
+                       }
+               },
+               
+               _init: function() {
+                       _enabled = true;
+                       
+                       this._initSearchBar();
+                       this._initButtonGroupNavigation();
+                       this._initMessages();
+                       this._initMobileMenu();
+                       
+                       UiCloseOverlay.add('WoltLabSuite/Core/Ui/Mobile', this._closeAllMenus.bind(this));
+                       DomChangeListener.add('WoltLabSuite/Core/Ui/Mobile', this._initButtonGroupNavigation.bind(this));
+               },
+               
+               _initSearchBar: function() {
+                       var _searchBar = elById('pageHeaderSearch');
+                       var _searchInput = elById('pageHeaderSearchInput');
+                       
+                       EventHandler.add('com.woltlab.wcf.MainMenuMobile', 'more', function(data) {
+                               if (data.identifier === 'com.woltlab.wcf.search') {
+                                       _searchBar.style.setProperty('top', elById('pageHeader').offsetHeight + 'px', '');
+                                       _searchBar.classList.add('open');
+                                       _searchInput.focus();
+                                       
+                                       data.handler.close(true);
+                               }
+                       });
+                       
+                       _main.addEventListener(WCF_CLICK_EVENT, function() { _searchBar.classList.remove('open'); });
+               },
+               
+               _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');
+                               
+                               navigation.parentNode.classList.add('hasMobileNavigation');
+                               
+                               var button = elCreate('a');
+                               button.className = 'dropdownLabel';
+                               
+                               var span = elCreate('span');
+                               span.className = 'icon icon24 fa-ellipsis-v';
+                               button.appendChild(span);
+                               
+                               var list = elBySel('.buttonList', navigation);
+                               list.addEventListener(WCF_CLICK_EVENT, function(event) {
+                                       event.stopPropagation();
+                               });
+                               
+                               (function(navigation, button) {
+                                       button.addEventListener(WCF_CLICK_EVENT, function(event) {
+                                               event.preventDefault();
+                                               event.stopPropagation();
+                                               
+                                               navigation.classList.toggle('open');
+                                       });
+                               })(navigation, button);
+                               
+                               navigation.insertBefore(button, navigation.firstChild);
+                       }
+               },
+               
+               _initMessages: function() {
+                       Array.prototype.forEach.call(_messages, function(message) {
+                               if (_knownMessages.has(message)) {
+                                       return;
+                               }
+                               
+                               var navigation = elBySel('.jsMobileNavigation', message);
+                               var quickOptions = elBySel('.messageQuickOptions', message); 
+                               
+                               if (quickOptions) {
+                                       quickOptions.addEventListener(WCF_CLICK_EVENT, function (event) {
+                                               if (_enabled) {
+                                                       event.preventDefault();
+                                                       event.stopPropagation();
+                                                       
+                                                       navigation.classList.toggle('open');
+                                               }
+                                       });
+                                       
+                                       navigation.addEventListener(WCF_CLICK_EVENT, function(event) {
+                                               event.stopPropagation();
+                                       });
+                               }
+                               
+                               _knownMessages.add(message);
+                       });
+               },
+               
+               _initMobileMenu: function() {
+                       if (_options.enableMobileMenu) {
+                               _pageMenuMain = new UiPageMenuMain();
+                               _pageMenuUser = new UiPageMenuUser();
+                       }
+                       
+                       elBySelAll('.boxMenu', null, function(boxMenu) {
+                               boxMenu.addEventListener(WCF_CLICK_EVENT, function(event) {
+                                       event.stopPropagation();
+                                       
+                                       if (event.target === boxMenu) {
+                                               event.preventDefault();
+                                               
+                                               boxMenu.classList.add('open');
+                                       }
+                               });
+                       });
+               },
+               
+               _closeAllMenus: function() {
+                       elBySelAll('.jsMobileButtonGroupNavigation.open, .jsMobileNavigation.open, .boxMenu.open', null, function (menu) {
+                               menu.classList.remove('open');
+                       });
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Notification.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Notification.js
new file mode 100644 (file)
index 0000000..6e0ea84
--- /dev/null
@@ -0,0 +1,84 @@
+/**
+ * Simple notification overlay.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Notification
+ */
+define(['Language'], function(Language) {
+       "use strict";
+       
+       var _busy = false;
+       var _callback = null;
+       var _message = null;
+       var _notificationElement = null;
+       var _timeout = null;
+       
+       var _callbackHide = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Notification
+        */
+       var UiNotification = {
+               /**
+                * Shows a notification.
+                * 
+                * @param       {string}        message         message
+                * @param       {function=}     callback        callback function to be executed once notification is being hidden
+                * @param       {string=}       cssClassName    alternate CSS class name, defaults to 'success'
+                */
+               show: function(message, callback, cssClassName) {
+                       if (_busy) {
+                               return;
+                       }
+                       
+                       this._init();
+                       
+                       _callback = (typeof callback === 'function') ? callback : null;
+                       _message.className = cssClassName || 'success';
+                       _message.textContent = Language.get(message || 'wcf.global.success');
+                       
+                       _busy = true;
+                       
+                       _notificationElement.classList.add('active');
+                       
+                       _timeout = setTimeout(_callbackHide, 2000);
+               },
+               
+               /**
+                * Initializes the UI elements.
+                */
+               _init: function() {
+                       if (_notificationElement === null) {
+                               _callbackHide = this._hide.bind(this);
+                               
+                               _notificationElement = elCreate('div');
+                               _notificationElement.id = 'systemNotification';
+                               
+                               _message = elCreate('p');
+                               _message.addEventListener(WCF_CLICK_EVENT, _callbackHide);
+                               _notificationElement.appendChild(_message);
+                               
+                               document.body.appendChild(_notificationElement);
+                       }
+               },
+               
+               /**
+                * Hides the notification and invokes the callback if provided.
+                */
+               _hide: function() {
+                       clearTimeout(_timeout);
+                       
+                       _notificationElement.classList.remove('active');
+                       
+                       if (_callback !== null) {
+                               _callback();
+                       }
+                       
+                       _busy = false;
+               }
+       };
+       
+       return UiNotification;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Action.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Action.js
new file mode 100644 (file)
index 0000000..30a4ac0
--- /dev/null
@@ -0,0 +1,172 @@
+/**
+ * Provides page actions such as "jump to top" and clipboard actions.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/Action
+ */
+define(['Dictionary', 'Dom/Util'], function(Dictionary, DomUtil) {
+       "use strict";
+       
+       var _buttons = new Dictionary();
+       var _container = null;
+       var _didInit = false;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Page/Action
+        */
+       return {
+               /**
+                * Initializes the page action container.
+                */
+               setup: function() {
+                       _didInit = true;
+                       
+                       _container = elCreate('ul');
+                       _container.className = 'pageAction';
+                       document.body.appendChild(_container);
+               },
+               
+               /**
+                * Adds a button to the page action list. You can optionally provide a button name to
+                * insert the button right before it. Unmatched button names or empty value will cause
+                * the button to be prepended to the list.
+                * 
+                * @param       {string}        buttonName              unique identifier
+                * @param       {Element}       button                  button element, must not be wrapped in a <li>
+                * @param       {string=}       insertBeforeButton      insert button before element identified by provided button name
+                */
+               add: function(buttonName, button, insertBeforeButton) {
+                       if (_didInit === false) this.setup();
+                       
+                       var listItem = elCreate('li');
+                       button.classList.add('button');
+                       button.classList.add('buttonPrimary');
+                       listItem.appendChild(button);
+                       elAttr(listItem, 'aria-hidden', (buttonName === 'toTop' ? 'true' : 'false'));
+                       elData(listItem, 'name', buttonName);
+                       
+                       // force 'to top' button to be always at the most outer position
+                       if (buttonName === 'toTop') {
+                               listItem.className = 'toTop initiallyHidden';
+                               _container.appendChild(listItem);
+                       }
+                       else {
+                               var insertBefore = null;
+                               if (insertBeforeButton) {
+                                       insertBefore = _buttons.get(insertBeforeButton);
+                                       if (insertBefore !== undefined) {
+                                               insertBefore = insertBefore.parentNode;
+                                       }
+                               }
+                               
+                               if (insertBefore === null && _container.childElementCount) {
+                                       insertBefore = _container.children[0];
+                               }
+                               
+                               if (insertBefore === null) {
+                                       DomUtil.prepend(listItem, _container);
+                               }
+                               else {
+                                       _container.insertBefore(listItem, insertBefore);
+                               }
+                       }
+                       
+                       _buttons.set(buttonName, button);
+                       this._renderContainer();
+               },
+               
+               /**
+                * Returns true if there is a registered button with the provided name.
+                * 
+                * @param       {string}        buttonName      unique identifier
+                * @return      {boolean}       true if there is a registered button with this name
+                */
+               has: function (buttonName) {
+                       return _buttons.has(buttonName);
+               },
+               
+               /**
+                * Returns the stored button by name or undefined.
+                * 
+                * @param       {string}        buttonName      unique identifier
+                * @return      {Element}       button element or undefined
+                */
+               get: function(buttonName) {
+                       return _buttons.get(buttonName);
+               },
+               
+               /**
+                * Removes a button by its button name.
+                * 
+                * @param       {string}        buttonName      unique identifier
+                */
+               remove: function(buttonName) {
+                       var button = _buttons.get(buttonName);
+                       if (button !== undefined) {
+                               var listItem = button.parentNode;
+                               listItem.addEventListener('animationend', function () {
+                                       try {
+                                               _container.removeChild(listItem);
+                                               _buttons.delete(buttonName);
+                                       }
+                                       catch (e) {
+                                               // ignore errors if the element has already been removed
+                                       }
+                               });
+                               
+                               this.hide(buttonName);
+                       }
+               },
+               
+               /**
+                * Hides a button by its button name.
+                * 
+                * @param       {string}        buttonName      unique identifier
+                */
+               hide: function(buttonName) {
+                       var button = _buttons.get(buttonName);
+                       if (button) {
+                               elAttr(button.parentNode, 'aria-hidden', 'true');
+                               this._renderContainer();
+                       }
+               },
+               
+               /**
+                * Shows a button by its button name.
+                * 
+                * @param       {string}        buttonName      unique identifier
+                */
+               show: function(buttonName) {
+                       var button = _buttons.get(buttonName);
+                       if (button) {
+                               if (button.parentNode.classList.contains('initiallyHidden')) {
+                                       button.parentNode.classList.remove('initiallyHidden');
+                               }
+                               
+                               elAttr(button.parentNode, 'aria-hidden', 'false');
+                               this._renderContainer();
+                       }
+               },
+               
+               /**
+                * Toggles the container's visibility.
+                * 
+                * @protected
+                */
+               _renderContainer: function() {
+                       var hasVisibleItems = false;
+                       if (_container.childElementCount) {
+                               for (var i = 0, length = _container.childElementCount; i < length; i++) {
+                                       if (elAttr(_container.children[i], 'aria-hidden') === 'false') {
+                                               hasVisibleItems = true;
+                                               break;
+                                       }
+                               }
+                       }
+                       
+                       _container.classList[(hasVisibleItems ? 'add' : 'remove')]('active');
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Header/Fixed.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Header/Fixed.js
new file mode 100644 (file)
index 0000000..27c77a8
--- /dev/null
@@ -0,0 +1,116 @@
+/**
+ * Manages the sticky page header.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/Header/Fixed
+ */
+define(['Core', 'EventHandler', 'Ui/Alignment', 'Ui/CloseOverlay', 'Ui/Screen', 'Ui/SimpleDropdown'], function(Core, EventHandler, UiAlignment, UiCloseOverlay, UiScreen, UiSimpleDropdown) {
+       "use strict";
+       
+       var _pageHeader, _pageHeaderContainer, _searchInputContainer, _triggerHeight;
+       var _isFixed = false, _isMobile = false;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Page/Header/Fixed
+        */
+       return {
+               /**
+                * Initializes the sticky page header handler.
+                */
+               init: function() {
+                       _pageHeader = elById('pageHeader');
+                       _pageHeaderContainer = elById('pageHeaderContainer');
+                       
+                       this._initStickyPageHeader();
+                       this._initSearchBar();
+                       
+                       UiScreen.on('screen-md-down', {
+                               match: function() { _isMobile = true; },
+                               unmatch: function() { _isMobile = false; },
+                               setup: function() { _isMobile = true; }
+                       });
+               },
+               
+               /**
+                * Enforces a min-height for the original header's location to prevent collapsing
+                * when setting the header to `position: fixed`.
+                * 
+                * @protected
+                */
+               _initStickyPageHeader: function() {
+                       if (_pageHeader.clientHeight) {
+                               _pageHeader.style.setProperty('min-height', _pageHeader.clientHeight + 'px');
+                       }
+                       
+                       _triggerHeight = _pageHeader.clientHeight - elBySel('.mainMenu', _pageHeader).clientHeight;
+                       
+                       this._scroll();
+                       window.addEventListener('scroll', this._scroll.bind(this));
+               },
+               
+               /**
+                * Provides the collapsible search bar.
+                * 
+                * @protected
+                */
+               _initSearchBar: function() {
+                       var searchContainer = elById('pageHeaderSearch');
+                       searchContainer.addEventListener(WCF_CLICK_EVENT, function(event) {
+                               event.stopPropagation();
+                       });
+                       
+                       var searchInput = elById('pageHeaderSearchInput');
+                       
+                       var searchLabel = elBySel('.pageHeaderSearchLabel');
+                       _searchInputContainer = elById('pageHeaderSearchInputContainer');
+                       
+                       var menu = elById('topMenu');
+                       searchLabel.addEventListener(WCF_CLICK_EVENT, function() {
+                               if ((_isFixed || _isMobile) && !_pageHeader.classList.contains('searchBarOpen')) {
+                                       UiAlignment.set(_searchInputContainer, menu, {
+                                               horizontal: 'right'
+                                       });
+                                       
+                                       _pageHeader.classList.add('searchBarOpen');
+                                       WCF.Dropdown.Interactive.Handler.closeAll();
+                                       searchInput.focus();
+                               }
+                       });
+                       
+                       UiCloseOverlay.add('WoltLabSuite/Core/Ui/Page/Header/Fixed', function() {
+                               _pageHeader.classList.remove('searchBarOpen');
+                       });
+                       
+                       EventHandler.add('com.woltlab.wcf.MainMenuMobile', 'more', (function(data) {
+                               if (data.identifier === 'com.woltlab.wcf.search') {
+                                       data.handler.close(true);
+                                       
+                                       Core.triggerEvent(elById('pageHeaderSearchInput'), WCF_CLICK_EVENT);
+                               }
+                       }).bind(this));
+               },
+               
+               /**
+                * Updates the page header state after scrolling.
+                * 
+                * @protected
+                */
+               _scroll: function() {
+                       var wasFixed = _isFixed;
+                       
+                       _isFixed = (window.scrollY > _triggerHeight);
+                       
+                       _pageHeader.classList[_isFixed ? 'add' : 'remove']('sticky');
+                       _pageHeaderContainer.classList[_isFixed ? 'add' : 'remove']('stickyPageHeader');
+                       
+                       if (!_isFixed && wasFixed) {
+                               _pageHeader.classList.remove('searchBarOpen');
+                               ['bottom', 'left', 'right', 'top'].forEach(function(propertyName) {
+                                       _searchInputContainer.style.removeProperty(propertyName);
+                               });
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/JumpTo.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/JumpTo.js
new file mode 100644 (file)
index 0000000..e61e0c1
--- /dev/null
@@ -0,0 +1,140 @@
+/**
+ * Utility class to provide a 'Jump To' overlay. 
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/JumpTo
+ */
+define(['Language', 'ObjectMap', 'Ui/Dialog'], function(Language, ObjectMap, UiDialog) {
+       "use strict";
+       
+       var _activeElement = null;
+       var _buttonSubmit = null;
+       var _description = null;
+       var _elements = new ObjectMap();
+       var _input = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Page/JumpTo
+        */
+       var UiPageJumpTo = {
+               /**
+                * Initializes a 'Jump To' element.
+                * 
+                * @param       {Element}       element         trigger element
+                * @param       {function}      callback        callback function, receives the page number as first argument
+                */
+               init: function(element, callback) {
+                       callback = callback || null;
+                       if (callback === null) {
+                               var redirectUrl = elData(element, 'link');
+                               if (redirectUrl) {
+                                       callback = function(pageNo) {
+                                               window.location = redirectUrl.replace(/pageNo=%d/, 'pageNo=' + pageNo);
+                                       };
+                               }
+                               else {
+                                       callback = function() {};
+                               }
+                               
+                       }
+                       else if (typeof callback !== 'function') {
+                               throw new TypeError("Expected a valid function for parameter 'callback'.");
+                       }
+                       
+                       if (!_elements.has(element)) {
+                               elBySelAll('.jumpTo', element, (function(jumpTo) {
+                                       jumpTo.addEventListener(WCF_CLICK_EVENT, this._click.bind(this, element));
+                                       _elements.set(element, { callback: callback });
+                               }).bind(this));
+                       }
+               },
+               
+               /**
+                * Handles clicks on the trigger element.
+                * 
+                * @param       {Element}       element         trigger element
+                * @param       {object}        event           event object
+                */
+               _click: function(element, event) {
+                       _activeElement = element;
+                       
+                       if (typeof event === 'object') {
+                               event.preventDefault();
+                       }
+                       
+                       UiDialog.open(this);
+                       
+                       var pages = elData(element, 'pages');
+                       _input.value = pages;
+                       _input.setAttribute('max', pages);
+                       _input.select();
+                       
+                       _description.textContent = Language.get('wcf.page.jumpTo.description').replace(/#pages#/, pages);
+               },
+               
+               /**
+                * Handles changes to the page number input field.
+                * 
+                * @param       {object}        event           event object
+                */
+               _keyUp: function(event) {
+                       if (event.which === 13 && _buttonSubmit.disabled === false) {
+                               this._submit();
+                               return;
+                       }
+                       
+                       var pageNo = ~~_input.value;
+                       if (pageNo < 1 || pageNo > ~~elAttr(_input, 'max')) {
+                               _buttonSubmit.disabled = true;
+                       }
+                       else {
+                               _buttonSubmit.disabled = false;
+                       }
+               },
+               
+               /**
+                * Invokes the callback with the chosen page number as first argument.
+                * 
+                * @param       {object}        event           event object
+                */
+               _submit: function(event) {
+                       _elements.get(_activeElement).callback(~~_input.value);
+                       
+                       UiDialog.close(this);
+               },
+               
+               _dialogSetup: function() {
+                       var source = '<dl>'
+                                       + '<dt><label for="jsPaginationPageNo">' + Language.get('wcf.page.jumpTo') + '</label></dt>'
+                                       + '<dd>'
+                                               + '<input type="number" id="jsPaginationPageNo" value="1" min="1" max="1" class="tiny">'
+                                               + '<small></small>'
+                                       + '</dd>'
+                               + '</dl>'
+                               + '<div class="formSubmit">'
+                                       + '<button class="buttonPrimary">' + Language.get('wcf.global.button.submit') + '</button>'
+                               + '</div>';
+                       
+                       return {
+                               id: 'paginationOverlay',
+                               options: {
+                                       onSetup: (function(content) {
+                                               _input = elByTag('input', content)[0];
+                                               _input.addEventListener('keyup', this._keyUp.bind(this));
+                                               
+                                               _description = elByTag('small', content)[0];
+                                               
+                                               _buttonSubmit = elByTag('button', content)[0];
+                                               _buttonSubmit.addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
+                                       }).bind(this),
+                                       title: Language.get('wcf.global.page.pagination')
+                               },
+                               source: source
+                       };
+               }
+       };
+       
+       return UiPageJumpTo;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/JumpToTop.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/JumpToTop.js
new file mode 100644 (file)
index 0000000..9448169
--- /dev/null
@@ -0,0 +1,83 @@
+/**
+ * Provides a link to scroll to top once the page is scrolled by at least 50% the height of the window.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/JumpToTop
+ */
+define(['Environment', 'Language', './Action'], function(Environment, Language, PageAction) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function JumpToTop() { this.init(); }
+       JumpToTop.prototype = {
+               /**
+                * Initializes the top link for desktop browsers only.
+                */
+               init: function() {
+                       // top link is not available on smartphones and tablets (they have a built-in function to accomplish this)
+                       if (Environment.platform() !== 'desktop') {
+                               return;
+                       }
+                       
+                       this._callbackScrollEnd = this._afterScroll.bind(this);
+                       this._timeoutScroll = null;
+                       
+                       var button = elCreate('a');
+                       button.className = 'jsTooltip';
+                       button.href = '#';
+                       elAttr(button, 'title', Language.get('wcf.global.scrollUp'));
+                       button.innerHTML = '<span class="icon icon32 fa-angle-up"></span>';
+                       
+                       button.addEventListener(WCF_CLICK_EVENT, this._jump.bind(this));
+                       
+                       PageAction.add('toTop', button);
+                       
+                       window.addEventListener('scroll', this._scroll.bind(this));
+                       
+                       // invoke callback on page load
+                       this._afterScroll();
+               },
+               
+               /**
+                * Handles clicks on the top link.
+                * 
+                * @param       {Event}         event   event object
+                * @protected
+                */
+               _jump: function(event) {
+                       event.preventDefault();
+                       
+                       elById('top').scrollIntoView({ behavior: 'smooth' });
+               },
+               
+               /**
+                * Callback executed whenever the window is being scrolled.
+                * 
+                * @protected
+                */
+               _scroll: function() {
+                       if (this._timeoutScroll !== null) {
+                               window.clearTimeout(this._timeoutScroll);
+                       }
+                       
+                       this._timeoutScroll = window.setTimeout(this._callbackScrollEnd, 100);
+               },
+               
+               /**
+                * Delayed callback executed once the page has not been scrolled for a certain amount of time.
+                * 
+                * @protected
+                */
+               _afterScroll: function() {
+                       this._timeoutScroll = null;
+                       
+                       PageAction[(window.scrollY >= window.innerHeight / 2) ? 'show' : 'hide']('toTop');
+               }
+       };
+       
+       return JumpToTop;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Abstract.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Abstract.js
new file mode 100644 (file)
index 0000000..2ceb9da
--- /dev/null
@@ -0,0 +1,375 @@
+/**
+ * Provides a touch-friendly fullscreen menu.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/Menu/Abstract
+ */
+define(['Environment', 'EventHandler', 'ObjectMap', 'Dom/Traverse', 'Dom/Util', 'Ui/Screen'], function(Environment, EventHandler, ObjectMap, DomTraverse, DomUtil, UiScreen) {
+       "use strict";
+       
+       var _pageContainer = elById('pageContainer');
+       
+       /**
+        * @param       {string}        eventIdentifier         event namespace
+        * @param       {string}        elementId               menu element id
+        * @param       {string}        buttonSelector          CSS selector for toggle button
+        * @constructor
+        */
+       function UiPageMenuAbstract(eventIdentifier, elementId, buttonSelector) { this.init(eventIdentifier, elementId, buttonSelector); }
+       UiPageMenuAbstract.prototype = {
+               /**
+                * Initializes a touch-friendly fullscreen menu.
+                * 
+                * @param       {string}        eventIdentifier         event namespace
+                * @param       {string}        elementId               menu element id
+                * @param       {string}        buttonSelector          CSS selector for toggle button
+                */
+               init: function(eventIdentifier, elementId, buttonSelector) {
+                       this._activeList = [];
+                       this._depth = 0;
+                       this._enabled = true;
+                       this._eventIdentifier = eventIdentifier;
+                       this._items = new ObjectMap();
+                       this._menu = elById(elementId);
+                       this._removeActiveList = false;
+                       
+                       var callbackOpen = this.open.bind(this);
+                       var button = elBySel(buttonSelector);
+                       button.addEventListener(WCF_CLICK_EVENT, callbackOpen);
+                       
+                       this._initItems();
+                       this._initHeader();
+                       
+                       EventHandler.add(this._eventIdentifier, 'open', callbackOpen);
+                       EventHandler.add(this._eventIdentifier, 'close', this.close.bind(this));
+                       
+                       var itemList, itemLists = elByClass('menuOverlayItemList', this._menu);
+                       this._menu.addEventListener('animationend', (function() {
+                               if (!this._menu.classList.contains('open')) {
+                                       for (var i = 0, length = itemLists.length; i < length; i++) {
+                                               itemList = itemLists[i];
+                                               
+                                               // force the main list to be displayed
+                                               itemList.classList.remove('active');
+                                               itemList.classList.remove('hidden');
+                                       }
+                               }
+                       }).bind(this));
+                       
+                       this._menu.children[0].addEventListener('transitionend', (function() {
+                               this._menu.classList.add('allowScroll');
+                               
+                               if (this._removeActiveList) {
+                                       this._removeActiveList = false;
+                                       
+                                       var list = this._activeList.pop();
+                                       if (list) {
+                                               list.classList.remove('activeList');
+                                       }
+                               }
+                       }).bind(this));
+                       
+                       var backdrop = elCreate('div');
+                       backdrop.className = 'menuOverlayMobileBackdrop';
+                       backdrop.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
+                       
+                       DomUtil.insertAfter(backdrop, this._menu);
+               },
+               
+               /**
+                * Opens the menu.
+                * 
+                * @param       {Event}         event   event object
+                * @return      {boolean}       true if menu has been opened
+                */
+               open: function(event) {
+                       if (!this._enabled) {
+                               return false;
+                       }
+                       
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       this._menu.classList.add('open');
+                       this._menu.classList.add('allowScroll');
+                       this._menu.children[0].classList.add('activeList');
+                       
+                       UiScreen.scrollDisable();
+                       
+                       _pageContainer.classList.add('menuOverlay-' + this._menu.id);
+                       
+                       document.documentElement.classList.add('pageOverlayActive');
+                       
+                       return true;
+               },
+               
+               /**
+                * Closes the menu.
+                * 
+                * @param       {(Event|boolean)}       event   event object or boolean true to force close the menu
+                * @return      {boolean}               true if menu was open
+                */
+               close: function(event) {
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       if (this._menu.classList.contains('open')) {
+                               this._menu.classList.remove('open');
+                               
+                               UiScreen.scrollEnable();
+                               
+                               _pageContainer.classList.remove('menuOverlay-' + this._menu.id);
+                               
+                               document.documentElement.classList.remove('pageOverlayActive');
+                               
+                               return true;
+                       }
+                       
+                       return false;
+               },
+               
+               /**
+                * Enables the touch menu.
+                */
+               enable: function() {
+                       this._enabled = true;
+               },
+               
+               /**
+                * Disables the touch menu.
+                */
+               disable: function() {
+                       this._enabled = false;
+                       
+                       this.close(true);
+               },
+               
+               /**
+                * Initializes all menu items.
+                * 
+                * @protected
+                */
+               _initItems: function() {
+                       elBySelAll('.menuOverlayItemLink', this._menu, this._initItem.bind(this));
+               },
+               
+               /**
+                * Initializes a single menu item.
+                * 
+                * @param       {Element}       item    menu item
+                * @protected
+                */
+               _initItem: function(item) {
+                       // check if it should contain a 'more' link w/ an external callback
+                       var parent = item.parentNode;
+                       var more = elData(parent, 'more');
+                       if (more) {
+                               item.addEventListener(WCF_CLICK_EVENT, (function(event) {
+                                       event.preventDefault();
+                                       event.stopPropagation();
+                                       
+                                       EventHandler.fire(this._eventIdentifier, 'more', {
+                                               handler: this,
+                                               identifier: more,
+                                               item: item,
+                                               parent: parent
+                                       });
+                               }).bind(this));
+                               
+                               return;
+                       }
+                       
+                       var itemList = item.nextElementSibling, wrapper;
+                       if (itemList === null) {
+                               return;
+                       }
+                       
+                       // handle static items with an icon-type button next to it (acp menu)
+                       if (itemList.nodeName !== 'OL' && itemList.classList.contains('menuOverlayItemLinkIcon')) {
+                               // add wrapper
+                               wrapper = elCreate('span');
+                               wrapper.className = 'menuOverlayItemWrapper';
+                               parent.insertBefore(wrapper, item);
+                               wrapper.appendChild(item);
+                               
+                               while (wrapper.nextElementSibling) {
+                                       wrapper.appendChild(wrapper.nextElementSibling);
+                               }
+                               
+                               return;
+                       }
+                       
+                       var isLink = (elAttr(item, 'href') !== '#');
+                       var parentItemList = parent.parentNode;
+                       var itemTitle = elData(itemList, 'title');
+                       
+                       this._items.set(item, {
+                               itemList: itemList,
+                               parentItemList: parentItemList
+                       });
+                       
+                       if (itemTitle === '') {
+                               itemTitle = DomTraverse.childByClass(item, 'menuOverlayItemTitle').textContent;
+                               elData(itemList, 'title', itemTitle);
+                       }
+                       
+                       var callbackLink = this._showItemList.bind(this, item);
+                       if (isLink) {
+                               wrapper = elCreate('span');
+                               wrapper.className = 'menuOverlayItemWrapper';
+                               parent.insertBefore(wrapper, item);
+                               wrapper.appendChild(item);
+                               
+                               var moreLink = elCreate('a');
+                               elAttr(moreLink, 'href', '#');
+                               moreLink.className = 'menuOverlayItemLinkIcon' + (item.classList.contains('active') ? ' active' : '');
+                               moreLink.innerHTML = '<span class="icon icon24 fa-angle-right"></span>';
+                               moreLink.addEventListener(WCF_CLICK_EVENT, callbackLink);
+                               wrapper.appendChild(moreLink);
+                       }
+                       else {
+                               item.classList.add('menuOverlayItemLinkMore');
+                               item.addEventListener(WCF_CLICK_EVENT, callbackLink);
+                       }
+                       
+                       var backLinkItem = elCreate('li');
+                       backLinkItem.className = 'menuOverlayHeader';
+                       
+                       wrapper = elCreate('span');
+                       wrapper.className = 'menuOverlayItemWrapper';
+                       
+                       var backLink = elCreate('a');
+                       elAttr(backLink, 'href', '#');
+                       backLink.className = 'menuOverlayItemLink menuOverlayBackLink';
+                       backLink.textContent = elData(parentItemList, 'title');
+                       backLink.addEventListener(WCF_CLICK_EVENT, this._hideItemList.bind(this, item));
+                       
+                       var closeLink = elCreate('a');
+                       elAttr(closeLink, 'href', '#');
+                       closeLink.className = 'menuOverlayItemLinkIcon';
+                       closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
+                       closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
+                       
+                       wrapper.appendChild(backLink);
+                       wrapper.appendChild(closeLink);
+                       backLinkItem.appendChild(wrapper);
+                       
+                       itemList.insertBefore(backLinkItem, itemList.firstElementChild);
+                       
+                       if (!backLinkItem.nextElementSibling.classList.contains('menuOverlayTitle')) {
+                               var titleItem = elCreate('li');
+                               titleItem.className = 'menuOverlayTitle';
+                               var title = elCreate('span');
+                               title.textContent = itemTitle;
+                               titleItem.appendChild(title);
+                               
+                               itemList.insertBefore(titleItem, backLinkItem.nextElementSibling);
+                       }
+               },
+               
+               /**
+                * Renders the menu item list header.
+                * 
+                * @protected
+                */
+               _initHeader: function() {
+                       var listItem = elCreate('li');
+                       listItem.className = 'menuOverlayHeader';
+                       
+                       var wrapper = elCreate('span');
+                       wrapper.className = 'menuOverlayItemWrapper';
+                       listItem.appendChild(wrapper);
+                       
+                       var logoWrapper = elCreate('span');
+                       logoWrapper.className = 'menuOverlayLogoWrapper';
+                       wrapper.appendChild(logoWrapper);
+                       
+                       var logo = elCreate('span');
+                       logo.className = 'menuOverlayLogo';
+                       logo.style.setProperty('background-image', 'url("' + elData(this._menu, 'page-logo') + '")', '');
+                       logoWrapper.appendChild(logo);
+                       
+                       var closeLink = elCreate('a');
+                       elAttr(closeLink, 'href', '#');
+                       closeLink.className = 'menuOverlayItemLinkIcon';
+                       closeLink.innerHTML = '<span class="icon icon24 fa-times"></span>';
+                       closeLink.addEventListener(WCF_CLICK_EVENT, this.close.bind(this));
+                       wrapper.appendChild(closeLink);
+                       
+                       var list = DomTraverse.childByClass(this._menu, 'menuOverlayItemList');
+                       list.insertBefore(listItem, list.firstElementChild);
+               },
+               
+               /**
+                * Hides an item list, return to the parent item list.
+                * 
+                * @param       {Element}       item    menu item
+                * @param       {Event}         event   event object
+                * @protected
+                */
+               _hideItemList: function(item, event) {
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       this._menu.classList.remove('allowScroll');
+                       this._removeActiveList = true;
+                       
+                       var data = this._items.get(item);
+                       data.parentItemList.classList.remove('hidden');
+                       
+                       this._updateDepth(false);
+               },
+               
+               /**
+                * Shows the child item list.
+                * 
+                * @param       {Element}       item    menu item
+                * @param event
+                * @private
+                */
+               _showItemList: function(item, event) {
+                       if (event instanceof Event) {
+                               event.preventDefault();
+                       }
+                       
+                       var data = this._items.get(item);
+                       
+                       var load = elData(data.itemList, 'load');
+                       if (load) {
+                               if (!elDataBool(item, 'loaded')) {
+                                       var icon = event.currentTarget.firstElementChild;
+                                       if (icon.classList.contains('fa-angle-right')) {
+                                               icon.classList.remove('fa-angle-right');
+                                               icon.classList.add('fa-spinner');
+                                       }
+                                       
+                                       EventHandler.fire(this._eventIdentifier, 'load_' + load);
+                                       
+                                       return;
+                               }
+                       }
+                       
+                       this._menu.classList.remove('allowScroll');
+                       
+                       data.itemList.classList.add('activeList');
+                       data.parentItemList.classList.add('hidden');
+                       
+                       this._activeList.push(data.itemList);
+                       
+                       this._updateDepth(true);
+               },
+               
+               _updateDepth: function(increase) {
+                       this._depth += (increase) ? 1 : -1;
+                       
+                       this._menu.children[0].style.setProperty('transform', 'translateX(' + (this._depth * -100) + '%)', '');
+               }
+       };
+       
+       return UiPageMenuAbstract;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Main.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/Main.js
new file mode 100644 (file)
index 0000000..2c98df8
--- /dev/null
@@ -0,0 +1,109 @@
+/**
+ * Provides the touch-friendly fullscreen main menu.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/Menu/Main
+ */
+define(['Core', 'Dom/Traverse', './Abstract'], function(Core, DomTraverse, UiPageMenuAbstract) {
+       "use strict";
+       
+       var _container = null, _hasItems = null, _list = null, _navigationList = null, _spacer = null;
+       
+       /**
+        * @constructor
+        */
+       function UiPageMenuMain() { this.init(); }
+       Core.inherit(UiPageMenuMain, UiPageMenuAbstract, {
+               /**
+                * Initializes the touch-friendly fullscreen main menu.
+                */
+               init: function() {
+                       UiPageMenuMain._super.prototype.init.call(
+                               this,
+                               'com.woltlab.wcf.MainMenuMobile',
+                               'pageMainMenuMobile',
+                               '#pageHeader .mainMenu'
+                       );
+                       
+                       _container = elById('pageMainMenuMobilePageOptionsContainer');
+                       if (_container !== null) {
+                               _list = DomTraverse.childByClass(_container, 'menuOverlayItemList');
+                               _navigationList = elBySel('.jsPageNavigationIcons');
+                               _spacer = _container.nextElementSibling;
+                               
+                               // remove placeholder item
+                               elRemove(DomTraverse.childByClass(_list, 'jsMenuOverlayItemPlaceholder'));
+                       }
+               },
+               
+               open: function (event) {
+                       if (!UiPageMenuMain._super.prototype.open.call(this, event)) {
+                               return false;
+                       }
+                       
+                       if (_container === null) {
+                               return true;
+                       }
+                       
+                       _hasItems = _navigationList.childElementCount > 0;
+                       
+                       if (_hasItems) {
+                               var item, link;
+                               while (_navigationList.childElementCount) {
+                                       item = _navigationList.children[0];
+                                       
+                                       item.classList.add('menuOverlayItem');
+                                       
+                                       link = item.children[0];
+                                       link.classList.add('menuOverlayItemLink');
+                                       link.classList.add('box24');
+                                       
+                                       link.children[1].classList.remove('invisible');
+                                       link.children[1].classList.add('menuOverlayItemTitle');
+                                       
+                                       _list.appendChild(item);
+                               }
+                               
+                               elShow(_container);
+                               elShow(_spacer);
+                       }
+                       else {
+                               elHide(_container);
+                               elHide(_spacer);
+                       }
+                       
+                       return true;
+               },
+               
+               close: function(event) {
+                       if (!UiPageMenuMain._super.prototype.close.call(this, event)) {
+                               return false;
+                       }
+                       
+                       if (_hasItems) {
+                               elHide(_container);
+                               elHide(_spacer);
+                               
+                               var item, link, title = DomTraverse.childByClass(_list, 'menuOverlayTitle');
+                               while (item = title.nextElementSibling) {
+                                       item.classList.remove('menuOverlayItem');
+                                       
+                                       link = item.children[0];
+                                       link.classList.remove('menuOverlayItemLink');
+                                       link.classList.remove('box24');
+                                       
+                                       link.children[1].classList.add('invisible');
+                                       link.children[1].classList.remove('menuOverlayItemTitle');
+                                       
+                                       _navigationList.appendChild(item);
+                               }
+                       }
+                       
+                       return true;
+               }
+       });
+       
+       return UiPageMenuMain;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/User.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Menu/User.js
new file mode 100644 (file)
index 0000000..5392ef7
--- /dev/null
@@ -0,0 +1,31 @@
+/**
+ * Provides the touch-friendly fullscreen user menu.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/Menu/User
+ */
+define(['Core', 'EventHandler', './Abstract'], function(Core, EventHandler, UiPageMenuAbstract) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiPageMenuUser() { this.init(); }
+       Core.inherit(UiPageMenuUser, UiPageMenuAbstract, {
+               /**
+                * Initializes the touch-friendly fullscreen user menu.
+                */
+               init: function() {
+                       UiPageMenuUser._super.prototype.init.call(
+                               this,
+                               'com.woltlab.wcf.UserMenuMobile',
+                               'pageUserMenuMobile',
+                               '#pageHeader .userPanel'
+                       );
+               }
+       });
+       
+       return UiPageMenuUser;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search.js
new file mode 100644 (file)
index 0000000..917a227
--- /dev/null
@@ -0,0 +1,130 @@
+define(['Ajax', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function(Ajax, EventKey, Language, StringUtil, DomUtil, UiDialog) {
+       "use strict";
+       
+       var _callbackSelect, _resultContainer, _resultList, _searchInput = null;
+       
+       return {
+               open: function(callbackSelect) {
+                       _callbackSelect = callbackSelect;
+                       
+                       UiDialog.open(this);
+               },
+               
+               _search: function (event) {
+                       event.preventDefault();
+                       
+                       var inputContainer = _searchInput.parentNode;
+                       var innerError = inputContainer.nextSibling;
+                       if (innerError && innerError.nodeName === 'SMALL') elRemove(innerError);
+                       
+                       var value = _searchInput.value.trim();
+                       if (value.length < 3) {
+                               innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               innerError.textContent = Language.get('wcf.page.search.error.tooShort');
+                               DomUtil.insertAfter(innerError, inputContainer);
+                               return;
+                       }
+                       
+                       Ajax.api(this, {
+                               parameters: {
+                                       searchString: value
+                               }
+                       });
+               },
+               
+               _click: function (event) {
+                       event.preventDefault();
+                       
+                       _callbackSelect(elData(event.currentTarget, 'page-id'));
+                       
+                       UiDialog.close(this);
+               },
+               
+               _ajaxSuccess: function(data) {
+                       var html = '', page;
+                       //noinspection JSUnresolvedVariable
+                       for (var i = 0, length = data.returnValues.length; i < length; i++) {
+                               //noinspection JSUnresolvedVariable
+                               page = data.returnValues[i];
+                               
+                               html += '<li>'
+                                               + '<div class="containerHeadline pointer" data-page-id="' + page.pageID + '">'
+                                                       + '<h3>' + StringUtil.escapeHTML(page.name) + '</h3>'
+                                                       + '<small>' + StringUtil.escapeHTML(page.displayLink) + '</small>'
+                                               + '</div>'
+                                       + '</li>';
+                       }
+                       
+                       _resultList.innerHTML = html;
+                       
+                       window[html ? 'elShow' : 'elHide'](_resultContainer);
+                       
+                       if (html) {
+                               elBySelAll('.containerHeadline', _resultList, (function(item) {
+                                       item.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+                               }).bind(this));
+                       }
+                       else {
+                               var innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               innerError.textContent = Language.get('wcf.page.search.error.noResults');
+                               DomUtil.insertAfter(innerError, _searchInput.parentNode);
+                       }
+               },
+               
+               _ajaxSetup: function () {
+                       return {
+                               data: {
+                                       actionName: 'search',
+                                       className: 'wcf\\data\\page\\PageAction'
+                               }
+                       };
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'wcfUiPageSearch',
+                               options: {
+                                       onSetup: (function() {
+                                               var callbackSearch = this._search.bind(this);
+                                               
+                                               _searchInput = elById('wcfUiPageSearchInput');
+                                               _searchInput.addEventListener('keydown', function(event) {
+                                                       if (EventKey.Enter(event)) {
+                                                               callbackSearch(event);
+                                                       }
+                                               });
+                                               
+                                               _searchInput.nextElementSibling.addEventListener(WCF_CLICK_EVENT, callbackSearch);
+                                               
+                                               _resultContainer = elById('wcfUiPageSearchResultContainer');
+                                               _resultList = elById('wcfUiPageSearchResultList');
+                                       }).bind(this),
+                                       onShow: function() {
+                                               _searchInput.focus();
+                                       },
+                                       title: Language.get('wcf.page.search')
+                               },
+                               source: '<div class="section">'
+                                       + '<dl>'
+                                               + '<dt><label for="wcfUiPageSearchInput">' + Language.get('wcf.page.search.name') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<div class="inputAddon">'
+                                                               + '<input type="text" id="wcfUiPageSearchInput" class="long">'
+                                                               + '<a href="#" class="inputSuffix"><span class="icon icon16 fa-search"></span></a>'
+                                                       + '</div>'
+                                               + '</dd>'
+                                       + '</dl>'
+                               + '</div>'
+                               + '<section id="wcfUiPageSearchResultContainer" class="section" style="display: none;">'
+                                       + '<header class="sectionHeader">'
+                                               + '<h2 class="sectionTitle">' + Language.get('wcf.page.search.results') + '</h2>'
+                                               + '<p class="sectionDescription">' + Language.get('wcf.page.search.results.description') + '</p>'
+                                       + '</header>'
+                                       + '<ol id="wcfUiPageSearchResultList" class="containerList"></ol>'
+                               + '</section>'
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search/Handler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search/Handler.js
new file mode 100644 (file)
index 0000000..eacc194
--- /dev/null
@@ -0,0 +1,176 @@
+/**
+ * Provides access to the lookup function of page handlers, allowing the user to search and
+ * select page object ids.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/Search/Handler
+ */
+define(['Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', './Input'], function(Language, StringUtil, DomUtil, UiDialog, UiPageSearchInput) {
+       "use strict";
+       
+       var _callback = null;
+       var _searchInput = null;
+       var _searchInputHandler = null;
+       var _resultList = null;
+       var _resultListContainer = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Page/Search/Handler
+        */
+       return {
+               /**
+                * Opens the lookup overlay for provided page id.
+                * 
+                * @param       {int}           pageId          page id
+                * @param       {string}        title           dialog title
+                * @param       {function}      callback        callback function provided with the user-selected object id
+                */
+               open: function (pageId, title, callback) {
+                       _callback = callback;
+                       
+                       UiDialog.open(this);
+                       UiDialog.setTitle(this, title);
+                       
+                       this._getSearchInputHandler().setPageId(pageId);
+               },
+               
+               /**
+                * Builds the result list.
+                * 
+                * @param       {Object}        data            AJAX response data
+                * @protected
+                */
+               _buildList: function(data) {
+                       this._resetList();
+                       
+                       // no matches
+                       if (!Array.isArray(data.returnValues) || data.returnValues.length === 0) {
+                               var innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               innerError.textContent = Language.get('wcf.page.pageObjectID.search.noResults');
+                               DomUtil.insertAfter(innerError, _searchInput);
+                               
+                               return;
+                       }
+                       
+                       var image, item, listItem;
+                       for (var i = 0, length = data.returnValues.length; i < length; i++) {
+                               item = data.returnValues[i];
+                               image = item.image;
+                               if (/^fa-/.test(image)) {
+                                       image = '<span class="icon icon48 ' + image + '"></span>';
+                               }
+                               
+                               listItem = elCreate('li');
+                               elData(listItem, 'object-id', item.objectID);
+                               
+                               listItem.innerHTML = '<div class="box48">'
+                                       + image
+                                       + '<div>'
+                                               + '<div class="containerHeadline">'
+                                                       + '<h3><a href="' + StringUtil.escapeHTML(item.link) + '">' + StringUtil.escapeHTML(item.title) + '</a></h3>'
+                                                       + (item.description ? '<p>' + item.description + '</p>' : '')
+                                               + '</div>'
+                                       + '</div>'
+                               + '</div>';
+                               
+                               listItem.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+                               
+                               _resultList.appendChild(listItem);
+                       }
+                       
+                       elShow(_resultListContainer);
+               },
+               
+               /**
+                * Resets the list and removes any error elements.
+                * 
+                * @protected
+                */
+               _resetList: function() {
+                       var innerError = _searchInput.nextElementSibling;
+                       if (innerError && innerError.classList.contains('innerError')) elRemove(innerError);
+                       
+                       _resultList.innerHTML = '';
+                       
+                       elHide(_resultListContainer);
+               },
+               
+               /**
+                * Initializes the search input handler and returns the instance.
+                * 
+                * @returns     {UiPageSearchInput}     search input handler
+                * @protected
+                */
+               _getSearchInputHandler: function() {
+                       if (_searchInputHandler === null) {
+                               var callback = this._buildList.bind(this);
+                               _searchInputHandler = new UiPageSearchInput(elById('wcfUiPageSearchInput'), {
+                                       callbackSuccess: callback
+                               });
+                       }
+                       
+                       return _searchInputHandler;
+               },
+               
+               /**
+                * Handles clicks on the item unless the click occured directly on a link.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _click: function(event) {
+                       if (event.target.nodeName === 'A') {
+                               return;
+                       }
+                       
+                       event.stopPropagation();
+                       
+                       _callback(elData(event.currentTarget, 'object-id'));
+                       UiDialog.close(this);
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'wcfUiPageSearchHandler',
+                               options: {
+                                       onShow: function() {
+                                               if (_searchInput === null) {
+                                                       _searchInput = elById('wcfUiPageSearchInput');
+                                                       _resultList = elById('wcfUiPageSearchResultList');
+                                                       _resultListContainer = elById('wcfUiPageSearchResultListContainer');
+                                               }
+                                               
+                                               // clear search input
+                                               _searchInput.value = '';
+                                               
+                                               // reset results
+                                               elHide(_resultListContainer);
+                                               _resultList.innerHTML = '';
+                                               
+                                               _searchInput.focus();
+                                       },
+                                       title: ''
+                               },
+                               source: '<div class="section">'
+                                               + '<dl>'
+                                                       + '<dt><label for="wcfUiPageSearchInput">' + Language.get('wcf.page.pageObjectID.search.terms') + '</label></dt>'
+                                                       + '<dd>'
+                                                               + '<input type="text" id="wcfUiPageSearchInput" class="long">'
+                                                               + '<small>' + Language.get('wcf.page.pageObjectID.search.terms.description') + '</small>'
+                                                       + '</dd>'
+                                               + '</dl>'
+                                       + '</div>'
+                                       + '<section id="wcfUiPageSearchResultListContainer" class="section sectionContainerList">'
+                                               + '<header class="sectionHeader">'
+                                                       + '<h2 class="sectionTitle">' + Language.get('wcf.page.pageObjectID.search.results') + '</h2>'
+                                                       + '<p class="sectionDescription">' + Language.get('wcf.page.pageObjectID.search.results.description') + '</p>'
+                                               + '</header>'
+                                               + '<ul id="wcfUiPageSearchResultList" class="containerList wcfUiPageSearchResultList"></ul>'
+                                       + '</section>'
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search/Input.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Page/Search/Input.js
new file mode 100644 (file)
index 0000000..d3c36fe
--- /dev/null
@@ -0,0 +1,60 @@
+/**
+ * Suggestions for page object ids with external response data processing.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Page/Search/Input
+ * @extends     module:WoltLabSuite/Core/Ui/Search/Input
+ */
+define(['Core', 'WoltLabSuite/Core/Ui/Search/Input'], function(Core, UiSearchInput) {
+       "use strict";
+       
+       /**
+        * @param       {Element}       element         input element
+        * @param       {Object=}       options         search options and settings
+        * @constructor
+        */
+       function UiPageSearchInput(element, options) { this.init(element, options); }
+       Core.inherit(UiPageSearchInput, UiSearchInput, {
+               init: function(element, options) {
+                       options = Core.extend({
+                               ajax: {
+                                       className: 'wcf\\data\\page\\PageAction'
+                               },
+                               callbackSuccess: null
+                       }, options);
+                       
+                       if (typeof options.callbackSuccess !== 'function') {
+                               throw new Error("Expected a valid callback function for 'callbackSuccess'.");
+                       }
+                       
+                       UiPageSearchInput._super.prototype.init.call(this, element, options);
+                       
+                       this._pageId = 0;
+               },
+               
+               /**
+                * Sets the target page id.
+                * 
+                * @param       {int}   pageId  target page id
+                */
+               setPageId: function(pageId) {
+                       this._pageId = pageId;
+               },
+               
+               _getParameters: function(value) {
+                       var data = UiPageSearchInput._super.prototype._getParameters.call(this, value);
+                       
+                       data.objectIDs = [this._pageId];
+                       
+                       return data;
+               },
+               
+               _ajaxSuccess: function(data) {
+                       this._options.callbackSuccess(data);
+               }
+       });
+       
+       return UiPageSearchInput;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Pagination.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Pagination.js
new file mode 100644 (file)
index 0000000..a392f27
--- /dev/null
@@ -0,0 +1,237 @@
+/**
+ * Callback-based pagination.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Pagination
+ */
+define(['Core', 'Language', 'ObjectMap', 'StringUtil', 'WoltLabSuite/Core/Ui/Page/JumpTo'], function(Core, Language, ObjectMap, StringUtil, UiPageJumpTo) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiPagination(element, options) { this.init(element, options); }
+       UiPagination.prototype = {
+               /**
+                * maximum number of displayed page links, should match the PHP implementation
+                * @var {int}
+                */
+               SHOW_LINKS: 11,
+               
+               /**
+                * Initializes the pagination.
+                * 
+                * @param       {Element}       element         container element
+                * @param       {object}        options         list of initilization options
+                */
+               init: function(element, options) {
+                       this._element = element;
+                       this._options = Core.extend({
+                               activePage: 1,
+                               maxPage: 1,
+                               
+                               callbackShouldSwitch: null,
+                               callbackSwitch: null
+                       }, options);
+                       
+                       if (typeof this._options.callbackShouldSwitch !== 'function') this._options.callbackShouldSwitch = null;
+                       if (typeof this._options.callbackSwitch !== 'function') this._options.callbackSwitch = null;
+                       
+                       this._element.classList.add('pagination');
+                       
+                       this._rebuild(this._element);
+               },
+               
+               /**
+                * Rebuilds the entire pagination UI.
+                */
+               _rebuild: function() {
+                       var hasHiddenPages = false;
+                       
+                       // clear content
+                       this._element.innerHTML = '';
+                       
+                       var list = elCreate('ul'), link;
+                       
+                       var listItem = elCreate('li');
+                       listItem.className = 'skip';
+                       list.appendChild(listItem);
+                       
+                       var iconClassNames = 'icon icon16 fa-chevron-left';
+                       if (this._options.activePage > 1) {
+                               link = elCreate('a');
+                               link.className = iconClassNames + ' jsTooltip';
+                               link.href = '#';
+                               link.title = Language.get('wcf.global.page.previous');
+                               listItem.appendChild(link);
+                               
+                               link.addEventListener(WCF_CLICK_EVENT, this.switchPage.bind(this, this._options.activePage - 1));
+                       }
+                       else {
+                               listItem.innerHTML = '<span class="' + iconClassNames + '"></span>';
+                               listItem.classList.add('disabled');
+                       }
+                       
+                       // add first page
+                       list.appendChild(this._createLink(1));
+                       
+                       // calculate page links
+                       var maxLinks = this.SHOW_LINKS - 4;
+                       var linksBefore = this._options.activePage - 2;
+                       if (linksBefore < 0) linksBefore = 0;
+                       var linksAfter = this._options.maxPage - (this._options.activePage + 1);
+                       if (linksAfter < 0) linksAfter = 0;
+                       if (this._options.activePage > 1 && this._options.activePage < this._options.maxPage) maxLinks--;
+                       
+                       var half = maxLinks / 2;
+                       var left = this._options.activePage;
+                       var right = this._options.activePage;
+                       if (left < 1) left = 1;
+                       if (right < 1) right = 1;
+                       if (right > this._options.maxPage - 1) right = this._options.maxPage - 1;
+                       
+                       if (linksBefore >= half) {
+                               left -= half;
+                       }
+                       else {
+                               left -= linksBefore;
+                               right += half - linksBefore;
+                       }
+                       
+                       if (linksAfter >= half) {
+                               right += half;
+                       }
+                       else {
+                               right += linksAfter;
+                               left -= half - linksAfter;
+                       }
+                       
+                       right = Math.ceil(right);
+                       left = Math.ceil(left);
+                       if (left < 1) left = 1;
+                       if (right > this._options.maxPage) right = this._options.maxPage;
+                       
+                       // left ... links
+                       var jumpToHtml = '<a class="jsTooltip" title="' + Language.get('wcf.page.jumpTo') + '">&hellip;</a>';
+                       if (left > 1) {
+                               if (left - 1 < 2) {
+                                       list.appendChild(this._createLink(2));
+                               }
+                               else {
+                                       listItem = elCreate('li');
+                                       listItem.className = 'jumpTo';
+                                       listItem.innerHTML = jumpToHtml;
+                                       list.appendChild(listItem);
+                                       
+                                       hasHiddenPages = true;
+                               }
+                       }
+                       
+                       // visible links
+                       for (var i = left + 1; i < right; i++) {
+                               list.appendChild(this._createLink(i));
+                       }
+                       
+                       // right ... links
+                       if (right < this._options.maxPage) {
+                               if (this._options.maxPage - right < 2) {
+                                       list.appendChild(this._createLink(this._options.maxPage - 1));
+                               }
+                               else {
+                                       listItem = elCreate('li');
+                                       listItem.className = 'jumpTo';
+                                       listItem.innerHTML = jumpToHtml;
+                                       list.appendChild(listItem);
+                                       
+                                       hasHiddenPages = true;
+                               }
+                       }
+                       
+                       // add last page
+                       list.appendChild(this._createLink(this._options.maxPage));
+                       
+                       // add next button
+                       listItem = elCreate('li');
+                       listItem.className = 'skip';
+                       list.appendChild(listItem);
+                       
+                       iconClassNames = 'icon icon16 fa-chevron-right';
+                       if (this._options.activePage < this._options.maxPage) {
+                               link = elCreate('a');
+                               link.className = iconClassNames + ' jsTooltip';
+                               link.href = '#';
+                               link.title = Language.get('wcf.global.page.next');
+                               listItem.appendChild(link);
+                               
+                               link.addEventListener(WCF_CLICK_EVENT, this.switchPage.bind(this, this._options.activePage + 1));
+                       }
+                       else {
+                               listItem.innerHTML = '<span class="' + iconClassNames + '"></span>';
+                               listItem.classList.add('disabled');
+                       }
+                       
+                       if (hasHiddenPages) {
+                               elData(list, 'pages', this._options.maxPage);
+                               
+                               UiPageJumpTo.init(list, this.switchPage.bind(this));
+                       }
+                       
+                       this._element.appendChild(list);
+               },
+               
+               /**
+                * Creates a link to a specific page.
+                * 
+                * @param       {int}           pageNo          page number
+                * @return      {Element}       link element
+                */
+               _createLink: function(pageNo) {
+                       var listItem = elCreate('li');
+                       if (pageNo !== this._options.activePage) {
+                               var link = elCreate('a');
+                               link.textContent = StringUtil.addThousandsSeparator(pageNo);
+                               link.addEventListener(WCF_CLICK_EVENT, this.switchPage.bind(this, pageNo));
+                               listItem.appendChild(link);
+                       }
+                       else {
+                               listItem.classList.add('active');
+                               listItem.innerHTML = '<span>' + StringUtil.addThousandsSeparator(pageNo) + '</span><span class="invisible">' + Language.get('wcf.page.pagePosition', { pageNo: pageNo, pages: this._options.maxPage }) + '</span>';
+                       }
+                       
+                       return listItem;
+               },
+               
+               /**
+                * Switches to given page number.
+                * 
+                * @param       {int}           pageNo          page number
+                * @param       {object}        event           event object
+                */
+               switchPage: function(pageNo, event) {
+                       if (typeof event === 'object') {
+                               event.preventDefault();
+                       }
+                       
+                       pageNo = ~~pageNo;
+                       
+                       if (pageNo > 0 && this._options.activePage !== pageNo && pageNo <= this._options.maxPage) {
+                               if (this._options.callbackShouldSwitch !== null) {
+                                       if (this._options.callbackShouldSwitch(pageNo) !== true) {
+                                               return;
+                                       }
+                               }
+                               
+                               this._options.activePage = pageNo;
+                               this._rebuild();
+                               
+                               if (this._options.callbackSwitch !== null) {
+                                       this._options.callbackSwitch(pageNo);
+                               }
+                       }
+               }
+       };
+       
+       return UiPagination;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Autosave.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Autosave.js
new file mode 100644 (file)
index 0000000..74fc977
--- /dev/null
@@ -0,0 +1,200 @@
+/**
+ * Manages the autosave process storing the current editor message in the local
+ * storage to recover it on browser crash or accidental navigation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Redactor/Autosave
+ */
+define(['Dom/Traverse'], function(DomTraverse) {
+       "use strict";
+       
+       // time between save requests in seconds
+       var _frequency = 15;
+       
+       //noinspection JSUnresolvedVariable
+       var _prefix = 'wsc' + window.WCF_PATH.hashCode() + '-';
+       
+       /**
+        * @param       {Element}       element         textarea element
+        * @constructor
+        */
+       function UiRedactorAutosave(element) { this.init(element); }
+       UiRedactorAutosave.prototype = {
+               /**
+                * Initializes the autosave handler and removes outdated messages from storage.
+                * 
+                * @param       {Element}       element         textarea element
+                */
+               init: function (element) {
+                       this._editor = null;
+                       this._element = element;
+                       this._key = _prefix + elData(this._element, 'autosave');
+                       this._lastMessage = '';
+                       this._timer = null;
+                       
+                       this._cleanup();
+                       
+                       // remove attribute to prevent Redactor's built-in autosave to kick in
+                       this._element.removeAttribute('data-autosave');
+                       
+                       var form = DomTraverse.parentByTag(this._element, 'FORM');
+                       if (form !== null) {
+                               form.addEventListener('submit', this.destroy.bind(this));
+                       }
+               },
+               
+               /**
+                * Returns the initial value for the textarea, used to inject message
+                * from storage into the editor before initialization.
+                * 
+                * @return      {string}        message content
+                */
+               getInitialValue: function() {
+                       var value = '';
+                       try {
+                               value = window.localStorage.getItem(this._key);
+                       }
+                       catch (e) {
+                               window.console.warn("Unable to access local storage: " + e.message);
+                       }
+                       
+                       try {
+                               value = JSON.parse(value);
+                       }
+                       catch (e) {
+                               value = '';
+                       }
+                       
+                       // check if storage is outdated
+                       if (value !== null && typeof value === 'object') {
+                               var lastEditTime = ~~elData(this._element, 'autosave-last-edit-time');
+                               if (lastEditTime * 1000 > value.timestamp) {
+                                       //noinspection JSUnresolvedVariable
+                                       return this._element.value;
+                               }
+                               
+                               return value.content;
+                       }
+                       
+                       //noinspection JSUnresolvedVariable
+                       return this._element.value;
+               },
+               
+               /**
+                * Enables periodical save of editor contents to local storage.
+                * 
+                * @param       {$.Redactor}    editor  redactor instance
+                */
+               watch: function(editor) {
+                       this._editor = editor;
+                       
+                       if (this._timer !== null) {
+                               throw new Error("Autosave timer is already active.");
+                       }
+                       
+                       this._timer = window.setInterval(this._saveToStorage.bind(this), _frequency * 1000);
+                       
+                       this._saveToStorage();
+               },
+               
+               /**
+                * Disables autosave handler, for use on editor destruction.
+                */
+               destroy: function () {
+                       this.clear();
+                       
+                       this._editor = null;
+                       
+                       window.clearInterval(this._timer);
+                       this._timer = null;
+               },
+               
+               /**
+                * Removed the stored message, for use after a message has been submitted.
+                */
+               clear: function () {
+                       this._lastMessage = '';
+                       
+                       try {
+                               window.localStorage.removeItem(this._key);
+                       }
+                       catch (e) {
+                               window.console.warn("Unable to remove from local storage: " + e.message);
+                       }
+               },
+               
+               /**
+                * Saves the current message to storage unless there was no change.
+                * 
+                * @protected
+                */
+               _saveToStorage: function() {
+                       var content = this._editor.code.get();
+                       if (this._editor.utils.isEmpty(content)) {
+                               content = '';
+                       }
+                       
+                       if (this._lastMessage === content) {
+                               // break if content hasn't changed
+                               return;
+                       }
+                       
+                       try {
+                               window.localStorage.setItem(this._key, JSON.stringify({
+                                       content: content,
+                                       timestamp: Date.now()
+                               }));
+                               
+                               this._lastMessage = content;
+                       }
+                       catch (e) {
+                               window.console.warn("Unable to write to local storage: " + e.message);
+                       }
+               },
+               
+               /**
+                * Removes stored messages older than one week.
+                * 
+                * @protected
+                */
+               _cleanup: function () {
+                       var oneWeekAgo = Date.now() - (7 * 24 * 3600 * 1000);
+                       var key, value;
+                       for (var i = 0, length = window.localStorage.length; i < length; i++) {
+                               key = window.localStorage.key(i);
+                               
+                               // check if key matches our prefix
+                               if (key.indexOf(_prefix) !== 0) {
+                                       continue;
+                               }
+                               
+                               try {
+                                       value = window.localStorage.getItem(key);
+                               }
+                               catch (e) {
+                                       window.console.warn("Unable to access local storage: " + e.message);
+                               }
+                               
+                               try {
+                                       value = JSON.parse(value);
+                               }
+                               catch (e) {
+                                       value = { timestamp: 0 };
+                               }
+                               
+                               if (!value || value.timestamp < oneWeekAgo) {
+                                       try {
+                                               window.localStorage.removeItem(key);
+                                       }
+                                       catch (e) {
+                                               window.console.warn("Unable to remove from local storage: " + e.message);
+                                       }
+                               }
+                       }
+               }
+       };
+       
+       return UiRedactorAutosave;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Code.js
new file mode 100644 (file)
index 0000000..a521310
--- /dev/null
@@ -0,0 +1,233 @@
+/**
+ * Manages code blocks.
+ *
+ * @author      Alexander Ebert
+ * @copyright   2001-2016 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module      WoltLabSuite/Core/Ui/Redactor/Code
+ */
+define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
+       "use strict";
+       
+       var _headerHeight = 0;
+       
+       /**
+        * @param       {Object}        editor  editor instance
+        * @constructor
+        */
+       function UiRedactorCode(editor) { this.init(editor); }
+       UiRedactorCode.prototype = {
+               /**
+                * Initializes the source code management.
+                * 
+                * @param       {Object}        editor  editor instance
+                */
+               init: function(editor) {
+                       this._editor = editor;
+                       this._elementId = this._editor.$element[0].id;
+                       this._pre = null;
+                       
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_code_' + this._elementId, this._bbcodeCode.bind(this));
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
+                       
+                       // support for active button marking
+                       this._editor.opts.activeButtonsStates.pre = 'code';
+                       
+                       // static bind to ensure that removing works
+                       this._callbackEdit = this._edit.bind(this);
+                       
+                       // bind listeners on init
+                       this._observeLoad();
+               },
+               
+               /**
+                * Intercepts the insertion of `[code]` tags and uses a native `<pre>` instead.
+                * 
+                * @param       {Object}        data    event data
+                * @protected
+                */
+               _bbcodeCode: function(data) {
+                       data.cancel = true;
+                       
+                       this._editor.button.toggle({}, 'pre', 'func', 'block.format');
+                       
+                       var pre = this._editor.selection.block();
+                       if (pre && pre.nodeName === 'PRE') {
+                               this._setTitle(pre);
+                               
+                               pre.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                       }
+               },
+               
+               /**
+                * Binds event listeners and sets quote title on both editor
+                * initialization and when switching back from code view.
+                * 
+                * @protected
+                */
+               _observeLoad: function() {
+                       elBySelAll('pre', this._editor.$editor[0], (function(pre) {
+                               pre.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                               this._setTitle(pre);
+                       }).bind(this));
+               },
+               
+               /**
+                * Opens the dialog overlay to edit the code's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _edit: function(event) {
+                       var pre = event.currentTarget;
+                       
+                       if (_headerHeight === 0) {
+                               _headerHeight = ~~window.getComputedStyle(pre).paddingTop.replace(/px$/, '');
+                               
+                               var styles = window.getComputedStyle(pre, '::before');
+                               _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
+                               _headerHeight += ~~styles.height.replace(/px$/, '');
+                               _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
+                       }
+                       
+                       // check if the click hit the header
+                       var offset = DomUtil.offset(pre);
+                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
+                               event.preventDefault();
+                               
+                               this._pre = pre;
+                               
+                               UiDialog.open(this);
+                       }
+               },
+               
+               /**
+                * Saves the changes to the code's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _save: function(event) {
+                       event.preventDefault();
+                       
+                       var id = 'redactor-code-' + this._elementId;
+                       
+                       ['file', 'highlighter', 'line'].forEach((function (attr) {
+                               elData(this._pre, attr, elById(id + '-' + attr).value);
+                       }).bind(this));
+                       
+                       this._setTitle(this._pre);
+                       this._editor.caret.after(this._pre);
+                       
+                       UiDialog.close(this);
+               },
+               
+               /**
+                * Sets or updates the code's header title.
+                * 
+                * @param       {Element}       pre     code element
+                * @protected
+                */
+               _setTitle: function(pre) {
+                       var file = elData(pre, 'file'),
+                           highlighter = elData(pre, 'highlighter');
+                       
+                       //noinspection JSUnresolvedVariable
+                       highlighter = (this._editor.opts.woltlab.highlighters.hasOwnProperty(highlighter)) ? this._editor.opts.woltlab.highlighters[highlighter] : '';
+                       
+                       var title = Language.get('wcf.editor.code.title', {
+                               file: file,
+                               highlighter: highlighter
+                       });
+                       
+                       if (elData(pre, 'title') !== title) {
+                               elData(pre, 'title', title);
+                       }
+               },
+               
+               _dialogSetup: function() {
+                       var id = 'redactor-code-' + this._elementId,
+                           idButtonSave = id + '-button-save',
+                           idFile = id + '-file',
+                           idHighlighter = id + '-highlighter',
+                           idLine = id + '-line';
+                       
+                       return {
+                               id: id,
+                               options: {
+                                       onSetup: (function() {
+                                               elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
+                                               
+                                               // set highlighters
+                                               var highlighters = '<option value="">' + Language.get('wcf.editor.code.highlighter.detect') + '</option>';
+                                               
+                                               var value, values = [];
+                                               //noinspection JSUnresolvedVariable
+                                               for (var highlighter in this._editor.opts.woltlab.highlighters) {
+                                                       //noinspection JSUnresolvedVariable
+                                                       if (this._editor.opts.woltlab.highlighters.hasOwnProperty(highlighter)) {
+                                                               //noinspection JSUnresolvedVariable
+                                                               values.push([highlighter, this._editor.opts.woltlab.highlighters[highlighter]]);
+                                                       }
+                                               }
+                                               
+                                               // sort by label
+                                               values.sort(function(a, b) {
+                                                       if (a[1] < b[1]) {
+                                                               return  -1;
+                                                       }
+                                                       else if (a[1] > b[1]) {
+                                                               return 1;
+                                                       }
+                                                       
+                                                       return 0;
+                                               });
+                                               
+                                               values.forEach((function(value) {
+                                                       highlighters += '<option value="' + value[0] + '">' + StringUtil.escapeHTML(value[1]) + '</option>';
+                                               }).bind(this));
+                                               
+                                               elById(idHighlighter).innerHTML = highlighters;
+                                       }).bind(this),
+                                       
+                                       onShow: (function() {
+                                               elById(idHighlighter).value = elData(this._pre, 'highlighter');
+                                               var line = elData(this._pre, 'line');
+                                               elById(idLine).value = (line === '') ? 1 : ~~line;
+                                               elById(idFile).value = elData(this._pre, 'file');
+                                       }).bind(this),
+                                       
+                                       title: Language.get('wcf.editor.code.edit')
+                               },
+                               source: '<div class="section">'
+                                       + '<dl>'
+                                               + '<dt><label for="' + idHighlighter + '">' + Language.get('wcf.editor.code.highlighter') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<select id="' + idHighlighter + '"></select>'
+                                                       + '<small>' + Language.get('wcf.editor.code.highlighter.description') + '</small>'
+                                               + '</dd>'
+                                       + '</dl>'
+                                       + '<dl>'
+                                               + '<dt><label for="' + idLine + '">' + Language.get('wcf.editor.code.line') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<input type="number" id="' + idLine + '" min="0" value="1" class="long">'
+                                                       + '<small>' + Language.get('wcf.editor.code.line.description') + '</small>'
+                                               + '</dd>'
+                                       + '</dl>'
+                                       + '<dl>'
+                                               + '<dt><label for="' + idFile + '">' + Language.get('wcf.editor.code.file') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<input type="text" id="' + idFile + '" class="long">'
+                                                       + '<small>' + Language.get('wcf.editor.code.file.description') + '</small>'
+                                               + '</dd>'
+                                       + '</dl>'
+                               + '</div>'
+                               + '<div class="formSubmit">'
+                                       + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
+                               + '</div>'
+                       };
+               }
+       };
+       
+       return UiRedactorCode;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Format.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Format.js
new file mode 100644 (file)
index 0000000..cbe2769
--- /dev/null
@@ -0,0 +1,245 @@
+/**
+ * Provides helper methods to add and remove format elements. These methods should in
+ * theory work with non-editor elements but has not been tested and any usage outside
+ * the editor is not recommended.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Redactor/Format
+ */
+define(['Dom/Util'], function(DomUtil) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Redactor/Format
+        */
+       return {
+               /**
+                * Applies format elements to the selected text.
+                * 
+                * @param       {Element}       editorElement   editor element
+                * @param       {string}        tagName         format tag name
+                * @param       {string=}       className       optional CSS class for the format tag
+                * @param       {Object=}       attributes      optional list of attributes for the format tag
+                */
+               format: function(editorElement, tagName, className, attributes) {
+                       var selection = window.getSelection();
+                       if (!selection.rangeCount) {
+                               // no active selection
+                               return;
+                       }
+                       
+                       var range = selection.getRangeAt(0);
+                       var tmpElement = null;
+                       if (range.collapsed) {
+                               tmpElement = elCreate('strike');
+                               tmpElement.textContent = '\u200B';
+                               range.insertNode(tmpElement);
+                               
+                               range = document.createRange();
+                               range.selectNodeContents(tmpElement);
+                               
+                               selection.removeAllRanges();
+                               selection.addRange(range);
+                       }
+                       
+                       if (tmpElement === null) {
+                               document.execCommand('strikethrough');
+                       }
+                       
+                       var elements = elBySelAll('strike', editorElement), formatElement, property, strike;
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               strike = elements[i];
+                               
+                               formatElement = elCreate(tagName);
+                               if (className) formatElement.className = className;
+                               if (typeof attributes === 'object') {
+                                       for (property in attributes) {
+                                               if (attributes.hasOwnProperty(property)) {
+                                                       elAttr(formatElement, key, attributes[key]);
+                                               }
+                                       }
+                               }
+                               
+                               DomUtil.replaceElement(strike, formatElement);
+                       }
+               },
+               
+               /**
+                * Removes a format element from the current selection.
+                * 
+                * The removal uses a few techniques to remove the target element(s) without harming
+                * nesting nor any other formatting present. The steps taken are described below:
+                * 
+                * 1. The browser will wrap all parts of the selection into <strike> tags
+                * 
+                *      This isn't the most efficient way to isolate each selected node, but is the
+                *      most reliable way to accomplish this because the browser will insert them
+                *      exactly where the range spans without harming the node nesting.
+                *      
+                *      Basically it is a trade-off between efficiency and reliability, the performance
+                *      is still excellent but could be better at the expense of an increased complexity,
+                *      which simply doesn't exactly pay off.
+                * 
+                * 2. Iterate over each inserted <strike> and isolate all relevant ancestors
+                * 
+                *      Format tags can appear both as a child of the <strike> as well as once or multiple
+                *      times as an ancestor.
+                *      
+                *      It uses ranges to select the contents before the <strike> element up to the start
+                *      of the last matching ancestor and cuts out the nodes. The browser will ensure that
+                *      the resulting fragment will include all relevant ancestors that were present before.
+                *      
+                *      The example below will use the fictional <bar> elements as the tag to remove, the
+                *      pipe ("|") is used to denote the outer node boundaries.
+                *      
+                *      Before:
+                *      |<bar>This is <foo>a <strike>simple <bar>example</bar></strike></foo></bar>|
+                *      After:
+                *      |<bar>This is <foo>a </foo></bar>|<bar><foo>simple <bar>example</bar></strike></foo></bar>|
+                *      
+                *      As a result we can now remove <bar> both inside the <strike> element as well as
+                *      the outer <bar> without harming the effect of <bar> for the preceding siblings.
+                *      
+                *      This process is repeated for siblings appearing after the <strike> element too, it
+                *      works as described above but flipped. This is an expensive operation and will only
+                *      take place if there are any matching ancestors that need to be considered.
+                *      
+                *      Inspired by http://stackoverflow.com/a/12899461
+                * 
+                * 3. Remove all matching ancestors, child elements and last the <strike> element itself
+                * 
+                *      Depending on the amount of nested matching nodes, this process will move a lot of
+                *      nodes around. Removing the <bar> element will require all its child nodes to be moved
+                *      in front of <bar>, they will actually become a sibling of <bar>. Afterwards the
+                *      (now empty) <bar> element can be safely removed without losing any nodes.
+                * 
+                * 
+                * One last hint: This method will not check if the selection at some point contains at
+                * least one target element, it assumes that the user will not take any action that invokes
+                * this method for no reason (unless they want to waste CPU cycles, in that case they're
+                * welcome).
+                * 
+                * This is especially important for developers as this method shouldn't be called for
+                * no good reason. Even though it is super fast, it still comes with expensive DOM operations
+                * and especially low-end devices (such as cheap smartphones) might not exactly like executing
+                * this method on large documents.
+                * 
+                * If you fell the need to invoke this method anyway, go ahead. I'm a comment, not a cop.
+                * 
+                * @param       {Element}       editorElement   editor element
+                * @param       {string}        tagName         format tag name that should be removed
+                */
+               removeFormat: function(editorElement, tagName) {
+                       tagName = tagName.toUpperCase();
+                       
+                       var strikeElements = elByTag('strike', editorElement);
+                       
+                       // remove any <strike> element first, all though there shouldn't be any at all
+                       while (strikeElements.length) {
+                               DomUtil.unwrapChildNodes(strikeElements[0]);
+                       }
+                       
+                       document.execCommand('strikethrough');
+                       
+                       var elements, lastMatchingParent, strikeElement;
+                       while (strikeElements.length) {
+                               strikeElement = strikeElements[0];
+                               lastMatchingParent = this._getLastMatchingParent(strikeElement, editorElement, tagName);
+                               
+                               if (lastMatchingParent !== null) {
+                                       this._handleParentNodes(strikeElement, lastMatchingParent, tagName);
+                               }
+                               
+                               // remove offending elements from child nodes
+                               elements = elByTag(tagName.toLowerCase(), strikeElement);
+                               while (elements.length) {
+                                       DomUtil.unwrapChildNodes(elements[0]);
+                               }
+                               
+                               // remove strike element itself
+                               DomUtil.unwrapChildNodes(strikeElement);
+                       }
+               },
+               
+               /**
+                * Slices relevant parent nodes and removes matching ancestors.
+                * 
+                * @param       {Element}       strikeElement           strike element representing the text selection
+                * @param       {Element}       lastMatchingParent      last matching ancestor element
+                * @param       {string}        tagName                 format tag name that should be removed
+                * @protected
+                */
+               _handleParentNodes: function(strikeElement, lastMatchingParent, tagName) {
+                       var range;
+                       
+                       // selection does not begin at parent node start, slice all relevant parent
+                       // nodes to ensure that selection is then at the beginning while preserving
+                       // all proper ancestor elements
+                       // 
+                       // before: (the pipe represents the node boundary)
+                       // |otherContent <-- selection -->
+                       // after:
+                       // |otherContent| |<-- selection -->
+                       if (!DomUtil.isAtNodeStart(strikeElement, lastMatchingParent)) {
+                               range = document.createRange();
+                               range.setStartBefore(lastMatchingParent);
+                               range.setEndBefore(strikeElement);
+                               
+                               var fragment = range.extractContents();
+                               lastMatchingParent.parentNode.insertBefore(fragment, lastMatchingParent);
+                       }
+                       
+                       // selection does not end at parent node end, slice all relevant parent nodes
+                       // to ensure that selection is then at the end while preserving all proper
+                       // ancestor elements
+                       // 
+                       // before: (the pipe represents the node boundary)
+                       // <-- selection --> otherContent|
+                       // after:
+                       // <-- selection -->| |otherContent|
+                       if (!DomUtil.isAtNodeEnd(strikeElement, lastMatchingParent)) {
+                               range = document.createRange();
+                               range.setStartAfter(strikeElement);
+                               range.setEndAfter(lastMatchingParent);
+                               
+                               fragment = range.extractContents();
+                               lastMatchingParent.parentNode.insertBefore(fragment, lastMatchingParent.nextSibling);
+                       }
+                       
+                       // the strike element is now some kind of isolated, meaning we can now safely
+                       // remove all offending parent nodes without influcing formatting of any content
+                       // before or after the element
+                       var elements = elByTag(tagName, lastMatchingParent);
+                       while (elements.length) {
+                               DomUtil.unwrapChildNodes(elements[0]);
+                       }
+                       
+                       // finally remove the parent itself
+                       DomUtil.unwrapChildNodes(lastMatchingParent);
+               },
+               
+               /**
+                * Finds the last matching ancestor until it reaches the editor element.
+                * 
+                * @param       {Element}               strikeElement   strike element representing the text selection
+                * @param       {Element}               editorElement   editor element
+                * @param       {string}                tagName         format tag name that should be removed
+                * @returns     {(Element|null)}        last matching ancestor element or null if there is none
+                * @protected
+                */
+               _getLastMatchingParent: function(strikeElement, editorElement, tagName) {
+                       var parent = strikeElement.parentNode, match = null;
+                       while (parent !== editorElement) {
+                               if (parent.nodeName === tagName) {
+                                       match = parent;
+                               }
+                               
+                               parent = parent.parentNode;
+                       }
+                       
+                       return match;
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Link.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Link.js
new file mode 100644 (file)
index 0000000..f278af6
--- /dev/null
@@ -0,0 +1,68 @@
+define(['Language', 'Ui/Dialog'], function(Language, UiDialog) {
+       "use strict";
+       
+       var _boundListener = false;
+       var _callback = null;
+       
+       return {
+               showDialog: function(options) {
+                       UiDialog.open(this);
+                       
+                       UiDialog.setTitle(this, Language.get('wcf.editor.link.' + (options.insert ? 'add' : 'edit')));
+                       
+                       var submitButton = elById('redactor-modal-button-action');
+                       submitButton.textContent = Language.get('wcf.global.button.' + (options.insert ? 'insert' : 'save'));
+                       
+                       _callback = options.submitCallback;
+                       
+                       if (!_boundListener) {
+                               _boundListener = true;
+                               
+                               submitButton.addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
+                       }
+               },
+               
+               _submit: function() {
+                       if (_callback()) {
+                               UiDialog.close(this);
+                       }
+                       else {
+                               var url = elById('redactor-link-url');
+                               var small = (url.nextElementSibling && url.nextElementSibling.nodeName === 'SMALL') ? url.nextElementSibling : null;
+                               
+                               if (small === null) {
+                                       small = elCreate('small');
+                                       small.className = 'innerError';
+                                       small.textContent = Language.get('wcf.global.form.error.empty');
+                                       url.parentNode.appendChild(small);
+                               }
+                       }
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'redactorDialogLink',
+                               options: {
+                                       onClose: function() {
+                                               var url = elById('redactor-link-url');
+                                               var small = (url.nextElementSibling && url.nextElementSibling.nodeName === 'SMALL') ? url.nextElementSibling : null;
+                                               if (small !== null) {
+                                                       elRemove(small);
+                                               }
+                                       }
+                               },
+                               source: '<dl>'
+                                               + '<dt><label for="redactor-link-url">' + Language.get('wcf.editor.link.url') + '</label></dt>'
+                                               + '<dd><input type="url" id="redactor-link-url" class="long"></dd>'
+                                       + '</dl>'
+                                       + '<dl>'
+                                               + '<dt><label for="redactor-link-url-text">' + Language.get('wcf.editor.link.text') + '</label></dt>'
+                                               + '<dd><input type="text" id="redactor-link-url-text" class="long"></dd>'
+                                       + '</dl>'
+                                       + '<div class="formSubmit">'
+                                               + '<button id="redactor-modal-button-action" class="buttonPrimary"></button>'
+                                       + '</div>'
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Mention.js
new file mode 100644 (file)
index 0000000..b23defc
--- /dev/null
@@ -0,0 +1,345 @@
+define(['Ajax', 'Environment', 'EventHandler', 'Ui/Alignment'], function(Ajax, Environment, EventHandler, UiAlignment) {
+       "use strict";
+       
+       function UiRedactorMention(redactor) { this.init(redactor); }
+       UiRedactorMention.prototype = {
+               init: function(redactor) {
+                       this._active = false;
+                       this._caret = null;
+                       this._dropdownActive = false;
+                       this._dropdownMenu = null;
+                       this._itemIndex = 0;
+                       this._lineHeight = null;
+                       this._mentionStart = '';
+                       this._redactor = redactor;
+                       this._timer = null;
+                       
+                       redactor.WoltLabEvent.register('keydown', this._keyDown.bind(this));
+                       redactor.WoltLabEvent.register('keyup', this._keyUp.bind(this));
+               },
+               
+               _keyDown: function(data) {
+                       if (!this._dropdownActive) {
+                               return;
+                       }
+                       
+                       /** @var Event event */
+                       var event = data.event;
+                       
+                       switch (event.which) {
+                               // enter
+                               case 13:
+                                       this._setUsername(null, this._dropdownMenu.children[this._itemIndex].children[0]);
+                                       break;
+                               
+                               // arrow up
+                               case 38:
+                                       this._selectItem(-1);
+                                       break;
+                               
+                               // arrow down
+                               case 40:
+                                       this._selectItem(1);
+                                       break;
+                               
+                               default:
+                                       return;
+                                       break;
+                       }
+                       
+                       event.preventDefault();
+                       data.cancel = true;
+               },
+               
+               _keyUp: function(data) {
+                       /** @var Event event */
+                       var event = data.event;
+                       
+                       // ignore return key
+                       if (event.which === 13) {
+                               this._active = false;
+                               
+                               return;
+                       }
+                       
+                       var text = this._getTextLineInFrontOfCaret();
+                       if (text.length) {
+                               var match = text.match(/@([^,]{3,})$/);
+                               if (match) {
+                                       // if mentioning is at text begin or there's a whitespace character
+                                       // before the '@', everything is fine
+                                       if (!match.index || text[match.index - 1].match(/\s/)) {
+                                               this._mentionStart = match[1];
+                                               
+                                               if (this._timer !== null) {
+                                                       window.clearTimeout(this._timer);
+                                                       this._timer = null;
+                                               }
+                                               
+                                               this._timer = window.setTimeout((function() {
+                                                       Ajax.api(this, {
+                                                               parameters: {
+                                                                       data: {
+                                                                               searchString: this._mentionStart
+                                                                       }
+                                                               }
+                                                       });
+                                                       
+                                                       this._timer = null;
+                                               }).bind(this), 500);
+                                       }
+                               }
+                               else {
+                                       this._hideDropdown();
+                               }
+                       }
+                       else {
+                               this._hideDropdown();
+                       }
+               },
+               
+               _setUsername: function(event, item) {
+                       if (event) {
+                               event.preventDefault();
+                               item = event.currentTarget;
+                       }
+                       
+                       /*if (this._timer !== null) {
+                               this._timer.stop();
+                               this._timer = null;
+                       }
+                       this._proxy.abortPrevious();*/
+                       
+                       var selection = window.getSelection();
+                       
+                       // restore caret position
+                       selection.removeAllRanges();
+                       selection.addRange(this._caret);
+                       
+                       var orgRange = selection.getRangeAt(0).cloneRange();
+                       
+                       // allow redactor to undo this
+                       this._redactor.buffer.set();
+                       
+                       var startContainer = orgRange.startContainer;
+                       var startOffset = orgRange.startOffset - (this._mentionStart.length + 1);
+                       
+                       // navigating with the keyboard before hitting enter will cause the text node to be split
+                       if (startOffset < 0) {
+                               startContainer = startContainer.previousSibling;
+                               startOffset = startContainer.length - (this._mentionStart.length + 1) - (orgRange.startOffset - 1);
+                       }
+                       
+                       var newRange = document.createRange();
+                       newRange.setStart(startContainer, startOffset);
+                       newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
+                       
+                       selection.removeAllRanges();
+                       selection.addRange(newRange);
+                       
+                       var range = getSelection().getRangeAt(0);
+                       range.deleteContents();
+                       range.collapse(true);
+                       
+                       var mention = elCreate('woltlab-mention');
+                       elAttr(mention, 'contenteditable', 'false');
+                       elData(mention, 'user-id', elData(item, 'user-id'));
+                       elData(mention, 'username', elData(item, 'username'));
+                       mention.textContent = elData(item, 'username');
+                       
+                       // U+200C = zero width non-joiner
+                       var text = document.createTextNode('\u200c');
+                       
+                       range.insertNode(text);
+                       range.insertNode(mention);
+                       
+                       newRange = document.createRange();
+                       newRange.selectNode(text);
+                       newRange.collapse(false);
+                       
+                       selection.removeAllRanges();
+                       selection.addRange(newRange);
+                       
+                       this._redactor.selection.save();
+                       
+                       this._hideDropdown();
+               },
+               
+               _getTextLineInFrontOfCaret: function() {
+                       /** @var Range range */
+                       var range = window.getSelection().getRangeAt(0);
+                       if (!range.collapsed) {
+                               return '';
+                       }
+                       
+                       // in Firefox, blurring and refocusing the browser creates separate text nodes
+                       if (Environment.browser() === 'firefox' && range.startContainer.nodeType === Node.TEXT_NODE) {
+                               range.startContainer.parentNode.normalize();
+                       }
+                       
+                       var text = range.startContainer.textContent.substr(0, range.startOffset);
+                       
+                       // remove unicode zero-width space and non-breaking space
+                       var textBackup = text;
+                       text = '';
+                       var hadSpace = false;
+                       for (var i = 0; i < textBackup.length; i++) {
+                               var byte = textBackup.charCodeAt(i).toString(16);
+                               if (byte !== '200b' && (!/\s/.test(textBackup[i]) || ((byte === 'a0' || byte === '20') && !hadSpace))) {
+                                       if (byte === 'a0' || byte === '20') {
+                                               hadSpace = true;
+                                       }
+                                       
+                                       if (textBackup[i] === '@' && i && /\s/.test(textBackup[i - 1])) {
+                                               hadSpace = false;
+                                               text = '';
+                                       }
+                                       
+                                       text += textBackup[i];
+                               }
+                               else {
+                                       hadSpace = false;
+                                       text = '';
+                               }
+                       }
+                       
+                       return text;
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'getSearchResultList',
+                                       className: 'wcf\\data\\user\\UserAction',
+                                       interfaceName: 'wcf\\data\\ISearchAction',
+                                       parameters: {
+                                               data: {
+                                                       includeUserGroups: false
+                                               }
+                                       }
+                               }
+                       };
+               },
+               
+               _ajaxSuccess: function(data) {
+                       if (!Array.isArray(data.returnValues) || !data.returnValues.length) {
+                               this._hideDropdown();
+                               
+                               return;
+                       }
+                       
+                       if (this._dropdownMenu === null) {
+                               this._dropdownMenu = elCreate('ol');
+                               this._dropdownMenu.className = 'dropdownMenu';
+                               elById('dropdownMenuContainer').appendChild(this._dropdownMenu);
+                       }
+                       
+                       this._dropdownMenu.innerHTML = '';
+                       
+                       var callbackClick = this._setUsername.bind(this), link, listItem, user;
+                       for (var i = 0, length = data.returnValues.length; i < length; i++) {
+                               user = data.returnValues[i];
+                               
+                               listItem = elCreate('li');
+                               link = elCreate('a');
+                               link.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                               link.className = 'box16';
+                               link.innerHTML = '<span>' + user.icon + '</span> <span>' + user.label + '</span>';
+                               elData(link, 'user-id', user.objectID);
+                               elData(link, 'username', user.label);
+                               
+                               listItem.appendChild(link);
+                               this._dropdownMenu.appendChild(listItem);
+                       }
+                       
+                       this._dropdownMenu.classList.add('dropdownOpen');
+                       this._dropdownActive = true;
+                       
+                       this._updateDropdownPosition();
+               },
+               
+               _getDropdownMenuPosition: function() {
+                       this._redactor.selection.save();
+                       
+                       var selection = window.getSelection();
+                       var orgRange = selection.getRangeAt(0).cloneRange();
+                       
+                       // mark the entire text, starting from the '@' to the current cursor position
+                       var newRange = document.createRange();
+                       newRange.setStart(orgRange.startContainer, orgRange.startOffset - (this._mentionStart.length + 1));
+                       newRange.setEnd(orgRange.startContainer, orgRange.startOffset);
+                       
+                       selection.removeAllRanges();
+                       selection.addRange(newRange);
+                       
+                       // get the offsets of the bounding box of current text selection
+                       var rect = selection.getRangeAt(0).getBoundingClientRect();
+                       var offsets = {
+                               top: Math.round(rect.bottom) + window.scrollY,
+                               left: Math.round(rect.left) + document.body.scrollLeft
+                       };
+                       
+                       if (this._lineHeight === null) {
+                               this._lineHeight = Math.round(rect.bottom - rect.top - window.scrollY);
+                       }
+                       
+                       // restore caret position
+                       this._redactor.selection.restore();
+                       
+                       this._caret = orgRange;
+                       
+                       return offsets;
+               },
+               
+               _updateDropdownPosition: function() {
+                       try {
+                               var offset = this._getDropdownMenuPosition();
+                               offset.top += 7; // add a little vertical gap
+                               
+                               this._dropdownMenu.style.setProperty('left', offset.left + 'px', '');
+                               this._dropdownMenu.style.setProperty('top', offset.top + 'px', '');
+                               
+                               this._selectItem(0);
+                               
+                               if (offset.top + this._dropdownMenu.offsetHeight + 10 > window.innerHeight + window.scrollY) {
+                                       this._dropdownMenu.classList.add('dropdownArrowBottom');
+                                       
+                                       this._dropdownMenu.style.setProperty('top', offset.top - this._dropdownMenu.offsetHeight - 2 * this._lineHeight + 7 + 'px', '');
+                               }
+                               else {
+                                       this._dropdownMenu.classList.remove('dropdownArrowBottom');
+                               }
+                       }
+                       catch (e) {
+                               console.debug(e);
+                               // ignore errors that are caused by pressing enter to
+                               // often in a short period of time
+                       }
+               },
+               
+               _selectItem: function(step) {
+                       // find currently active item
+                       var item = elBySel('.active', this._dropdownMenu);
+                       if (item !== null) {
+                               item.classList.remove('active');
+                       }
+                       
+                       this._itemIndex += step;
+                       if (this._itemIndex === -1) {
+                               this._itemIndex = this._dropdownMenu.childElementCount - 1;
+                       }
+                       else if (this._itemIndex === this._dropdownMenu.childElementCount) {
+                               this._itemIndex = 0;
+                       }
+                       
+                       this._dropdownMenu.children[this._itemIndex].classList.add('active');
+               },
+               
+               _hideDropdown: function() {
+                       if (this._dropdownMenu !== null) this._dropdownMenu.classList.remove('dropdownOpen');
+                       this._dropdownActive = false;
+               }
+       };
+       
+       return UiRedactorMention;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Metacode.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Metacode.js
new file mode 100644 (file)
index 0000000..8aff60b
--- /dev/null
@@ -0,0 +1,154 @@
+/**
+ * Converts `<woltlab-metacode>` into the bbcode representation.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Redactor/Metacode
+ */
+define(['Dom/Util'], function(DomUtil) {
+       "use strict";
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Redactor/Metacode
+        */
+       return {
+               /**
+                * Converts `<woltlab-metacode>` into the bbcode representation.
+                * 
+                * @param       {Element}       element         textarea element
+                */
+               convert: function(element) {
+                       var div = elCreate('div');
+                       div.innerHTML = element.textContent;
+                       
+                       var attributes, metacode, metacodes = elByTag('woltlab-metacode', div), name, tagClose, tagOpen;
+                       while (metacodes.length) {
+                               metacode = metacodes[0];
+                               name = elData(metacode, 'name');
+                               attributes = elData(metacode, 'attributes');
+                               
+                               tagOpen = this._getOpeningTag(name, attributes);
+                               tagClose = this._getClosingTag(name);
+                               
+                               if (metacode.parentNode === div) {
+                                       DomUtil.prepend(tagOpen, this._getFirstParagraph(metacode));
+                                       this._getLastParagraph(metacode).appendChild(tagClose);
+                               }
+                               else {
+                                       DomUtil.prepend(tagOpen, metacode);
+                                       metacode.appendChild(tagClose);
+                               }
+                               
+                               DomUtil.unwrapChildNodes(metacode);
+                       }
+                       
+                       element.textContent = div.innerHTML;
+               },
+               
+               /**
+                * Returns a text node representing the opening bbcode tag.
+                * 
+                * @param       {string}        name            bbcode tag
+                * @param       {string}        attributes      base64- and JSON-encoded attributes
+                * @returns     {Text}          text node containing the opening bbcode tag
+                * @protected
+                */
+               _getOpeningTag: function(name, attributes) {
+                       try {
+                               attributes = JSON.parse(atob(attributes));
+                       }
+                       catch (e) { /* invalid base64 data or invalid json */ }
+                       
+                       if (!Array.isArray(attributes)) {
+                               attributes = [];
+                       }
+                       
+                       var buffer = '[' + name;
+                       if (attributes.length) {
+                               for (var i = 0, length = attributes.length; i < length; i++) {
+                                       if (!/^'.*'$/.test(attributes[i])) {
+                                               attributes[i] = "'" + attributes[i] + "'";
+                                       }
+                               }
+                               
+                               buffer += '=' + attributes.join(',');
+                       }
+                       
+                       return document.createTextNode(buffer + ']');
+               },
+               
+               /**
+                * Returns a text node representing the closing bbcode tag.
+                * 
+                * @param       {string}        name            bbcode tag
+                * @returns     {Text}          text node containing the closing bbcode tag
+                * @protected
+                */
+               _getClosingTag: function(name) {
+                       return document.createTextNode('[/' + name + ']');
+               },
+               
+               /**
+                * Returns the first paragraph of provided element. If there are no children or
+                * the first child is not a paragraph, a new paragraph is created and inserted
+                * as first child.
+                * 
+                * @param       {Element}       element         metacode element
+                * @returns     {Element}       paragraph that is the first child of provided element
+                * @protected
+                */
+               _getFirstParagraph: function (element) {
+                       var firstChild, paragraph;
+                       
+                       if (element.childElementCount === 0) {
+                               paragraph = elCreate('p');
+                               element.appendChild(paragraph);
+                       }
+                       else {
+                               firstChild = element.children[0];
+                               
+                               if (firstChild.nodeName === 'P') {
+                                       paragraph = firstChild;
+                               }
+                               else {
+                                       paragraph = elCreate('p');
+                                       element.insertBefore(paragraph, firstChild);
+                               }
+                       }
+                       
+                       return paragraph;
+               },
+               
+               /**
+                * Returns the last paragraph of provided element. If there are no children or
+                * the last child is not a paragraph, a new paragraph is created and inserted
+                * as last child.
+                * 
+                * @param       {Element}       element         metacode element
+                * @returns     {Element}       paragraph that is the last child of provided element
+                * @protected
+                */
+               _getLastParagraph: function (element) {
+                       var count = element.childElementCount, lastChild, paragraph;
+                       
+                       if (count === 0) {
+                               paragraph = elCreate('p');
+                               element.appendChild(paragraph);
+                       }
+                       else {
+                               lastChild = element.children[count - 1];
+                               
+                               if (lastChild.nodeName === 'P') {
+                                       paragraph = lastChild;
+                               }
+                               else {
+                                       paragraph = elCreate('p');
+                                       element.appendChild(paragraph);
+                               }
+                       }
+                       
+                       return paragraph;
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Page.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Page.js
new file mode 100644 (file)
index 0000000..96fdf8e
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Converts `<woltlab-metacode>` into the bbcode representation.
+ *
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Redactor/Metacode
+ */
+define(['WoltLabSuite/Core/Ui/Page/Search'], function(UiPageSearch) {
+       "use strict";
+       
+       function UiRedactorPage(editor, button) { this.init(editor, button); }
+       UiRedactorPage.prototype = {
+               init: function (editor, button) {
+                       this._editor = editor;
+                       
+                       button.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+               },
+               
+               _click: function (event) {
+                       event.preventDefault();
+                       
+                       UiPageSearch.open(this._insert.bind(this));
+               },
+               
+               _insert: function (pageID) {
+                       this._editor.buffer.set();
+                       
+                       this._editor.insert.text("[wsp='" + pageID + "'][/wsp]");
+               }
+       };
+       
+       return UiRedactorPage;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Quote.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Quote.js
new file mode 100644 (file)
index 0000000..e89bf94
--- /dev/null
@@ -0,0 +1,262 @@
+/**
+ * Manages quotes.
+ *
+ * @author      Alexander Ebert
+ * @copyright   2001-2016 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module      WoltLabSuite/Core/Ui/Redactor/Quote
+ */
+define(['Core', 'EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (Core, EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
+       "use strict";
+       
+       var _headerHeight = 0;
+       
+       /**
+        * @param       {Object}        editor  editor instance
+        * @param       {jQuery}        button  toolbar button
+        * @constructor
+        */
+       function UiRedactorQuote(editor, button) { this.init(editor, button); }
+       UiRedactorQuote.prototype = {
+               /**
+                * Initializes the quote management.
+                * 
+                * @param       {Object}        editor  editor instance
+                * @param       {jQuery}        button  toolbar button
+                */
+               init: function(editor, button) {
+                       this._blockquote = null;
+                       this._editor = editor;
+                       this._elementId = this._editor.$element[0].id;
+                       
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
+                       
+                       this._editor.button.addCallback(button, this._click.bind(this));
+                       
+                       // support for active button marking
+                       this._editor.opts.activeButtonsStates.blockquote = 'woltlabQuote';
+                       
+                       // static bind to ensure that removing works
+                       this._callbackEdit = this._edit.bind(this);
+                       
+                       // bind listeners on init
+                       this._observeLoad();
+                       
+                       // quote manager
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'insertQuote_' + this._elementId, this._insertQuote.bind(this));
+               },
+               
+               /**
+                * Inserts a quote.
+                * 
+                * @param       {Object}        data            quote data
+                * @protected
+                */
+               _insertQuote: function (data) {
+                       this._editor.buffer.set();
+                       
+                       // caret must be within a `<p>`, if it is not move it
+                       /** @type Node */
+                       var block = this._editor.selection.block();
+                       if (block === false) {
+                               this._editor.selection.restore();
+                               
+                               block = this._editor.selection.block();
+                       }
+                       
+                       if (block.nodeName !== 'P') {
+                               var redactor = this._editor.core.editor()[0];
+                               
+                               // find parent before Redactor
+                               while (block.parentNode !== redactor) {
+                                       block = block.parentNode;
+                               }
+                               
+                               // caret.after() requires a following element
+                               var next = this._editor.caret.next(block);
+                               if (next === undefined || next.nodeName !== 'P') {
+                                       var p = elCreate('p');
+                                       p.textContent = '\u200B';
+                                       
+                                       DomUtil.insertAfter(p, block);
+                               }
+                               
+                               this._editor.caret.after(block);
+                       }
+                       
+                       var content = '';
+                       if (data.isText) content = this._editor.marker.html();
+                       else content = data.content;
+                       
+                       var quoteId = Core.getUuid();
+                       this._editor.insert.html('<blockquote id="' + quoteId + '">' + content + '</blockquote>');
+                       
+                       var quote = elById(quoteId);
+                       elData(quote, 'author', data.author);
+                       elData(quote, 'link', data.link);
+                       
+                       if (data.isText) {
+                               this.insert.text(data.content);
+                       }
+                       
+                       quote.removeAttribute('id');
+                       
+                       this._editor.caret.after(quote);
+                       this._editor.selection.save();
+               },
+               
+               /**
+                * Toggles the quote block on button click.
+                * 
+                * @protected
+                */
+               _click: function() {
+                       this._editor.button.toggle({}, 'blockquote', 'func', 'block.format');
+                       
+                       var blockquote = this._editor.selection.block();
+                       if (blockquote && blockquote.nodeName === 'BLOCKQUOTE') {
+                               this._setTitle(blockquote);
+                               
+                               blockquote.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                       }
+               },
+               
+               /**
+                * Binds event listeners and sets quote title on both editor
+                * initialization and when switching back from code view.
+                * 
+                * @protected
+                */
+               _observeLoad: function() {
+                       elBySelAll('blockquote', this._editor.$editor[0], (function(blockquote) {
+                               blockquote.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                               this._setTitle(blockquote);
+                       }).bind(this));
+               },
+               
+               /**
+                * Opens the dialog overlay to edit the quote's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _edit: function(event) {
+                       var blockquote = event.currentTarget;
+                       
+                       if (_headerHeight === 0) {
+                               _headerHeight = ~~window.getComputedStyle(blockquote).paddingTop.replace(/px$/, '');
+                               
+                               var styles = window.getComputedStyle(blockquote, '::before');
+                               _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
+                               _headerHeight += ~~styles.height.replace(/px$/, '');
+                               _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
+                       }
+                       
+                       // check if the click hit the header
+                       var offset = DomUtil.offset(blockquote);
+                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
+                               event.preventDefault();
+                               
+                               this._blockquote = blockquote;
+                               
+                               UiDialog.open(this);
+                       }
+               },
+               
+               /**
+                * Saves the changes to the quote's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _save: function(event) {
+                       event.preventDefault();
+                       
+                       var id = 'redactor-quote-' + this._elementId;
+                       var urlInput = elById(id + '-url');
+                       var innerError = elBySel('.innerError', urlInput.parentNode);
+                       if (innerError !== null) elRemove(innerError);
+                       
+                       var url = urlInput.value.replace(/\u200B/g, '').trim();
+                       // simple test to check if it at least looks like it could be a valid url
+                       if (url.length && !/^https?:\/\/[^\/]+/.test(url)) {
+                               innerError = elCreate('small');
+                               innerError.className = 'innerError';
+                               innerError.textContent = Language.get('wcf.editor.quote.url.error.invalid');
+                               urlInput.parentNode.insertBefore(innerError, urlInput.nextElementSibling);
+                               return;
+                       }
+                       
+                       // set author
+                       elData(this._blockquote, 'author', elById(id + '-author').value);
+                       
+                       // set url
+                       elData(this._blockquote, 'url', url);
+                       
+                       this._setTitle(this._blockquote);
+                       this._editor.caret.after(this._blockquote);
+                       
+                       UiDialog.close(this);
+               },
+               
+               /**
+                * Sets or updates the quote's header title.
+                * 
+                * @param       {Element}       blockquote     quote element
+                * @protected
+                */
+               _setTitle: function(blockquote) {
+                       var title = Language.get('wcf.editor.quote.title', {
+                               author: elData(blockquote, 'author'),
+                               url: elData(blockquote, 'url')
+                       });
+                       
+                       if (elData(blockquote, 'title') !== title) {
+                               elData(blockquote, 'title', title);
+                       }
+               },
+               
+               _dialogSetup: function() {
+                       var id = 'redactor-quote-' + this._elementId,
+                           idAuthor = id + '-author',
+                           idButtonSave = id + '-button-save',
+                           idUrl = id + '-url';
+                       
+                       return {
+                               id: id,
+                               options: {
+                                       onSetup: (function() {
+                                               elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
+                                       }).bind(this),
+                                       
+                                       onShow: (function() {
+                                               elById(idAuthor).value = elData(this._blockquote, 'author');
+                                               elById(idUrl).value = elData(this._blockquote, 'url');
+                                       }).bind(this),
+                                       
+                                       title: Language.get('wcf.editor.quote.edit')
+                               },
+                               source: '<div class="section">'
+                                       + '<dl>'
+                                               + '<dt><label for="' + idAuthor + '">' + Language.get('wcf.editor.quote.author') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<input type="text" id="' + idAuthor + '" class="long">'
+                                               + '</dd>'
+                                       + '</dl>'
+                                       + '<dl>'
+                                               + '<dt><label for="' + idUrl + '">' + Language.get('wcf.editor.quote.url') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<input type="text" id="' + idUrl + '" class="long">'
+                                                       + '<small>' + Language.get('wcf.editor.quote.url.description') + '</small>'
+                                               + '</dd>'
+                                       + '</dl>'
+                               + '</div>'
+                               + '<div class="formSubmit">'
+                                       + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
+                               + '</div>'
+                       };
+               }
+       };
+       
+       return UiRedactorQuote;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Spoiler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Redactor/Spoiler.js
new file mode 100644 (file)
index 0000000..1a95278
--- /dev/null
@@ -0,0 +1,174 @@
+/**
+ * Manages spoilers.
+ *
+ * @author      Alexander Ebert
+ * @copyright   2001-2016 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module      WoltLabSuite/Core/Ui/Redactor/Spoiler
+ */
+define(['EventHandler', 'EventKey', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog'], function (EventHandler, EventKey, Language, StringUtil, DomUtil, UiDialog) {
+       "use strict";
+       
+       var _headerHeight = 0;
+       
+       /**
+        * @param       {Object}        editor  editor instance
+        * @constructor
+        */
+       function UiRedactorSpoiler(editor) { this.init(editor); }
+       UiRedactorSpoiler.prototype = {
+               /**
+                * Initializes the spoiler management.
+                * 
+                * @param       {Object}        editor  editor instance
+                */
+               init: function(editor) {
+                       this._editor = editor;
+                       this._elementId = this._editor.$element[0].id;
+                       this._spoiler = null;
+                       
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'bbcode_spoiler_' + this._elementId, this._bbcodeSpoiler.bind(this));
+                       EventHandler.add('com.woltlab.wcf.redactor2', 'observe_load_' + this._elementId, this._observeLoad.bind(this));
+                       
+                       // register custom block element
+                       this._editor.WoltLabBlock.register('woltlab-spoiler', true);
+                       
+                       // support for active button marking
+                       this._editor.opts.activeButtonsStates['woltlab-spoiler'] = 'woltlabSpoiler';
+                       
+                       // static bind to ensure that removing works
+                       this._callbackEdit = this._edit.bind(this);
+                       
+                       // bind listeners on init
+                       this._observeLoad();
+               },
+               
+               /**
+                * Intercepts the insertion of `[spoiler]` tags and uses
+                * the custom `<woltlab-spoiler>` element instead.
+                * 
+                * @param       {Object}        data    event data
+                * @protected
+                */
+               _bbcodeSpoiler: function(data) {
+                       data.cancel = true;
+                       
+                       this._editor.button.toggle({}, 'woltlab-spoiler', 'func', 'block.format');
+                       
+                       var spoiler = this._editor.selection.block();
+                       if (spoiler && spoiler.nodeName === 'WOLTLAB-SPOILER') {
+                               this._setTitle(spoiler);
+                               
+                               spoiler.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                       }
+               },
+               
+               /**
+                * Binds event listeners and sets quote title on both editor
+                * initialization and when switching back from code view.
+                * 
+                * @protected
+                */
+               _observeLoad: function() {
+                       elBySelAll('woltlab-spoiler', this._editor.$editor[0], (function(spoiler) {
+                               spoiler.addEventListener(WCF_CLICK_EVENT, this._callbackEdit);
+                               this._setTitle(spoiler);
+                       }).bind(this));
+               },
+               
+               /**
+                * Opens the dialog overlay to edit the spoiler's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _edit: function(event) {
+                       var spoiler = event.currentTarget;
+                       
+                       if (_headerHeight === 0) {
+                               _headerHeight = ~~window.getComputedStyle(spoiler).paddingTop.replace(/px$/, '');
+                               
+                               var styles = window.getComputedStyle(spoiler, '::before');
+                               _headerHeight += ~~styles.paddingTop.replace(/px$/, '');
+                               _headerHeight += ~~styles.height.replace(/px$/, '');
+                               _headerHeight += ~~styles.paddingBottom.replace(/px$/, '');
+                       }
+                       
+                       // check if the click hit the header
+                       var offset = DomUtil.offset(spoiler);
+                       if (event.pageY > offset.top && event.pageY < (offset.top + _headerHeight)) {
+                               event.preventDefault();
+                               
+                               this._spoiler = spoiler;
+                               
+                               UiDialog.open(this);
+                       }
+               },
+               
+               /**
+                * Saves the changes to the spoiler's properties.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _save: function(event) {
+                       event.preventDefault();
+                       
+                       elData(this._spoiler, 'label', elById('redactor-spoiler-' + this._elementId + '-label').value);
+                       
+                       this._setTitle(this._spoiler);
+                       this._editor.caret.after(this._spoiler);
+                       
+                       UiDialog.close(this);
+               },
+               
+               /**
+                * Sets or updates the spoiler's header title.
+                * 
+                * @param       {Element}       spoiler     spoiler element
+                * @protected
+                */
+               _setTitle: function(spoiler) {
+                       var title = Language.get('wcf.editor.spoiler.title', { label: elData(spoiler, 'label') });
+                       
+                       if (elData(spoiler, 'title') !== title) {
+                               elData(spoiler, 'title', title);
+                       }
+               },
+               
+               _dialogSetup: function() {
+                       var id = 'redactor-spoiler-' + this._elementId,
+                           idButtonSave = id + '-button-save',
+                           idLabel = id + '-label';
+                       
+                       return {
+                               id: id,
+                               options: {
+                                       onSetup: (function() {
+                                               elById(idButtonSave).addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
+                                       }).bind(this),
+                                       
+                                       onShow: (function() {
+                                               elById(idLabel).value = elData(this._spoiler, 'label');
+                                       }).bind(this),
+                                       
+                                       title: Language.get('wcf.editor.spoiler.edit')
+                               },
+                               source: '<div class="section">'
+                                       + '<dl>'
+                                               + '<dt><label for="' + idLabel + '">' + Language.get('wcf.editor.spoiler.label') + '</label></dt>'
+                                               + '<dd>'
+                                                       + '<input type="text" id="' + idLabel + '" class="long">'
+                                                       + '<small>' + Language.get('wcf.editor.spoiler.label.description') + '</small>'
+                                               + '</dd>'
+                                       + '</dl>'
+                               + '</div>'
+                               + '<div class="formSubmit">'
+                                       + '<button id="' + idButtonSave + '" class="buttonPrimary">' + Language.get('wcf.global.button.save') + '</button>'
+                               + '</div>'
+                       };
+               }
+       };
+       
+       return UiRedactorSpoiler;
+});
\ No newline at end of file
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Screen.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Screen.js
new file mode 100644 (file)
index 0000000..ae4d518
--- /dev/null
@@ -0,0 +1,175 @@
+/**
+ * Provides consistent support for media queries and body scrolling.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Screen
+ */
+define(['Core', 'Dictionary'], function(Core, Dictionary) {
+       "use strict";
+       
+       var _mql = new Dictionary();
+       var _scrollDisableCounter = 0;
+       
+       var _mqMap = Dictionary.fromObject({
+               'screen-xs': '(max-width: 544px)',                              /* smartphone */
+               'screen-sm': '(min-width: 545px) and (max-width: 768px)',       /* tablet (portrait) */
+               'screen-sm-down': '(max-width: 768px)',                         /* smartphone + tablet (portrait) */
+               'screen-sm-up': '(min-width: 545px)',                           /* tablet (portrait) + tablet (landscape) + desktop */
+               'screen-sm-md': '(min-width: 545px) and (max-width: 1024px)',   /* tablet (portrait) + tablet (landscape) */
+               'screen-md': '(min-width: 769px) and (max-width: 1024px)',      /* tablet (landscape) */
+               'screen-md-down': '(max-width: 1024px)',                        /* smartphone + tablet (portrait) + tablet (landscape) */
+               'screen-md-up': '(min-width: 1024px)',                          /* tablet (landscape) + desktop */
+               'screen-lg': '(min-width: 1025px)'                              /* desktop */
+       });
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Screen
+        */
+       return {
+               /**
+                * Registers event listeners for media query match/unmatch.
+                * 
+                * The `callbacks` object may contain the following keys:
+                *  - `match`, triggered when media query matches
+                *  - `unmatch`, triggered when media query no longer matches
+                *  - `setup`, invoked when media query first matches
+                * 
+                * Returns a UUID that is used to internal identify the callbacks, can be used
+                * to remove binding by calling the `remove` method.
+                * 
+                * @param       {string}        query           media query
+                * @param       {object}        callbacks       callback functions
+                * @return      {string}        UUID for listener removal
+                */
+               on: function(query, callbacks) {
+                       var uuid = Core.getUuid(), queryObject = this._getQueryObject(query);
+                       
+                       if (typeof callbacks.match === 'function') {
+                               queryObject.callbacksMatch.set(uuid, callbacks.match);
+                       }
+                       
+                       if (typeof callbacks.unmatch === 'function') {
+                               queryObject.callbacksUnmatch.set(uuid, callbacks.unmatch);
+                       }
+                       
+                       if (typeof callbacks.setup === 'function') {
+                               if (queryObject.mql.matches) {
+                                       callbacks.setup();
+                               }
+                               else {
+                                       queryObject.callbacksSetup.set(uuid, callbacks.setup);
+                               }
+                       }
+                       
+                       return uuid;
+               },
+               
+               /**
+                * Removes all listeners identified by their common UUID.
+                *
+                * @param       {string}        query   must match the `query` argument used when calling `on()`
+                * @param       {string}        uuid    UUID received when calling `on()`
+                */
+               remove: function(query, uuid) {
+                       var queryObject = this._getQueryObject(query);
+                       
+                       queryObject.callbacksMatch.delete(uuid);
+                       queryObject.callbacksUnmatch.delete(uuid);
+                       queryObject.callbacksSetup.delete(uuid);
+               },
+               
+               /**
+                * Returns a boolean value if a media query expression currently matches.
+                * 
+                * @param       {string}        query   CSS media query
+                * @returns     {boolean}       true if query matches
+                */
+               is: function(query) {
+                       return this._getQueryObject(query).mql.matches;
+               },
+               
+               /**
+                * Disables scrolling of body element.
+                */
+               scrollDisable: function() {
+                       if (_scrollDisableCounter === 0) {
+                               document.documentElement.classList.add('disableScrolling');
+                       }
+                       
+                       _scrollDisableCounter++;
+               },
+               
+               /**
+                * Re-enables scrolling of body element.
+                */
+               scrollEnable: function() {
+                       if (_scrollDisableCounter) {
+                               _scrollDisableCounter--;
+                               
+                               if (_scrollDisableCounter === 0) {
+                                       document.documentElement.classList.remove('disableScrolling');
+                               }
+                       }
+               },
+               
+               /**
+                * 
+                * @param       {string}        query   CSS media query
+                * @return      {Object}        object containing callbacks and MediaQueryList
+                * @protected
+                */
+               _getQueryObject: function(query) {
+                       if (typeof query !== 'string' || query.trim() === '') {
+                               throw new TypeError("Expected a non-empty string for parameter 'query'.");
+                       }
+                       
+                       if (_mqMap.has(query)) query = _mqMap.get(query);
+                       
+                       var queryObject = _mql.get(query);
+                       if (!queryObject) {
+                               queryObject = {
+                                       callbacksMatch: new Dictionary(),
+                                       callbacksUnmatch: new Dictionary(),
+                                       callbacksSetup: new Dictionary(),
+                                       mql: window.matchMedia(query)
+                               };
+                               queryObject.mql.addListener(this._mqlChange.bind(this));
+                               
+                               _mql.set(query, queryObject);
+                       }
+                       
+                       return queryObject;
+               },
+               
+               /**
+                * Triggered whenever a registered media query now matches or no longer matches.
+                * 
+                * @param       {Event} event   event object
+                * @protected
+                */
+               _mqlChange: function(event) {
+                       var queryObject = this._getQueryObject(event.media);
+                       if (event.matches) {
+                               if (queryObject.callbacksSetup.size) {
+                                       queryObject.callbacksSetup.forEach(function(callback) {
+                                               callback();
+                                       });
+                                       
+                                       // discard all setup callbacks after execution
+                                       queryObject.callbacksSetup = new Dictionary();
+                               }
+                               
+                               queryObject.callbacksMatch.forEach(function(callback) {
+                                       callback();
+                               });
+                       }
+                       else {
+                               queryObject.callbacksUnmatch.forEach(function(callback) {
+                                       callback();
+                               });
+                       }
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Scroll.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Scroll.js
new file mode 100644 (file)
index 0000000..a694d2c
--- /dev/null
@@ -0,0 +1,84 @@
+/**
+ * Smoothly scrolls to an element while accounting for potential sticky headers.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Scroll
+ */
+define(['Dom/Util'], function(DomUtil) {
+       "use strict";
+       
+       var _callback = null;
+       var _callbackScroll = null;
+       var _timeoutScroll = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/Scroll
+        */
+       return {
+               /**
+                * Scrolls to target element, optionally invoking the provided callback once scrolling has ended.
+                * 
+                * @param       {Element}       element         target element
+                * @param       {function=}     callback        callback invoked once scrolling has ended
+                */
+               element: function(element, callback) {
+                       if (!(element instanceof Element)) {
+                               throw new TypeError("Expected a valid DOM element.");
+                       }
+                       else if (callback !== undefined && typeof callback !== 'function') {
+                               throw new TypeError("Expected a valid callback function.");
+                       }
+                       else if (!document.body.contains(element)) {
+                               throw new Error("Element must be part of the visible DOM.");
+                       }
+                       else if (_callback !== null) {
+                               throw new Error("Cannot scroll to element, a concurrent request is running.");
+                       }
+                       
+                       if (callback) {
+                               _callback = callback;
+                               
+                               if (_callbackScroll === null) {
+                                       _callbackScroll = this._onScroll.bind(this);
+                               }
+                               
+                               window.addEventListener('scroll', _callbackScroll);
+                       }
+                       
+                       var y = DomUtil.offset(element).top;
+                       
+                       if (y <= 50) {
+                               y = 0;
+                       }
+                       else {
+                               // add an offset of 50 pixel to account for a sticky header
+                               y -= 50;
+                       }
+                       
+                       window.scrollTo({
+                               left: 0,
+                               top: y,
+                               behavior: 'smooth'
+                       });
+               },
+               
+               /**
+                * Monitors scroll event to only execute the callback once scrolling has ended.
+                * 
+                * @protected
+                */
+               _onScroll: function() {
+                       if (_timeoutScroll !== null) window.clearTimeout(_timeoutScroll);
+                       
+                       _timeoutScroll = window.setTimeout(function() {
+                               _callback();
+                               
+                               window.removeEventListener('scroll', _callbackScroll);
+                               _callback = null;
+                               _timeoutScroll = null;
+                       }, 100);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Search/Input.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Search/Input.js
new file mode 100644 (file)
index 0000000..2137209
--- /dev/null
@@ -0,0 +1,364 @@
+/**
+ * Provides suggestions using an input field, designed to work with `wcf\data\ISearchAction`.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Search/Input
+ */
+define(['Ajax', 'Core', 'EventKey', 'Dom/Util', 'Ui/SimpleDropdown'], function(Ajax, Core, EventKey, DomUtil, UiSimpleDropdown) {
+       "use strict";
+       
+       /**
+        * @param       {Element}       element         target input[type="text"]
+        * @param       {Object}        options         search options and settings
+        * @constructor
+        */
+       function UiSearchInput(element, options) { this.init(element, options); }
+       UiSearchInput.prototype = {
+               /**
+                * Initializes the search input field.
+                * 
+                * @param       {Element}       element         target input[type="text"]
+                * @param       {Object}        options         search options and settings
+                */
+               init: function(element, options) {
+                       this._element = element;
+                       if (!(this._element instanceof Element)) {
+                               throw new TypeError("Expected a valid DOM element.");
+                       }
+                       else if (this._element.nodeName !== 'INPUT' || (this._element.type !== 'search' && this._element.type !== 'text')) {
+                               throw new Error('Expected an input[type="text"].');
+                       }
+                       
+                       this._activeItem = null;
+                       this._dropdownContainerId = '';
+                       this._lastValue = '';
+                       this._list = null;
+                       this._request = null;
+                       this._timerDelay = null;
+                       
+                       this._options = Core.extend({
+                               ajax: {
+                                       actionName: 'getSearchResultList',
+                                       className: '',
+                                       interfaceName: 'wcf\\data\\ISearchAction'
+                               },
+                               callbackDropdownInit: null,
+                               callbackSelect: null,
+                               delay: 500,
+                               minLength: 3,
+                               noResultPlaceholder: '',
+                               preventSubmit: false
+                       }, options);
+                       
+                       // disable auto-complete as it collides with the suggestion dropdown
+                       elAttr(this._element, 'autocomplete', 'off');
+                       
+                       this._element.addEventListener('keydown', this._keydown.bind(this));
+                       this._element.addEventListener('keyup', this._keyup.bind(this));
+               },
+               
+               /**
+                * Handles the 'keydown' event.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _keydown: function(event) {
+                       if ((this._activeItem !== null && UiSimpleDropdown.isOpen(this._dropdownContainerId)) || this._options.preventSubmit) {
+                               if (EventKey.Enter(event)) {
+                                       event.preventDefault();
+                               }
+                       }
+                       
+                       if (EventKey.ArrowUp(event) || EventKey.ArrowDown(event) || EventKey.Escape(event)) {
+                               event.preventDefault();
+                       }
+               },
+               
+               /**
+                * Handles the 'keyup' event, provides keyboard navigation and executes search queries.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _keyup: function(event) {
+                       // handle dropdown keyboard navigation
+                       if (this._activeItem !== null) {
+                               if (!UiSimpleDropdown.isOpen(this._dropdownContainerId)) {
+                                       return;
+                               }
+                               
+                               if (EventKey.ArrowUp(event)) {
+                                       event.preventDefault();
+                                       
+                                       return this._keyboardPreviousItem();
+                               }
+                               else if (EventKey.ArrowDown(event)) {
+                                       event.preventDefault();
+                                       
+                                       return this._keyboardNextItem();
+                               }
+                               else if (EventKey.Enter(event)) {
+                                       event.preventDefault();
+                                       
+                                       return this._keyboardSelectItem();
+                               }
+                       }
+                       
+                       // close list on escape
+                       if (EventKey.Escape(event)) {
+                               UiSimpleDropdown.close(this._dropdownContainerId);
+                               
+                               return;
+                       }
+                       
+                       var value = this._element.value.trim();
+                       if (this._lastValue === value) {
+                               // value did not change, e.g. previously it was "Test" and now it is "Test ",
+                               // but the trailing whitespace has been ignored
+                               return;
+                       }
+                       
+                       this._lastValue = value;
+                       
+                       if (value.length < this._options.minLength) {
+                               if (this._dropdownContainerId) {
+                                       UiSimpleDropdown.close(this._dropdownContainerId);
+                               }
+                               
+                               // value below threshold
+                               return;
+                       }
+                       
+                       if (this._options.delay) {
+                               if (this._timerDelay !== null) {
+                                       window.clearTimeout(this._timerDelay);
+                               }
+                               
+                               this._timerDelay = window.setTimeout((function() {
+                                       this._search(value);
+                               }).bind(this), this._options.delay);
+                       }
+                       else {
+                               this._search(value);
+                       }
+               },
+               
+               /**
+                * Queries the server with the provided search string.
+                * 
+                * @param       {string}        value   search string
+                * @protected
+                */
+               _search: function(value) {
+                       if (this._request) {
+                               this._request.abortPrevious();
+                       }
+                       
+                       this._request = Ajax.api(this, this._getParameters(value));
+               },
+               
+               /**
+                * Returns additional AJAX parameters.
+                * 
+                * @param       {string}        value   search string
+                * @return      {Object}        additional AJAX parameters
+                * @protected
+                */
+               _getParameters: function(value) {
+                       return {
+                               parameters: {
+                                       data: {
+                                               searchString: value
+                                       }
+                               }
+                       };
+               },
+               
+               /**
+                * Selects the next dropdown item.
+                * 
+                * @protected
+                */
+               _keyboardNextItem: function() {
+                       this._activeItem.classList.remove('active');
+                       
+                       if (this._activeItem.nextElementSibling) {
+                               this._activeItem = this._activeItem.nextElementSibling;
+                       }
+                       else {
+                               this._activeItem = this._list.children[0];
+                       }
+                       
+                       this._activeItem.classList.add('active');
+               },
+               
+               /**
+                * Selects the previous dropdown item.
+                * 
+                * @protected
+                */
+               _keyboardPreviousItem: function() {
+                       this._activeItem.classList.remove('active');
+                       
+                       if (this._activeItem.previousElementSibling) {
+                               this._activeItem = this._activeItem.previousElementSibling;
+                       }
+                       else {
+                               this._activeItem = this._list.children[this._list.childElementCount - 1];
+                       }
+                       
+                       this._activeItem.classList.add('active');
+               },
+               
+               /**
+                * Selects the active item from the dropdown.
+                * 
+                * @protected
+                */
+               _keyboardSelectItem: function() {
+                       this._selectItem(this._activeItem);
+               },
+               
+               /**
+                * Selects an item from the dropdown by clicking it.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _clickSelectItem: function(event) {
+                       this._selectItem(event.currentTarget);
+               },
+               
+               /**
+                * Selects an item.
+                * 
+                * @param       {Element}       item    selected item
+                * @protected
+                */
+               _selectItem: function(item) {
+                       if (this._options.callbackSelect && this._options.callbackSelect(item) === false) {
+                               this._element.value = '';
+                       }
+                       else {
+                               this._element.value = elData(item, 'label');
+                       }
+                       
+                       this._activeItem = null;
+                       UiSimpleDropdown.close(this._dropdownContainerId);
+               },
+               
+               /**
+                * Handles successful AJAX requests.
+                * 
+                * @param       {Object}        data    response data
+                * @protected
+                */
+               _ajaxSuccess: function(data) {
+                       var createdList = false;
+                       if (this._list === null) {
+                               this._list = elCreate('ul');
+                               this._list.className = 'dropdownMenu';
+                               
+                               createdList = true;
+                               
+                               if (typeof this._options.callbackDropdownInit === 'function') {
+                                       this._options.callbackDropdownInit(this._list);
+                               }
+                       }
+                       else {
+                               // reset current list
+                               this._list.innerHTML = '';
+                       }
+                       
+                       if (typeof data.returnValues === 'object') {
+                               var callbackClick = this._clickSelectItem.bind(this), listItem;
+                               
+                               for (var key in data.returnValues) {
+                                       if (data.returnValues.hasOwnProperty(key)) {
+                                               listItem = this._createListItem(data.returnValues[key]);
+                                               
+                                               listItem.addEventListener(WCF_CLICK_EVENT, callbackClick);
+                                               this._list.appendChild(listItem);
+                                       }
+                               }
+                       }
+                       
+                       if (createdList) {
+                               DomUtil.insertAfter(this._list, this._element);
+                               UiSimpleDropdown.initFragment(this._element.parentNode, this._list);
+                               
+                               this._dropdownContainerId = DomUtil.identify(this._element.parentNode);
+                       }
+                       
+                       if (this._dropdownContainerId) {
+                               this._activeItem = null;
+                               
+                               if (!this._list.childElementCount && this._handleEmptyResult() === false) {
+                                       UiSimpleDropdown.close(this._dropdownContainerId);
+                               }
+                               else {
+                                       UiSimpleDropdown.open(this._dropdownContainerId);
+                                       
+                                       // mark first item as active
+                                       if (this._list.childElementCount && ~~elData(this._list.children[0], 'object-id')) {
+                                               this._activeItem = this._list.children[0];
+                                               this._activeItem.classList.add('active');
+                                       }
+                               }
+                       }
+               },
+               
+               /**
+                * Handles an empty result set, return a boolean false to hide the dropdown.
+                * 
+                * @return      {boolean}      false to close the dropdown
+                * @protected
+                */
+               _handleEmptyResult: function() {
+                       if (!this._options.noResultPlaceholder) {
+                               return false;
+                       }
+                       
+                       var listItem = elCreate('li');
+                       listItem.className = 'dropdownText';
+                       
+                       var span = elCreate('span');
+                       span.textContent = this._options.noResultPlaceholder;
+                       listItem.appendChild(span);
+                       
+                       this._list.appendChild(listItem);
+                       
+                       return true;
+               },
+               
+               /**
+                * Creates an list item from response data.
+                * 
+                * @param       {Object}        item    response data
+                * @return      {Element}       list item
+                * @protected
+                */
+               _createListItem: function(item) {
+                       var listItem = elCreate('li');
+                       elData(listItem, 'object-id', item.objectID);
+                       elData(listItem, 'label', item.label);
+                       
+                       var span = elCreate('span');
+                       span.textContent = item.label;
+                       listItem.appendChild(span);
+                       
+                       return listItem;
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: this._options.ajax
+                       };
+               }
+       };
+       
+       return UiSearchInput;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Search/Page.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Search/Page.js
new file mode 100644 (file)
index 0000000..522352a
--- /dev/null
@@ -0,0 +1,72 @@
+define(['Core', 'Dom/Util', 'Ui/SimpleDropdown', './Input'], function(Core, DomUtil, UiSimpleDropdown, UiSearchInput) {
+       "use strict";
+       
+       return {
+               init: function (objectType) {
+                       var searchInput = elById('pageHeaderSearchInput');
+                       
+                       new UiSearchInput(searchInput, {
+                               ajax: {
+                                       className: 'wcf\\data\\search\\keyword\\SearchKeywordAction'
+                               },
+                               callbackDropdownInit: function(dropdownMenu) {
+                                       dropdownMenu.classList.add('dropdownMenuPageSearch');
+                                       
+                                       elData(dropdownMenu, 'dropdown-alignment-horizontal', 'right');
+                                       
+                                       var minWidth = searchInput.clientWidth;
+                                       dropdownMenu.style.setProperty('min-width', minWidth + 'px', '');
+                                       
+                                       // calculate offset to ignore the width caused by the submit button
+                                       var parent = searchInput.parentNode;
+                                       var offsetRight = (DomUtil.offset(parent).left + parent.clientWidth) - (DomUtil.offset(searchInput).left + minWidth);
+                                       var offsetTop = DomUtil.styleAsInt(window.getComputedStyle(parent), 'padding-bottom');
+                                       dropdownMenu.style.setProperty('transform', 'translateX(-' + Math.ceil(offsetRight) + 'px) translateY(-' + offsetTop + 'px)', '');
+                               }
+                       });
+                       
+                       var dropdownMenu = UiSimpleDropdown.getDropdownMenu(DomUtil.identify(elBySel('.pageHeaderSearchType')));
+                       var callback = this._click.bind(this);
+                       elBySelAll('a[data-object-type]', dropdownMenu, function(link) {
+                               link.addEventListener(WCF_CLICK_EVENT, callback);
+                       });
+                       
+                       // trigger click on init
+                       var link = elBySel('a[data-object-type="' + objectType + '"]', dropdownMenu);
+                       Core.triggerEvent(link, WCF_CLICK_EVENT);
+               },
+               
+               _click: function(event) {
+                       event.preventDefault();
+                       
+                       var objectType = elData(event.currentTarget, 'object-type');
+                       
+                       var container = elById('pageHeaderSearchParameters');
+                       container.innerHTML = '';
+                       
+                       var parameters = elData(event.currentTarget, 'parameters');
+                       if (parameters) {
+                               parameters = JSON.parse(parameters);
+                       }
+                       else {
+                               parameters = {};
+                       }
+                       
+                       if (objectType) parameters['types[]'] = objectType;
+                       
+                       for (var key in parameters) {
+                               if (parameters.hasOwnProperty(key)) {
+                                       var input = elCreate('input');
+                                       input.type = 'hidden';
+                                       input.name = key;
+                                       input.value = parameters[key];
+                                       container.appendChild(input);
+                               }
+                       }
+                       
+                       // update label
+                       var button = elBySel('.pageHeaderSearchType > .button', elById('pageHeaderSearchInputContainer'));
+                       button.textContent = event.currentTarget.textContent;
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Suggestion.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Suggestion.js
new file mode 100644 (file)
index 0000000..5cfeb40
--- /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     WoltLabSuite/Core/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 = elById(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(WCF_CLICK_EVENT, 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: elData(item.children[0], 'object-id'), value: item.textContent });
+                       
+                       if (isEvent) {
+                               this._element.focus();
+                       }
+               },
+               
+               /**
+                * Performs a search for the input value unless it is below the threshold.
+                * 
+                * @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 = elCreate('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 = elCreate('a');
+                                       anchor.textContent = item.label;
+                                       elData(anchor, 'object-id', item.objectID);
+                                       anchor.addEventListener(WCF_CLICK_EVENT, this._select.bind(this));
+                                       
+                                       listItem = elCreate('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/WoltLabSuite/Core/Ui/TabMenu.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu.js
new file mode 100644 (file)
index 0000000..8340ef5
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * Common interface for tab menu access.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/TabMenu
+ */
+define(['Dictionary', 'Dom/ChangeListener', 'Dom/Util', 'Ui/CloseOverlay', './TabMenu/Simple'], function(Dictionary, DomChangeListener, DomUtil, UiCloseOverlay, SimpleTabMenu) {
+       "use strict";
+       
+       var _activeList = null;
+       var _tabMenus = new Dictionary();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/TabMenu
+        */
+       return {
+               /**
+                * Sets up tab menus and binds listeners.
+                */
+               setup: function() {
+                       this._init();
+                       this._selectErroneousTabs();
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Ui/TabMenu', this._init.bind(this));
+                       UiCloseOverlay.add('WoltLabSuite/Core/Ui/TabMenu', function() {
+                               if (_activeList) {
+                                       _activeList.classList.remove('active');
+                                       
+                                       _activeList = null;
+                               }
+                       });
+               },
+               
+               /**
+                * Initializes available tab menus.
+                */
+               _init: function() {
+                       var container, containerId, list, returnValue, tabMenu, tabMenus = elBySelAll('.tabMenuContainer:not(.staticTabMenuContainer)');
+                       for (var i = 0, length = tabMenus.length; i < length; i++) {
+                               container = tabMenus[i];
+                               containerId = DomUtil.identify(container);
+                               
+                               if (_tabMenus.has(containerId)) {
+                                       continue;
+                               }
+                               
+                               tabMenu = new SimpleTabMenu(container);
+                               if (tabMenu.validate()) {
+                                       returnValue = tabMenu.init();
+                                       
+                                       _tabMenus.set(containerId, tabMenu);
+                                       
+                                       if (returnValue instanceof Element) {
+                                               tabMenu = this.getTabMenu(returnValue.parentNode.id);
+                                               tabMenu.select(returnValue.id, null, true);
+                                       }
+                                       
+                                       list = elBySel('#' + containerId + ' > nav > ul');
+                                       (function(list) {
+                                               list.addEventListener(WCF_CLICK_EVENT, function(event) {
+                                                       event.preventDefault();
+                                                       event.stopPropagation();
+                                                       
+                                                       if (event.target === list) {
+                                                               list.classList.add('active');
+                                                               
+                                                               _activeList = list;
+                                                       }
+                                                       else {
+                                                               list.classList.remove('active');
+                                                               
+                                                               _activeList = null;
+                                                       }
+                                               });
+                                       })(list);
+                               }
+                       }
+               },
+               
+               /**
+                * 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 && elByClass('formError', container).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);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/TabMenu/Simple.js
new file mode 100644 (file)
index 0000000..3239fea
--- /dev/null
@@ -0,0 +1,358 @@
+/**
+ * Simple tab menu implementation with a straight-forward logic.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/TabMenu/Simple
+ */
+define(['Dictionary', 'EventHandler', 'Dom/Traverse', 'Dom/Util'], function(Dictionary, EventHandler, DomTraverse, DomUtil) {
+       "use strict";
+       
+       /**
+        * @param       {Element}       container       container element
+        * @constructor
+        */
+       function TabMenuSimple(container) {
+               this._container = container;
+               this._containers = new Dictionary();
+               this._isLegacy = null;
+               this._store = 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 = elByTag('li', nav);
+                       if (tabs.length === 0) {
+                               return false;
+                       }
+                       
+                       var container, containers = DomTraverse.childrenByTag(this._container, 'DIV'), name, i, length;
+                       for (i = 0, length = containers.length; i < length; i++) {
+                               container = containers[i];
+                               name = elData(container, 'name');
+                               
+                               if (!name) {
+                                       name = DomUtil.identify(container);
+                               }
+                               
+                               elData(container, 'name', name);
+                               this._containers.set(name, container);
+                       }
+                       
+                       var containerId = this._container.id, tab;
+                       for (i = 0, length = tabs.length; i < length; i++) {
+                               tab = tabs[i];
+                               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: '" + containerId + "') exists more than once.");
+                               }
+                               
+                               container = this._containers.get(name);
+                               if (container === undefined) {
+                                       throw new Error("Expected content element for li[data-name='" + name + "'] (tab menu id: '" + containerId + "').");
+                               }
+                               else if (container.parentNode !== this._container) {
+                                       throw new Error("Expected content element '" + name + "' (tab menu id: '" + 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: '" + containerId + "').");
+                               }
+                               
+                               this._tabs.set(name, tab);
+                       }
+                       
+                       if (!this._tabs.size) {
+                               throw new Error("Expected at least one tab (tab menu id: '" + containerId + "').");
+                       }
+                       
+                       if (this._isLegacy) {
+                               elData(this._container, 'is-legacy', true);
+                               
+                               this._tabs.forEach(function(tab, name) {
+                                       elAttr(tab, 'aria-controls', name);
+                               });
+                       }
+                       
+                       return true;
+               },
+               
+               /**
+                * Initializes this tab menu.
+                * 
+                * @param       {Dictionary=}   oldTabs         previous list of tabs
+                * @return      {?Element}      parent tab for selection or null
+                */
+               init: function(oldTabs) {
+                       oldTabs = oldTabs || null;
+                       
+                       // bind listeners
+                       this._tabs.forEach((function(tab) {
+                               if (!oldTabs || oldTabs.get(elData(tab, 'name')) !== tab) {
+                                       tab.children[0].addEventListener(WCF_CLICK_EVENT, this._onClick.bind(this));
+                               }
+                       }).bind(this));
+                       
+                       var returnValue = null;
+                       if (!oldTabs) {
+                               var hash = window.location.hash.replace(/^#/, ''), selectTab = null;
+                               if (hash !== '') {
+                                       selectTab = this._tabs.get(hash);
+                                       
+                                       // check for parent tab menu
+                                       if (selectTab && this._container.parentNode.classList.contains('tabMenuContainer')) {
+                                               returnValue = this._container;
+                                       }
+                               }
+                               
+                               if (!selectTab) {
+                                       var preselect = elData(this._container, 'preselect') || elData(this._container, 'active');
+                                       if (preselect === "true" || !preselect) preselect = true;
+                                       
+                                       if (preselect === true) {
+                                               this._tabs.forEach(function(tab) {
+                                                       if (!selectTab && !tab.previousElementSibling) {
+                                                               selectTab = tab;
+                                                       }
+                                               });
+                                       }
+                                       else if (preselect !== "false") {
+                                               selectTab = this._tabs.get(preselect);
+                                       }
+                               }
+                               
+                               if (selectTab) {
+                                       this._containers.forEach(function(container) {
+                                               container.classList.add('hidden');
+                                       });
+                                       
+                                       this.select(null, selectTab, true);
+                               }
+                               
+                               var store = elData(this._container, 'store');
+                               if (store) {
+                                       var input = elCreate('input');
+                                       input.type = 'hidden';
+                                       input.name = store;
+                                       
+                                       this._container.appendChild(input);
+                                       
+                                       this._store = input;
+                               }
+                       }
+                       
+                       return returnValue;
+               },
+               
+               /**
+                * Selects a tab.
+                * 
+                * @param       {?(string|int)}         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);
+                       
+                       if (!tab) {
+                               // 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) {
+                                       throw new Error("Expected a valid tab name, '" + name + "' given (tab menu id: '" + this._container.id + "').");
+                               }
+                       }
+                       
+                       name = name || elData(tab, 'name');
+                       
+                       // unmark active tab
+                       var oldTab = this.getActiveTab();
+                       var oldContent = null;
+                       if (oldTab) {
+                               if (elData(oldTab, 'name') === name) {
+                                       // same tab
+                                       return;
+                               }
+                               
+                               oldTab.classList.remove('active');
+                               oldContent = this._containers.get(elData(oldTab, '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');
+                       newContent.classList.remove('hidden');
+                       
+                       if (this._isLegacy) {
+                               tab.classList.add('ui-state-active');
+                               newContent.classList.add('ui-state-active');
+                       }
+                       
+                       if (this._store) {
+                               this._store.value = name;
+                       }
+                       
+                       if (!disableEvent) {
+                               EventHandler.fire('com.woltlab.wcf.simpleTabMenu_' + this._container.id, 'select', {
+                                       active: tab,
+                                       activeName: name,
+                                       previous: oldTab,
+                                       previousName: oldTab ? elData(oldTab, 'name') : null
+                               });
+                               
+                               var jQuery = (this._isLegacy && typeof window.jQuery === 'function') ? window.jQuery : null;
+                               if (jQuery) {
+                                       // simulate jQuery UI Tabs event
+                                       jQuery(this._container).trigger('wcftabsbeforeactivate', {
+                                               newTab: jQuery(tab),
+                                               oldTab: jQuery(oldTab),
+                                               newPanel: jQuery(newContent),
+                                               oldPanel: jQuery(oldContent)
+                                       });
+                               }
+                               
+                               // update history
+                               window.history.replaceState(
+                                       undefined,
+                                       undefined,
+                                       window.location.href.replace(/#[^#]+$/, '') + '#' + name
+                               );
+                       }
+               },
+               
+               /**
+                * 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 = new Dictionary();
+                       oldTabs.merge(this._tabs);
+                       
+                       this.validate();
+                       this.init(oldTabs);
+               },
+               
+               /**
+                * Handles clicks on a tab.
+                * 
+                * @param       {object}        event   event object
+                */
+               _onClick: function(event) {
+                       event.preventDefault();
+                       
+                       this.select(null, event.currentTarget.parentNode);
+               },
+               
+               /**
+                * Returns the tab name.
+                * 
+                * @param       {Element}       tab     tab element
+                * @return      {string}        tab name
+                */
+               _getTabName: function(tab) {
+                       var name = elData(tab, 'name');
+                       
+                       // handle legacy tab menus
+                       if (!name) {
+                               if (tab.childElementCount === 1 && tab.children[0].nodeName === 'A') {
+                                       if (tab.children[0].href.match(/#([^#]+)$/)) {
+                                               name = RegExp.$1;
+                                               
+                                               if (elById(name) === null) {
+                                                       name = null;
+                                               }
+                                               else {
+                                                       this._isLegacy = true;
+                                                       elData(tab, 'name', name);
+                                               }
+                                       }
+                               }
+                       }
+                       
+                       return name;
+               },
+               
+               /**
+                * Returns the currently active tab.
+                *
+                * @return      {Element}       active tab
+                */
+               getActiveTab: function() {
+                       return elBySel('#' + this._container.id + ' > nav > ul > li.active');
+               },
+               
+               /**
+                * 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/WoltLabSuite/Core/Ui/Toggle/Input.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Toggle/Input.js
new file mode 100644 (file)
index 0000000..c0a6975
--- /dev/null
@@ -0,0 +1,108 @@
+/**
+ * Provides a simple toggle to show or hide certain elements when the
+ * target element is checked.
+ * 
+ * Be aware that the list of elements to show or hide accepts selectors
+ * which will be passed to `elBySel()`, causing only the first matched
+ * element to be used. If you require a whole list of elements identified
+ * by a single selector to be handled, please provide the actual list of
+ * elements instead.
+ * 
+ * Usage:
+ * 
+ * new UiToggleInput('input[name="foo"][value="bar"]', {
+ *      show: ['#showThisContainer', '.makeThisVisibleToo'],
+ *      hide: ['.notRelevantStuff', elById('fooBar')]
+ * });
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Toggle/Input
+ */
+define(['Core'], function(Core) {
+       "use strict";
+       
+       /**
+        * @param       {string}        elementSelector         element selector used with `elBySel()`
+        * @param       {Object}        options                 toggle options
+        * @constructor
+        */
+       function UiToggleInput(elementSelector, options) { this.init(elementSelector, options); }
+       UiToggleInput.prototype = {
+               /**
+                * Initializes a new input toggle.
+                * 
+                * @param       {string}        elementSelector         element selector used with `elBySel()`
+                * @param       {Object}        options                 toggle options
+                */
+               init: function(elementSelector, options) {
+                       this._element = elBySel(elementSelector);
+                       if (this._element === null) {
+                               throw new Error("Unable to find element by selector '" + elementSelector + "'.");
+                       }
+                       
+                       var type = (this._element.nodeName === 'INPUT') ? elAttr(this._element, 'type') : '';
+                       if (type !== 'checkbox' && type !== 'radio') {
+                               throw new Error("Illegal element, expected input[type='checkbox'] or input[type='radio'].");
+                       }
+                       
+                       this._options = Core.extend({
+                               hide: [],
+                               show: []
+                       }, options);
+                       
+                       ['hide', 'show'].forEach((function(type) {
+                               var element, i, length;
+                               for (i = 0, length = this._options[type].length; i < length; i++) {
+                                       element = this._options[type][i];
+                                       
+                                       if (typeof element !== 'string' && !(element instanceof Element)) {
+                                               throw new TypeError("The array '" + type + "' may only contain string selectors or DOM elements.");
+                                       }
+                               }
+                       }).bind(this));
+                       
+                       this._element.addEventListener('change', this._change.bind(this));
+               },
+               
+               /**
+                * Triggered when element is checked / unchecked.
+                * 
+                * @param       {Event}         event   event object
+                * @protected
+                */
+               _change: function(event) {
+                       var showElements = event.currentTarget.checked;
+                       
+                       this._handleElements(this._options.show, showElements);
+                       this._handleElements(this._options.hide, !showElements);
+               },
+               
+               /**
+                * Loops through the target elements and shows / hides them.
+                * 
+                * @param       {Array}         elements        list of elements or selectors
+                * @param       {boolean}       showElement     true if elements should be shown
+                * @protected
+                */
+               _handleElements: function(elements, showElement) {
+                       var element, tmp;
+                       for (var i = 0, length = elements.length; i < length; i++) {
+                               element = elements[i];
+                               if (typeof element === 'string') {
+                                       tmp = elBySel(element);
+                                       if (tmp === null) {
+                                               throw new Error("Unable to find element by selector '" + element + "'.");
+                                       }
+                                       
+                                       elements[i] = element = tmp;
+                               }
+                               
+                               window[(showElement ? 'elShow' : 'elHide')](element);
+                       }
+               }
+       };
+       
+       return UiToggleInput;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Tooltip.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Tooltip.js
new file mode 100644 (file)
index 0000000..f3dbc58
--- /dev/null
@@ -0,0 +1,119 @@
+/**
+ * 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     WoltLabSuite/Core/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     WoltLabSuite/Core/Ui/Tooltip
+        */
+       return {
+               /**
+                * Initializes the tooltip element and binds event listener.
+                */
+               setup: function() {
+                       if (Environment.platform() !== 'desktop') return;
+                       
+                       _tooltip = elCreate('div');
+                       elAttr(_tooltip, 'id', 'balloonTooltip');
+                       _tooltip.classList.add('balloonTooltip');
+                       
+                       _text = elCreate('span');
+                       elAttr(_text, 'id', 'balloonTooltipText');
+                       _tooltip.appendChild(_text);
+                       
+                       _pointer = elCreate('span');
+                       _pointer.classList.add('elementPointer');
+                       _pointer.appendChild(elCreate('span'));
+                       _tooltip.appendChild(_pointer);
+                       
+                       document.body.appendChild(_tooltip);
+                       
+                       _elements = elByClass('jsTooltip');
+                       
+                       this.init();
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Ui/Tooltip', this.init.bind(this));
+                       window.addEventListener('scroll', this._mouseLeave.bind(this));
+               },
+               
+               /**
+                * Initializes tooltip elements.
+                */
+               init: function() {
+                       var element, title;
+                       while (_elements.length) {
+                               element = _elements[0];
+                               element.classList.remove('jsTooltip');
+                               
+                               title = elAttr(element, 'title').trim();
+                               if (title.length) {
+                                       elData(element, 'tooltip', title);
+                                       element.removeAttribute('title');
+                                       
+                                       element.addEventListener('mouseenter', this._mouseEnter.bind(this));
+                                       element.addEventListener('mouseleave', this._mouseLeave.bind(this));
+                                       element.addEventListener(WCF_CLICK_EVENT, this._mouseLeave.bind(this));
+                               }
+                       }
+               },
+               
+               /**
+                * Displays the tooltip on mouse enter.
+                * 
+                * @param       {Event}         event   event object
+                */
+               _mouseEnter: function(event) {
+                       var element = event.currentTarget;
+                       var title = elAttr(element, 'title');
+                       title = (typeof title === 'string') ? title.trim() : '';
+                       
+                       if (title !== '') {
+                               elData(element, 'tooltip', title);
+                               element.removeAttribute('title');
+                       }
+                       
+                       title = elData(element, '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',
+                               verticalOffset: 4,
+                               pointer: true,
+                               pointerClassNames: ['inverse'],
+                               vertical: 'top'
+                       });
+               },
+               
+               /**
+                * Hides the tooltip once the mouse leaves the element.
+                */
+               _mouseLeave: function() {
+                       _tooltip.classList.remove('active');
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Editor.js
new file mode 100644 (file)
index 0000000..2f1e576
--- /dev/null
@@ -0,0 +1,228 @@
+/**
+ * Simple notification overlay.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/User/Editor
+ */
+define(['Ajax', 'Language', 'StringUtil', 'Dom/Util', 'Ui/Dialog', 'Ui/Notification'], function(Ajax, Language, StringUtil, DomUtil, UiDialog, UiNotification) {
+       "use strict";
+       
+       var _actionName = '';
+       var _userHeader = null;
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/User/Editor
+        */
+       return {
+               /**
+                * Initializes the user editor.
+                */
+               init: function() {
+                       _userHeader = elBySel('.userProfileUser');
+                       
+                       // init buttons
+                       ['ban', 'disableAvatar', 'disableSignature', 'enable'].forEach((function(action) {
+                               var button = elBySel('.userProfileButtonMenu .jsButtonUser' + StringUtil.ucfirst(action));
+                               
+                               // button is missing if users lacks the permission
+                               if (button) {
+                                       elData(button, 'action', action);
+                                       button.addEventListener(WCF_CLICK_EVENT, this._click.bind(this));
+                               }
+                       }).bind(this));
+               },
+               
+               /**
+                * Handles clicks on action buttons.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _click: function(event) {
+                       event.preventDefault();
+                       
+                       //noinspection JSCheckFunctionSignatures
+                       var action = elData(event.currentTarget, 'action');
+                       var actionName = '';
+                       switch (action) {
+                               case 'ban':
+                                       if (elDataBool(_userHeader, 'banned')) {
+                                               actionName = 'unban';
+                                       }
+                                       break;
+                               
+                               case 'disableAvatar':
+                                       if (elDataBool(_userHeader, 'disable-avatar')) {
+                                               actionName = 'enableAvatar';
+                                       }
+                                       break;
+                               
+                               case 'disableSignature':
+                                       if (elDataBool(_userHeader, 'disable-signature')) {
+                                               actionName = 'enableSignature';
+                                       }
+                                       break;
+                               
+                               case 'enable':
+                                       actionName = (elDataBool(_userHeader, 'is-disabled')) ? 'enable' : 'disable';
+                                       break;
+                       }
+                       
+                       if (actionName === '') {
+                               _actionName = action;
+                               
+                               UiDialog.open(this);
+                       }
+                       else {
+                               Ajax.api(this, {
+                                       actionName: actionName
+                               });
+                       }
+               },
+               
+               /**
+                * Handles form submit and input validation.
+                * 
+                * @param       {Event}         event           event object
+                * @protected
+                */
+               _submit: function(event) {
+                       event.preventDefault();
+                       
+                       var label = elById('wcfUiUserEditorExpiresLabel');
+                       var innerError = label.previousElementSibling;
+                       if (innerError.classList.contains('innerError')) elRemove(innerError);
+                       
+                       var expires = '';
+                       if (!elById('wcfUiUserEditorNeverExpires').checked) {
+                               expires = elById('wcfUiUserEditorExpiresDatePicker').value;
+                               if (expires === '') {
+                                       innerError = elCreate('small');
+                                       innerError.className = 'innerError';
+                                       innerError.textContent = Language.get('wcf.global.form.error.empty');
+                                       label.parentNode.insertBefore(innerError, label);
+                               }
+                       }
+                       
+                       var parameters = {};
+                       parameters[_actionName + 'Expires'] = expires;
+                       parameters[_actionName + 'Reason'] = elById('wcfUiUserEditorReason').value.trim();
+                       
+                       Ajax.api(this, {
+                               actionName: _actionName,
+                               parameters: parameters
+                       });
+               },
+               
+               _ajaxSuccess: function(data) {
+                       switch (data.actionName) {
+                               case 'ban':
+                               case 'unban':
+                                       elData(_userHeader, 'banned', (data.actionName === 'ban'));
+                                       elBySel('.userProfileButtonMenu .jsButtonUserBan').textContent = Language.get('wcf.user.' + (data.actionName === 'ban' ? 'unban' : 'ban'));
+                                       
+                                       var contentTitle = elBySel('.contentTitle', _userHeader);
+                                       var banIcon = elBySel('.jsUserBanned', contentTitle);
+                                       if (data.actionName === 'ban') {
+                                               banIcon = elCreate('span');
+                                               banIcon.className = 'icon icon16 fa-lock jsUserBanned jsTooltip';
+                                               banIcon.title = Language.get('wcf.user.banned');
+                                               contentTitle.appendChild(banIcon);
+                                       }
+                                       else if (banIcon) {
+                                               elRemove(banIcon);
+                                       }
+                                       
+                                       break;
+                               
+                               case 'disableAvatar':
+                               case 'enableAvatar':
+                                       elData(_userHeader, 'disable-avatar', (data.actionName === 'disableAvatar'));
+                                       elBySel('.userProfileButtonMenu .jsButtonUserDisableAvatar').textContent = Language.get('wcf.user.' + (data.actionName === 'disableAvatar' ? 'enable' : 'disable') + 'Avatar');
+                                       
+                                       break;
+                               
+                               case 'disableSignature':
+                               case 'enableSignature':
+                                       elData(_userHeader, 'disable-signature', (data.actionName === 'disableSignature'));
+                                       elBySel('.userProfileButtonMenu .jsButtonUserDisableSignature').textContent = Language.get('wcf.user.' + (data.actionName === 'disableSignature' ? 'enable' : 'disable') + 'Signature');
+                                       
+                                       break;
+                               
+                               case 'enable':
+                               case 'disable':
+                                       elData(_userHeader, 'is-disabled', (data.actionName === 'disable'));
+                                       elBySel('.userProfileButtonMenu .jsButtonUserEnable').textContent = Language.get('wcf.acp.user.' + (data.actionName === 'enable' ? 'disable' : 'enable'));
+                                       
+                                       break;
+                       }
+                       
+                       if (data.actionName === 'ban' || data.actionName === 'disableAvatar' || data.actionName === 'disableSignature') {
+                               UiDialog.close(this);
+                       }
+                       
+                       UiNotification.show();
+               },
+               
+               _ajaxSetup: function () {
+                       return {
+                               data: {
+                                       className: 'wcf\\data\\user\\UserAction',
+                                       objectIDs: [ elData(_userHeader, 'object-id') ]
+                               }
+                       };
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: 'wcfUiUserEditor',
+                               options: {
+                                       onSetup: (function (content) {
+                                               elById('wcfUiUserEditorNeverExpires').addEventListener('change', function () {
+                                                       window[(this.checked) ? 'elHide' : 'elShow'](elById('wcfUiUserEditorExpiresSettings'));
+                                               });
+                                               
+                                               elBySel('button.buttonPrimary', content).addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
+                                       }).bind(this),
+                                       onShow: function(content) {
+                                               UiDialog.setTitle('wcfUiUserEditor', Language.get('wcf.user.' + _actionName + '.confirmMessage'));
+                                               
+                                               var label = elById('wcfUiUserEditorReason').nextElementSibling;
+                                               var phrase = 'wcf.user.' + _actionName + '.reason.description';
+                                               label.textContent = Language.get(phrase);
+                                               window[(label.textContent === phrase) ? 'elHide' : 'elShow'](label);
+                                               
+                                               label = elById('wcfUiUserEditorNeverExpires').nextElementSibling;
+                                               label.textContent = Language.get('wcf.user.' + _actionName + '.neverExpires');
+                                               
+                                               label = elBySel('label[for="wcfUiUserEditorExpires"]', content);
+                                               label.textContent = Language.get('wcf.user.' + _actionName + '.expires');
+                                               
+                                               label = elById('wcfUiUserEditorExpiresLabel');
+                                               label.textContent = Language.get('wcf.user.' + _actionName + '.expires.description');
+                                       }
+                               },
+                               source: '<div class="section">'
+                                               + '<dl>'
+                                                       + '<dt><label for="wcfUiUserEditorReason">' + Language.get('wcf.global.reason') + '</label></dt>'
+                                                       + '<dd><textarea id="wcfUiUserEditorReason" cols="40" rows="3"></textarea><small></small></dd>'
+                                               + '</dl>'
+                                               + '<dl>'
+                                                       + '<dt></dt>'
+                                                       + '<dd><label><input type="checkbox" id="wcfUiUserEditorNeverExpires" checked> <span></span></label></dd>'
+                                               + '</dl>'
+                                               + '<dl id="wcfUiUserEditorExpiresSettings" style="display: none">'
+                                                       + '<dt><label for="wcfUiUserEditorExpires"></label></dt>'
+                                                       + '<dd>'
+                                                               + '<input type="date" name="wcfUiUserEditorExpires" id="wcfUiUserEditorExpires" class="medium" min="' + new Date(TIME_NOW * 1000).toISOString() + '" data-ignore-timezone="true">'
+                                                               + '<small id="wcfUiUserEditorExpiresLabel"></small>'
+                                                       + '</dd>'
+                                               +'</dl>'
+                                       + '</div>'
+                                       + '<div class="formSubmit"><button class="buttonPrimary">' + Language.get('wcf.global.button.submit') + '</button></div>'
+                       };
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Ignore.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Ignore.js
new file mode 100644 (file)
index 0000000..7a9c842
--- /dev/null
@@ -0,0 +1,65 @@
+/**
+ * Provides global helper methods to interact with ignored content.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/User/Ignore
+ */
+define(['List', 'Dom/ChangeListener'], function(List, DomChangeListener) {
+       "use strict";
+       
+       var _availableMessages = elByClass('ignoredUserMessage');
+       var _callback = null;
+       var _knownMessages = new List();
+       
+       /**
+        * @exports     WoltLabSuite/Core/Ui/User/Ignore
+        */
+       return {
+               /**
+                * Initializes the click handler for each ignored message and listens for
+                * newly inserted messages.
+                */
+               init: function () {
+                       _callback = this._removeClass.bind(this);
+                       
+                       this._rebuild();
+                       
+                       DomChangeListener.add('WoltLabSuite/Core/Ui/User/Ignore', this._rebuild.bind(this));
+               },
+               
+               /**
+                * Adds ignored messages to the collection.
+                * 
+                * @protected
+                */
+               _rebuild: function() {
+                       var message;
+                       for (var i = 0, length = _availableMessages.length; i < length; i++) {
+                               message = _availableMessages[i];
+                               
+                               if (!_knownMessages.has(message)) {
+                                       message.addEventListener(WCF_CLICK_EVENT, _callback);
+                                       
+                                       _knownMessages.add(message);
+                               }
+                       }
+               },
+               
+               /**
+                * Reveals a message on click/tap and disables the listener.
+                * 
+                * @param       {Event}         event   event object
+                * @protected
+                */
+               _removeClass: function(event) {
+                       event.preventDefault();
+                       
+                       var message = event.currentTarget;
+                       message.classList.remove('ignoredUserMessage');
+                       message.removeEventListener(WCF_CLICK_EVENT, _callback);
+                       _knownMessages.delete(message);
+               }
+       };
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/List.js
new file mode 100644 (file)
index 0000000..5e15d01
--- /dev/null
@@ -0,0 +1,111 @@
+/**
+ * Object-based user list.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/User/List
+ */
+define(['Ajax', 'Core', 'Dictionary', 'Dom/Util', 'Ui/Dialog', 'WoltLabSuite/Core/Ui/Pagination'], function(Ajax, Core, Dictionary, DomUtil, UiDialog, UiPagination) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiUserList(options) { this.init(options); }
+       UiUserList.prototype = {
+               /**
+                * Initializes the user list.
+                * 
+                * @param       {object}        options         list of initialization options
+                */
+               init: function(options) {
+                       this._cache = new Dictionary();
+                       this._pageCount = 0;
+                       this._pageNo = 1;
+                       
+                       this._options = Core.extend({
+                               className: '',
+                               dialogTitle: '',
+                               parameters: {}
+                       }, options);
+               },
+               
+               /**
+                * Opens the user list.
+                */
+               open: function() {
+                       this._pageNo = 1;
+                       this._showPage();
+               },
+               
+               /**
+                * Shows the current or given page.
+                * 
+                * @param       {int=}          pageNo          page number
+                */
+               _showPage: function(pageNo) {
+                       if (typeof pageNo === 'number') {
+                               this._pageNo = ~~pageNo;
+                       }
+                       
+                       if (this._pageCount !== 0 && (this._pageNo < 1 || this._pageNo > this._pageCount)) {
+                               throw new RangeError("pageNo must be between 1 and " + this._pageCount + " (" + this._pageNo + " given).");
+                       }
+                       
+                       if (this._cache.has(this._pageNo)) {
+                               var dialog = UiDialog.open(this, this._cache.get(this._pageNo));
+                               
+                               if (this._pageCount > 1) {
+                                       var element = elBySel('.jsPagination', dialog.content);
+                                       if (element !== null) {
+                                               new UiPagination(element, {
+                                                       activePage: this._pageNo,
+                                                       maxPage: this._pageCount,
+                                                       
+                                                       callbackSwitch: this._showPage.bind(this)
+                                               });
+                                       }
+                               }
+                       }
+                       else {
+                               this._options.parameters.pageNo = this._pageNo;
+                               
+                               Ajax.api(this, {
+                                       parameters: this._options.parameters
+                               });
+                       }
+               },
+               
+               _ajaxSuccess: function(data) {
+                       if (data.returnValues.pageCount !== undefined) {
+                               this._pageCount = ~~data.returnValues.pageCount;
+                       }
+                       
+                       this._cache.set(this._pageNo, data.returnValues.template);
+                       this._showPage();
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'getGroupedUserList',
+                                       className: this._options.className,
+                                       interfaceName: 'wcf\\data\\IGroupedUserListAction'
+                               }
+                       };
+               },
+               
+               _dialogSetup: function() {
+                       return {
+                               id: DomUtil.getUniqueId(),
+                               options: {
+                                       title: this._options.dialogTitle
+                               },
+                               source: null
+                       };
+               }
+       };
+       
+       return UiUserList;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Abstract.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Abstract.js
new file mode 100644 (file)
index 0000000..9f416f8
--- /dev/null
@@ -0,0 +1,129 @@
+/**
+ * Default implementation for user interaction menu items used in the user profile.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Abstract
+ */
+define(['Ajax', 'Dom/Util'], function(Ajax, DomUtil) {
+       "use strict";
+       
+       /**
+        * Creates a new user profile menu item.
+        * 
+        * @param       {int}           userId          user id
+        * @param       {boolean}       isActive        true if item is initially active
+        * @constructor
+        */
+       function UiUserProfileMenuItemAbstract(userId, isActive) {}
+       UiUserProfileMenuItemAbstract.prototype = {
+               /**
+                * Creates a new user profile menu item.
+                * 
+                * @param       {int}           userId          user id
+                * @param       {boolean}       isActive        true if item is initially active
+                */
+               init: function(userId, isActive) {
+                       this._userId = userId;
+                       this._isActive = (isActive !== false);
+                       
+                       this._initButton();
+                       this._updateButton();
+               },
+               
+               /**
+                * Initializes the menu item.
+                * 
+                * @protected
+                */
+               _initButton: function() {
+                       var button = elCreate('a');
+                       button.href = '#';
+                       button.addEventListener(WCF_CLICK_EVENT, this._toggle.bind(this));
+                       
+                       var listItem = elCreate('li');
+                       listItem.appendChild(button);
+                       
+                       var menu = elBySel('.userProfileButtonMenu[data-menu="interaction"]');
+                       DomUtil.prepend(listItem, menu);
+                       
+                       this._button = button;
+                       this._listItem = listItem;
+               },
+               
+               /**
+                * Handles clicks on the menu item button.
+                * 
+                * @param       {Event}         event   event object
+                * @protected
+                */
+               _toggle: function(event) {
+                       event.preventDefault();
+                       
+                       Ajax.api(this, {
+                               actionName: this._getAjaxActionName(),
+                               parameters: {
+                                       data: {
+                                               userID: this._userId
+                                       }
+                               }
+                       });
+               },
+               
+               /**
+                * Updates the button state and label.
+                * 
+                * @protected
+                */
+               _updateButton: function() {
+                       this._button.textContent = this._getLabel();
+                       this._listItem.classList[(this._isActive ? 'add' : 'remove')]('active');
+               },
+               
+               /**
+                * Returns the button label.
+                * 
+                * @return      {string}        button label
+                * @protected
+                * @abstract
+                */
+               _getLabel: function() {
+                       throw new Error("Implement me!");
+               },
+               
+               /**
+                * Returns the Ajax action name.
+                * 
+                * @return      {string}        ajax action name
+                * @protected
+                * @abstract
+                */
+               _getAjaxActionName: function() {
+                       throw new Error("Implement me!");
+               },
+               
+               /**
+                * Handles successful Ajax requests.
+                * 
+                * @protected
+                * @abstract
+                */
+               _ajaxSuccess: function() {
+                       throw new Error("Implement me!");
+               },
+               
+               /**
+                * Returns the default Ajax request data
+                * 
+                * @return      {Object}        ajax request data
+                * @protected
+                * @abstract
+                */
+               _ajaxSetup: function() {
+                       throw new Error("Implement me!");
+               }
+       };
+       
+       return UiUserProfileMenuItemAbstract;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Follow.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Follow.js
new file mode 100644 (file)
index 0000000..67fa968
--- /dev/null
@@ -0,0 +1,31 @@
+define(['Core', 'Language', 'Ui/Notification', './Abstract'], function(Core, Language, UiNotification, UiUserProfileMenuItemAbstract) {
+       "use strict";
+       
+       function UiUserProfileMenuItemFollow(userId, isActive) { this.init(userId, isActive); }
+       Core.inherit(UiUserProfileMenuItemFollow, UiUserProfileMenuItemAbstract, {
+               _getLabel: function() {
+                       return Language.get('wcf.user.button.' + (this._isActive ? 'un' : '') + 'follow');
+               },
+               
+               _getAjaxActionName: function() {
+                       return this._isActive ? 'unfollow' : 'follow';
+               },
+               
+               _ajaxSuccess: function(data) {
+                       this._isActive = (data.returnValues.following ? true : false);
+                       this._updateButton();
+                       
+                       UiNotification.show();
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       className: 'wcf\\data\\user\\follow\\UserFollowAction'
+                               }
+                       };
+               }
+       });
+       
+       return UiUserProfileMenuItemFollow;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Ignore.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Profile/Menu/Item/Ignore.js
new file mode 100644 (file)
index 0000000..92e5a13
--- /dev/null
@@ -0,0 +1,31 @@
+define(['Core', 'Language', 'Ui/Notification', './Abstract'], function(Core, Language, UiNotification, UiUserProfileMenuItemAbstract) {
+       "use strict";
+       
+       function UiUserProfileMenuItemIgnore(userId, isActive) { this.init(userId, isActive); }
+       Core.inherit(UiUserProfileMenuItemIgnore, UiUserProfileMenuItemAbstract, {
+               _getLabel: function() {
+                       return Language.get('wcf.user.button.' + (this._isActive ? 'un' : '') + 'ignore');
+               },
+               
+               _getAjaxActionName: function() {
+                       return this._isActive ? 'unignore' : 'ignore';
+               },
+               
+               _ajaxSuccess: function(data) {
+                       this._isActive = (data.returnValues.isIgnoredUser ? true : false);
+                       this._updateButton();
+                       
+                       UiNotification.show();
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       className: 'wcf\\data\\user\\ignore\\UserIgnoreAction'
+                               }
+                       };
+               }
+       });
+       
+       return UiUserProfileMenuItemIgnore;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Search/Input.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/Search/Input.js
new file mode 100644 (file)
index 0000000..fd383fa
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Provides suggestions for users, optionally supporting groups.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/User/Search/Input
+ * @see         module:WoltLabSuite/Core/Ui/Search/Input
+ */
+define(['Core', 'WoltLabSuite/Core/Ui/Search/Input'], function(Core, UiSearchInput) {
+       "use strict";
+       
+       /**
+        * @param       {Element}       element         input element
+        * @param       {Object=}       options         search options and settings
+        * @constructor
+        */
+       function UiUserSearchInput(element, options) { this.init(element, options); }
+       Core.inherit(UiUserSearchInput, UiSearchInput, {
+               init: function(element, options) {
+                       var includeUserGroups = (Core.isPlainObject(options) && options.includeUserGroups === true);
+                       
+                       options = Core.extend({
+                               ajax: {
+                                       className: 'wcf\\data\\user\\UserAction',
+                                       parameters: {
+                                               data: {
+                                                       includeUserGroups: (includeUserGroups ? 1 : 0)
+                                               }
+                                       }
+                               }
+                       }, options);
+                       
+                       UiUserSearchInput._super.prototype.init.call(this, element, options);
+               },
+               
+               _createListItem: function(item) {
+                       var listItem = UiUserSearchInput._super.prototype._createListItem.call(this, item);
+                       elData(listItem, 'type', item.type);
+                       
+                       var box = elCreate('div');
+                       box.className = 'box16';
+                       box.innerHTML = (item.type === 'group') ? '<span class="icon icon16 fa-users"></span>' : item.icon;
+                       box.appendChild(listItem.children[0]);
+                       listItem.appendChild(box);
+                       
+                       return listItem;
+               }
+       });
+       
+       return UiUserSearchInput;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Upload.js
new file mode 100644 (file)
index 0000000..958c83f
--- /dev/null
@@ -0,0 +1,341 @@
+/**
+ * Uploads file via AJAX.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Upload
+ */
+define(['AjaxRequest', 'Core', 'Dom/ChangeListener', 'Language', 'Dom/Util', 'Dom/Traverse'], function(AjaxRequest, Core, DomChangeListener, Language, DomUtil, DomTraverse) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function Upload(buttonContainerId, targetId, options) {
+               options = options || {};
+               
+               if (options.className === undefined) {
+                       throw new Error("Missing class name.");
+               }
+               
+               // set default options
+               this._options = Core.extend({
+                       // name of the PHP action
+                       action: 'upload',
+                       // is true if multiple files can be uploaded at once
+                       multiple: false,
+                       // name if the upload field
+                       name: '__files[]',
+                       // is true if every file from a multi-file selection is uploaded in its own request
+                       singleFileRequests: false,
+                       // url for uploading file
+                       url: 'index.php/AJAXUpload/?t=' + SECURITY_TOKEN
+               }, options);
+               
+               this._options.url = WCF.convertLegacyURL(this._options.url);
+               
+               this._buttonContainer = elById(buttonContainerId);
+               if (this._buttonContainer === null) {
+                       throw new Error("Element id '" + buttonContainerId + "' is unknown.");
+               }
+               
+               this._target = elById(targetId);
+               if (targetId === null) {
+                       throw new Error("Element id '" + targetId + "' is unknown.");
+               }
+               if (options.multiple && this._target.nodeName !== 'UL' && this._target.nodeName !== 'OL') {
+                       throw new Error("Target element has to be list when allowing upload of multiple files.");
+               }
+               
+               this._fileElements = [];
+               this._internalFileId = 0;
+               
+               this._createButton();
+       }
+       Upload.prototype = {
+               /**
+                * Creates the upload button.
+                */
+               _createButton: function() {
+                       this._fileUpload = elCreate('input');
+                       elAttr(this._fileUpload, 'type', 'file');
+                       elAttr(this._fileUpload, 'name', this._options.name);
+                       if (this._options.multiple) {
+                               elAttr(this._fileUpload, 'multiple', 'true');
+                       }
+                       this._fileUpload.addEventListener('change', this._upload.bind(this));
+                       
+                       this._button = elCreate('p');
+                       this._button.classList.add('button');
+                       this._button.classList.add('uploadButton');
+                       
+                       var span = elCreate('span');
+                       span.textContent = Language.get('wcf.global.button.upload');
+                       this._button.appendChild(span);
+                       
+                       DomUtil.prepend(this._fileUpload, this._button);
+                       
+                       this._insertButton();
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * Creates the document element for an uploaded file.
+                * 
+                * @param       {File}          file            uploaded file
+                */
+               _createFileElement: function(file) {
+                       var progress = elCreate('progress');
+                       elAttr(progress, 'max', 100);
+                       
+                       if (this._target.nodeName === 'OL' || this._target.nodeName === 'UL') {
+                               var li = elCreate('li');
+                               li.innerText = file.name;
+                               li.appendChild(progress);
+                               
+                               this._target.appendChild(li);
+                               
+                               return li;
+                       }
+                       else {
+                               var p = elCreate('p');
+                               p.appendChild(progress);
+                               
+                               this._target.appendChild(p);
+                               
+                               return p;
+                       }
+               },
+               
+               /**
+                * Creates the document elements for uploaded files.
+                * 
+                * @param       {(FileList|Array.<File>)}       files           uploaded files
+                */
+               _createFileElements: function(files) {
+                       if (files.length) {
+                               var uploadId = this._fileElements.length;
+                               this._fileElements[uploadId] = [];
+                               
+                               for (var i = 0, length = files.length; i < length; i++) {
+                                       var file = files[i];
+                                       var fileElement = this._createFileElement(file);
+                                       
+                                       if (!fileElement.classList.contains('uploadFailed')) {
+                                               elData(fileElement, 'filename', file.name);
+                                               elData(fileElement, 'internal-file-id', this._internalFileId++);
+                                               this._fileElements[uploadId][i] = fileElement;
+                                       }
+                               }
+                               
+                               DomChangeListener.trigger();
+                               
+                               return uploadId;
+                       }
+                       
+                       return null;
+               },
+               
+               /**
+                * Handles a failed file upload.
+                * 
+                * @param       {int}                   uploadId        identifier of a file upload
+                * @param       {object<string, *>}     data            response data
+                * @param       {string}                responseText    response
+                * @param       {XMLHttpRequest}        xhr             request object
+                * @param       {object<string, *>}     requestOptions  options used to send AJAX request
+                * @return      {boolean}       true if the error message should be shown
+                */
+               _failure: function(uploadId, data, responseText, xhr, requestOptions) {
+                       // does nothing
+                       return true;
+               },
+               
+               /**
+                * Return additional parameters for upload requests.
+                * 
+                * @return      {object<string, *>}     additional parameters
+                */
+               _getParameters: function() {
+                       return {};
+               },
+               
+               /**
+                * Inserts the created button to upload files into the button container.
+                */
+               _insertButton: function() {
+                       DomUtil.prepend(this._button, this._buttonContainer);
+               },
+               
+               /**
+                * Updates the progress of an upload.
+                * 
+                * @param       {int}                           uploadId        internal upload identifier
+                * @param       {XMLHttpRequestProgressEvent}   event           progress event object
+                */
+               _progress: function(uploadId, event) {
+                       var percentComplete = Math.round(event.loaded / event.total * 100);
+                       
+                       for (var i in this._fileElements[uploadId]) {
+                               var progress = elByTag('PROGRESS', this._fileElements[uploadId][i]);
+                               if (progress.length === 1) {
+                                       elAttr(progress[0], 'value', percentComplete);
+                               }
+                       }
+               },
+               
+               /**
+                * Removes the button to upload files.
+                */
+               _removeButton: function() {
+                       elRemove(this._button);
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * Handles a successful file upload.
+                * 
+                * @param       {int}                   uploadId        identifier of a file upload
+                * @param       {object<string, *>}     data            response data
+                * @param       {string}                responseText    response
+                * @param       {XMLHttpRequest}        xhr             request object
+                * @param       {object<string, *>}     requestOptions  options used to send AJAX request
+                */
+               _success: function(uploadId, data, responseText, xhr, requestOptions) {
+                       // does nothing
+               },
+               
+               /**
+                * File input change callback to upload files.
+                * 
+                * @param       {Event}         event           input change event object
+                * @param       {File}          file            uploaded file
+                * @param       {Blob}          blob            file blob
+                * @return      {(int|Array.<int>|null)}        identifier(s) for the uploaded files
+                */
+               _upload: function(event, file, blob) {
+                       // remove failed upload elements first
+                       var failedUploads = DomTraverse.childrenByClass(this._target, 'uploadFailed');
+                       for (var i = 0, length = failedUploads.length; i < length; i++) {
+                               elRemove(failedUploads[i]);
+                       }
+                       
+                       var uploadId = null;
+                       
+                       var files = [];
+                       if (file) {
+                               files.push(file);
+                       }
+                       else if (blob) {
+                               var fileExtension = '';
+                               switch (blob.type) {
+                                       case 'image/jpeg':
+                                               fileExtension = '.jpg';
+                                       break;
+                                       
+                                       case 'image/gif':
+                                               fileExtension = '.gif';
+                                       break;
+                                       
+                                       case 'image/png':
+                                               fileExtension = '.png';
+                                       break;
+                               }
+                               
+                               files.push({
+                                       name: 'pasted-from-clipboard' + fileExtension
+                               });
+                       }
+                       else {
+                               files = this._fileUpload.files;
+                       }
+                       
+                       if (files.length) {
+                               if (this._options.singleFileRequests) {
+                                       uploadId = [];
+                                       for (var i = 0, length = files.length; i < length; i++) {
+                                               uploadId.push(this._uploadFiles([ files[i] ], blob));
+                                       }
+                               }
+                               else {
+                                       uploadId = this._uploadFiles(files, blob);
+                               }
+                       }
+                       
+                       // re-create upload button to effectively reset the 'files'
+                       // property of the input element
+                       this._removeButton();
+                       this._createButton();
+                       
+                       return uploadId;
+               },
+               
+               /**
+                * Sends the request to upload files.
+                * 
+                * @param       {(FileList|Array.<File>)}       files           uploaded files
+                * @param       {Blob}                          blob            file blob
+                * @return      {(int|null)}    identifier for the uploaded files
+                */
+               _uploadFiles: function(files, blob) {
+                       var uploadId = this._createFileElements(files);
+                       
+                       // no more files left, abort
+                       if (!this._fileElements[uploadId].length) {
+                               return null;
+                       }
+                       
+                       var formData = new FormData();
+                       for (var i = 0, length = files.length; i < length; i++) {
+                               if (this._fileElements[uploadId][i]) {
+                                       var internalFileId = elData(this._fileElements[uploadId][i], 'internal-file-id');
+                                       
+                                       if (blob) {
+                                               formData.append('__files[' + internalFileId + ']', blob, files[i].name);
+                                       }
+                                       else {
+                                               formData.append('__files[' + internalFileId + ']', files[i]);
+                                       }
+                               }
+                       }
+                       
+                       formData.append('actionName', this._options.action);
+                       formData.append('className', this._options.className);
+                       formData.append('interfaceName', 'wcf\\data\\IUploadAction');
+                       
+                       // recursively append additional parameters to form data
+                       var appendFormData = function(parameters, prefix) {
+                               prefix = prefix || '';
+                               
+                               for (var name in parameters) {
+                                       if (typeof parameters[name] === 'object') {
+                                               appendFormData(parameters[name], prefix + '[' + name + ']');
+                                       }
+                                       else {
+                                               formData.append('parameters' + prefix + '[' + name + ']', parameters[name]);
+                                       }
+                               }
+                       };
+                       
+                       appendFormData(this._getParameters());
+                       
+                       var request = new AjaxRequest({
+                               data: formData,
+                               contentType: false,
+                               failure: this._failure.bind(this, uploadId),
+                               silent: true,
+                               success: this._success.bind(this, uploadId),
+                               uploadProgress: this._progress.bind(this, uploadId),
+                               url: this._options.url
+                       });
+                       request.sendRequest();
+                       
+                       return uploadId;
+               }
+       };
+       
+       return Upload;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/User.js b/wcfsetup/install/files/js/WoltLabSuite/Core/User.js
new file mode 100644 (file)
index 0000000..b5ca164
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Provides data of the active user.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2016 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/User
+ */
+define([], function() {
+       "use strict";
+       
+       var _didInit = false;
+       
+       /**
+        * @exports     WoltLabSuite/Core/User
+        */
+       return {
+               /**
+                * Initializes the user object.
+                * 
+                * @param       {int}           userId          id of the user, `0` for guests
+                * @param       {string}        username        name of the user, empty for guests
+                */
+               init: function(userId, username) {
+                       if (_didInit) {
+                               throw new Error('User has already been initialized.');
+                       }
+                       
+                       // define non-writeable properties for userId and username
+                       Object.defineProperty(this, 'userId', {
+                               value: userId,
+                               writable: false
+                       });
+                       Object.defineProperty(this, 'username', {
+                               value: username,
+                               writable: false
+                       });
+                       
+                       _didInit = true;
+               }
+       };
+});
index dbaf368a8ecab087ba7d320027e5f520ab3c3811..9ea0942251c2ebfc80235efa2f735738135d2205 100644 (file)
@@ -1,38 +1,38 @@
 ({
        mainConfigFile: 'require.config.js',
-       name: "WoltLab/_Meta",
+       name: "WoltLabSuite/_Meta",
        out: "WCF.ACP.min.js",
        useStrict: true,
        preserveLicenseComments: false,
        optimize: 'uglify2',
        uglify2: {},
        excludeShallow: [
-               'WoltLab/_Meta'
+               'WoltLabSuite/_Meta'
        ],
        exclude: [
-               'WoltLab/WCF/Bootstrap'
+               'WoltLabSuite/WCF/Bootstrap'
        ],
        rawText: {
-               'WoltLab/_Meta': 'define([], function() {});'
+               'WoltLabSuite/_Meta': 'define([], function() {});'
        },
        onBuildRead: function(moduleName, path, contents) {
                if (!process.versions.node) {
                        throw new Error('You need to run node.js');
                }
                
-               if (moduleName === 'WoltLab/_Meta') {
+               if (moduleName === 'WoltLabSuite/_Meta') {
                        if (global.allModules == undefined) {
                                var fs   = module.require('fs'),
                                    path = module.require('path');
                                global.allModules = [];
                                
-                               var queue = ['WoltLab/WCF/Acp'];
+                               var queue = ['WoltLabSuite/Core/Acp'];
                                var folder;
                                while (folder = queue.shift()) {
                                        var files = fs.readdirSync(folder);
                                        for (var i = 0; i < files.length; i++) {
                                                var filename = path.join(folder, files[i]);
-
+                                               
                                                if (path.extname(filename) == '.js') {
                                                        global.allModules.push(filename);
                                                }
index 12ac7bb7aaa70fbb619ca73209c8a5999b0fb6ab..9cb1bdced3e06f79d13c360e58fb3f3f0470652f 100644 (file)
@@ -1,6 +1,6 @@
 ({
        mainConfigFile: 'require.config.js',
-       name: "WoltLab/_Meta",
+       name: "WoltLabSuite/_Meta",
        out: "WCF.Core.min.js",
        useStrict: true,
        preserveLicenseComments: false,
                "require.linearExecution"
        ],
        excludeShallow: [
-               'WoltLab/_Meta'
+               'WoltLabSuite/_Meta'
        ],
        rawText: {
-               'WoltLab/_Meta': 'define([], function() {});'
+               'WoltLabSuite/_Meta': 'define([], function() {});'
        },
        onBuildRead: function(moduleName, path, contents) {
                if (!process.versions.node) {
                        throw new Error('You need to run node.js');
                }
                
-               if (moduleName === 'WoltLab/_Meta') {
+               if (moduleName === 'WoltLabSuite/_Meta') {
                        if (global.allModules == undefined) {
                                var fs   = module.require('fs'),
                                    path = module.require('path');
@@ -42,8 +42,8 @@
                                        var files = fs.readdirSync(folder);
                                        for (var i = 0; i < files.length; i++) {
                                                var filename = path.join(folder, files[i]);
-                                               if (filename === 'WoltLab/WCF/Acp') continue;
-
+                                               if (filename === 'WoltLabSuite/Core/Acp') continue;
+                                               
                                                if (path.extname(filename) == '.js') {
                                                        global.allModules.push(filename);
                                                }
index 56b0daba3422b559e6fae388ea151f77266c39e1..ed1e519a898fe26c5b9c79a8b64866b6298432e2 100644 (file)
@@ -11,35 +11,35 @@ requirejs.config({
        },
        map: {
                '*': {
-                       'Ajax': 'WoltLab/WCF/Ajax',
-                       'AjaxJsonp': 'WoltLab/WCF/Ajax/Jsonp',
-                       'AjaxRequest': 'WoltLab/WCF/Ajax/Request',
-                       'CallbackList': 'WoltLab/WCF/CallbackList',
-                       'Core': 'WoltLab/WCF/Core',
-                       'DateUtil': 'WoltLab/WCF/Date/Util',
-                       'Dictionary': 'WoltLab/WCF/Dictionary',
-                       'Dom/ChangeListener': 'WoltLab/WCF/Dom/Change/Listener',
-                       'Dom/Traverse': 'WoltLab/WCF/Dom/Traverse',
-                       'Dom/Util': 'WoltLab/WCF/Dom/Util',
-                       'Environment': 'WoltLab/WCF/Environment',
-                       'EventHandler': 'WoltLab/WCF/Event/Handler',
-                       'EventKey': 'WoltLab/WCF/Event/Key',
-                       'Language': 'WoltLab/WCF/Language',
-                       'List': 'WoltLab/WCF/List',
-                       'ObjectMap': 'WoltLab/WCF/ObjectMap',
-                       'Permission': 'WoltLab/WCF/Permission',
-                       'StringUtil': 'WoltLab/WCF/StringUtil',
-                       'Ui/Alignment': 'WoltLab/WCF/Ui/Alignment',
-                       'Ui/CloseOverlay': 'WoltLab/WCF/Ui/CloseOverlay',
-                       'Ui/Confirmation': 'WoltLab/WCF/Ui/Confirmation',
-                       'Ui/Dialog': 'WoltLab/WCF/Ui/Dialog',
-                       'Ui/Notification': 'WoltLab/WCF/Ui/Notification',
-                       'Ui/ReusableDropdown': 'WoltLab/WCF/Ui/Dropdown/Reusable',
-                       'Ui/Screen': 'WoltLab/WCF/Ui/Screen',
-                       'Ui/SimpleDropdown': 'WoltLab/WCF/Ui/Dropdown/Simple',
-                       'Ui/TabMenu': 'WoltLab/WCF/Ui/TabMenu',
-                       'Upload': 'WoltLab/WCF/Upload',
-                       'User': 'WoltLab/WCF/User'
+                       'Ajax': 'WoltLabSuite/Core/Ajax',
+                       'AjaxJsonp': 'WoltLabSuite/Core/Ajax/Jsonp',
+                       'AjaxRequest': 'WoltLabSuite/Core/Ajax/Request',
+                       'CallbackList': 'WoltLabSuite/Core/CallbackList',
+                       'Core': 'WoltLabSuite/Core/Core',
+                       'DateUtil': 'WoltLabSuite/Core/Date/Util',
+                       'Dictionary': 'WoltLabSuite/Core/Dictionary',
+                       'Dom/ChangeListener': 'WoltLabSuite/Core/Dom/Change/Listener',
+                       'Dom/Traverse': 'WoltLabSuite/Core/Dom/Traverse',
+                       'Dom/Util': 'WoltLabSuite/Core/Dom/Util',
+                       'Environment': 'WoltLabSuite/Core/Environment',
+                       'EventHandler': 'WoltLabSuite/Core/Event/Handler',
+                       'EventKey': 'WoltLabSuite/Core/Event/Key',
+                       'Language': 'WoltLabSuite/Core/Language',
+                       'List': 'WoltLabSuite/Core/List',
+                       'ObjectMap': 'WoltLabSuite/Core/ObjectMap',
+                       'Permission': 'WoltLabSuite/Core/Permission',
+                       'StringUtil': 'WoltLabSuite/Core/StringUtil',
+                       'Ui/Alignment': 'WoltLabSuite/Core/Ui/Alignment',
+                       'Ui/CloseOverlay': 'WoltLabSuite/Core/Ui/CloseOverlay',
+                       'Ui/Confirmation': 'WoltLabSuite/Core/Ui/Confirmation',
+                       'Ui/Dialog': 'WoltLabSuite/Core/Ui/Dialog',
+                       'Ui/Notification': 'WoltLabSuite/Core/Ui/Notification',
+                       'Ui/ReusableDropdown': 'WoltLabSuite/Core/Ui/Dropdown/Reusable',
+                       'Ui/Screen': 'WoltLabSuite/Core/Ui/Screen',
+                       'Ui/SimpleDropdown': 'WoltLabSuite/Core/Ui/Dropdown/Simple',
+                       'Ui/TabMenu': 'WoltLabSuite/Core/Ui/TabMenu',
+                       'Upload': 'WoltLabSuite/Core/Upload',
+                       'User': 'WoltLabSuite/Core/User'
                }
        }
 });