2 * Class and function collection for WCF ACP
4 * @author Alexander Ebert, Matthias Schmidt
5 * @copyright 2001-2019 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
10 * Initialize WCF.ACP namespace
15 * Namespace for ACP application management.
17 WCF
.ACP
.Application
= { };
20 * Namespace for ACP cronjob management.
22 WCF
.ACP
.Cronjob
= { };
25 * Handles the manual execution of cronjobs.
27 WCF
.ACP
.Cronjob
.ExecutionHandler
= Class
.extend({
30 * @var WCF.System.Notification
36 * @var WCF.Action.Proxy
41 * Initializes WCF.ACP.Cronjob.ExecutionHandler object.
44 this._proxy
= new WCF
.Action
.Proxy({
45 success
: $.proxy(this._success
, this)
48 $('.jsCronjobRow .jsExecuteButton').click($.proxy(this._click
, this));
50 this._notification
= new WCF
.System
.Notification(WCF
.Language
.get('wcf.global.success'), 'success');
54 * Handles a click on an execute button.
58 _click: function(event
) {
59 this._proxy
.setOption('data', {
60 actionName
: 'execute',
61 className
: 'wcf\\data\\cronjob\\CronjobAction',
62 objectIDs
: [ $(event
.target
).data('objectID') ]
65 this._proxy
.sendRequest();
69 * Handles successful cronjob execution.
72 * @param string textStatus
75 _success: function(data
, textStatus
, jqXHR
) {
76 $('.jsCronjobRow').each($.proxy(function(index
, row
) {
77 var $button
= $(row
).find('.jsExecuteButton');
78 var $objectID
= ($button
).data('objectID');
80 if (WCF
.inArray($objectID
, data
.objectIDs
)) {
81 if (data
.returnValues
[$objectID
]) {
82 // insert feedback here
83 $(row
).find('td.columnNextExec').html(data
.returnValues
[$objectID
].formatted
);
84 $(row
).wcfHighlight();
87 this._notification
.show();
96 * Handles the cronjob log list.
98 WCF
.ACP
.Cronjob
.LogList
= Class
.extend({
100 * error message dialog
106 * Initializes WCF.ACP.Cronjob.LogList object.
109 // bind event listener to delete cronjob log button
110 $('.jsCronjobLogDelete').click(function() {
111 WCF
.System
.Confirmation
.show(WCF
.Language
.get('wcf.acp.cronjob.log.clear.confirm'), function(action
) {
112 if (action
== 'confirm') {
113 new WCF
.Action
.Proxy({
116 actionName
: 'clearAll',
117 className
: 'wcf\\data\\cronjob\\log\\CronjobLogAction'
119 success: function() {
120 window
.location
.reload();
127 // bind event listeners to error badges
128 $('.jsCronjobError').click($.proxy(this._showError
, this));
132 * Shows certain error message
134 * @param object event
136 _showError: function(event
) {
137 var $errorBadge
= $(event
.currentTarget
);
139 if (this._dialog
=== null) {
140 this._dialog
= $('<div style="overflow: auto"><pre>' + $errorBadge
.next().html() + '</pre></div>').hide().appendTo(document
.body
);
141 this._dialog
.wcfDialog({
142 title
: WCF
.Language
.get('wcf.acp.cronjob.log.error.details')
146 this._dialog
.html('<pre>' + $errorBadge
.next().html() + '</pre>');
147 this._dialog
.wcfDialog('open');
153 * Namespace for ACP package management.
155 WCF
.ACP
.Package
= { };
158 * Provides the package installation.
160 * @param integer queueID
161 * @param string actionName
163 WCF
.ACP
.Package
.Installation
= Class
.extend({
165 * package installation type
168 _actionName
: 'InstallPackage',
171 * additional parameters send in all requests
174 _additionalRequestParameters
: {},
177 * true, if rollbacks are supported
180 _allowRollback
: false,
189 * name of the language item with the title of the dialog
196 * @var WCF.Action.Proxy
201 * package installation queue id
207 * true, if dialog should be rendered again
210 _shouldRender
: false,
213 * Initializes the WCF.ACP.Package.Installation class.
215 * @param integer queueID
216 * @param string actionName
217 * @param boolean allowRollback
218 * @param boolean isUpdate
219 * @param object additionalRequestParameters
221 init: function(queueID
, actionName
, allowRollback
, isUpdate
, additionalRequestParameters
) {
222 this._actionName
= (actionName
) ? actionName
: 'InstallPackage';
223 this._allowRollback
= (allowRollback
=== true);
224 this._queueID
= queueID
;
225 this._additionalRequestParameters
= additionalRequestParameters
|| {};
227 this._dialogTitle
= 'wcf.acp.package.' + (isUpdate
? 'update' : 'install') + '.title';
228 if (this._actionName
=== 'UninstallPackage') {
229 this._dialogTitle
= 'wcf.acp.package.uninstallation.title';
237 * Initializes the WCF.Action.Proxy object.
239 _initProxy: function() {
240 var $actionName
= '';
241 var $parts
= this._actionName
.split(/([A-Z][a-z0-9]+)/);
242 for (var $i
= 0, $length
= $parts
.length
; $i
< $length
; $i
++) {
243 var $part
= $parts
[$i
];
245 if ($actionName
.length
) $actionName
+= '-';
246 $actionName
+= $part
.toLowerCase();
250 this._proxy
= new WCF
.Action
.Proxy({
251 failure
: $.proxy(this._failure
, this),
252 showLoadingOverlay
: false,
253 success
: $.proxy(this._success
, this),
254 url
: 'index.php?' + $actionName
+ '/&t=' + SECURITY_TOKEN
259 * Initializes the package installation.
262 const button
= document
.getElementById('submitButton');
263 button
?.addEventListener(
266 button
.disabled
= true;
267 this.prepareInstallation();
273 * Handles erroneous AJAX requests.
275 _failure: function() {
276 if (this._dialog
!== null) {
277 $('#packageInstallationProgress').removeAttr('value');
278 this._setIcon('xmark');
281 if (!this._allowRollback
) {
285 if (this._dialog
!== null) {
286 this._purgeTemplateContent($.proxy(function() {
287 var $form
= $('<div class="formSubmit" />').appendTo($('#packageInstallationInnerContent'));
288 $('<button type="button" class="button buttonPrimary">' + WCF
.Language
.get('wcf.acp.package.installation.rollback') + '</button>').appendTo($form
).click($.proxy(this._rollback
, this));
290 $('#packageInstallationInnerContentContainer').show();
292 this._dialog
.wcfDialog('render');
298 * Performs a rollback.
300 * @param object event
302 _rollback: function(event
) {
303 this._setIcon('spinner');
306 $(event
.currentTarget
).disable();
309 this._executeStep('rollback');
313 * Prepares installation dialog.
315 prepareInstallation: function() {
316 if (document
.activeElement
) {
317 document
.activeElement
.blur();
320 require(['WoltLabSuite/Core/Ajax/Status'], ({show
}) => show());
321 this._proxy
.setOption('data', this._getParameters());
322 this._proxy
.sendRequest();
326 * Returns parameters to prepare installation.
330 _getParameters: function() {
331 return $.extend({}, this._additionalRequestParameters
, {
332 queueID
: this._queueID
,
338 * Handles successful AJAX requests.
341 * @param string textStatus
342 * @param jQuery jqXHR
344 _success: function(data
, textStatus
, jqXHR
) {
345 this._shouldRender
= false;
347 if (typeof window
._trackPackageStep
=== 'function') window
._trackPackageStep(this._actionName
, data
);
349 if (this._dialog
=== null) {
350 this._dialog
= $('<div id="package' + (this._actionName
=== 'UninstallPackage' ? 'Uni' : 'I') + 'nstallationDialog" />').hide().appendTo(document
.body
);
351 this._dialog
.wcfDialog({
353 title
: WCF
.Language
.get(this._dialogTitle
)
355 require(['WoltLabSuite/Core/Ajax/Status'], ({hide
}) => hide());
358 this._setIcon('spinner');
360 if (data
.step
== 'rollback') {
361 this._dialog
.wcfDialog('close');
362 this._dialog
.remove();
364 setTimeout(function () {
365 var $uninstallation
= new WCF
.ACP
.Package
.Uninstallation();
366 $uninstallation
.start(data
.packageID
);
372 // receive new queue id
374 this._queueID
= data
.queueID
;
378 if (data
.template
&& !data
.ignoreTemplate
) {
379 this._dialog
.html(data
.template
);
380 this._shouldRender
= true;
385 $('#packageInstallationProgress').attr('value', data
.progress
).text(data
.progress
+ '%');
386 $('#packageInstallationProgressLabel').text(data
.progress
+ '%');
390 if (data
.currentAction
) {
391 $('#packageInstallationAction').html(data
.currentAction
);
395 if (data
.step
=== 'success') {
396 this._setIcon('check');
398 this._purgeTemplateContent($.proxy(function() {
399 var $form
= $('<div class="formSubmit" />').appendTo($('#packageInstallationInnerContent'));
400 var $button
= $('<button type="button" class="button buttonPrimary">' + WCF
.Language
.get('wcf.global.button.next') + '</button>').appendTo($form
).click(function() {
402 window
.location
= data
.redirectLocation
;
405 $('#packageInstallationInnerContentContainer').show();
407 $(document
).keydown(function(event
) {
408 if (event
.which
=== $.ui
.keyCode
.ENTER
) {
409 $button
.trigger('click');
413 this._dialog
.wcfDialog('render');
419 // handle inner template
420 if (data
.innerTemplate
) {
422 $('#packageInstallationInnerContent').html(data
.innerTemplate
).find('input').keyup(function(event
) {
423 if (event
.keyCode
=== $.ui
.keyCode
.ENTER
) {
428 // create button to handle next step
429 if (data
.step
&& data
.node
) {
430 $('#packageInstallationProgress').removeAttr('value');
431 this._setIcon('question');
433 var $form
= $('<div class="formSubmit" />').appendTo($('#packageInstallationInnerContent'));
434 $('<button type="button" class="button buttonPrimary">' + WCF
.Language
.get('wcf.global.button.next') + '</button>').appendTo($form
).click($.proxy(function(event
) {
435 $(event
.currentTarget
).disable();
441 $('#packageInstallationInnerContentContainer').show();
443 this._dialog
.wcfDialog('render');
448 this._purgeTemplateContent($.proxy(function() {
450 if (this._shouldRender
) {
451 this._dialog
.wcfDialog('render');
455 if (data
.step
&& data
.node
) {
456 this._executeStep(data
.step
, data
.node
);
462 * Submits the dialog content.
466 _submit: function(data
) {
467 this._setIcon('spinner');
469 // collect form values
470 var $additionalData
= { };
471 $('#packageInstallationInnerContent input').each(function(index
, inputElement
) {
472 var $inputElement
= $(inputElement
);
473 var $type
= $inputElement
.attr('type');
475 if (($type
!= 'checkbox' && $type
!= 'radio') || $inputElement
.prop('checked')) {
476 var $name
= $inputElement
.attr('name');
477 if ($name
.match(/(.*)\[([^[]*)\]$/)) {
481 if ($additionalData
[$name
] === undefined) {
483 $additionalData
[$name
] = { };
486 $additionalData
[$name
] = [ ];
491 $additionalData
[$name
][$key
] = $inputElement
.val();
494 $additionalData
[$name
].push($inputElement
.val());
498 $additionalData
[$name
] = $inputElement
.val();
503 this._executeStep(data
.step
, data
.node
, $additionalData
);
507 * Purges template content.
509 * @param function callback
511 _purgeTemplateContent: function(callback
) {
512 if ($('#packageInstallationInnerContent').children().length
) {
513 $('#packageInstallationInnerContentContainer').hide();
514 $('#packageInstallationInnerContent').empty();
516 this._shouldRender
= true;
523 * Executes the next installation step.
527 * @param object additionalData
529 _executeStep: function(step
, node
, additionalData
) {
530 if (!additionalData
) additionalData
= { };
532 var $data
= $.extend({}, this._additionalRequestParameters
, {
534 queueID
: this._queueID
,
538 this._proxy
.setOption('data', $data
);
539 this._proxy
.sendRequest();
543 * Sets the icon with the given name as the current installation status icon.
545 * @param string iconName
547 _setIcon: function(iconName
) {
548 const icon
= this._dialog
.find('.jsPackageInstallationStatus fa-icon');
549 if (icon
.length
=== 1) {
550 icon
[0].setIcon(iconName
);
556 * Handles canceling the package installation at the package installation
559 WCF
.ACP
.Package
.Installation
.Cancel
= Class
.extend({
561 * Creates a new instance of WCF.ACP.Package.Installation.Cancel.
563 * @param integer queueID
565 init: function(queueID
) {
566 $('#backButton').click(function() {
567 new WCF
.Action
.Proxy({
570 actionName
: 'cancelInstallation',
571 className
: 'wcf\\data\\package\\installation\\queue\\PackageInstallationQueueAction',
572 objectIDs
: [ queueID
]
574 success: function(data
) {
575 window
.location
= data
.returnValues
.url
;
583 * Provides the package uninstallation.
585 * @param jQuery elements
586 * @param string wcfPackageListURL
588 WCF
.ACP
.Package
.Uninstallation
= WCF
.ACP
.Package
.Installation
.extend({
590 * list of uninstallation buttons
602 * Initializes the WCF.ACP.Package.Uninstallation class.
604 * @param jQuery elements
606 init: function(elements
) {
607 this._elements
= elements
;
610 if (this._elements
!== undefined && this._elements
.length
) {
611 this._super(0, 'UninstallPackage');
616 * Begins a package uninstallation without user action.
618 * @param integer packageID
620 start: function(packageID
) {
621 this._actionName
= 'UninstallPackage';
622 this._packageID
= packageID
;
624 this._dialogTitle
= 'wcf.acp.package.uninstallation.title';
627 this.prepareInstallation();
631 * @see WCF.ACP.Package.Installation.init()
634 this._elements
.click($.proxy(this._showConfirmationDialog
, this));
638 * Displays a confirmation dialog prior to package uninstallation.
640 * @param object event
642 _showConfirmationDialog: function(event
) {
643 var $element
= $(event
.currentTarget
);
646 WCF
.System
.Confirmation
.show($element
.data('confirmMessage'), function(action
) {
647 if (action
=== 'confirm') {
648 self
._packageID
= $element
.data('objectID');
649 self
.prepareInstallation();
651 }, undefined, undefined, true);
655 * @see WCF.ACP.Package.Installation._getParameters()
657 _getParameters: function() {
659 packageID
: this._packageID
,
665 WCF
.ACP
.Package
.Server
= { };
667 WCF
.ACP
.Package
.Server
.Installation
= Class
.extend({
669 _selectedPackage
: '',
673 this._selectedPackage
= null;
675 this._proxy
= new WCF
.Action
.Proxy({
676 success
: $.proxy(this._success
, this)
681 $('.jsButtonPackageInstall').removeClass('jsButtonPackageInstall').click($.proxy(this._click
, this));
685 * Prepares a package installation.
687 * @param object event
689 _click: function(event
) {
690 var $button
= $(event
.currentTarget
);
691 WCF
.System
.Confirmation
.show($button
.data('confirmMessage'), $.proxy(function(action
) {
692 if (action
=== 'confirm') {
693 this._selectedPackage
= $button
.data('package');
694 this._selectedPackageVersion
= $button
.data('packageVersion');
695 this._prepareInstallation();
697 }, this), undefined, undefined, true);
701 * Handles successful AJAX requests.
705 _success: function(data
) {
706 if (data
.returnValues
.queueID
) {
707 if (this._dialog
!== null) {
708 this._dialog
.wcfDialog('close');
711 var $installation
= new WCF
.ACP
.Package
.Installation(data
.returnValues
.queueID
, undefined, false);
712 $installation
.prepareInstallation();
714 else if (data
.returnValues
.template
) {
715 if (this._dialog
=== null) {
716 this._dialog
= $('<div>' + data
.returnValues
.template
+ '</div>').hide().appendTo(document
.body
);
717 this._dialog
.wcfDialog({
718 title
: WCF
.Language
.get('wcf.acp.package.update.unauthorized')
722 this._dialog
.html(data
.returnValues
.template
).wcfDialog('open');
725 this._dialog
.find('.formSubmit > button').click($.proxy(this._submitAuthentication
, this));
730 * Submits authentication data for current update server.
732 * @param object event
734 _submitAuthentication: function(event
) {
735 var $usernameField
= $('#packageUpdateServerUsername');
736 var $passwordField
= $('#packageUpdateServerPassword');
738 // remove error messages if any
739 $usernameField
.next('small.innerError').remove();
740 $passwordField
.next('small.innerError').remove();
742 var $continue = true;
743 if ($.trim($usernameField
.val()) === '') {
744 $('<small class="innerError">' + WCF
.Language
.get('wcf.global.form.error.empty') + '</small>').insertAfter($usernameField
);
748 if ($.trim($passwordField
.val()) === '') {
749 $('<small class="innerError">' + WCF
.Language
.get('wcf.global.form.error.empty') + '</small>').insertAfter($passwordField
);
754 this._prepareInstallation($(event
.currentTarget
).data('packageUpdateServerID'));
759 * Prepares package installation.
761 * @param integer packageUpdateServerID
763 _prepareInstallation: function(packageUpdateServerID
) {
767 $parameters
['packages'][this._selectedPackage
] = this._selectedPackageVersion
;
769 if (packageUpdateServerID
) {
770 $parameters
.authData
= {
771 packageUpdateServerID
: packageUpdateServerID
,
772 password
: $.trim($('#packageUpdateServerPassword').val()),
773 saveCredentials
: ($('#packageUpdateServerSaveCredentials:checked').length
? true : false),
774 username
: $.trim($('#packageUpdateServerUsername').val())
778 this._proxy
.setOption('data', {
779 actionName
: 'prepareInstallation',
780 className
: 'wcf\\data\\package\\update\\PackageUpdateAction',
781 parameters
: $parameters
783 this._proxy
.sendRequest();
788 * Namespace for package update related classes.
790 WCF
.ACP
.Package
.Update
= { };
793 * Searches for available updates.
795 * @param boolean bindOnExistingButtons
797 WCF
.ACP
.Package
.Update
.Search
= Class
.extend({
798 /** @var {Element} */
808 * Initializes the WCF.ACP.Package.SearchForUpdates class.
810 * @param {boolean} bindOnExistingButtons
812 init: function(bindOnExistingButtons
) {
815 if (!bindOnExistingButtons
=== true) {
817 <button type="button" class="button jsButtonSearchForUpdates">
818 <fa-icon size="16" name="arrows-rotate"></fa-icon>
819 <span>${WCF.Language.get('wcf.acp.package.searchForUpdates')}</span>
821 </li>`).prependTo($('.contentHeaderNavigation > ul'));
824 this._button
= elBySel('.jsButtonSearchForUpdates');
826 this._button
.addEventListener('click', this._click
.bind(this));
828 const url
= new URL(window
.location
.href
);
829 if (url
.searchParams
.has("searchForUpdates")) {
836 * Handles clicks on the search button.
838 * @param {Event} event
840 _click: function(event
) {
841 event
?.preventDefault();
843 if (this._button
.classList
.contains('disabled')) {
847 this._button
.classList
.add('disabled');
849 if (this._dialog
=== null) {
850 new WCF
.Action
.Proxy({
853 actionName
: 'searchForUpdates',
854 className
: 'wcf\\data\\package\\update\\PackageUpdateAction',
859 success
: $.proxy(this._success
, this)
863 this._dialog
.wcfDialog('open');
868 * Handles successful AJAX requests.
871 * @param string textStatus
872 * @param jQuery jqXHR
874 _success: function(data
, textStatus
, jqXHR
) {
875 if (typeof window
._trackSearchForUpdates
=== 'function') {
876 window
._trackSearchForUpdates(data
);
880 if (data
.returnValues
.url
) {
881 window
.location
= data
.returnValues
.url
;
884 this._dialog
= $('<div>' + WCF
.Language
.get('wcf.acp.package.searchForUpdates.noResults') + '</div>').hide().appendTo(document
.body
);
885 this._dialog
.wcfDialog({
886 title
: WCF
.Language
.get('wcf.acp.package.searchForUpdates')
889 this._button
.classList
.remove('disabled');
895 * Worker support for ACP.
897 * @param string dialogID
898 * @param string className
899 * @param string title
900 * @param object parameters
901 * @param object callback
903 * @deprecated 3.1 - please use `WoltLabSuite/Core/Acp/Ui/Worker` instead
905 WCF
.ACP
.Worker
= Class
.extend({
907 * Initializes a new worker instance.
909 * @param string dialogID
910 * @param string className
911 * @param string title
912 * @param object parameters
913 * @param object callback
915 init: function(dialogID
, className
, title
, parameters
, callback
) {
916 if (typeof callback
=== 'function') {
917 throw new Error("The callback parameter is no longer supported, please migrate to 'WoltLabSuite/Core/Acp/Ui/Worker'.");
920 require(['WoltLabSuite/Core/Acp/Ui/Worker'], function(AcpUiWorker
) {
927 className
: className
,
928 parameters
: parameters
935 * Namespace for category-related functions.
937 WCF
.ACP
.Category
= { };
940 * Handles collapsing categories.
942 * @param string className
943 * @param integer objectTypeID
945 WCF
.ACP
.Category
.Collapsible
= WCF
.Collapsible
.SimpleRemote
.extend({
947 * @see WCF.Collapsible.Remote.init()
949 init: function(className
) {
950 var sortButton
= $('.formSubmit > button[data-type="submit"]');
952 sortButton
.click($.proxy(this._sort
, this));
955 this._super(className
);
959 * @see WCF.Collapsible.Remote._getButtonContainer()
961 _getButtonContainer: function(containerID
) {
962 return $('#' + containerID
+ ' > .buttons');
966 * @see WCF.Collapsible.Remote._getContainers()
968 _getContainers: function() {
969 return $('.jsCategory').has('ol').has('li');
973 * @see WCF.Collapsible.Remote._getTarget()
975 _getTarget: function(containerID
) {
976 return $('#' + containerID
+ ' > ol');
980 * Handles a click on the sort button.
983 // remove existing collapsible buttons
984 $('.collapsibleButton').remove();
987 this._containers
= { };
988 this._containerData
= { };
990 var $containers
= this._getContainers();
991 if ($containers
.length
== 0) {
992 console
.debug('[WCF.ACP.Category.Collapsible] Empty container set given, aborting.');
994 $containers
.each($.proxy(function(index
, container
) {
995 var $container
= $(container
);
996 var $containerID
= $container
.wcfIdentify();
997 this._containers
[$containerID
] = $container
;
999 this._initContainer($containerID
);
1005 * Provides the search dropdown for ACP
1007 * @see WCF.Search.Base
1009 WCF
.ACP
.Search
= WCF
.Search
.Base
.extend({
1013 * name of the selected search provider
1019 * @see WCF.Search.Base.init()
1022 this._className
= 'wcf\\data\\acp\\search\\provider\\ACPSearchProviderAction';
1023 this._super('#pageHeaderSearch input[name=q]');
1025 // disable form submitting
1026 $('#pageHeaderSearch > form').on('submit', function(event
) {
1027 event
.preventDefault();
1030 var $dropdown
= WCF
.Dropdown
.getDropdownMenu('pageHeaderSearchType');
1031 $dropdown
.find('a[data-provider-name]').on('click', $.proxy(function(event
) {
1032 event
.preventDefault();
1033 var $button
= $(event
.target
);
1034 $('.pageHeaderSearchType > .button > .pageHeaderSearchTypeLabel').text($button
.text());
1036 var $oldProviderName
= this._providerName
;
1037 this._providerName
= ($button
.data('providerName') != 'everywhere' ? $button
.data('providerName') : '');
1039 if ($oldProviderName
!= this._providerName
) {
1040 var $searchString
= $.trim(this._searchInput
.val());
1041 if ($searchString
) {
1044 excludedSearchValues
: this._excludedSearchValues
,
1045 searchString
: $searchString
1048 this._queryServer($parameters
);
1053 const searchInput
= document
.querySelector("#pageHeaderSearch input[name=q]");
1054 document
.addEventListener("keydown", (event
) => {
1055 if (event
.key
!== "s") {
1059 if (!event
.defaultPrevented
&& document
.activeElement
=== document
.body
) {
1060 searchInput
.focus();
1062 event
.preventDefault();
1068 searchInput
.addEventListener("keydown", (event
) => {
1069 if (event
.key
!== "Escape") {
1073 if (!event
.defaultPrevented
&& searchInput
.value
.trim() === "") {
1074 event
.preventDefault();
1084 * @see WCF.Search.Base._createListItem()
1086 _createListItem: function(resultList
) {
1087 // add a divider between result lists
1088 if (this._list
.children('li').length
> 0) {
1089 $('<li class="dropdownDivider" />').appendTo(this._list
);
1093 $('<li class="dropdownText">' + resultList
.title
+ '</li>').appendTo(this._list
);
1096 for (var $i
in resultList
.items
) {
1097 var $item
= resultList
.items
[$i
];
1099 $('<li><a href="' + $item
.link
+ '"><span>' + WCF
.String
.escapeHTML($item
.title
) + '</span>' + ($item
.subtitle
? '<small>' + WCF
.String
.escapeHTML($item
.subtitle
) + '</small>' : '') + '</a></li>').appendTo(this._list
);
1106 * @see WCF.Search.Base._openDropdown()
1108 _openDropdown: function() {
1109 this._list
.find('small').each(function(index
, element
) {
1110 while (element
.scrollWidth
> element
.clientWidth
) {
1111 element
.innerText
= '\u2026 ' + element
.innerText
.substr(3);
1117 * @see WCF.Search.Base._handleEmptyResult()
1119 _handleEmptyResult: function() {
1120 $('<li class="dropdownText">' + WCF
.Language
.get('wcf.acp.search.noResults') + '</li>').appendTo(this._list
);
1126 * @see WCF.Search.Base._highlightSelectedElement()
1128 _highlightSelectedElement: function() {
1129 this._list
.find('li').removeClass('dropdownNavigationItem');
1130 this._list
.find('li:not(.dropdownDivider):not(.dropdownText)').eq(this._itemIndex
).addClass('dropdownNavigationItem');
1134 * @see WCF.Search.Base._selectElement()
1136 _selectElement: function(event
) {
1137 if (this._itemIndex
=== -1) {
1141 window
.location
= this._list
.find('li.dropdownNavigationItem > a').attr('href');
1144 _success: function(data
) {
1147 const container
= document
.getElementById("pageHeaderSearch").querySelector(".pageHeaderSearchInputContainer");
1148 const { bottom
} = container
.getBoundingClientRect();
1149 this._list
[0].style
.setProperty("top", `${Math.trunc(bottom)}px`, "important");
1150 this._list
[0].classList
.add("acpSearchDropdown");
1151 this._list
[0].dataset
.dropdownIgnorePageScroll
= "true";
1155 * @see WCF.Search.Base._getParameters()
1157 _getParameters: function(parameters
) {
1158 parameters
.data
.providerName
= this._providerName
;
1165 * Namespace for user management.
1170 * Generic implementation to ban users.
1172 WCF
.ACP
.User
.BanHandler
= {
1187 * @var WCF.Action.Proxy
1192 * Initializes WCF.ACP.User.BanHandler on first use.
1195 this._proxy
= new WCF
.Action
.Proxy({
1196 success
: $.proxy(this._success
, this)
1199 $('.jsBanButton').click($.proxy(function(event
) {
1200 var $button
= $(event
.currentTarget
);
1201 if ($button
.data('banned')) {
1202 this.unban([ $button
.data('objectID') ]);
1205 this.ban([ $button
.data('objectID') ]);
1209 require(['EventHandler'], function(EventHandler
) {
1210 EventHandler
.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.user', this._clipboardAction
.bind(this));
1215 * Reacts to executed clipboard actions.
1217 * @param {object<string, *>} actionData data of the executed clipboard action
1219 _clipboardAction: function(actionData
) {
1220 if (actionData
.data
.actionName
=== 'com.woltlab.wcf.user.ban') {
1221 this.ban(actionData
.data
.parameters
.objectIDs
);
1228 * @param array<integer> userIDs
1230 unban: function(userIDs
) {
1231 this._proxy
.setOption('data', {
1232 actionName
: 'unban',
1233 className
: 'wcf\\data\\user\\UserAction',
1236 this._proxy
.sendRequest();
1242 * @param array<integer> userIDs
1244 ban: function(userIDs
) {
1245 if (this._dialog
=== null) {
1247 this._dialog
= $('<div />').hide().appendTo(document
.body
);
1248 this._dialog
.append($('<div class="section"><dl><dt><label for="userBanReason">' + WCF
.Language
.get('wcf.acp.user.banReason') + '</label></dt><dd><textarea id="userBanReason" cols="40" rows="3" /><small>' + WCF
.Language
.get('wcf.acp.user.banReason.description') + '</small></dd></dl><dl><dt></dt><dd><label for="userBanNeverExpires"><input type="checkbox" name="userBanNeverExpires" id="userBanNeverExpires" checked> ' + WCF
.Language
.get('wcf.acp.user.ban.neverExpires') + '</label></dd></dl><dl id="userBanExpiresSettings" style="display: none;"><dt><label for="userBanExpires">' + WCF
.Language
.get('wcf.acp.user.ban.expires') + '</label></dt><dd><input type="date" name="userBanExpires" id="userBanExpires" class="medium" min="' + new Date(TIME_NOW
* 1000).toISOString() + '" data-ignore-timezone="true" /><small>' + WCF
.Language
.get('wcf.acp.user.ban.expires.description') + '</small></dd></dl></div>'));
1249 this._dialog
.append($('<div class="formSubmit"><button type="button" class="button buttonPrimary" accesskey="s">' + WCF
.Language
.get('wcf.global.button.submit') + '</button></div>'));
1251 this._dialog
.find('#userBanNeverExpires').change(function() {
1252 $('#userBanExpiresSettings').toggle();
1255 this._dialog
.find('button').click($.proxy(this._submit
, this));
1259 $('#userBanReason').val('');
1260 $('#userBanNeverExpires').prop('checked', true);
1261 $('#userBanExpiresSettings').hide();
1262 $('#userBanExpiresDatePicker, #userBanExpires').val('');
1265 this._dialog
.data('userIDs', userIDs
);
1266 this._dialog
.wcfDialog({
1267 title
: WCF
.Language
.get('wcf.acp.user.ban.sure')
1272 * Handles submitting the ban dialog.
1274 _submit: function() {
1275 this._dialog
.find('.innerError').remove();
1277 var $banExpires
= '';
1278 if (!$('#userBanNeverExpires').is(':checked')) {
1279 var $banExpires
= $('#userBanExpiresDatePicker').val();
1281 this._dialog
.find('#userBanExpiresSettings > dd > small').prepend($('<small class="innerError" />').text(WCF
.Language
.get('wcf.global.form.error.empty')));
1286 this._proxy
.setOption('data', {
1288 className
: 'wcf\\data\\user\\UserAction',
1289 objectIDs
: this._dialog
.data('userIDs'),
1291 banReason
: $('#userBanReason').val(),
1292 banExpires
: $banExpires
1295 this._proxy
.sendRequest();
1299 * Handles successful AJAX calls.
1301 * @param object data
1302 * @param string textStatus
1303 * @param jQuery jqXHR
1305 _success: function(data
, textStatus
, jqXHR
) {
1306 elBySelAll('.jsUserRow', undefined, function(userRow
) {
1307 var userId
= parseInt(elData(userRow
, 'object-id'), 10);
1308 if (data
.objectIDs
.indexOf(userId
) !== -1) {
1309 elData(userRow
, 'banned', data
.actionName
=== 'ban');
1313 $('.jsBanButton').each(function(index
, button
) {
1314 var $button
= $(button
);
1315 if (WCF
.inArray($button
.data('objectID'), data
.objectIDs
)) {
1316 if (data
.actionName
== 'unban') {
1317 $button
.data('banned', false).attr('data-tooltip', $button
.data('banMessage'));
1318 $button
[0].querySelector("fa-icon").setIcon("unlock");
1321 $button
.data('banned', true).attr('data-tooltip', $button
.data('unbanMessage'));
1322 $button
[0].querySelector("fa-icon").setIcon("lock");
1327 var $notification
= new WCF
.System
.Notification();
1328 $notification
.show();
1330 WCF
.Clipboard
.reload();
1332 if (data
.actionName
== 'ban') {
1333 this._dialog
.wcfDialog('close');
1336 WCF
.System
.Event
.fireEvent('com.woltlab.wcf.acp.user', 'refresh', {userIds
: data
.objectIDs
});
1341 * Namespace for user group management.
1343 WCF
.ACP
.User
.Group
= { };
1346 * Handles copying user groups.
1348 WCF
.ACP
.User
.Group
.Copy
= Class
.extend({
1350 * id of the copied group
1356 * Initializes a new instance of WCF.ACP.User.Group.Copy.
1358 * @param integer groupID
1360 init: function(groupID
) {
1361 this._groupID
= groupID
;
1363 $('.jsButtonUserGroupCopy').click($.proxy(this._click
, this));
1367 * Handles clicking on a 'copy user group' button.
1369 _click: function() {
1370 var $template
= $('<div class="section" />');
1371 $template
.append($('<dl class="wide"><dt /><dd><label><input type="checkbox" id="copyMembers" value="1" /> ' + WCF
.Language
.get('wcf.acp.group.copy.copyMembers') + '</label><small>' + WCF
.Language
.get('wcf.acp.group.copy.copyMembers.description') + '</small></dd></dl>'));
1372 $template
.append($('<dl class="wide"><dt /><dd><label><input type="checkbox" id="copyUserGroupOptions" value="1" /> ' + WCF
.Language
.get('wcf.acp.group.copy.copyUserGroupOptions') + '</label><small>' + WCF
.Language
.get('wcf.acp.group.copy.copyUserGroupOptions.description') + '</small></dd></dl>'));
1373 $template
.append($('<dl class="wide"><dt /><dd><label><input type="checkbox" id="copyACLOptions" value="1" /> ' + WCF
.Language
.get('wcf.acp.group.copy.copyACLOptions') + '</label><small>' + WCF
.Language
.get('wcf.acp.group.copy.copyACLOptions.description') + '</small></dd></dl>'));
1375 WCF
.System
.Confirmation
.show(WCF
.Language
.get('wcf.acp.group.copy.confirmMessage'), $.proxy(function(action
) {
1376 if (action
=== 'confirm') {
1377 new WCF
.Action
.Proxy({
1381 className
: 'wcf\\data\\user\\group\\UserGroupAction',
1382 objectIDs
: [ this._groupID
],
1384 copyACLOptions
: $('#copyACLOptions').is(':checked'),
1385 copyMembers
: $('#copyMembers').is(':checked'),
1386 copyUserGroupOptions
: $('#copyUserGroupOptions').is(':checked')
1389 success: function(data
) {
1390 window
.location
= data
.returnValues
.redirectURL
;
1394 }, this), '', $template
, true);
1399 * Generic implementation to enable users.
1401 WCF
.ACP
.User
.EnableHandler
= {
1404 * @var WCF.Action.Proxy
1409 * Initializes WCF.ACP.User.EnableHandler on first use.
1412 this._proxy
= new WCF
.Action
.Proxy({
1413 success
: $.proxy(this._success
, this)
1416 $('.jsEnableButton').click($.proxy(function(event
) {
1417 var $button
= $(event
.currentTarget
);
1418 if ($button
.data('enabled')) {
1419 this.disable([ $button
.data('objectID') ]);
1422 this.enable([ $button
.data('objectID') ]);
1426 require(['EventHandler'], function(EventHandler
) {
1427 EventHandler
.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.user', this._clipboardAction
.bind(this));
1432 * Reacts to executed clipboard actions.
1434 * @param {object<string, *>} actionData data of the executed clipboard action
1436 _clipboardAction: function(actionData
) {
1437 if (actionData
.data
.actionName
=== 'com.woltlab.wcf.user.enable') {
1438 this.enable(actionData
.data
.parameters
.objectIDs
);
1445 * @param array<integer> userIDs
1447 disable: function(userIDs
) {
1448 this._proxy
.setOption('data', {
1449 actionName
: 'disable',
1450 className
: 'wcf\\data\\user\\UserAction',
1453 this._proxy
.sendRequest();
1459 * @param array<integer> userIDs
1461 enable: function(userIDs
) {
1462 this._proxy
.setOption('data', {
1463 actionName
: 'enable',
1464 className
: 'wcf\\data\\user\\UserAction',
1467 this._proxy
.sendRequest();
1471 * Handles successful AJAX calls.
1473 * @param object data
1474 * @param string textStatus
1475 * @param jQuery jqXHR
1477 _success: function(data
, textStatus
, jqXHR
) {
1478 elBySelAll('.jsUserRow', undefined, function(userRow
) {
1479 var userId
= parseInt(elData(userRow
, 'object-id'), 10);
1480 if (data
.objectIDs
.indexOf(userId
) !== -1) {
1481 elData(userRow
, 'enabled', data
.actionName
=== 'enable');
1485 $('.jsEnableButton').each(function(index
, button
) {
1486 var $button
= $(button
);
1487 if (WCF
.inArray($button
.data('objectID'), data
.objectIDs
)) {
1488 if (data
.actionName
== 'disable') {
1489 $button
.data('enabled', false).attr('data-tooltip', $button
.data('enableMessage'));
1490 $button
[0].querySelector("fa-icon").setIcon("square");
1493 $button
.data('enabled', true).attr('data-tooltip', $button
.data('disableMessage'));
1494 $button
[0].querySelector("fa-icon").setIcon("square-check");
1499 var $notification
= new WCF
.System
.Notification();
1500 $notification
.show(function() { window
.location
.reload(); });
1502 WCF
.System
.Event
.fireEvent('com.woltlab.wcf.acp.user', 'refresh', {userIds
: data
.objectIDs
});
1507 * Handles the send new password clipboard action.
1509 WCF
.ACP
.User
.SendNewPasswordHandler
= {
1511 * Initializes WCF.ACP.User.SendNewPasswordHandler on first use.
1514 require(['EventHandler'], function(EventHandler
) {
1515 EventHandler
.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.user', this._clipboardAction
.bind(this));
1520 * Reacts to executed clipboard actions.
1522 * @param {object<string, *>} actionData data of the executed clipboard action
1524 _clipboardAction: function(actionData
) {
1525 if (actionData
.data
.actionName
=== 'com.woltlab.wcf.user.sendNewPassword') {
1526 require(['Language', 'Ui/Confirmation', 'WoltLabSuite/Core/Acp/Ui/Worker'], function(Language
, UiConfirmation
, AcpUiWorker
) {
1527 UiConfirmation
.show({
1530 dialogId
: 'sendingNewPasswords',
1531 dialogTitle
: Language
.get('wcf.acp.user.sendNewPassword.workerTitle'),
1532 className
: 'wcf\\system\\worker\\SendNewPasswordWorker',
1534 userIDs
: actionData
.data
.parameters
.objectIDs
1538 message
: actionData
.data
.parameters
.confirmMessage
,
1546 * Namespace for stat-related classes.
1551 * Shows the daily stat chart.
1553 WCF
.ACP
.Stat
.Chart
= Class
.extend({
1555 this._proxy
= new WCF
.Action
.Proxy({
1556 success
: $.proxy(this._success
, this)
1559 $('#statRefreshButton').click($.proxy(this._refresh
, this));
1564 _refresh: function() {
1565 var $objectTypeIDs
= [ ];
1566 $('input[name=objectTypeID]:checked').each(function() {
1567 $objectTypeIDs
.push($(this).val());
1570 if (!$objectTypeIDs
.length
) return;
1572 this._proxy
.setOption('data', {
1573 className
: 'wcf\\data\\stat\\daily\\StatDailyAction',
1574 actionName
: 'getData',
1576 startDate
: $('#startDateDatePicker').val(),
1577 endDate
: $('#endDateDatePicker').val(),
1578 value
: $('input[name=value]:checked').val(),
1579 dateGrouping
: $('input[name=dateGrouping]:checked').val(),
1580 objectTypeIDs
: $objectTypeIDs
1583 this._proxy
.sendRequest();
1586 _success: function(data
) {
1587 switch ($('input[name=dateGrouping]:checked').val()) {
1589 var $minTickSize
= [1, "year"];
1590 var $timeFormat
= WCF
.Language
.get('wcf.acp.stat.timeFormat.yearly');
1593 var $minTickSize
= [1, "month"];
1594 var $timeFormat
= WCF
.Language
.get('wcf.acp.stat.timeFormat.monthly');
1597 var $minTickSize
= [7, "day"];
1598 var $timeFormat
= WCF
.Language
.get('wcf.acp.stat.timeFormat.weekly');
1601 var $minTickSize
= [1, "day"];
1602 var $timeFormat
= WCF
.Language
.get('wcf.acp.stat.timeFormat.daily');
1619 minTickSize
: $minTickSize
,
1620 timeformat
: $timeFormat
,
1621 monthNames
: WCF
.Language
.get('__monthsShort')
1626 tickFormatter: function(val
) {
1627 return WCF
.String
.addThousandsSeparator(val
);
1633 for (var $key
in data
.returnValues
) {
1634 var $row
= data
.returnValues
[$key
];
1635 for (var $i
= 0; $i
< $row
.data
.length
; $i
++) {
1636 $row
.data
[$i
][0] *= 1000;
1642 $.plot("#chart", $data
, options
);
1644 require(['Ui/Alignment'], function (UiAlignment
) {
1645 var span
= elCreate('span');
1646 span
.style
.setProperty('position', 'absolute', '');
1647 document
.body
.appendChild(span
);
1648 $("#chart").on("plothover", function(event
, pos
, item
) {
1650 span
.style
.setProperty('top', item
.pageY
+ 'px', '');
1651 span
.style
.setProperty('left', item
.pageX
+ 'px', '');
1652 $("#chartTooltip").html(item
.series
.xaxis
.tickFormatter(item
.datapoint
[0], item
.series
.xaxis
) + ', ' + WCF
.String
.formatNumeric(item
.datapoint
[1]) + ' ' + item
.series
.label
).show();
1653 UiAlignment
.set($("#chartTooltip")[0], span
, {
1655 horizontal
: 'center',
1660 $("#chartTooltip").hide();
1665 if (!$data
.length
) {
1666 $('#chart').append('<p style="position: absolute; font-size: 1.2rem; text-align: center; top: 50%; margin-top: -20px; width: 100%">' + WCF
.Language
.get('wcf.acp.stat.noData') + '</p>');
1669 elBySel('.contentHeader > .contentTitle').scrollIntoView({ behavior
: 'smooth' });
1674 * Namespace for ACP ad management.
1679 * Handles the location of an ad during ad creation/editing.
1681 WCF
.ACP
.Ad
.LocationHandler
= Class
.extend({
1683 * fieldset of the page conditions
1686 _pageConditions
: null,
1689 * select elements for the page controller condition
1695 * page controller condition container
1698 _pageSelectionContainer
: null,
1701 * Initializes a new WCF.ACP.Ad.LocationHandler object.
1703 * @param {object} variablesDescriptions
1705 init: function(variablesDescriptions
) {
1706 this._variablesDescriptions
= variablesDescriptions
;
1708 this._pageConditions
= $('#pageConditions');
1709 this._pageInputs
= $('input[name="pageIDs[]"]');
1711 this._variablesDescriptionsList
= $('#ad').next('small').children('ul');
1713 this._pageSelectionContainer
= $(this._pageInputs
[0]).parents('dl:eq(0)');
1715 // hide the page controller elements
1716 this._hidePageSelection(true);
1718 $('#objectTypeID').on('change', $.proxy(this._setPageController
, this));
1720 this._setPageController();
1722 $('#adForm').submit($.proxy(this._submit
, this));
1726 * Hides the page selection form field.
1730 _hidePageSelection: function(addEventListeners
) {
1731 this._pageSelectionContainer
.prev('dl').hide();
1732 this._pageSelectionContainer
.hide();
1734 // fix the margin of a potentially next page condition element
1735 this._pageSelectionContainer
.next('dl').css('margin-top', 0);
1737 var section
= this._pageSelectionContainer
.parent('section');
1738 if (!section
.children('dl:visible').length
) {
1741 var nextSection
= section
.next('section');
1743 nextSection
.css('margin-top', 0);
1745 if (addEventListeners
) {
1746 require(['EventHandler'], function(EventHandler
) {
1747 EventHandler
.add('com.woltlab.wcf.pageConditionDependence', 'checkVisivility', function() {
1748 if (section
.is(':visible')) {
1749 nextSection
.css('margin-top', '40px');
1752 nextSection
.css('margin-top', 0);
1762 * Shows the page selection form field.
1766 _showPageSelection: function() {
1767 this._pageSelectionContainer
.prev('dl').show();
1768 this._pageSelectionContainer
.show();
1769 this._pageSelectionContainer
.next('dl').css('margin-top', '40px');
1771 var section
= this._pageSelectionContainer
.parent('section');
1774 var nextSection
= section
.next('section');
1776 nextSection
.css('margin-top', '40px');
1781 * Sets the page controller based on the selected ad location.
1783 _setPageController: function() {
1784 var option
= $('#objectTypeID').find('option:checked');
1785 var parent
= option
.parent();
1787 // the page controller can be explicitly set for global positions
1788 if (parent
.is('optgroup') && parent
.data('categoryName') === 'com.woltlab.wcf.global') {
1789 this._showPageSelection();
1792 this._hidePageSelection();
1794 require(['Core'], function(Core
) {
1795 var input
, triggerEvent
;
1797 // select the related page
1798 for (var i
= 0, length
= this._pageInputs
.length
; i
< length
; i
++) {
1799 input
= this._pageInputs
[i
];
1800 triggerEvent
= false;
1802 if (option
.data('page') && elData(input
, 'identifier') === option
.data('page')) {
1803 if (!input
.checked
) triggerEvent
= true;
1805 input
.checked
= true;
1808 if (input
.checked
) triggerEvent
= true;
1810 input
.checked
= false;
1813 if (triggerEvent
) Core
.triggerEvent(this._pageInputs
[i
], 'change');
1818 this._variablesDescriptionsList
.children(':not(.jsDefaultItem)').remove();
1820 var objectTypeId
= $('#objectTypeID').val();
1821 if (objectTypeId
in this._variablesDescriptions
) {
1822 this._variablesDescriptionsList
[0].innerHTML
+= this._variablesDescriptions
[objectTypeId
];
1827 * Handles submitting the ad form.
1829 _submit: function() {
1830 if (this._pageConditions
.is(':hidden')) {
1831 // remove hidden page condition form elements to avoid creation
1832 // of these conditions
1833 this._pageConditions
.find('select, input').remove();
1835 else if (this._pageSelectionContainer
.is(':hidden')) {
1836 // reset page controller conditions to avoid creation of
1837 // unnecessary conditions
1838 for (var i
= 0, length
= this._pageInputs
.length
; i
< length
; i
++) {
1839 this._pageInputs
[i
].checked
= false;
1846 * Initialize WCF.ACP.Tag namespace.
1851 * Handles setting tags as synonyms of another tag by clipboard.
1853 WCF
.ACP
.Tag
.SetAsSynonymsHandler
= Class
.extend({
1855 * dialog to select the "main" tag
1861 * ids of the selected tags
1862 * @var array<integer>
1867 * Initializes the SetAsSynonymsHandler object.
1870 require(['EventHandler'], function(EventHandler
) {
1871 EventHandler
.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.tag', this._clipboardAction
.bind(this));
1876 * Reacts to executed clipboard actions.
1878 * @param {object<string, *>} actionData data of the executed clipboard action
1880 _clipboardAction: function(actionData
) {
1881 if (actionData
.data
.actionName
=== 'com.woltlab.wcf.tag.setAsSynonyms') {
1882 this._objectIDs
= actionData
.data
.parameters
.objectIDs
;
1883 if (this._dialog
=== null) {
1884 this._dialog
= $('<div id="setAsSynonymsDialog" />').hide().appendTo(document
.body
);
1885 this._dialog
.wcfDialog({
1887 title
: WCF
.Language
.get('wcf.acp.tag.setAsSynonyms')
1891 this._dialog
.html(actionData
.data
.parameters
.template
);
1892 $button
= this._dialog
.find('button[data-type="submit"]').disable().click($.proxy(this._submit
, this));
1893 this._dialog
.find('input[type=radio]').change(function() { $button
.enable(); });
1898 * Saves the tags as synonyms.
1900 _submit: function() {
1901 new WCF
.Action
.Proxy({
1904 actionName
: 'setAsSynonyms',
1905 className
: 'wcf\\data\\tag\\TagAction',
1906 objectIDs
: this._objectIDs
,
1908 tagID
: this._dialog
.find('input[name="tagID"]:checked').val()
1911 success
: $.proxy(function() {
1912 this._dialog
.wcfDialog('close');
1914 new WCF
.System
.Notification().show(function() {
1915 window
.location
.reload();