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};
</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>
{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,
{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>
{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>
});
</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}' ],
</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}': {
</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}': {
{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}'
});
</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}',
{* 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>
{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>
{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}': {
</ul>
<script data-relocate="true">
- require(['WoltLab/WCF/Ui/Message/Share'], function(UiMessageShare) {
+ require(['WoltLabSuite/Core/Ui/Message/Share'], function(UiMessageShare) {
UiMessageShare.init();
});
</script>
</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}],
{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}',
{/content}
<script data-relocate="true">
- require(['WoltLab/WCF/Controller/Notice/Dismiss'], function(ControllerNoticeDismiss) {
+ require(['WoltLabSuite/Core/Controller/Notice/Dismiss'], function(ControllerNoticeDismiss) {
ControllerNoticeDismiss.setup();
});
</script>
{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}',
*
* @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({
/**
<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}');
});
</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>
</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>
<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}],
</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}'
});
{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>
{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}',
</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}'
});
{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) {
<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}' ],
<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>
</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}': {
{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;
<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({
{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}',
{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}'
});
{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}',
</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}',
</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}'
});
{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},
</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}],
{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>
{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}',
return {
init: function() {
- require(['WoltLab/WCF/Ui/Redactor/Code'], (function (UiRedactorCode) {
+ require(['WoltLabSuite/Core/Ui/Redactor/Code'], (function (UiRedactorCode) {
new UiRedactorCode(this);
}).bind(this));
}
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);
},
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');
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;
});
},
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
});
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));
}
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));
}
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));
}
},
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_/, ''));
},
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');
return {
init: function() {
- require(['WoltLab/WCF/Ui/Redactor/Spoiler'], (function (UiRedactorSpoiler) {
+ require(['WoltLabSuite/Core/Ui/Redactor/Spoiler'], (function (UiRedactorSpoiler) {
new UiRedactorSpoiler(this);
}).bind(this));
}
* @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({
/**
* @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));
},
/**
* 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
*/
* @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,
_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));
* 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) {
/**
* 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();
});
}
});
// reset date picker
- require(['WoltLab/WCF/Date/Picker'], (function(UiDatePicker) {
+ require(['WoltLabSuite/Core/Date/Picker'], (function(UiDatePicker) {
UiDatePicker.clear('pollEndTime_' + this._editorId);
}).bind(this));
},
},
/**
- * @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) {
/**
* Clipboard API
*
- * @deprecated 3.0 - please use `WoltLab/WCF/Controller/Clipboard` instead
+ * @deprecated 3.0 - please use `WoltLabSuite/Core/Controller/Clipboard` instead
*/
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,
* 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({
/**
/**
* 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();
});
},
* 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();
});
},
/**
* 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
*/
// 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) {
/**
* Number utilities.
- * @deprecated Use WoltLab/WCF/NumberUtil
+ * @deprecated Use WoltLabSuite/Core/NumberUtil
*/
WCF.Number = {
/**
/**
* String utilities.
- * @deprecated Use WoltLab/WCF/StringUtil
+ * @deprecated Use WoltLabSuite/Core/StringUtil
*/
WCF.String = {
/**
* Initializes all TabMenus
*/
init: function() {
- require(['WoltLab/WCF/Ui/TabMenu'], function(UiTabMenu) {
+ require(['WoltLabSuite/Core/Ui/TabMenu'], function(UiTabMenu) {
UiTabMenu.setup();
});
},
};
/**
- * @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();
});
},
/**
* 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({
/**
* 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({
/**
* @param string containerID
*/
registerMenu: function(containerID) {
- require(['WoltLab/WCF/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
+ require(['WoltLabSuite/Core/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
UiFlexibleMenu.register(containerID);
});
},
* @param string containerID
*/
rebuild: function(containerID) {
- require(['WoltLab/WCF/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
+ require(['WoltLabSuite/Core/Ui/FlexibleMenu'], function(UiFlexibleMenu) {
UiFlexibleMenu.rebuild(containerID);
});
}
* @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);
}
*/
getData: function(captchaID) {
var returnValue;
- require(['WoltLab/WCF/Controller/Captcha'], function(ControllerCaptcha) {
+ require(['WoltLabSuite/Core/Controller/Captcha'], function(ControllerCaptcha) {
try {
returnValue = ControllerCaptcha.getData(captchaID);
}
* 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);
}
/**
* 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);
/**
* Default implementation for ajax file uploads.
*
- * @deprecated Use WoltLab/WCF/Upload
+ * @deprecated Use WoltLabSuite/Core/Upload
*
* @param jquery buttonSelector
* @param jquery fileListSelector
/**
* Default implementation for parallel AJAX file uploads.
*
- * @deprecated Use WoltLab/WCF/Upload
+ * @deprecated Use WoltLabSuite/Core/Upload
*/
WCF.Upload.Parallel = WCF.Upload.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,
* @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({
/**
* @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);
});
}
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);
/**
* 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,
* 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,
+++ /dev/null
-/**
- * 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();
- }
- };
-});
+++ /dev/null
-/**
- * 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')
- }
- };
- }
- };
-});
+++ /dev/null
-/**
- * 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')
- }
- };
- }
- };
-});
+++ /dev/null
-/**
- * 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
- }
- });
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- }
- };
-});
+++ /dev/null
-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;
-});
+++ /dev/null
-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;
-});
+++ /dev/null
-/**
- * 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);
- });
- }
- };
-});
+++ /dev/null
-/**
- * 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')
- }
- };
- }
- };
-});
+++ /dev/null
-/**
- * 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;
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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(/&/g, '&');
- message = message.replace(/</g, '<');
- message = message.replace(/>/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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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, '&');
- message = message.replace(/</g, '<');
- message = message.replace(/>/g, '>');
-
- 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, '<').replace(/>/g, '>');
- 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;
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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)
- });
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-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
+++ /dev/null
-/**
- * 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)();
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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(); }
- });
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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);
- }
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
- }
- };
-});
+++ /dev/null
-/**
- * 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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
- },
-
- /**
- * 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(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
- }
- };
-});
+++ /dev/null
-/**
- * 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;
+++ /dev/null
-
-
-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
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-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;
-});
+++ /dev/null
-/**
- * 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
- };
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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 {};
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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');
- }
- };
-});
+++ /dev/null
-/**
- * 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');
- });
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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');
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- });
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-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>'
- };
- }
- };
-});
+++ /dev/null
-/**
- * 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>'
- };
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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') + '">…</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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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
+++ /dev/null
-/**
- * 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;
- }
- };
-});
+++ /dev/null
-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>'
- };
- }
- };
-});
+++ /dev/null
-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;
-});
+++ /dev/null
-/**
- * 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;
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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
+++ /dev/null
-/**
- * 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
+++ /dev/null
-/**
- * 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();
- });
- }
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-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;
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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');
- }
- };
-});
+++ /dev/null
-/**
- * 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>'
- };
- }
- };
-});
+++ /dev/null
-/**
- * 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);
- }
- };
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-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;
-});
+++ /dev/null
-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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
-});
+++ /dev/null
-/**
- * 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;
- }
- };
-});
--- /dev/null
+/**
+ * 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();
+ }
+ };
+});
--- /dev/null
+/**
+ * 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')
+ }
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * 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')
+ }
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * 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
+ }
+ });
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ };
+});
--- /dev/null
+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;
+});
--- /dev/null
+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;
+});
--- /dev/null
+/**
+ * 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);
+ });
+ }
+ };
+});
--- /dev/null
+/**
+ * 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')
+ }
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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(/&/g, '&');
+ message = message.replace(/</g, '<');
+ message = message.replace(/>/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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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, '&');
+ message = message.replace(/</g, '<');
+ message = message.replace(/>/g, '>');
+
+ 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, '<').replace(/>/g, '>');
+ 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;
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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)
+ });
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+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
--- /dev/null
+/**
+ * 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)();
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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(); }
+ });
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+ }
+ };
+});
--- /dev/null
+/**
+ * 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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
+ },
+
+ /**
+ * 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(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
--- /dev/null
+
+
+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
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+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;
+});
--- /dev/null
+/**
+ * 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
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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 {};
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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');
+ }
+ };
+});
--- /dev/null
+/**
+ * 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');
+ });
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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');
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ });
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+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>'
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * 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>'
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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') + '">…</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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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
--- /dev/null
+/**
+ * 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;
+ }
+ };
+});
--- /dev/null
+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>'
+ };
+ }
+ };
+});
--- /dev/null
+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;
+});
--- /dev/null
+/**
+ * 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;
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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
--- /dev/null
+/**
+ * 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
--- /dev/null
+/**
+ * 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();
+ });
+ }
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+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;
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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');
+ }
+ };
+});
--- /dev/null
+/**
+ * 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>'
+ };
+ }
+ };
+});
--- /dev/null
+/**
+ * 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);
+ }
+ };
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+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;
+});
--- /dev/null
+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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+});
--- /dev/null
+/**
+ * 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;
+ }
+ };
+});
({
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);
}
({
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');
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);
}
},
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'
}
}
});