From 90a1f58094a6c512a73db78b3c857d50ee5962f0 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Thu, 26 Nov 2020 18:08:17 +0100 Subject: [PATCH] Convert `Controller/Clipboard` to TypeScript --- .../WoltLabSuite/Core/Controller/Clipboard.js | 772 ++++++++---------- .../WoltLabSuite/Core/Controller/Clipboard.js | 732 ----------------- .../WoltLabSuite/Core/Controller/Clipboard.ts | 710 ++++++++++++++++ 3 files changed, 1069 insertions(+), 1145 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.ts diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js index 7710ccc175..ed443bf0c2 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Clipboard.js @@ -1,201 +1,158 @@ /** * Clipboard API Handler. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Controller/Clipboard + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Controller/Clipboard */ -define([ - 'Ajax', 'Core', 'Dictionary', 'EventHandler', - 'Language', 'List', 'ObjectMap', 'Dom/ChangeListener', - 'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown', - 'WoltLabSuite/Core/Ui/Page/Action', 'Ui/Screen' -], function (Ajax, Core, Dictionary, EventHandler, Language, List, ObjectMap, DomChangeListener, DomTraverse, DomUtil, UiConfirmation, UiSimpleDropdown, UiPageAction, UiScreen) { +define(["require", "exports", "tslib", "../Ajax", "../Core", "../Dom/Change/Listener", "../Dom/Util", "../Event/Handler", "../Language", "../Ui/Confirmation", "../Ui/Dropdown/Simple", "../Ui/Page/Action", "../Ui/Screen"], function (require, exports, tslib_1, Ajax, Core, Listener_1, Util_1, EventHandler, Language, UiConfirmation, Simple_1, UiPageAction, UiScreen) { "use strict"; - if (!COMPILER_TARGET_DEFAULT) { - return { - setup: function () { }, - reload: function () { }, - _initContainers: function () { }, - _loadMarkedItems: function () { }, - _markAll: function () { }, - _mark: function () { }, - _saveState: function () { }, - _executeAction: function () { }, - _executeProxyAction: function () { }, - _unmarkAll: function () { }, - _ajaxSetup: function () { }, - _ajaxSuccess: function () { }, - _rebuildMarkings: function () { }, - hideEditor: function () { }, - showEditor: function () { }, - unmark: function () { } - }; - } - var _containers = new Dictionary(); - var _editors = new Dictionary(); - var _editorDropdowns = new Dictionary(); - var _elements = elByClass('jsClipboardContainer'); - var _itemData = new ObjectMap(); - var _knownCheckboxes = new List(); - var _options = {}; - var _reloadPageOnSuccess = new Dictionary(); - var _callbackCheckbox = null; - var _callbackItem = null; - var _callbackUnmarkAll = null; - var _specialCheckboxSelector = '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]'; - /** - * Clipboard API - * - * @exports WoltLabSuite/Core/Controller/Clipboard - */ - return { + Object.defineProperty(exports, "__esModule", { value: true }); + exports.unmark = exports.showEditor = exports.hideEditor = exports.reload = exports.setup = void 0; + Ajax = tslib_1.__importStar(Ajax); + Core = tslib_1.__importStar(Core); + Listener_1 = tslib_1.__importDefault(Listener_1); + Util_1 = tslib_1.__importDefault(Util_1); + EventHandler = tslib_1.__importStar(EventHandler); + Language = tslib_1.__importStar(Language); + UiConfirmation = tslib_1.__importStar(UiConfirmation); + Simple_1 = tslib_1.__importDefault(Simple_1); + UiPageAction = tslib_1.__importStar(UiPageAction); + UiScreen = tslib_1.__importStar(UiScreen); + const _specialCheckboxSelector = '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]'; + class ControllerClipboard { + constructor() { + this.containers = new Map(); + this.editors = new Map(); + this.editorDropdowns = new Map(); + this.itemData = new WeakMap(); + this.knownCheckboxes = new WeakSet(); + this.pageClassNames = []; + this.pageObjectId = 0; + this.reloadPageOnSuccess = new Map(); + } /** * Initializes the clipboard API handler. - * - * @param {Object} options initialization options */ - setup: function (options) { + setup(options) { if (!options.pageClassName) { throw new Error("Expected a non-empty string for parameter 'pageClassName'."); } - if (_callbackCheckbox === null) { - _callbackCheckbox = this._mark.bind(this); - _callbackItem = this._executeAction.bind(this); - _callbackUnmarkAll = this._unmarkAll.bind(this); - _options = Core.extend({ - hasMarkedItems: false, - pageClassNames: [options.pageClassName], - pageObjectId: 0 - }, options); - delete _options.pageClassName; - } - else { - if (options.pageObjectId) { - throw new Error("Cannot load secondary clipboard with page object id set."); - } - _options.pageClassNames.push(options.pageClassName); - } - if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.msMatchesSelector; + let hasMarkedItems = false; + if (this.pageClassNames.length === 0) { + hasMarkedItems = options.hasMarkedItems; + this.pageObjectId = options.pageObjectId; } - this._initContainers(); - if (_options.hasMarkedItems && _elements.length) { - this._loadMarkedItems(); + this.pageClassNames.push(options.pageClassName); + this.initContainers(); + if (hasMarkedItems && this.containers.size) { + this.loadMarkedItems(); } - DomChangeListener.add('WoltLabSuite/Core/Controller/Clipboard', this._initContainers.bind(this)); - }, + Listener_1.default.add("WoltLabSuite/Core/Controller/Clipboard", () => this.initContainers()); + } /** * Reloads the clipboard data. */ - reload: function () { - if (_containers.size) { - this._loadMarkedItems(); + reload() { + if (this.containers.size) { + this.loadMarkedItems(); } - }, + } /** * Initializes clipboard containers. */ - _initContainers: function () { - for (var i = 0, length = _elements.length; i < length; i++) { - var container = _elements[i]; - var containerId = DomUtil.identify(container); - var containerData = _containers.get(containerId); + initContainers() { + document.querySelectorAll(".jsClipboardContainer").forEach((container) => { + const containerId = Util_1.default.identify(container); + let containerData = this.containers.get(containerId); if (containerData === undefined) { - var markAll = elBySel('.jsClipboardMarkAll', container); + const markAll = container.querySelector(".jsClipboardMarkAll"); if (markAll !== null) { if (markAll.matches(_specialCheckboxSelector)) { - var label = markAll.closest('label'); - elAttr(label, 'role', 'checkbox'); - elAttr(label, 'tabindex', '0'); - elAttr(label, 'aria-checked', false); - elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.markAll')); - label.addEventListener('keyup', function (event) { - if (event.keyCode === 13 || event.keyCode === 32) { - checkbox.click(); + const label = markAll.closest("label"); + label.setAttribute("role", "checkbox"); + label.tabIndex = 0; + label.setAttribute("aria-checked", "false"); + label.setAttribute("aria-label", Language.get("wcf.clipboard.item.markAll")); + label.addEventListener("keyup", (event) => { + if (event.key === "Enter" || event.key === "Space") { + markAll.click(); } }); } - elData(markAll, 'container-id', containerId); - markAll.addEventListener('click', this._markAll.bind(this)); + markAll.dataset.containerId = containerId; + markAll.addEventListener("click", (ev) => this.markAll(ev)); } containerData = { - checkboxes: elByClass('jsClipboardItem', container), + checkboxes: container.getElementsByClassName("jsClipboardItem"), element: container, markAll: markAll, - markedObjectIds: new List() + markedObjectIds: new Set(), }; - _containers.set(containerId, containerData); + this.containers.set(containerId, containerData); } - for (var j = 0, innerLength = containerData.checkboxes.length; j < innerLength; j++) { - var checkbox = containerData.checkboxes[j]; - if (!_knownCheckboxes.has(checkbox)) { - elData(checkbox, 'container-id', containerId); - (function (checkbox) { - if (checkbox.matches(_specialCheckboxSelector)) { - var label = checkbox.closest('label'); - elAttr(label, 'role', 'checkbox'); - elAttr(label, 'tabindex', '0'); - elAttr(label, 'aria-checked', false); - elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.mark')); - label.addEventListener('keyup', function (event) { - if (event.keyCode === 13 || event.keyCode === 32) { - checkbox.click(); - } - }); - } - var link = checkbox.closest('a'); - if (link === null) { - checkbox.addEventListener('click', _callbackCheckbox); - } - else { - // Firefox will always trigger the link if the checkbox is - // inside of one. Since 2000. Thanks Firefox. - checkbox.addEventListener('click', function (event) { - event.preventDefault(); - window.setTimeout(function () { - checkbox.checked = !checkbox.checked; - _callbackCheckbox(null, checkbox); - }, 10); - }); + Array.from(containerData.checkboxes).forEach((checkbox) => { + if (this.knownCheckboxes.has(checkbox)) { + return; + } + checkbox.dataset.containerId = containerId; + if (checkbox.matches(_specialCheckboxSelector)) { + const label = checkbox.closest("label"); + label.setAttribute("role", "checkbox"); + label.tabIndex = 0; + label.setAttribute("aria-checked", "false"); + label.setAttribute("aria-label", Language.get("wcf.clipboard.item.mark")); + label.addEventListener("keyup", (event) => { + if (event.key === "Enter" || event.key === "Space") { + checkbox.click(); } - })(checkbox); - _knownCheckboxes.add(checkbox); + }); } - } - } - }, + const link = checkbox.closest("a"); + if (link === null) { + checkbox.addEventListener("click", (ev) => this.mark(ev)); + } + else { + // Firefox will always trigger the link if the checkbox is + // inside of one. Since 2000. Thanks Firefox. + checkbox.addEventListener("click", (event) => { + event.preventDefault(); + window.setTimeout(() => { + checkbox.checked = !checkbox.checked; + this.mark(checkbox); + }, 10); + }); + } + this.knownCheckboxes.add(checkbox); + }); + }); + } /** * Loads marked items from clipboard. */ - _loadMarkedItems: function () { + loadMarkedItems() { Ajax.api(this, { - actionName: 'getMarkedItems', + actionName: "getMarkedItems", parameters: { - pageClassNames: _options.pageClassNames, - pageObjectID: _options.pageObjectId - } + pageClassNames: this.pageClassNames, + pageObjectID: this.pageObjectId, + }, }); - }, + } /** * Marks or unmarks all visible items at once. - * - * @param {object} event event object */ - _markAll: function (event) { - var checkbox = event.currentTarget; - var isMarked = (checkbox.nodeName !== 'INPUT' || checkbox.checked); - if (elAttr(checkbox.parentNode, 'role') === 'checkbox') { - elAttr(checkbox.parentNode, 'aria-checked', isMarked); - } - var objectIds = []; - var containerId = elData(checkbox, 'container-id'); - var data = _containers.get(containerId); - var type = elData(data.element, 'type'); - for (var i = 0, length = data.checkboxes.length; i < length; i++) { - var item = data.checkboxes[i]; - var objectId = ~~elData(item, 'object-id'); + markAll(event) { + const checkbox = event.currentTarget; + const isMarked = checkbox.nodeName !== "INPUT" || checkbox.checked; + this.setParentAsMarked(checkbox, isMarked); + const objectIds = []; + const containerId = checkbox.dataset.containerId; + const data = this.containers.get(containerId); + const type = data.element.dataset.type; + Array.from(data.checkboxes).forEach((item) => { + const objectId = ~~item.dataset.objectId; if (isMarked) { if (!item.checked) { item.checked = true; @@ -206,128 +163,115 @@ define([ else { if (item.checked) { item.checked = false; - data.markedObjectIds['delete'](objectId); + data.markedObjectIds["delete"](objectId); objectIds.push(objectId); } } - if (elAttr(item.parentNode, 'role') === 'checkbox') { - elAttr(item.parentNode, 'aria-checked', isMarked); - } - var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject'); + this.setParentAsMarked(item, isMarked); + const clipboardObject = checkbox.closest(".jsClipboardObject"); if (clipboardObject !== null) { - clipboardObject.classList[(isMarked ? 'addClass' : 'removeClass')]('jsMarked'); + if (isMarked) { + clipboardObject.classList.add("jsMarked"); + } + else { + clipboardObject.classList.remove("jsMarked"); + } } - } - this._saveState(type, objectIds, isMarked); - }, + }); + this.saveState(type, objectIds, isMarked); + } /** * Marks or unmarks an individual item. * - * @param {object} event event object - * @param {Element=} checkbox checkbox element */ - _mark: function (event, checkbox) { - checkbox = (event instanceof Event) ? event.currentTarget : checkbox; - var objectId = ~~elData(checkbox, 'object-id'); - var isMarked = checkbox.checked; - var containerId = elData(checkbox, 'container-id'); - var data = _containers.get(containerId); - var type = elData(data.element, 'type'); - var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject'); - data.markedObjectIds[(isMarked ? 'add' : 'delete')](objectId); - clipboardObject.classList[(isMarked) ? 'add' : 'remove']('jsMarked'); - if (data.markAll !== null) { - var markedAll = true; - for (var i = 0, length = data.checkboxes.length; i < length; i++) { - if (!data.checkboxes[i].checked) { - markedAll = false; - break; - } - } - data.markAll.checked = markedAll; - if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') { - elAttr(data.markAll.parentNode, 'aria-checked', isMarked); - } + mark(event) { + const checkbox = event instanceof Event ? event.currentTarget : event; + const objectId = ~~checkbox.dataset.objectId; + const isMarked = checkbox.checked; + const containerId = checkbox.dataset.containerId; + const data = this.containers.get(containerId); + const type = data.element.dataset.type; + const clipboardObject = checkbox.closest(".jsClipboardObject"); + if (isMarked) { + data.markedObjectIds.add(objectId); + clipboardObject.classList.add("jsMarked"); + } + else { + data.markedObjectIds.delete(objectId); + clipboardObject.classList.remove("jsMarked"); } - if (elAttr(checkbox.parentNode, 'role') === 'checkbox') { - elAttr(checkbox.parentNode, 'aria-checked', checkbox.checked); + if (data.markAll !== null) { + data.markAll.checked = Array.from(data.checkboxes).some((item) => !item.checked); + this.setParentAsMarked(data.markAll, isMarked); } - this._saveState(type, [objectId], isMarked); - }, + this.setParentAsMarked(checkbox, checkbox.checked); + this.saveState(type, [objectId], isMarked); + } /** * Saves the state for given item object ids. - * - * @param {string} type object type - * @param {int[]} objectIds item object ids - * @param {boolean} isMarked true if marked */ - _saveState: function (type, objectIds, isMarked) { + saveState(objectType, objectIds, isMarked) { Ajax.api(this, { - actionName: (isMarked ? 'mark' : 'unmark'), + actionName: isMarked ? "mark" : "unmark", parameters: { - pageClassNames: _options.pageClassNames, - pageObjectID: _options.pageObjectId, + pageClassNames: this.pageClassNames, + pageObjectID: this.pageObjectId, objectIDs: objectIds, - objectType: type - } + objectType, + }, }); - }, + } /** * Executes an editor action. - * - * @param {object} event event object */ - _executeAction: function (event) { - var listItem = event.currentTarget; - var data = _itemData.get(listItem); + executeAction(event) { + const listItem = event.currentTarget; + const data = this.itemData.get(listItem); if (data.url) { window.location.href = data.url; return; } - var triggerEvent = function () { - var type = elData(listItem, 'type'); - EventHandler.fire('com.woltlab.wcf.clipboard', type, { - data: data, - listItem: listItem, - responseData: null + function triggerEvent() { + const type = listItem.dataset.type; + EventHandler.fire("com.woltlab.wcf.clipboard", type, { + data, + listItem, + responseData: null, }); - }; - //noinspection JSUnresolvedVariable - var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : ''; - var fireEvent = true; - if (typeof data.parameters === 'object' && data.parameters.actionName && data.parameters.className) { - if (data.parameters.actionName === 'unmarkAll' || Array.isArray(data.parameters.objectIDs)) { - if (confirmMessage.length) { - //noinspection JSUnresolvedVariable - var template = (typeof data.internalData.template === 'string') ? data.internalData.template : ''; + } + const message = typeof data.internalData.confirmMessage === "string" ? data.internalData.confirmMessage : ""; + let fireEvent = true; + if (Core.isPlainObject(data.parameters) && data.parameters.actionName && data.parameters.className) { + if (data.parameters.actionName === "unmarkAll" || Array.isArray(data.parameters.objectIDs)) { + if (message.length) { + const template = typeof data.internalData.template === "string" ? data.internalData.template : ""; UiConfirmation.show({ - confirm: (function () { - var formData = {}; + confirm: () => { + const formData = {}; if (template.length) { - var items = elBySelAll('input, select, textarea', UiConfirmation.getContentElement()); - for (var i = 0, length = items.length; i < length; i++) { - var item = items[i]; - var name = elAttr(item, 'name'); + UiConfirmation.getContentElement() + .querySelectorAll("input, select, textarea") + .forEach((item) => { + const name = item.name; switch (item.nodeName) { - case 'INPUT': + case "INPUT": if ((item.type !== "checkbox" && item.type !== "radio") || item.checked) { - formData[name] = elAttr(item, 'value'); + formData[name] = item.value; } break; - case 'SELECT': + case "SELECT": formData[name] = item.value; break; - case 'TEXTAREA': + case "TEXTAREA": formData[name] = item.value.trim(); break; } - } + }); } - //noinspection JSUnresolvedFunction this._executeProxyAction(listItem, data, formData); - }).bind(this), - message: confirmMessage, - template: template + }, + message, + template, }); } else { @@ -335,199 +279,160 @@ define([ } } } - else if (confirmMessage.length) { + else if (message.length) { fireEvent = false; UiConfirmation.show({ confirm: triggerEvent, - message: confirmMessage + message, }); } if (fireEvent) { triggerEvent(); } - }, + } /** * Forwards clipboard actions to an individual handler. - * - * @param {Element} listItem dropdown item element - * @param {Object} data action data - * @param {Object?} formData form data */ - _executeProxyAction: function (listItem, data, formData) { - formData = formData || {}; - var objectIds = (data.parameters.actionName !== 'unmarkAll') ? data.parameters.objectIDs : []; - var parameters = { data: formData }; - //noinspection JSUnresolvedVariable - if (typeof data.internalData.parameters === 'object') { - //noinspection JSUnresolvedVariable - for (var key in data.internalData.parameters) { - //noinspection JSUnresolvedVariable - if (data.internalData.parameters.hasOwnProperty(key)) { - //noinspection JSUnresolvedVariable - parameters[key] = data.internalData.parameters[key]; - } - } + _executeProxyAction(listItem, data, formData = {}) { + const objectIds = data.parameters.actionName !== "unmarkAll" ? data.parameters.objectIDs : []; + const parameters = { data: formData }; + if (Core.isPlainObject(data.internalData.parameters)) { + Object.entries(data.internalData.parameters).forEach(([key, value]) => { + parameters[key] = value; + }); } Ajax.api(this, { actionName: data.parameters.actionName, className: data.parameters.className, objectIDs: objectIds, - parameters: parameters - }, (function (responseData) { - if (data.actionName !== 'unmarkAll') { - var type = elData(listItem, 'type'); - EventHandler.fire('com.woltlab.wcf.clipboard', type, { - data: data, - listItem: listItem, - responseData: responseData + parameters, + }, (responseData) => { + if (data.actionName !== "unmarkAll") { + const type = listItem.dataset.type; + EventHandler.fire("com.woltlab.wcf.clipboard", type, { + data, + listItem, + responseData, }); - if (_reloadPageOnSuccess.has(type) && _reloadPageOnSuccess.get(type).indexOf(responseData.actionName) !== -1) { + const reloadPageOnSuccess = this.reloadPageOnSuccess.get(type); + if (reloadPageOnSuccess && reloadPageOnSuccess.includes(responseData.actionName)) { window.location.reload(); return; } } - this._loadMarkedItems(); - }).bind(this)); - }, + this.loadMarkedItems(); + }); + } /** * Unmarks all clipboard items for an object type. - * - * @param {object} event event object */ - _unmarkAll: function (event) { - var type = elData(event.currentTarget, 'type'); + unmarkAll(event) { + const listItem = event.currentTarget; Ajax.api(this, { - actionName: 'unmarkAll', + actionName: "unmarkAll", parameters: { - objectType: type - } + objectType: listItem.dataset.type, + }, }); - }, + } /** * Sets up ajax request object. - * - * @return {object} request options */ - _ajaxSetup: function () { + _ajaxSetup() { return { data: { - className: 'wcf\\data\\clipboard\\item\\ClipboardItemAction' - } + className: "wcf\\data\\clipboard\\item\\ClipboardItemAction", + }, }; - }, + } /** * Handles successful AJAX requests. - * - * @param {object} data response data */ - _ajaxSuccess: function (data) { - if (data.actionName === 'unmarkAll') { - _containers.forEach((function (containerData) { - if (elData(containerData.element, 'type') === data.returnValues.objectType) { - var clipboardObjects = elByClass('jsMarked', containerData.element); - while (clipboardObjects.length) { - clipboardObjects[0].classList.remove('jsMarked'); - } - if (containerData.markAll !== null) { - containerData.markAll.checked = false; - if (elAttr(containerData.markAll.parentNode, 'role') === 'checkbox') { - elAttr(containerData.markAll.parentNode, 'aria-checked', false); - } - } - for (var i = 0, length = containerData.checkboxes.length; i < length; i++) { - containerData.checkboxes[i].checked = false; - if (elAttr(containerData.checkboxes[i].parentNode, 'role') === 'checkbox') { - elAttr(containerData.checkboxes[i].parentNode, 'aria-checked', false); - } - } - UiPageAction.remove('wcfClipboard-' + data.returnValues.objectType); + _ajaxSuccess(data) { + if (data.actionName === "unmarkAll") { + const objectType = data.returnValues.objectType; + this.containers.forEach((containerData) => { + if (containerData.element.dataset.type !== objectType) { + return; + } + containerData.element.querySelectorAll(".jsMarked").forEach((element) => element.classList.remove("jsMarked")); + if (containerData.markAll !== null) { + containerData.markAll.checked = false; + this.setParentAsMarked(containerData.markAll, false); } - }).bind(this)); + Array.from(containerData.checkboxes).forEach((checkbox) => { + checkbox.checked = false; + this.setParentAsMarked(checkbox, false); + }); + UiPageAction.remove(`wcfClipboard-${objectType}`); + }); return; } - _itemData = new ObjectMap(); - _reloadPageOnSuccess = new Dictionary(); + this.itemData = new WeakMap(); + this.reloadPageOnSuccess.clear(); // rebuild markings - _containers.forEach((function (containerData) { - var typeName = elData(containerData.element, 'type'); - //noinspection JSUnresolvedVariable - var objectIds = (data.returnValues.markedItems && data.returnValues.markedItems.hasOwnProperty(typeName)) ? data.returnValues.markedItems[typeName] : []; - this._rebuildMarkings(containerData, objectIds); - }).bind(this)); - var keepEditors = [], typeName; - if (data.returnValues && data.returnValues.items) { - for (typeName in data.returnValues.items) { - if (data.returnValues.items.hasOwnProperty(typeName)) { - keepEditors.push(typeName); - } - } - } + const markings = Core.isPlainObject(data.returnValues.markedItems) ? data.returnValues.markedItems : {}; + this.containers.forEach((containerData) => { + const typeName = containerData.element.dataset.type; + const objectIds = Array.isArray(markings[typeName]) ? markings[typeName] : []; + this.rebuildMarkings(containerData, objectIds); + }); + const keepEditors = Object.keys(data.returnValues.items || {}); // clear editors - _editors.forEach(function (editor, typeName) { - if (keepEditors.indexOf(typeName) === -1) { - UiPageAction.remove('wcfClipboard-' + typeName); - _editorDropdowns.get(typeName).innerHTML = ''; + this.editors.forEach((editor, typeName) => { + if (keepEditors.includes(typeName)) { + UiPageAction.remove(`wcfClipboard-${typeName}`); + this.editorDropdowns.get(typeName).innerHTML = ""; } }); // no items - if (!data.returnValues || !data.returnValues.items) { + if (!data.returnValues.items) { return; } // rebuild editors - var actionName, created, dropdown, editor, typeData; - var divider, item, itemData, itemIndex, label, unmarkAll; - for (typeName in data.returnValues.items) { - if (!data.returnValues.items.hasOwnProperty(typeName)) { - continue; - } - typeData = data.returnValues.items[typeName]; - //noinspection JSUnresolvedVariable - _reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess); - created = false; - editor = _editors.get(typeName); - dropdown = _editorDropdowns.get(typeName); + Object.entries(data.returnValues.items).forEach(([typeName, typeData]) => { + this.reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess); + let created = false; + let editor = this.editors.get(typeName); + let dropdown = this.editorDropdowns.get(typeName); if (editor === undefined) { created = true; - editor = elCreate('a'); - editor.className = 'dropdownToggle'; + editor = document.createElement("a"); + editor.className = "dropdownToggle"; editor.textContent = typeData.label; - _editors.set(typeName, editor); - dropdown = elCreate('ol'); - dropdown.className = 'dropdownMenu'; - _editorDropdowns.set(typeName, dropdown); + this.editors.set(typeName, editor); + dropdown = document.createElement("ol"); + dropdown.className = "dropdownMenu"; + this.editorDropdowns.set(typeName, dropdown); } else { editor.textContent = typeData.label; - dropdown.innerHTML = ''; + dropdown.innerHTML = ""; } // create editor items - for (itemIndex in typeData.items) { - if (!typeData.items.hasOwnProperty(itemIndex)) { - continue; - } - itemData = typeData.items[itemIndex]; - item = elCreate('li'); - label = elCreate('span'); + Object.values(typeData.items).forEach((itemData) => { + const item = document.createElement("li"); + const label = document.createElement("span"); label.textContent = itemData.label; item.appendChild(label); dropdown.appendChild(item); - elData(item, 'type', typeName); - item.addEventListener('click', _callbackItem); - _itemData.set(item, itemData); - } - divider = elCreate('li'); - divider.classList.add('dropdownDivider'); + item.dataset.type = typeName; + item.addEventListener("click", (ev) => this.executeAction(ev)); + this.itemData.set(item, itemData); + }); + const divider = document.createElement("li"); + divider.classList.add("dropdownDivider"); dropdown.appendChild(divider); // add 'unmark all' - unmarkAll = elCreate('li'); - elData(unmarkAll, 'type', typeName); - label = elCreate('span'); - label.textContent = Language.get('wcf.clipboard.item.unmarkAll'); + const unmarkAll = document.createElement("li"); + unmarkAll.dataset.type = typeName; + const label = document.createElement("span"); + label.textContent = Language.get("wcf.clipboard.item.unmarkAll"); unmarkAll.appendChild(label); - unmarkAll.addEventListener('click', _callbackUnmarkAll); + unmarkAll.addEventListener("click", (ev) => this.unmarkAll(ev)); dropdown.appendChild(unmarkAll); if (keepEditors.indexOf(typeName) !== -1) { - actionName = 'wcfClipboard-' + typeName; + const actionName = `wcfClipboard-${typeName}`; if (UiPageAction.has(actionName)) { UiPageAction.show(actionName); } @@ -536,73 +441,114 @@ define([ } } if (created) { - editor.parentNode.classList.add('dropdown'); - editor.parentNode.appendChild(dropdown); - UiSimpleDropdown.init(editor); + const parent = editor.parentElement; + parent.classList.add("dropdown"); + parent.appendChild(dropdown); + Simple_1.default.init(editor); } - } - }, + }); + } /** * Rebuilds the mark state for each item. - * - * @param {Object} data container data - * @param {int[]} objectIds item object ids */ - _rebuildMarkings: function (data, objectIds) { - var markAll = true; - for (var i = 0, length = data.checkboxes.length; i < length; i++) { - var checkbox = data.checkboxes[i]; - var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject'); - var isMarked = (objectIds.indexOf(~~elData(checkbox, 'object-id')) !== -1); - if (!isMarked) + rebuildMarkings(data, objectIds) { + let markAll = true; + Array.from(data.checkboxes).forEach((checkbox) => { + const clipboardObject = checkbox.closest(".jsClipboardObject"); + const isMarked = objectIds.includes(~~checkbox.dataset.objectId); + if (!isMarked) { markAll = false; + } checkbox.checked = isMarked; - clipboardObject.classList[(isMarked ? 'add' : 'remove')]('jsMarked'); - if (elAttr(checkbox.parentNode, 'role') === 'checkbox') { - elAttr(checkbox.parentNode, 'aria-checked', isMarked); + if (isMarked) { + clipboardObject.classList.add("jsMarked"); } - } + else { + clipboardObject.classList.remove("jsMarked"); + } + this.setParentAsMarked(checkbox, isMarked); + }); if (data.markAll !== null) { data.markAll.checked = markAll; - if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') { - elAttr(data.markAll.parentNode, 'aria-checked', markAll); - } - var parent = data.markAll; - while (parent = parent.parentNode) { - if (parent instanceof Element && parent.classList.contains('columnMark')) { - parent = parent.parentNode; - break; - } - } + this.setParentAsMarked(data.markAll, markAll); + const parent = data.markAll.closest(".columnMark"); if (parent) { - parent.classList[(markAll ? 'add' : 'remove')]('jsMarked'); + if (markAll) { + parent.classList.add("jsMarked"); + } + else { + parent.classList.remove("jsMarked"); + } } } - }, + } + setParentAsMarked(element, isMarked) { + const parent = element.parentElement; + if (parent.getAttribute("role") === "checkbox") { + parent.setAttribute("aria-checked", isMarked ? "true" : "false"); + } + } /** * Hides the clipboard editor for the given object type. - * - * @param {string} objectType */ - hideEditor: function (objectType) { - UiPageAction.remove('wcfClipboard-' + objectType); + hideEditor(objectType) { + UiPageAction.remove("wcfClipboard-" + objectType); UiScreen.pageOverlayOpen(); - }, + } /** * Shows the clipboard editor. */ - showEditor: function () { - this._loadMarkedItems(); + showEditor() { + this.loadMarkedItems(); UiScreen.pageOverlayClose(); - }, + } /** * Unmarks the objects with given clipboard object type and ids. - * - * @param {string} objectType - * @param {int[]} objectIds */ - unmark: function (objectType, objectIds) { - this._saveState(objectType, objectIds, false); + unmark(objectType, objectIds) { + this.saveState(objectType, objectIds, false); } - }; + } + let controllerClipboard; + function getControllerClipboard() { + if (!controllerClipboard) { + controllerClipboard = new ControllerClipboard(); + } + return controllerClipboard; + } + /** + * Initializes the clipboard API handler. + */ + function setup(options) { + getControllerClipboard().setup(options); + } + exports.setup = setup; + /** + * Reloads the clipboard data. + */ + function reload() { + getControllerClipboard().reload(); + } + exports.reload = reload; + /** + * Hides the clipboard editor for the given object type. + */ + function hideEditor(objectType) { + getControllerClipboard().hideEditor(objectType); + } + exports.hideEditor = hideEditor; + /** + * Shows the clipboard editor. + */ + function showEditor() { + getControllerClipboard().showEditor(); + } + exports.showEditor = showEditor; + /** + * Unmarks the objects with given clipboard object type and ids. + */ + function unmark(objectType, objectIds) { + getControllerClipboard().unmark(objectType, objectIds); + } + exports.unmark = unmark; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.js deleted file mode 100644 index 25c0568cba..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.js +++ /dev/null @@ -1,732 +0,0 @@ -/** - * Clipboard API Handler. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Controller/Clipboard - */ -define( - [ - 'Ajax', 'Core', 'Dictionary', 'EventHandler', - 'Language', 'List', 'ObjectMap', 'Dom/ChangeListener', - 'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/SimpleDropdown', - 'WoltLabSuite/Core/Ui/Page/Action', 'Ui/Screen' - ], - function( - Ajax, Core, Dictionary, EventHandler, - Language, List, ObjectMap, DomChangeListener, - DomTraverse, DomUtil, UiConfirmation, UiSimpleDropdown, - UiPageAction, UiScreen - ) -{ - "use strict"; - - if (!COMPILER_TARGET_DEFAULT) { - return { - setup: function() {}, - reload: function() {}, - _initContainers: function() {}, - _loadMarkedItems: function() {}, - _markAll: function() {}, - _mark: function() {}, - _saveState: function() {}, - _executeAction: function() {}, - _executeProxyAction: function() {}, - _unmarkAll: function() {}, - _ajaxSetup: function() {}, - _ajaxSuccess: function() {}, - _rebuildMarkings: function() {}, - hideEditor: function() {}, - showEditor: function() {}, - unmark: function() {} - }; - } - - var _containers = new Dictionary(); - var _editors = new Dictionary(); - var _editorDropdowns = new Dictionary(); - var _elements = elByClass('jsClipboardContainer'); - var _itemData = new ObjectMap(); - var _knownCheckboxes = new List(); - var _options = {}; - var _reloadPageOnSuccess = new Dictionary(); - - var _callbackCheckbox = null; - var _callbackItem = null; - var _callbackUnmarkAll = null; - - var _specialCheckboxSelector = '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]'; - - /** - * Clipboard API - * - * @exports WoltLabSuite/Core/Controller/Clipboard - */ - return { - /** - * Initializes the clipboard API handler. - * - * @param {Object} options initialization options - */ - setup: function(options) { - if (!options.pageClassName) { - throw new Error("Expected a non-empty string for parameter 'pageClassName'."); - } - - if (_callbackCheckbox === null) { - _callbackCheckbox = this._mark.bind(this); - _callbackItem = this._executeAction.bind(this); - _callbackUnmarkAll = this._unmarkAll.bind(this); - - _options = Core.extend({ - hasMarkedItems: false, - pageClassNames: [options.pageClassName], - pageObjectId: 0 - }, options); - - delete _options.pageClassName; - } - else { - if (options.pageObjectId) { - throw new Error("Cannot load secondary clipboard with page object id set."); - } - - _options.pageClassNames.push(options.pageClassName); - } - - if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.msMatchesSelector; - } - - this._initContainers(); - - if (_options.hasMarkedItems && _elements.length) { - this._loadMarkedItems(); - } - - DomChangeListener.add('WoltLabSuite/Core/Controller/Clipboard', this._initContainers.bind(this)); - }, - - /** - * Reloads the clipboard data. - */ - reload: function() { - if (_containers.size) { - this._loadMarkedItems(); - } - }, - - /** - * Initializes clipboard containers. - */ - _initContainers: function() { - for (var i = 0, length = _elements.length; i < length; i++) { - var container = _elements[i]; - var containerId = DomUtil.identify(container); - var containerData = _containers.get(containerId); - - if (containerData === undefined) { - var markAll = elBySel('.jsClipboardMarkAll', container); - - if (markAll !== null) { - if (markAll.matches(_specialCheckboxSelector)) { - var label = markAll.closest('label'); - elAttr(label, 'role', 'checkbox'); - elAttr(label, 'tabindex', '0'); - elAttr(label, 'aria-checked', false); - elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.markAll')); - - label.addEventListener('keyup', function (event) { - if (event.keyCode === 13 || event.keyCode === 32) { - checkbox.click(); - } - }); - } - - elData(markAll, 'container-id', containerId); - markAll.addEventListener('click', this._markAll.bind(this)); - } - - containerData = { - checkboxes: elByClass('jsClipboardItem', container), - element: container, - markAll: markAll, - markedObjectIds: new List() - }; - _containers.set(containerId, containerData); - } - - for (var j = 0, innerLength = containerData.checkboxes.length; j < innerLength; j++) { - var checkbox = containerData.checkboxes[j]; - - if (!_knownCheckboxes.has(checkbox)) { - elData(checkbox, 'container-id', containerId); - - (function(checkbox) { - if (checkbox.matches(_specialCheckboxSelector)) { - var label = checkbox.closest('label'); - elAttr(label, 'role', 'checkbox'); - elAttr(label, 'tabindex', '0'); - elAttr(label, 'aria-checked', false); - elAttr(label, 'aria-label', Language.get('wcf.clipboard.item.mark')); - - label.addEventListener('keyup', function (event) { - if (event.keyCode === 13 || event.keyCode === 32) { - checkbox.click(); - } - }); - } - - var link = checkbox.closest('a'); - if (link === null) { - checkbox.addEventListener('click', _callbackCheckbox); - } - else { - // Firefox will always trigger the link if the checkbox is - // inside of one. Since 2000. Thanks Firefox. - checkbox.addEventListener('click', function (event) { - event.preventDefault(); - - window.setTimeout(function () { - checkbox.checked = !checkbox.checked; - - _callbackCheckbox(null, checkbox); - }, 10); - }); - } - })(checkbox); - - _knownCheckboxes.add(checkbox); - } - } - } - }, - - /** - * Loads marked items from clipboard. - */ - _loadMarkedItems: function() { - Ajax.api(this, { - actionName: 'getMarkedItems', - parameters: { - pageClassNames: _options.pageClassNames, - pageObjectID: _options.pageObjectId - } - }); - }, - - /** - * Marks or unmarks all visible items at once. - * - * @param {object} event event object - */ - _markAll: function(event) { - var checkbox = event.currentTarget; - var isMarked = (checkbox.nodeName !== 'INPUT' || checkbox.checked); - - if (elAttr(checkbox.parentNode, 'role') === 'checkbox') { - elAttr(checkbox.parentNode, 'aria-checked', isMarked); - } - - var objectIds = []; - - var containerId = elData(checkbox, 'container-id'); - var data = _containers.get(containerId); - var type = elData(data.element, 'type'); - - for (var i = 0, length = data.checkboxes.length; i < length; i++) { - var item = data.checkboxes[i]; - var objectId = ~~elData(item, 'object-id'); - - if (isMarked) { - if (!item.checked) { - item.checked = true; - - data.markedObjectIds.add(objectId); - objectIds.push(objectId); - } - } - else { - if (item.checked) { - item.checked = false; - - data.markedObjectIds['delete'](objectId); - objectIds.push(objectId); - } - } - - if (elAttr(item.parentNode, 'role') === 'checkbox') { - elAttr(item.parentNode, 'aria-checked', isMarked); - } - - var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject'); - if (clipboardObject !== null) { - clipboardObject.classList[(isMarked ? 'addClass' : 'removeClass')]('jsMarked'); - } - } - - this._saveState(type, objectIds, isMarked); - }, - - /** - * Marks or unmarks an individual item. - * - * @param {object} event event object - * @param {Element=} checkbox checkbox element - */ - _mark: function(event, checkbox) { - checkbox = (event instanceof Event) ? event.currentTarget : checkbox; - var objectId = ~~elData(checkbox, 'object-id'); - var isMarked = checkbox.checked; - var containerId = elData(checkbox, 'container-id'); - var data = _containers.get(containerId); - var type = elData(data.element, 'type'); - - var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject'); - data.markedObjectIds[(isMarked ? 'add' : 'delete')](objectId); - clipboardObject.classList[(isMarked) ? 'add' : 'remove']('jsMarked'); - - if (data.markAll !== null) { - var markedAll = true; - for (var i = 0, length = data.checkboxes.length; i < length; i++) { - if (!data.checkboxes[i].checked) { - markedAll = false; - - break; - } - } - - data.markAll.checked = markedAll; - - if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') { - elAttr(data.markAll.parentNode, 'aria-checked', isMarked); - } - } - - if (elAttr(checkbox.parentNode, 'role') === 'checkbox') { - elAttr(checkbox.parentNode, 'aria-checked', checkbox.checked); - } - - this._saveState(type, [ objectId ], isMarked); - }, - - /** - * Saves the state for given item object ids. - * - * @param {string} type object type - * @param {int[]} objectIds item object ids - * @param {boolean} isMarked true if marked - */ - _saveState: function(type, objectIds, isMarked) { - Ajax.api(this, { - actionName: (isMarked ? 'mark' : 'unmark'), - parameters: { - pageClassNames: _options.pageClassNames, - pageObjectID: _options.pageObjectId, - objectIDs: objectIds, - objectType: type - } - }); - }, - - /** - * Executes an editor action. - * - * @param {object} event event object - */ - _executeAction: function(event) { - var listItem = event.currentTarget; - var data = _itemData.get(listItem); - - if (data.url) { - window.location.href = data.url; - return; - } - - var triggerEvent = function() { - var type = elData(listItem, 'type'); - - EventHandler.fire('com.woltlab.wcf.clipboard', type, { - data: data, - listItem: listItem, - responseData: null - }); - }; - - //noinspection JSUnresolvedVariable - var confirmMessage = (typeof data.internalData.confirmMessage === 'string') ? data.internalData.confirmMessage : ''; - var fireEvent = true; - - if (typeof data.parameters === 'object' && data.parameters.actionName && data.parameters.className) { - if (data.parameters.actionName === 'unmarkAll' || Array.isArray(data.parameters.objectIDs)) { - if (confirmMessage.length) { - //noinspection JSUnresolvedVariable - var template = (typeof data.internalData.template === 'string') ? data.internalData.template : ''; - - UiConfirmation.show({ - confirm: (function() { - var formData = {}; - - if (template.length) { - var items = elBySelAll('input, select, textarea', UiConfirmation.getContentElement()); - for (var i = 0, length = items.length; i < length; i++) { - var item = items[i]; - var name = elAttr(item, 'name'); - - switch (item.nodeName) { - case 'INPUT': - if ((item.type !== "checkbox" && item.type !== "radio") || item.checked) { - formData[name] = elAttr(item, 'value'); - } - break; - - case 'SELECT': - formData[name] = item.value; - break; - - case 'TEXTAREA': - formData[name] = item.value.trim(); - break; - } - } - } - - //noinspection JSUnresolvedFunction - this._executeProxyAction(listItem, data, formData); - }).bind(this), - message: confirmMessage, - template: template - }); - } - else { - this._executeProxyAction(listItem, data); - } - } - } - else if (confirmMessage.length) { - fireEvent = false; - - UiConfirmation.show({ - confirm: triggerEvent, - message: confirmMessage - }); - } - - if (fireEvent) { - triggerEvent(); - } - }, - - /** - * Forwards clipboard actions to an individual handler. - * - * @param {Element} listItem dropdown item element - * @param {Object} data action data - * @param {Object?} formData form data - */ - _executeProxyAction: function(listItem, data, formData) { - formData = formData || {}; - - var objectIds = (data.parameters.actionName !== 'unmarkAll') ? data.parameters.objectIDs : []; - var parameters = { data: formData }; - - //noinspection JSUnresolvedVariable - if (typeof data.internalData.parameters === 'object') { - //noinspection JSUnresolvedVariable - for (var key in data.internalData.parameters) { - //noinspection JSUnresolvedVariable - if (data.internalData.parameters.hasOwnProperty(key)) { - //noinspection JSUnresolvedVariable - parameters[key] = data.internalData.parameters[key]; - } - } - } - - Ajax.api(this, { - actionName: data.parameters.actionName, - className: data.parameters.className, - objectIDs: objectIds, - parameters: parameters - }, (function(responseData) { - if (data.actionName !== 'unmarkAll') { - var type = elData(listItem, 'type'); - - EventHandler.fire('com.woltlab.wcf.clipboard', type, { - data: data, - listItem: listItem, - responseData: responseData - }); - - if (_reloadPageOnSuccess.has(type) && _reloadPageOnSuccess.get(type).indexOf(responseData.actionName) !== -1) { - window.location.reload(); - return; - } - } - - this._loadMarkedItems(); - }).bind(this)); - }, - - /** - * Unmarks all clipboard items for an object type. - * - * @param {object} event event object - */ - _unmarkAll: function(event) { - var type = elData(event.currentTarget, 'type'); - - Ajax.api(this, { - actionName: 'unmarkAll', - parameters: { - objectType: type - } - }); - }, - - /** - * Sets up ajax request object. - * - * @return {object} request options - */ - _ajaxSetup: function() { - return { - data: { - className: 'wcf\\data\\clipboard\\item\\ClipboardItemAction' - } - }; - }, - - /** - * Handles successful AJAX requests. - * - * @param {object} data response data - */ - _ajaxSuccess: function(data) { - if (data.actionName === 'unmarkAll') { - _containers.forEach((function(containerData) { - if (elData(containerData.element, 'type') === data.returnValues.objectType) { - var clipboardObjects = elByClass('jsMarked', containerData.element); - while (clipboardObjects.length) { - clipboardObjects[0].classList.remove('jsMarked'); - } - - if (containerData.markAll !== null) { - containerData.markAll.checked = false; - - if (elAttr(containerData.markAll.parentNode, 'role') === 'checkbox') { - elAttr(containerData.markAll.parentNode, 'aria-checked', false); - } - } - for (var i = 0, length = containerData.checkboxes.length; i < length; i++) { - containerData.checkboxes[i].checked = false; - - if (elAttr(containerData.checkboxes[i].parentNode, 'role') === 'checkbox') { - elAttr(containerData.checkboxes[i].parentNode, 'aria-checked', false); - } - } - - UiPageAction.remove('wcfClipboard-' + data.returnValues.objectType); - } - }).bind(this)); - - return; - } - - _itemData = new ObjectMap(); - _reloadPageOnSuccess = new Dictionary(); - - // rebuild markings - _containers.forEach((function(containerData) { - var typeName = elData(containerData.element, 'type'); - - //noinspection JSUnresolvedVariable - var objectIds = (data.returnValues.markedItems && data.returnValues.markedItems.hasOwnProperty(typeName)) ? data.returnValues.markedItems[typeName] : []; - this._rebuildMarkings(containerData, objectIds); - }).bind(this)); - - var keepEditors = [], typeName; - if (data.returnValues && data.returnValues.items) { - for (typeName in data.returnValues.items) { - if (data.returnValues.items.hasOwnProperty(typeName)) { - keepEditors.push(typeName); - } - } - } - - // clear editors - _editors.forEach(function(editor, typeName) { - if (keepEditors.indexOf(typeName) === -1) { - UiPageAction.remove('wcfClipboard-' + typeName); - - _editorDropdowns.get(typeName).innerHTML = ''; - } - }); - - // no items - if (!data.returnValues || !data.returnValues.items) { - return; - } - - // rebuild editors - var actionName, created, dropdown, editor, typeData; - var divider, item, itemData, itemIndex, label, unmarkAll; - for (typeName in data.returnValues.items) { - if (!data.returnValues.items.hasOwnProperty(typeName)) { - continue; - } - - typeData = data.returnValues.items[typeName]; - //noinspection JSUnresolvedVariable - _reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess); - created = false; - - editor = _editors.get(typeName); - dropdown = _editorDropdowns.get(typeName); - if (editor === undefined) { - created = true; - - editor = elCreate('a'); - editor.className = 'dropdownToggle'; - editor.textContent = typeData.label; - - _editors.set(typeName, editor); - - dropdown = elCreate('ol'); - dropdown.className = 'dropdownMenu'; - - _editorDropdowns.set(typeName, dropdown); - } - else { - editor.textContent = typeData.label; - dropdown.innerHTML = ''; - } - - // create editor items - for (itemIndex in typeData.items) { - if (!typeData.items.hasOwnProperty(itemIndex)) { - continue; - } - - itemData = typeData.items[itemIndex]; - - item = elCreate('li'); - label = elCreate('span'); - label.textContent = itemData.label; - item.appendChild(label); - dropdown.appendChild(item); - - elData(item, 'type', typeName); - item.addEventListener('click', _callbackItem); - - _itemData.set(item, itemData); - } - - divider = elCreate('li'); - divider.classList.add('dropdownDivider'); - dropdown.appendChild(divider); - - // add 'unmark all' - unmarkAll = elCreate('li'); - elData(unmarkAll, 'type', typeName); - label = elCreate('span'); - label.textContent = Language.get('wcf.clipboard.item.unmarkAll'); - unmarkAll.appendChild(label); - unmarkAll.addEventListener('click', _callbackUnmarkAll); - dropdown.appendChild(unmarkAll); - - if (keepEditors.indexOf(typeName) !== -1) { - actionName = 'wcfClipboard-' + typeName; - - if (UiPageAction.has(actionName)) { - UiPageAction.show(actionName); - } - else { - UiPageAction.add(actionName, editor); - } - } - - if (created) { - editor.parentNode.classList.add('dropdown'); - editor.parentNode.appendChild(dropdown); - UiSimpleDropdown.init(editor); - } - } - }, - - /** - * Rebuilds the mark state for each item. - * - * @param {Object} data container data - * @param {int[]} objectIds item object ids - */ - _rebuildMarkings: function(data, objectIds) { - var markAll = true; - - for (var i = 0, length = data.checkboxes.length; i < length; i++) { - var checkbox = data.checkboxes[i]; - var clipboardObject = DomTraverse.parentByClass(checkbox, 'jsClipboardObject'); - - var isMarked = (objectIds.indexOf(~~elData(checkbox, 'object-id')) !== -1); - if (!isMarked) markAll = false; - - checkbox.checked = isMarked; - clipboardObject.classList[(isMarked ? 'add' : 'remove')]('jsMarked'); - - if (elAttr(checkbox.parentNode, 'role') === 'checkbox') { - elAttr(checkbox.parentNode, 'aria-checked', isMarked); - } - } - - if (data.markAll !== null) { - data.markAll.checked = markAll; - - if (elAttr(data.markAll.parentNode, 'role') === 'checkbox') { - elAttr(data.markAll.parentNode, 'aria-checked', markAll); - } - - var parent = data.markAll; - while (parent = parent.parentNode) { - if (parent instanceof Element && parent.classList.contains('columnMark')) { - parent = parent.parentNode; - break; - } - } - - if (parent) { - parent.classList[(markAll ? 'add' : 'remove')]('jsMarked'); - } - } - }, - - /** - * Hides the clipboard editor for the given object type. - * - * @param {string} objectType - */ - hideEditor: function(objectType) { - UiPageAction.remove('wcfClipboard-' + objectType); - - UiScreen.pageOverlayOpen(); - }, - - /** - * Shows the clipboard editor. - */ - showEditor: function() { - this._loadMarkedItems(); - - UiScreen.pageOverlayClose(); - }, - - /** - * Unmarks the objects with given clipboard object type and ids. - * - * @param {string} objectType - * @param {int[]} objectIds - */ - unmark: function(objectType, objectIds) { - this._saveState(objectType, objectIds, false); - } - }; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.ts new file mode 100644 index 0000000000..8919e63066 --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Clipboard.ts @@ -0,0 +1,710 @@ +/** + * Clipboard API Handler. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Controller/Clipboard + */ + +import * as Ajax from "../Ajax"; +import { AjaxCallbackSetup } from "../Ajax/Data"; +import * as Core from "../Core"; +import DomChangeListener from "../Dom/Change/Listener"; +import DomUtil from "../Dom/Util"; +import * as EventHandler from "../Event/Handler"; +import * as Language from "../Language"; +import * as UiConfirmation from "../Ui/Confirmation"; +import UiDropdownSimple from "../Ui/Dropdown/Simple"; +import * as UiPageAction from "../Ui/Page/Action"; +import * as UiScreen from "../Ui/Screen"; + +interface ClipboardOptions { + hasMarkedItems: boolean; + pageClassName: string; + pageObjectId: number; +} + +interface ContainerData { + checkboxes: HTMLCollectionOf; + element: HTMLElement; + markAll: HTMLInputElement | null; + markedObjectIds: Set; +} + +interface ItemData { + items: { [key: string]: ClipboardActionData }; + label: string; + reloadPageOnSuccess: string[]; +} + +interface ClipboardActionData { + actionName: string; + internalData: ArbitraryObject; + label: string; + parameters: { + actionName?: string; + className?: string; + objectIDs: number[]; + template: string; + }; + url: string; +} + +interface AjaxResponseMarkedItems { + [key: string]: number[]; +} + +interface AjaxResponse { + actionName: string; + returnValues: { + action: string; + items?: { + // They key is the `typeName` + [key: string]: ItemData; + }; + markedItems?: AjaxResponseMarkedItems; + objectType: string; + }; +} + +const _specialCheckboxSelector = + '.messageCheckboxLabel > input[type="checkbox"], .message .messageClipboardCheckbox > input[type="checkbox"], .messageGroupList .columnMark > label > input[type="checkbox"]'; + +class ControllerClipboard { + private readonly containers = new Map(); + private readonly editors = new Map(); + private readonly editorDropdowns = new Map(); + private itemData = new WeakMap(); + private readonly knownCheckboxes = new WeakSet(); + private readonly pageClassNames: string[] = []; + private pageObjectId = 0; + private readonly reloadPageOnSuccess = new Map(); + + /** + * Initializes the clipboard API handler. + */ + setup(options: ClipboardOptions) { + if (!options.pageClassName) { + throw new Error("Expected a non-empty string for parameter 'pageClassName'."); + } + + let hasMarkedItems = false; + if (this.pageClassNames.length === 0) { + hasMarkedItems = options.hasMarkedItems; + this.pageObjectId = options.pageObjectId; + } + + this.pageClassNames.push(options.pageClassName); + + this.initContainers(); + + if (hasMarkedItems && this.containers.size) { + this.loadMarkedItems(); + } + + DomChangeListener.add("WoltLabSuite/Core/Controller/Clipboard", () => this.initContainers()); + } + + /** + * Reloads the clipboard data. + */ + reload(): void { + if (this.containers.size) { + this.loadMarkedItems(); + } + } + + /** + * Initializes clipboard containers. + */ + private initContainers(): void { + document.querySelectorAll(".jsClipboardContainer").forEach((container: HTMLElement) => { + const containerId = DomUtil.identify(container); + + let containerData = this.containers.get(containerId); + if (containerData === undefined) { + const markAll = container.querySelector(".jsClipboardMarkAll") as HTMLInputElement; + + if (markAll !== null) { + if (markAll.matches(_specialCheckboxSelector)) { + const label = markAll.closest("label") as HTMLLabelElement; + label.setAttribute("role", "checkbox"); + label.tabIndex = 0; + label.setAttribute("aria-checked", "false"); + label.setAttribute("aria-label", Language.get("wcf.clipboard.item.markAll")); + + label.addEventListener("keyup", (event) => { + if (event.key === "Enter" || event.key === "Space") { + markAll.click(); + } + }); + } + + markAll.dataset.containerId = containerId; + markAll.addEventListener("click", (ev) => this.markAll(ev)); + } + + containerData = { + checkboxes: container.getElementsByClassName("jsClipboardItem") as HTMLCollectionOf, + element: container, + markAll: markAll, + markedObjectIds: new Set(), + }; + this.containers.set(containerId, containerData); + } + + Array.from(containerData.checkboxes).forEach((checkbox) => { + if (this.knownCheckboxes.has(checkbox)) { + return; + } + + checkbox.dataset.containerId = containerId; + + if (checkbox.matches(_specialCheckboxSelector)) { + const label = checkbox.closest("label") as HTMLLabelElement; + label.setAttribute("role", "checkbox"); + label.tabIndex = 0; + label.setAttribute("aria-checked", "false"); + label.setAttribute("aria-label", Language.get("wcf.clipboard.item.mark")); + + label.addEventListener("keyup", (event) => { + if (event.key === "Enter" || event.key === "Space") { + checkbox.click(); + } + }); + } + + const link = checkbox.closest("a"); + if (link === null) { + checkbox.addEventListener("click", (ev) => this.mark(ev)); + } else { + // Firefox will always trigger the link if the checkbox is + // inside of one. Since 2000. Thanks Firefox. + checkbox.addEventListener("click", (event) => { + event.preventDefault(); + + window.setTimeout(() => { + checkbox.checked = !checkbox.checked; + + this.mark(checkbox); + }, 10); + }); + } + + this.knownCheckboxes.add(checkbox); + }); + }); + } + + /** + * Loads marked items from clipboard. + */ + private loadMarkedItems(): void { + Ajax.api(this, { + actionName: "getMarkedItems", + parameters: { + pageClassNames: this.pageClassNames, + pageObjectID: this.pageObjectId, + }, + }); + } + + /** + * Marks or unmarks all visible items at once. + */ + private markAll(event: MouseEvent): void { + const checkbox = event.currentTarget as HTMLInputElement; + const isMarked = checkbox.nodeName !== "INPUT" || checkbox.checked; + + this.setParentAsMarked(checkbox, isMarked); + + const objectIds: number[] = []; + + const containerId = checkbox.dataset.containerId!; + const data = this.containers.get(containerId)!; + const type = data.element.dataset.type!; + + Array.from(data.checkboxes).forEach((item) => { + const objectId = ~~item.dataset.objectId!; + + if (isMarked) { + if (!item.checked) { + item.checked = true; + + data.markedObjectIds.add(objectId); + objectIds.push(objectId); + } + } else { + if (item.checked) { + item.checked = false; + + data.markedObjectIds["delete"](objectId); + objectIds.push(objectId); + } + } + + this.setParentAsMarked(item, isMarked); + + const clipboardObject = checkbox.closest(".jsClipboardObject"); + if (clipboardObject !== null) { + if (isMarked) { + clipboardObject.classList.add("jsMarked"); + } else { + clipboardObject.classList.remove("jsMarked"); + } + } + }); + + this.saveState(type, objectIds, isMarked); + } + + /** + * Marks or unmarks an individual item. + * + */ + private mark(event: MouseEvent | HTMLInputElement): void { + const checkbox = event instanceof Event ? (event.currentTarget as HTMLInputElement) : event; + + const objectId = ~~checkbox.dataset.objectId!; + const isMarked = checkbox.checked; + const containerId = checkbox.dataset.containerId!; + const data = this.containers.get(containerId)!; + const type = data.element.dataset.type!; + + const clipboardObject = checkbox.closest(".jsClipboardObject") as HTMLElement; + if (isMarked) { + data.markedObjectIds.add(objectId); + clipboardObject.classList.add("jsMarked"); + } else { + data.markedObjectIds.delete(objectId); + clipboardObject.classList.remove("jsMarked"); + } + + if (data.markAll !== null) { + data.markAll.checked = Array.from(data.checkboxes).some((item) => !item.checked); + + this.setParentAsMarked(data.markAll, isMarked); + } + + this.setParentAsMarked(checkbox, checkbox.checked); + + this.saveState(type, [objectId], isMarked); + } + + /** + * Saves the state for given item object ids. + */ + private saveState(objectType: string, objectIds: number[], isMarked: boolean): void { + Ajax.api(this, { + actionName: isMarked ? "mark" : "unmark", + parameters: { + pageClassNames: this.pageClassNames, + pageObjectID: this.pageObjectId, + objectIDs: objectIds, + objectType, + }, + }); + } + + /** + * Executes an editor action. + */ + private executeAction(event: MouseEvent): void { + const listItem = event.currentTarget as HTMLLIElement; + const data = this.itemData.get(listItem)!; + + if (data.url) { + window.location.href = data.url; + return; + } + + function triggerEvent() { + const type = listItem.dataset.type!; + + EventHandler.fire("com.woltlab.wcf.clipboard", type, { + data, + listItem, + responseData: null, + }); + } + + const message = typeof data.internalData.confirmMessage === "string" ? data.internalData.confirmMessage : ""; + let fireEvent = true; + + if (Core.isPlainObject(data.parameters) && data.parameters.actionName && data.parameters.className) { + if (data.parameters.actionName === "unmarkAll" || Array.isArray(data.parameters.objectIDs)) { + if (message.length) { + const template = typeof data.internalData.template === "string" ? data.internalData.template : ""; + + UiConfirmation.show({ + confirm: () => { + const formData = {}; + + if (template.length) { + UiConfirmation.getContentElement() + .querySelectorAll("input, select, textarea") + .forEach((item: HTMLInputElement) => { + const name = item.name; + + switch (item.nodeName) { + case "INPUT": + if ((item.type !== "checkbox" && item.type !== "radio") || item.checked) { + formData[name] = item.value; + } + break; + + case "SELECT": + formData[name] = item.value; + break; + + case "TEXTAREA": + formData[name] = item.value.trim(); + break; + } + }); + } + + this._executeProxyAction(listItem, data, formData); + }, + message, + template, + }); + } else { + this._executeProxyAction(listItem, data); + } + } + } else if (message.length) { + fireEvent = false; + + UiConfirmation.show({ + confirm: triggerEvent, + message, + }); + } + + if (fireEvent) { + triggerEvent(); + } + } + + /** + * Forwards clipboard actions to an individual handler. + */ + private _executeProxyAction( + listItem: HTMLLIElement, + data: ClipboardActionData, + formData: ArbitraryObject = {}, + ): void { + const objectIds = data.parameters.actionName !== "unmarkAll" ? data.parameters.objectIDs : []; + const parameters = { data: formData }; + + if (Core.isPlainObject(data.internalData.parameters)) { + Object.entries(data.internalData.parameters as ArbitraryObject).forEach(([key, value]) => { + parameters[key] = value; + }); + } + + Ajax.api( + this, + { + actionName: data.parameters.actionName, + className: data.parameters.className, + objectIDs: objectIds, + parameters, + }, + (responseData: AjaxResponse) => { + if (data.actionName !== "unmarkAll") { + const type = listItem.dataset.type!; + + EventHandler.fire("com.woltlab.wcf.clipboard", type, { + data, + listItem, + responseData, + }); + + const reloadPageOnSuccess = this.reloadPageOnSuccess.get(type); + if (reloadPageOnSuccess && reloadPageOnSuccess.includes(responseData.actionName)) { + window.location.reload(); + return; + } + } + + this.loadMarkedItems(); + }, + ); + } + + /** + * Unmarks all clipboard items for an object type. + */ + private unmarkAll(event: MouseEvent): void { + const listItem = event.currentTarget as HTMLElement; + + Ajax.api(this, { + actionName: "unmarkAll", + parameters: { + objectType: listItem.dataset.type!, + }, + }); + } + + /** + * Sets up ajax request object. + */ + _ajaxSetup(): ReturnType { + return { + data: { + className: "wcf\\data\\clipboard\\item\\ClipboardItemAction", + }, + }; + } + + /** + * Handles successful AJAX requests. + */ + _ajaxSuccess(data: AjaxResponse): void { + if (data.actionName === "unmarkAll") { + const objectType = data.returnValues.objectType; + this.containers.forEach((containerData) => { + if (containerData.element.dataset.type !== objectType) { + return; + } + + containerData.element.querySelectorAll(".jsMarked").forEach((element) => element.classList.remove("jsMarked")); + + if (containerData.markAll !== null) { + containerData.markAll.checked = false; + + this.setParentAsMarked(containerData.markAll, false); + } + + Array.from(containerData.checkboxes).forEach((checkbox) => { + checkbox.checked = false; + + this.setParentAsMarked(checkbox, false); + }); + + UiPageAction.remove(`wcfClipboard-${objectType}`); + }); + + return; + } + + this.itemData = new WeakMap(); + this.reloadPageOnSuccess.clear(); + + // rebuild markings + const markings = Core.isPlainObject(data.returnValues.markedItems) ? data.returnValues.markedItems! : {}; + this.containers.forEach((containerData) => { + const typeName = containerData.element.dataset.type!; + + const objectIds = Array.isArray(markings[typeName]) ? markings[typeName] : []; + this.rebuildMarkings(containerData, objectIds); + }); + + const keepEditors: string[] = Object.keys(data.returnValues.items || {}); + + // clear editors + this.editors.forEach((editor, typeName) => { + if (keepEditors.includes(typeName)) { + UiPageAction.remove(`wcfClipboard-${typeName}`); + + this.editorDropdowns.get(typeName)!.innerHTML = ""; + } + }); + + // no items + if (!data.returnValues.items) { + return; + } + + // rebuild editors + Object.entries(data.returnValues.items).forEach(([typeName, typeData]) => { + this.reloadPageOnSuccess.set(typeName, typeData.reloadPageOnSuccess); + + let created = false; + + let editor = this.editors.get(typeName); + let dropdown = this.editorDropdowns.get(typeName)!; + if (editor === undefined) { + created = true; + + editor = document.createElement("a"); + editor.className = "dropdownToggle"; + editor.textContent = typeData.label; + + this.editors.set(typeName, editor); + + dropdown = document.createElement("ol"); + dropdown.className = "dropdownMenu"; + + this.editorDropdowns.set(typeName, dropdown); + } else { + editor.textContent = typeData.label; + dropdown.innerHTML = ""; + } + + // create editor items + Object.values(typeData.items).forEach((itemData) => { + const item = document.createElement("li"); + const label = document.createElement("span"); + label.textContent = itemData.label; + item.appendChild(label); + dropdown.appendChild(item); + + item.dataset.type = typeName; + item.addEventListener("click", (ev) => this.executeAction(ev)); + + this.itemData.set(item, itemData); + }); + + const divider = document.createElement("li"); + divider.classList.add("dropdownDivider"); + dropdown.appendChild(divider); + + // add 'unmark all' + const unmarkAll = document.createElement("li"); + unmarkAll.dataset.type = typeName; + const label = document.createElement("span"); + label.textContent = Language.get("wcf.clipboard.item.unmarkAll"); + unmarkAll.appendChild(label); + unmarkAll.addEventListener("click", (ev) => this.unmarkAll(ev)); + dropdown.appendChild(unmarkAll); + + if (keepEditors.indexOf(typeName) !== -1) { + const actionName = `wcfClipboard-${typeName}`; + + if (UiPageAction.has(actionName)) { + UiPageAction.show(actionName); + } else { + UiPageAction.add(actionName, editor); + } + } + + if (created) { + const parent = editor.parentElement!; + parent.classList.add("dropdown"); + parent.appendChild(dropdown); + UiDropdownSimple.init(editor); + } + }); + } + + /** + * Rebuilds the mark state for each item. + */ + private rebuildMarkings(data: ContainerData, objectIds: number[]): void { + let markAll = true; + + Array.from(data.checkboxes).forEach((checkbox) => { + const clipboardObject = checkbox.closest(".jsClipboardObject") as HTMLElement; + + const isMarked = objectIds.includes(~~checkbox.dataset.objectId!); + if (!isMarked) { + markAll = false; + } + + checkbox.checked = isMarked; + if (isMarked) { + clipboardObject.classList.add("jsMarked"); + } else { + clipboardObject.classList.remove("jsMarked"); + } + + this.setParentAsMarked(checkbox, isMarked); + }); + + if (data.markAll !== null) { + data.markAll.checked = markAll; + + this.setParentAsMarked(data.markAll, markAll); + + const parent = data.markAll.closest(".columnMark"); + if (parent) { + if (markAll) { + parent.classList.add("jsMarked"); + } else { + parent.classList.remove("jsMarked"); + } + } + } + } + + private setParentAsMarked(element: HTMLElement, isMarked: boolean): void { + const parent = element.parentElement!; + if (parent.getAttribute("role") === "checkbox") { + parent.setAttribute("aria-checked", isMarked ? "true" : "false"); + } + } + + /** + * Hides the clipboard editor for the given object type. + */ + hideEditor(objectType: string): void { + UiPageAction.remove("wcfClipboard-" + objectType); + + UiScreen.pageOverlayOpen(); + } + + /** + * Shows the clipboard editor. + */ + showEditor(): void { + this.loadMarkedItems(); + + UiScreen.pageOverlayClose(); + } + + /** + * Unmarks the objects with given clipboard object type and ids. + */ + unmark(objectType: string, objectIds: number[]): void { + this.saveState(objectType, objectIds, false); + } +} + +let controllerClipboard: ControllerClipboard; + +function getControllerClipboard(): ControllerClipboard { + if (!controllerClipboard) { + controllerClipboard = new ControllerClipboard(); + } + + return controllerClipboard; +} + +/** + * Initializes the clipboard API handler. + */ +export function setup(options: ClipboardOptions): void { + getControllerClipboard().setup(options); +} + +/** + * Reloads the clipboard data. + */ +export function reload(): void { + getControllerClipboard().reload(); +} + +/** + * Hides the clipboard editor for the given object type. + */ +export function hideEditor(objectType: string): void { + getControllerClipboard().hideEditor(objectType); +} + +/** + * Shows the clipboard editor. + */ +export function showEditor(): void { + getControllerClipboard().showEditor(); +} + +/** + * Unmarks the objects with given clipboard object type and ids. + */ +export function unmark(objectType: string, objectIds: number[]): void { + getControllerClipboard().unmark(objectType, objectIds); +} -- 2.20.1