Unify the terms 'Staff' and 'Team'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / acp / js / WCF.ACP.js
... / ...
CommitLineData
1/**
2 * Class and function collection for WCF ACP
3 *
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>
7 */
8
9/**
10 * Initialize WCF.ACP namespace
11 */
12WCF.ACP = { };
13
14/**
15 * Namespace for ACP application management.
16 */
17WCF.ACP.Application = { };
18
19/**
20 * Namespace for ACP cronjob management.
21 */
22WCF.ACP.Cronjob = { };
23
24/**
25 * Handles the manual execution of cronjobs.
26 */
27WCF.ACP.Cronjob.ExecutionHandler = Class.extend({
28 /**
29 * notification object
30 * @var WCF.System.Notification
31 */
32 _notification: null,
33
34 /**
35 * action proxy
36 * @var WCF.Action.Proxy
37 */
38 _proxy: null,
39
40 /**
41 * Initializes WCF.ACP.Cronjob.ExecutionHandler object.
42 */
43 init: function() {
44 this._proxy = new WCF.Action.Proxy({
45 success: $.proxy(this._success, this)
46 });
47
48 $('.jsCronjobRow .jsExecuteButton').click($.proxy(this._click, this));
49
50 this._notification = new WCF.System.Notification(WCF.Language.get('wcf.global.success'), 'success');
51 },
52
53 /**
54 * Handles a click on an execute button.
55 *
56 * @param object event
57 */
58 _click: function(event) {
59 this._proxy.setOption('data', {
60 actionName: 'execute',
61 className: 'wcf\\data\\cronjob\\CronjobAction',
62 objectIDs: [ $(event.target).data('objectID') ]
63 });
64
65 this._proxy.sendRequest();
66 },
67
68 /**
69 * Handles successful cronjob execution.
70 *
71 * @param object data
72 * @param string textStatus
73 * @param jQuery jqXHR
74 */
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');
79
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();
85 }
86
87 this._notification.show();
88
89 return false;
90 }
91 }, this));
92 }
93});
94
95/**
96 * Handles the cronjob log list.
97 */
98WCF.ACP.Cronjob.LogList = Class.extend({
99 /**
100 * error message dialog
101 * @var jQuery
102 */
103 _dialog: null,
104
105 /**
106 * Initializes WCF.ACP.Cronjob.LogList object.
107 */
108 init: function() {
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({
114 autoSend: true,
115 data: {
116 actionName: 'clearAll',
117 className: 'wcf\\data\\cronjob\\log\\CronjobLogAction'
118 },
119 success: function() {
120 window.location.reload();
121 }
122 });
123 }
124 });
125 });
126
127 // bind event listeners to error badges
128 $('.jsCronjobError').click($.proxy(this._showError, this));
129 },
130
131 /**
132 * Shows certain error message
133 *
134 * @param object event
135 */
136 _showError: function(event) {
137 var $errorBadge = $(event.currentTarget);
138
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')
143 });
144 }
145 else {
146 this._dialog.html('<pre>' + $errorBadge.next().html() + '</pre>');
147 this._dialog.wcfDialog('open');
148 }
149 }
150});
151
152/**
153 * Namespace for ACP package management.
154 */
155WCF.ACP.Package = { };
156
157/**
158 * Provides the package installation.
159 *
160 * @param integer queueID
161 * @param string actionName
162 */
163WCF.ACP.Package.Installation = Class.extend({
164 /**
165 * package installation type
166 * @var string
167 */
168 _actionName: 'InstallPackage',
169
170 /**
171 * additional parameters send in all requests
172 * @var object
173 */
174 _additionalRequestParameters: {},
175
176 /**
177 * true, if rollbacks are supported
178 * @var boolean
179 */
180 _allowRollback: false,
181
182 /**
183 * dialog object
184 * @var jQuery
185 */
186 _dialog: null,
187
188 /**
189 * name of the language item with the title of the dialog
190 * @var string
191 */
192 _dialogTitle: '',
193
194 /**
195 * action proxy
196 * @var WCF.Action.Proxy
197 */
198 _proxy: null,
199
200 /**
201 * package installation queue id
202 * @var integer
203 */
204 _queueID: 0,
205
206 /**
207 * true, if dialog should be rendered again
208 * @var boolean
209 */
210 _shouldRender: false,
211
212 /**
213 * Initializes the WCF.ACP.Package.Installation class.
214 *
215 * @param integer queueID
216 * @param string actionName
217 * @param boolean allowRollback
218 * @param boolean isUpdate
219 * @param object additionalRequestParameters
220 */
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 || {};
226
227 this._dialogTitle = 'wcf.acp.package.' + (isUpdate ? 'update' : 'install') + '.title';
228 if (this._actionName === 'UninstallPackage') {
229 this._dialogTitle = 'wcf.acp.package.uninstallation.title';
230 }
231
232 this._initProxy();
233 this._init();
234 },
235
236 /**
237 * Initializes the WCF.Action.Proxy object.
238 */
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];
244 if ($part.length) {
245 if ($actionName.length) $actionName += '-';
246 $actionName += $part.toLowerCase();
247 }
248 }
249
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
255 });
256 },
257
258 /**
259 * Initializes the package installation.
260 */
261 _init: function() {
262 const button = document.getElementById('submitButton');
263 button?.addEventListener(
264 'click',
265 () => {
266 button.disabled = true;
267 this.prepareInstallation();
268 }
269 );
270 },
271
272 /**
273 * Handles erroneous AJAX requests.
274 */
275 _failure: function() {
276 if (this._dialog !== null) {
277 $('#packageInstallationProgress').removeAttr('value');
278 this._setIcon('xmark');
279 }
280
281 if (!this._allowRollback) {
282 return;
283 }
284
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));
289
290 $('#packageInstallationInnerContentContainer').show();
291
292 this._dialog.wcfDialog('render');
293 }, this));
294 }
295 },
296
297 /**
298 * Performs a rollback.
299 *
300 * @param object event
301 */
302 _rollback: function(event) {
303 this._setIcon('spinner');
304
305 if (event) {
306 $(event.currentTarget).disable();
307 }
308
309 this._executeStep('rollback');
310 },
311
312 /**
313 * Prepares installation dialog.
314 */
315 prepareInstallation: function() {
316 if (document.activeElement) {
317 document.activeElement.blur();
318 }
319
320 require(['WoltLabSuite/Core/Ajax/Status'], ({show}) => show());
321 this._proxy.setOption('data', this._getParameters());
322 this._proxy.sendRequest();
323 },
324
325 /**
326 * Returns parameters to prepare installation.
327 *
328 * @return object
329 */
330 _getParameters: function() {
331 return $.extend({}, this._additionalRequestParameters, {
332 queueID: this._queueID,
333 step: 'prepare'
334 });
335 },
336
337 /**
338 * Handles successful AJAX requests.
339 *
340 * @param object data
341 * @param string textStatus
342 * @param jQuery jqXHR
343 */
344 _success: function(data, textStatus, jqXHR) {
345 this._shouldRender = false;
346
347 if (typeof window._trackPackageStep === 'function') window._trackPackageStep(this._actionName, data);
348
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({
352 closable: false,
353 title: WCF.Language.get(this._dialogTitle)
354 });
355 require(['WoltLabSuite/Core/Ajax/Status'], ({hide}) => hide());
356 }
357
358 this._setIcon('spinner');
359
360 if (data.step == 'rollback') {
361 this._dialog.wcfDialog('close');
362 this._dialog.remove();
363
364 setTimeout(function () {
365 var $uninstallation = new WCF.ACP.Package.Uninstallation();
366 $uninstallation.start(data.packageID);
367 }, 200);
368
369 return;
370 }
371
372 // receive new queue id
373 if (data.queueID) {
374 this._queueID = data.queueID;
375 }
376
377 // update template
378 if (data.template && !data.ignoreTemplate) {
379 this._dialog.html(data.template);
380 this._shouldRender = true;
381 }
382
383 // update progress
384 if (data.progress) {
385 $('#packageInstallationProgress').attr('value', data.progress).text(data.progress + '%');
386 $('#packageInstallationProgressLabel').text(data.progress + '%');
387 }
388
389 // update action
390 if (data.currentAction) {
391 $('#packageInstallationAction').html(data.currentAction);
392 }
393
394 // handle success
395 if (data.step === 'success') {
396 this._setIcon('check');
397
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() {
401 $(this).disable();
402 window.location = data.redirectLocation;
403 });
404
405 $('#packageInstallationInnerContentContainer').show();
406
407 $(document).keydown(function(event) {
408 if (event.which === $.ui.keyCode.ENTER) {
409 $button.trigger('click');
410 }
411 });
412
413 this._dialog.wcfDialog('render');
414 }, this));
415
416 return;
417 }
418
419 // handle inner template
420 if (data.innerTemplate) {
421 var self = this;
422 $('#packageInstallationInnerContent').html(data.innerTemplate).find('input').keyup(function(event) {
423 if (event.keyCode === $.ui.keyCode.ENTER) {
424 self._submit(data);
425 }
426 });
427
428 // create button to handle next step
429 if (data.step && data.node) {
430 $('#packageInstallationProgress').removeAttr('value');
431 this._setIcon('question');
432
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();
436
437 this._submit(data);
438 }, this));
439 }
440
441 $('#packageInstallationInnerContentContainer').show();
442
443 this._dialog.wcfDialog('render');
444 return;
445 }
446
447 // purge content
448 this._purgeTemplateContent($.proxy(function() {
449 // render container
450 if (this._shouldRender) {
451 this._dialog.wcfDialog('render');
452 }
453
454 // execute next step
455 if (data.step && data.node) {
456 this._executeStep(data.step, data.node);
457 }
458 }, this));
459 },
460
461 /**
462 * Submits the dialog content.
463 *
464 * @param object data
465 */
466 _submit: function(data) {
467 this._setIcon('spinner');
468
469 // collect form values
470 var $additionalData = { };
471 $('#packageInstallationInnerContent input').each(function(index, inputElement) {
472 var $inputElement = $(inputElement);
473 var $type = $inputElement.attr('type');
474
475 if (($type != 'checkbox' && $type != 'radio') || $inputElement.prop('checked')) {
476 var $name = $inputElement.attr('name');
477 if ($name.match(/(.*)\[([^[]*)\]$/)) {
478 $name = RegExp.$1;
479 $key = RegExp.$2;
480
481 if ($additionalData[$name] === undefined) {
482 if ($key) {
483 $additionalData[$name] = { };
484 }
485 else {
486 $additionalData[$name] = [ ];
487 }
488 }
489
490 if ($key) {
491 $additionalData[$name][$key] = $inputElement.val();
492 }
493 else {
494 $additionalData[$name].push($inputElement.val());
495 }
496 }
497 else {
498 $additionalData[$name] = $inputElement.val();
499 }
500 }
501 });
502
503 this._executeStep(data.step, data.node, $additionalData);
504 },
505
506 /**
507 * Purges template content.
508 *
509 * @param function callback
510 */
511 _purgeTemplateContent: function(callback) {
512 if ($('#packageInstallationInnerContent').children().length) {
513 $('#packageInstallationInnerContentContainer').hide();
514 $('#packageInstallationInnerContent').empty();
515
516 this._shouldRender = true;
517 }
518
519 callback();
520 },
521
522 /**
523 * Executes the next installation step.
524 *
525 * @param string step
526 * @param string node
527 * @param object additionalData
528 */
529 _executeStep: function(step, node, additionalData) {
530 if (!additionalData) additionalData = { };
531
532 var $data = $.extend({}, this._additionalRequestParameters, {
533 node: node,
534 queueID: this._queueID,
535 step: step
536 }, additionalData);
537
538 this._proxy.setOption('data', $data);
539 this._proxy.sendRequest();
540 },
541
542 /**
543 * Sets the icon with the given name as the current installation status icon.
544 *
545 * @param string iconName
546 */
547 _setIcon: function(iconName) {
548 const icon = this._dialog.find('.jsPackageInstallationStatus fa-icon');
549 if (icon.length === 1) {
550 icon[0].setIcon(iconName);
551 }
552 }
553});
554
555/**
556 * Handles canceling the package installation at the package installation
557 * confirm page.
558 */
559WCF.ACP.Package.Installation.Cancel = Class.extend({
560 /**
561 * Creates a new instance of WCF.ACP.Package.Installation.Cancel.
562 *
563 * @param integer queueID
564 */
565 init: function(queueID) {
566 $('#backButton').click(function() {
567 new WCF.Action.Proxy({
568 autoSend: true,
569 data: {
570 actionName: 'cancelInstallation',
571 className: 'wcf\\data\\package\\installation\\queue\\PackageInstallationQueueAction',
572 objectIDs: [ queueID ]
573 },
574 success: function(data) {
575 window.location = data.returnValues.url;
576 }
577 });
578 });
579 }
580});
581
582/**
583 * Provides the package uninstallation.
584 *
585 * @param jQuery elements
586 * @param string wcfPackageListURL
587 */
588WCF.ACP.Package.Uninstallation = WCF.ACP.Package.Installation.extend({
589 /**
590 * list of uninstallation buttons
591 * @var jQuery
592 */
593 _elements: null,
594
595 /**
596 * current package id
597 * @var integer
598 */
599 _packageID: 0,
600
601 /**
602 * Initializes the WCF.ACP.Package.Uninstallation class.
603 *
604 * @param jQuery elements
605 */
606 init: function(elements) {
607 this._elements = elements;
608 this._packageID = 0;
609
610 if (this._elements !== undefined && this._elements.length) {
611 this._super(0, 'UninstallPackage');
612 }
613 },
614
615 /**
616 * Begins a package uninstallation without user action.
617 *
618 * @param integer packageID
619 */
620 start: function(packageID) {
621 this._actionName = 'UninstallPackage';
622 this._packageID = packageID;
623 this._queueID = 0;
624 this._dialogTitle = 'wcf.acp.package.uninstallation.title';
625
626 this._initProxy();
627 this.prepareInstallation();
628 },
629
630 /**
631 * @see WCF.ACP.Package.Installation.init()
632 */
633 _init: function() {
634 this._elements.click($.proxy(this._showConfirmationDialog, this));
635 },
636
637 /**
638 * Displays a confirmation dialog prior to package uninstallation.
639 *
640 * @param object event
641 */
642 _showConfirmationDialog: function(event) {
643 var $element = $(event.currentTarget);
644
645 var self = this;
646 WCF.System.Confirmation.show($element.data('confirmMessage'), function(action) {
647 if (action === 'confirm') {
648 self._packageID = $element.data('objectID');
649 self.prepareInstallation();
650 }
651 }, undefined, undefined, true);
652 },
653
654 /**
655 * @see WCF.ACP.Package.Installation._getParameters()
656 */
657 _getParameters: function() {
658 return {
659 packageID: this._packageID,
660 step: 'prepare'
661 };
662 }
663});
664
665WCF.ACP.Package.Server = { };
666
667WCF.ACP.Package.Server.Installation = Class.extend({
668 _proxy: null,
669 _selectedPackage: '',
670
671 init: function() {
672 this._dialog = null;
673 this._selectedPackage = null;
674
675 this._proxy = new WCF.Action.Proxy({
676 success: $.proxy(this._success, this)
677 });
678 },
679
680 bind: function() {
681 $('.jsButtonPackageInstall').removeClass('jsButtonPackageInstall').click($.proxy(this._click, this));
682 },
683
684 /**
685 * Prepares a package installation.
686 *
687 * @param object event
688 */
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();
696 }
697 }, this), undefined, undefined, true);
698 },
699
700 /**
701 * Handles successful AJAX requests.
702 *
703 * @param object data
704 */
705 _success: function(data) {
706 if (data.returnValues.queueID) {
707 if (this._dialog !== null) {
708 this._dialog.wcfDialog('close');
709 }
710
711 var $installation = new WCF.ACP.Package.Installation(data.returnValues.queueID, undefined, false);
712 $installation.prepareInstallation();
713 }
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')
719 });
720 }
721 else {
722 this._dialog.html(data.returnValues.template).wcfDialog('open');
723 }
724
725 this._dialog.find('.formSubmit > button').click($.proxy(this._submitAuthentication, this));
726 }
727 },
728
729 /**
730 * Submits authentication data for current update server.
731 *
732 * @param object event
733 */
734 _submitAuthentication: function(event) {
735 var $usernameField = $('#packageUpdateServerUsername');
736 var $passwordField = $('#packageUpdateServerPassword');
737
738 // remove error messages if any
739 $usernameField.next('small.innerError').remove();
740 $passwordField.next('small.innerError').remove();
741
742 var $continue = true;
743 if ($.trim($usernameField.val()) === '') {
744 $('<small class="innerError">' + WCF.Language.get('wcf.global.form.error.empty') + '</small>').insertAfter($usernameField);
745 $continue = false;
746 }
747
748 if ($.trim($passwordField.val()) === '') {
749 $('<small class="innerError">' + WCF.Language.get('wcf.global.form.error.empty') + '</small>').insertAfter($passwordField);
750 $continue = false;
751 }
752
753 if ($continue) {
754 this._prepareInstallation($(event.currentTarget).data('packageUpdateServerID'));
755 }
756 },
757
758 /**
759 * Prepares package installation.
760 *
761 * @param integer packageUpdateServerID
762 */
763 _prepareInstallation: function(packageUpdateServerID) {
764 var $parameters = {
765 'packages': { }
766 };
767 $parameters['packages'][this._selectedPackage] = this._selectedPackageVersion;
768
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())
775 };
776 }
777
778 this._proxy.setOption('data', {
779 actionName: 'prepareInstallation',
780 className: 'wcf\\data\\package\\update\\PackageUpdateAction',
781 parameters: $parameters
782 });
783 this._proxy.sendRequest();
784 },
785});
786
787/**
788 * Namespace for package update related classes.
789 */
790WCF.ACP.Package.Update = { };
791
792/**
793 * Searches for available updates.
794 *
795 * @param boolean bindOnExistingButtons
796 */
797WCF.ACP.Package.Update.Search = Class.extend({
798 /** @var {Element} */
799 _button: null,
800
801 /**
802 * dialog overlay
803 * @var jQuery
804 */
805 _dialog: null,
806
807 /**
808 * Initializes the WCF.ACP.Package.SearchForUpdates class.
809 *
810 * @param {boolean} bindOnExistingButtons
811 */
812 init: function(bindOnExistingButtons) {
813 this._dialog = null;
814
815 if (!bindOnExistingButtons === true) {
816 $(`<li>
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>
820 </button>
821 </li>`).prependTo($('.contentHeaderNavigation > ul'));
822 }
823
824 this._button = elBySel('.jsButtonSearchForUpdates');
825 if (this._button) {
826 this._button.addEventListener('click', this._click.bind(this));
827
828 const url = new URL(window.location.href);
829 if (url.searchParams.has("searchForUpdates")) {
830 this._click();
831 }
832 }
833 },
834
835 /**
836 * Handles clicks on the search button.
837 *
838 * @param {Event} event
839 */
840 _click: function(event) {
841 event?.preventDefault();
842
843 if (this._button.classList.contains('disabled')) {
844 return;
845 }
846
847 this._button.classList.add('disabled');
848
849 if (this._dialog === null) {
850 new WCF.Action.Proxy({
851 autoSend: true,
852 data: {
853 actionName: 'searchForUpdates',
854 className: 'wcf\\data\\package\\update\\PackageUpdateAction',
855 parameters: {
856 ignoreCache: 1
857 }
858 },
859 success: $.proxy(this._success, this)
860 });
861 }
862 else {
863 this._dialog.wcfDialog('open');
864 }
865 },
866
867 /**
868 * Handles successful AJAX requests.
869 *
870 * @param object data
871 * @param string textStatus
872 * @param jQuery jqXHR
873 */
874 _success: function(data, textStatus, jqXHR) {
875 if (typeof window._trackSearchForUpdates === 'function') {
876 window._trackSearchForUpdates(data);
877 return;
878 }
879
880 if (data.returnValues.url) {
881 window.location = data.returnValues.url;
882 }
883 else {
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')
887 });
888
889 this._button.classList.remove('disabled');
890 }
891 }
892});
893
894/**
895 * Worker support for ACP.
896 *
897 * @param string dialogID
898 * @param string className
899 * @param string title
900 * @param object parameters
901 * @param object callback
902 *
903 * @deprecated 3.1 - please use `WoltLabSuite/Core/Acp/Ui/Worker` instead
904 */
905WCF.ACP.Worker = Class.extend({
906 /**
907 * Initializes a new worker instance.
908 *
909 * @param string dialogID
910 * @param string className
911 * @param string title
912 * @param object parameters
913 * @param object callback
914 */
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'.");
918 }
919
920 require(['WoltLabSuite/Core/Acp/Ui/Worker'], function(AcpUiWorker) {
921 new AcpUiWorker({
922 // dialog
923 dialogId: dialogID,
924 dialogTitle: title,
925
926 // ajax
927 className: className,
928 parameters: parameters
929 });
930 });
931 }
932});
933
934/**
935 * Namespace for category-related functions.
936 */
937WCF.ACP.Category = { };
938
939/**
940 * Handles collapsing categories.
941 *
942 * @param string className
943 * @param integer objectTypeID
944 */
945WCF.ACP.Category.Collapsible = WCF.Collapsible.SimpleRemote.extend({
946 /**
947 * @see WCF.Collapsible.Remote.init()
948 */
949 init: function(className) {
950 var sortButton = $('.formSubmit > button[data-type="submit"]');
951 if (sortButton) {
952 sortButton.click($.proxy(this._sort, this));
953 }
954
955 this._super(className);
956 },
957
958 /**
959 * @see WCF.Collapsible.Remote._getButtonContainer()
960 */
961 _getButtonContainer: function(containerID) {
962 return $('#' + containerID + ' > .buttons');
963 },
964
965 /**
966 * @see WCF.Collapsible.Remote._getContainers()
967 */
968 _getContainers: function() {
969 return $('.jsCategory').has('ol').has('li');
970 },
971
972 /**
973 * @see WCF.Collapsible.Remote._getTarget()
974 */
975 _getTarget: function(containerID) {
976 return $('#' + containerID + ' > ol');
977 },
978
979 /**
980 * Handles a click on the sort button.
981 */
982 _sort: function() {
983 // remove existing collapsible buttons
984 $('.collapsibleButton').remove();
985
986 // reinit containers
987 this._containers = { };
988 this._containerData = { };
989
990 var $containers = this._getContainers();
991 if ($containers.length == 0) {
992 console.debug('[WCF.ACP.Category.Collapsible] Empty container set given, aborting.');
993 }
994 $containers.each($.proxy(function(index, container) {
995 var $container = $(container);
996 var $containerID = $container.wcfIdentify();
997 this._containers[$containerID] = $container;
998
999 this._initContainer($containerID);
1000 }, this));
1001 }
1002});
1003
1004/**
1005 * Provides the search dropdown for ACP
1006 *
1007 * @see WCF.Search.Base
1008 */
1009WCF.ACP.Search = WCF.Search.Base.extend({
1010 _delay: 250,
1011
1012 /**
1013 * name of the selected search provider
1014 * @var string
1015 */
1016 _providerName: '',
1017
1018 /**
1019 * @see WCF.Search.Base.init()
1020 */
1021 init: function() {
1022 this._className = 'wcf\\data\\acp\\search\\provider\\ACPSearchProviderAction';
1023 this._super('#pageHeaderSearch input[name=q]');
1024
1025 // disable form submitting
1026 $('#pageHeaderSearch > form').on('submit', function(event) {
1027 event.preventDefault();
1028 });
1029
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());
1035
1036 var $oldProviderName = this._providerName;
1037 this._providerName = ($button.data('providerName') != 'everywhere' ? $button.data('providerName') : '');
1038
1039 if ($oldProviderName != this._providerName) {
1040 var $searchString = $.trim(this._searchInput.val());
1041 if ($searchString) {
1042 var $parameters = {
1043 data: {
1044 excludedSearchValues: this._excludedSearchValues,
1045 searchString: $searchString
1046 }
1047 };
1048 this._queryServer($parameters);
1049 }
1050 }
1051 }, this));
1052
1053 const searchInput = document.querySelector("#pageHeaderSearch input[name=q]");
1054 document.addEventListener("keydown", (event) => {
1055 if (event.key !== "s") {
1056 return;
1057 }
1058
1059 if (!event.defaultPrevented && document.activeElement === document.body) {
1060 searchInput.focus();
1061
1062 event.preventDefault();
1063 }
1064 }, {
1065 passive: false,
1066 });
1067
1068 searchInput.addEventListener("keydown", (event) => {
1069 if (event.key !== "Escape") {
1070 return;
1071 }
1072
1073 if (!event.defaultPrevented && searchInput.value.trim() === "") {
1074 event.preventDefault();
1075
1076 searchInput.blur();
1077 }
1078 }, {
1079 passive: false,
1080 });
1081 },
1082
1083 /**
1084 * @see WCF.Search.Base._createListItem()
1085 */
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);
1090 }
1091
1092 // add caption
1093 $('<li class="dropdownText">' + resultList.title + '</li>').appendTo(this._list);
1094
1095 // add menu items
1096 for (var $i in resultList.items) {
1097 var $item = resultList.items[$i];
1098
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);
1100
1101 this._itemCount++;
1102 }
1103 },
1104
1105 /**
1106 * @see WCF.Search.Base._openDropdown()
1107 */
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);
1112 }
1113 });
1114 },
1115
1116 /**
1117 * @see WCF.Search.Base._handleEmptyResult()
1118 */
1119 _handleEmptyResult: function() {
1120 $('<li class="dropdownText">' + WCF.Language.get('wcf.acp.search.noResults') + '</li>').appendTo(this._list);
1121
1122 return true;
1123 },
1124
1125 /**
1126 * @see WCF.Search.Base._highlightSelectedElement()
1127 */
1128 _highlightSelectedElement: function() {
1129 this._list.find('li').removeClass('dropdownNavigationItem');
1130 this._list.find('li:not(.dropdownDivider):not(.dropdownText)').eq(this._itemIndex).addClass('dropdownNavigationItem');
1131 },
1132
1133 /**
1134 * @see WCF.Search.Base._selectElement()
1135 */
1136 _selectElement: function(event) {
1137 if (this._itemIndex === -1) {
1138 return false;
1139 }
1140
1141 window.location = this._list.find('li.dropdownNavigationItem > a').attr('href');
1142 },
1143
1144 _success: function(data) {
1145 this._super(data);
1146
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";
1152 },
1153
1154 /**
1155 * @see WCF.Search.Base._getParameters()
1156 */
1157 _getParameters: function(parameters) {
1158 parameters.data.providerName = this._providerName;
1159
1160 return parameters;
1161 }
1162});
1163
1164/**
1165 * Namespace for user management.
1166 */
1167WCF.ACP.User = { };
1168
1169/**
1170 * Generic implementation to ban users.
1171 */
1172WCF.ACP.User.BanHandler = {
1173 /**
1174 * callback object
1175 * @var object
1176 */
1177 _callback: null,
1178
1179 /**
1180 * dialog overlay
1181 * @var jQuery
1182 */
1183 _dialog: null,
1184
1185 /**
1186 * action proxy
1187 * @var WCF.Action.Proxy
1188 */
1189 _proxy: null,
1190
1191 /**
1192 * Initializes WCF.ACP.User.BanHandler on first use.
1193 */
1194 init: function() {
1195 this._proxy = new WCF.Action.Proxy({
1196 success: $.proxy(this._success, this)
1197 });
1198
1199 $('.jsBanButton').click($.proxy(function(event) {
1200 var $button = $(event.currentTarget);
1201 if ($button.data('banned')) {
1202 this.unban([ $button.data('objectID') ]);
1203 }
1204 else {
1205 this.ban([ $button.data('objectID') ]);
1206 }
1207 }, this));
1208
1209 require(['EventHandler'], function(EventHandler) {
1210 EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.user', this._clipboardAction.bind(this));
1211 }.bind(this));
1212 },
1213
1214 /**
1215 * Reacts to executed clipboard actions.
1216 *
1217 * @param {object<string, *>} actionData data of the executed clipboard action
1218 */
1219 _clipboardAction: function(actionData) {
1220 if (actionData.data.actionName === 'com.woltlab.wcf.user.ban') {
1221 this.ban(actionData.data.parameters.objectIDs);
1222 }
1223 },
1224
1225 /**
1226 * Unbans users.
1227 *
1228 * @param array<integer> userIDs
1229 */
1230 unban: function(userIDs) {
1231 this._proxy.setOption('data', {
1232 actionName: 'unban',
1233 className: 'wcf\\data\\user\\UserAction',
1234 objectIDs: userIDs
1235 });
1236 this._proxy.sendRequest();
1237 },
1238
1239 /**
1240 * Bans users.
1241 *
1242 * @param array<integer> userIDs
1243 */
1244 ban: function(userIDs) {
1245 if (this._dialog === null) {
1246 // create dialog
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>'));
1250
1251 this._dialog.find('#userBanNeverExpires').change(function() {
1252 $('#userBanExpiresSettings').toggle();
1253 });
1254
1255 this._dialog.find('button').click($.proxy(this._submit, this));
1256 }
1257 else {
1258 // reset dialog
1259 $('#userBanReason').val('');
1260 $('#userBanNeverExpires').prop('checked', true);
1261 $('#userBanExpiresSettings').hide();
1262 $('#userBanExpiresDatePicker, #userBanExpires').val('');
1263 }
1264
1265 this._dialog.data('userIDs', userIDs);
1266 this._dialog.wcfDialog({
1267 title: WCF.Language.get('wcf.acp.user.ban.sure')
1268 });
1269 },
1270
1271 /**
1272 * Handles submitting the ban dialog.
1273 */
1274 _submit: function() {
1275 this._dialog.find('.innerError').remove();
1276
1277 var $banExpires = '';
1278 if (!$('#userBanNeverExpires').is(':checked')) {
1279 var $banExpires = $('#userBanExpiresDatePicker').val();
1280 if (!$banExpires) {
1281 this._dialog.find('#userBanExpiresSettings > dd > small').prepend($('<small class="innerError" />').text(WCF.Language.get('wcf.global.form.error.empty')));
1282 return
1283 }
1284 }
1285
1286 this._proxy.setOption('data', {
1287 actionName: 'ban',
1288 className: 'wcf\\data\\user\\UserAction',
1289 objectIDs: this._dialog.data('userIDs'),
1290 parameters: {
1291 banReason: $('#userBanReason').val(),
1292 banExpires: $banExpires
1293 }
1294 });
1295 this._proxy.sendRequest();
1296 },
1297
1298 /**
1299 * Handles successful AJAX calls.
1300 *
1301 * @param object data
1302 * @param string textStatus
1303 * @param jQuery jqXHR
1304 */
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');
1310 }
1311 });
1312
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");
1319 }
1320 else {
1321 $button.data('banned', true).attr('data-tooltip', $button.data('unbanMessage'));
1322 $button[0].querySelector("fa-icon").setIcon("lock");
1323 }
1324 }
1325 });
1326
1327 var $notification = new WCF.System.Notification();
1328 $notification.show();
1329
1330 WCF.Clipboard.reload();
1331
1332 if (data.actionName == 'ban') {
1333 this._dialog.wcfDialog('close');
1334 }
1335
1336 WCF.System.Event.fireEvent('com.woltlab.wcf.acp.user', 'refresh', {userIds: data.objectIDs});
1337 }
1338};
1339
1340/**
1341 * Namespace for user group management.
1342 */
1343WCF.ACP.User.Group = { };
1344
1345/**
1346 * Handles copying user groups.
1347 */
1348WCF.ACP.User.Group.Copy = Class.extend({
1349 /**
1350 * id of the copied group
1351 * @var integer
1352 */
1353 _groupID: 0,
1354
1355 /**
1356 * Initializes a new instance of WCF.ACP.User.Group.Copy.
1357 *
1358 * @param integer groupID
1359 */
1360 init: function(groupID) {
1361 this._groupID = groupID;
1362
1363 $('.jsButtonUserGroupCopy').click($.proxy(this._click, this));
1364 },
1365
1366 /**
1367 * Handles clicking on a 'copy user group' button.
1368 */
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>'));
1374
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({
1378 autoSend: true,
1379 data: {
1380 actionName: 'copy',
1381 className: 'wcf\\data\\user\\group\\UserGroupAction',
1382 objectIDs: [ this._groupID ],
1383 parameters: {
1384 copyACLOptions: $('#copyACLOptions').is(':checked'),
1385 copyMembers: $('#copyMembers').is(':checked'),
1386 copyUserGroupOptions: $('#copyUserGroupOptions').is(':checked')
1387 }
1388 },
1389 success: function(data) {
1390 window.location = data.returnValues.redirectURL;
1391 }
1392 });
1393 }
1394 }, this), '', $template, true);
1395 }
1396});
1397
1398/**
1399 * Generic implementation to enable users.
1400 */
1401WCF.ACP.User.EnableHandler = {
1402 /**
1403 * action proxy
1404 * @var WCF.Action.Proxy
1405 */
1406 _proxy: null,
1407
1408 /**
1409 * Initializes WCF.ACP.User.EnableHandler on first use.
1410 */
1411 init: function() {
1412 this._proxy = new WCF.Action.Proxy({
1413 success: $.proxy(this._success, this)
1414 });
1415
1416 $('.jsEnableButton').click($.proxy(function(event) {
1417 var $button = $(event.currentTarget);
1418 if ($button.data('enabled')) {
1419 this.disable([ $button.data('objectID') ]);
1420 }
1421 else {
1422 this.enable([ $button.data('objectID') ]);
1423 }
1424 }, this));
1425
1426 require(['EventHandler'], function(EventHandler) {
1427 EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.user', this._clipboardAction.bind(this));
1428 }.bind(this));
1429 },
1430
1431 /**
1432 * Reacts to executed clipboard actions.
1433 *
1434 * @param {object<string, *>} actionData data of the executed clipboard action
1435 */
1436 _clipboardAction: function(actionData) {
1437 if (actionData.data.actionName === 'com.woltlab.wcf.user.enable') {
1438 this.enable(actionData.data.parameters.objectIDs);
1439 }
1440 },
1441
1442 /**
1443 * Disables users.
1444 *
1445 * @param array<integer> userIDs
1446 */
1447 disable: function(userIDs) {
1448 this._proxy.setOption('data', {
1449 actionName: 'disable',
1450 className: 'wcf\\data\\user\\UserAction',
1451 objectIDs: userIDs
1452 });
1453 this._proxy.sendRequest();
1454 },
1455
1456 /**
1457 * Enables users.
1458 *
1459 * @param array<integer> userIDs
1460 */
1461 enable: function(userIDs) {
1462 this._proxy.setOption('data', {
1463 actionName: 'enable',
1464 className: 'wcf\\data\\user\\UserAction',
1465 objectIDs: userIDs
1466 });
1467 this._proxy.sendRequest();
1468 },
1469
1470 /**
1471 * Handles successful AJAX calls.
1472 *
1473 * @param object data
1474 * @param string textStatus
1475 * @param jQuery jqXHR
1476 */
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');
1482 }
1483 });
1484
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");
1491 }
1492 else {
1493 $button.data('enabled', true).attr('data-tooltip', $button.data('disableMessage'));
1494 $button[0].querySelector("fa-icon").setIcon("square-check");
1495 }
1496 }
1497 });
1498
1499 var $notification = new WCF.System.Notification();
1500 $notification.show(function() { window.location.reload(); });
1501
1502 WCF.System.Event.fireEvent('com.woltlab.wcf.acp.user', 'refresh', {userIds: data.objectIDs});
1503 }
1504};
1505
1506/**
1507 * Handles the send new password clipboard action.
1508 */
1509WCF.ACP.User.SendNewPasswordHandler = {
1510 /**
1511 * Initializes WCF.ACP.User.SendNewPasswordHandler on first use.
1512 */
1513 init: function() {
1514 require(['EventHandler'], function(EventHandler) {
1515 EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.user', this._clipboardAction.bind(this));
1516 }.bind(this));
1517 },
1518
1519 /**
1520 * Reacts to executed clipboard actions.
1521 *
1522 * @param {object<string, *>} actionData data of the executed clipboard action
1523 */
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({
1528 confirm: () => {
1529 new AcpUiWorker({
1530 dialogId: 'sendingNewPasswords',
1531 dialogTitle: Language.get('wcf.acp.user.sendNewPassword.workerTitle'),
1532 className: 'wcf\\system\\worker\\SendNewPasswordWorker',
1533 parameters: {
1534 userIDs: actionData.data.parameters.objectIDs
1535 },
1536 });
1537 },
1538 message: actionData.data.parameters.confirmMessage,
1539 })
1540 });
1541 }
1542 }
1543};
1544
1545/**
1546 * Namespace for stat-related classes.
1547 */
1548WCF.ACP.Stat = { };
1549
1550/**
1551 * Shows the daily stat chart.
1552 */
1553WCF.ACP.Stat.Chart = Class.extend({
1554 init: function() {
1555 this._proxy = new WCF.Action.Proxy({
1556 success: $.proxy(this._success, this)
1557 });
1558
1559 $('#statRefreshButton').click($.proxy(this._refresh, this));
1560
1561 this._refresh();
1562 },
1563
1564 _refresh: function() {
1565 var $objectTypeIDs = [ ];
1566 $('input[name=objectTypeID]:checked').each(function() {
1567 $objectTypeIDs.push($(this).val());
1568 });
1569
1570 if (!$objectTypeIDs.length) return;
1571
1572 this._proxy.setOption('data', {
1573 className: 'wcf\\data\\stat\\daily\\StatDailyAction',
1574 actionName: 'getData',
1575 parameters: {
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
1581 }
1582 });
1583 this._proxy.sendRequest();
1584 },
1585
1586 _success: function(data) {
1587 switch ($('input[name=dateGrouping]:checked').val()) {
1588 case 'yearly':
1589 var $minTickSize = [1, "year"];
1590 var $timeFormat = WCF.Language.get('wcf.acp.stat.timeFormat.yearly');
1591 break;
1592 case 'monthly':
1593 var $minTickSize = [1, "month"];
1594 var $timeFormat = WCF.Language.get('wcf.acp.stat.timeFormat.monthly');
1595 break;
1596 case 'weekly':
1597 var $minTickSize = [7, "day"];
1598 var $timeFormat = WCF.Language.get('wcf.acp.stat.timeFormat.weekly');
1599 break;
1600 default:
1601 var $minTickSize = [1, "day"];
1602 var $timeFormat = WCF.Language.get('wcf.acp.stat.timeFormat.daily');
1603 }
1604
1605 var options = {
1606 series: {
1607 lines: {
1608 show: true
1609 },
1610 points: {
1611 show: true
1612 }
1613 },
1614 grid: {
1615 hoverable: true
1616 },
1617 xaxis: {
1618 mode: "time",
1619 minTickSize: $minTickSize,
1620 timeformat: $timeFormat,
1621 monthNames: WCF.Language.get('__monthsShort')
1622 },
1623 yaxis: {
1624 min: 0,
1625 tickDecimals: 0,
1626 tickFormatter: function(val) {
1627 return WCF.String.addThousandsSeparator(val);
1628 }
1629 }
1630 };
1631
1632 var $data = [ ];
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;
1637 }
1638
1639 $data.push($row);
1640 }
1641
1642 $.plot("#chart", $data, options);
1643
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) {
1649 if (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, {
1654 verticalOffset: 5,
1655 horizontal: 'center',
1656 vertical: 'top'
1657 });
1658 }
1659 else {
1660 $("#chartTooltip").hide();
1661 }
1662 });
1663 });
1664
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>');
1667 }
1668
1669 elBySel('.contentHeader > .contentTitle').scrollIntoView({ behavior: 'smooth' });
1670 }
1671});
1672
1673/**
1674 * Namespace for ACP ad management.
1675 */
1676WCF.ACP.Ad = { };
1677
1678/**
1679 * Handles the location of an ad during ad creation/editing.
1680 */
1681WCF.ACP.Ad.LocationHandler = Class.extend({
1682 /**
1683 * fieldset of the page conditions
1684 * @var jQuery
1685 */
1686 _pageConditions: null,
1687
1688 /**
1689 * select elements for the page controller condition
1690 * @var jQuery[]
1691 */
1692 _pageInputs: [],
1693
1694 /**
1695 * page controller condition container
1696 * @var jQuery[]
1697 */
1698 _pageSelectionContainer: null,
1699
1700 /**
1701 * Initializes a new WCF.ACP.Ad.LocationHandler object.
1702 *
1703 * @param {object} variablesDescriptions
1704 */
1705 init: function(variablesDescriptions) {
1706 this._variablesDescriptions = variablesDescriptions;
1707
1708 this._pageConditions = $('#pageConditions');
1709 this._pageInputs = $('input[name="pageIDs[]"]');
1710
1711 this._variablesDescriptionsList = $('#ad').next('small').children('ul');
1712
1713 this._pageSelectionContainer = $(this._pageInputs[0]).parents('dl:eq(0)');
1714
1715 // hide the page controller elements
1716 this._hidePageSelection(true);
1717
1718 $('#objectTypeID').on('change', $.proxy(this._setPageController, this));
1719
1720 this._setPageController();
1721
1722 $('#adForm').submit($.proxy(this._submit, this));
1723 },
1724
1725 /**
1726 * Hides the page selection form field.
1727 *
1728 * @since 5.2
1729 */
1730 _hidePageSelection: function(addEventListeners) {
1731 this._pageSelectionContainer.prev('dl').hide();
1732 this._pageSelectionContainer.hide();
1733
1734 // fix the margin of a potentially next page condition element
1735 this._pageSelectionContainer.next('dl').css('margin-top', 0);
1736
1737 var section = this._pageSelectionContainer.parent('section');
1738 if (!section.children('dl:visible').length) {
1739 section.hide();
1740
1741 var nextSection = section.next('section');
1742 if (nextSection) {
1743 nextSection.css('margin-top', 0);
1744
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');
1750 }
1751 else {
1752 nextSection.css('margin-top', 0);
1753 }
1754 });
1755 });
1756 }
1757 }
1758 }
1759 },
1760
1761 /**
1762 * Shows the page selection form field.
1763 *
1764 * @since 5.2
1765 */
1766 _showPageSelection: function() {
1767 this._pageSelectionContainer.prev('dl').show();
1768 this._pageSelectionContainer.show();
1769 this._pageSelectionContainer.next('dl').css('margin-top', '40px');
1770
1771 var section = this._pageSelectionContainer.parent('section');
1772 section.show();
1773
1774 var nextSection = section.next('section');
1775 if (nextSection) {
1776 nextSection.css('margin-top', '40px');
1777 }
1778 },
1779
1780 /**
1781 * Sets the page controller based on the selected ad location.
1782 */
1783 _setPageController: function() {
1784 var option = $('#objectTypeID').find('option:checked');
1785 var parent = option.parent();
1786
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();
1790 }
1791 else {
1792 this._hidePageSelection();
1793
1794 require(['Core'], function(Core) {
1795 var input, triggerEvent;
1796
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;
1801
1802 if (option.data('page') && elData(input, 'identifier') === option.data('page')) {
1803 if (!input.checked) triggerEvent = true;
1804
1805 input.checked = true;
1806 }
1807 else {
1808 if (input.checked) triggerEvent = true;
1809
1810 input.checked = false;
1811 }
1812
1813 if (triggerEvent) Core.triggerEvent(this._pageInputs[i], 'change');
1814 }
1815 }.bind(this));
1816 }
1817
1818 this._variablesDescriptionsList.children(':not(.jsDefaultItem)').remove();
1819
1820 var objectTypeId = $('#objectTypeID').val();
1821 if (objectTypeId in this._variablesDescriptions) {
1822 this._variablesDescriptionsList[0].innerHTML += this._variablesDescriptions[objectTypeId];
1823 }
1824 },
1825
1826 /**
1827 * Handles submitting the ad form.
1828 */
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();
1834 }
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;
1840 }
1841 }
1842 }
1843});
1844
1845/**
1846 * Initialize WCF.ACP.Tag namespace.
1847 */
1848WCF.ACP.Tag = { };
1849
1850/**
1851 * Handles setting tags as synonyms of another tag by clipboard.
1852 */
1853WCF.ACP.Tag.SetAsSynonymsHandler = Class.extend({
1854 /**
1855 * dialog to select the "main" tag
1856 * @var jQuery
1857 */
1858 _dialog: null,
1859
1860 /**
1861 * ids of the selected tags
1862 * @var array<integer>
1863 */
1864 _objectIDs: [ ],
1865
1866 /**
1867 * Initializes the SetAsSynonymsHandler object.
1868 */
1869 init: function() {
1870 require(['EventHandler'], function(EventHandler) {
1871 EventHandler.add('com.woltlab.wcf.clipboard', 'com.woltlab.wcf.tag', this._clipboardAction.bind(this));
1872 }.bind(this));
1873 },
1874
1875 /**
1876 * Reacts to executed clipboard actions.
1877 *
1878 * @param {object<string, *>} actionData data of the executed clipboard action
1879 */
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({
1886 closable: false,
1887 title: WCF.Language.get('wcf.acp.tag.setAsSynonyms')
1888 });
1889 }
1890
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(); });
1894 }
1895 },
1896
1897 /**
1898 * Saves the tags as synonyms.
1899 */
1900 _submit: function() {
1901 new WCF.Action.Proxy({
1902 autoSend: true,
1903 data: {
1904 actionName: 'setAsSynonyms',
1905 className: 'wcf\\data\\tag\\TagAction',
1906 objectIDs: this._objectIDs,
1907 parameters: {
1908 tagID: this._dialog.find('input[name="tagID"]:checked').val()
1909 }
1910 },
1911 success: $.proxy(function() {
1912 this._dialog.wcfDialog('close');
1913
1914 new WCF.System.Notification().show(function() {
1915 window.location.reload();
1916 });
1917 }, this)
1918 });
1919 }
1920});