diff --git a/ts/WoltLabSuite/Core/Component/Attachment/List.ts b/ts/WoltLabSuite/Core/Component/Attachment/List.ts index aa6c7e510c1..e885384fd6f 100644 --- a/ts/WoltLabSuite/Core/Component/Attachment/List.ts +++ b/ts/WoltLabSuite/Core/Component/Attachment/List.ts @@ -3,6 +3,9 @@ import { CkeditorDropEvent } from "../File/Upload"; import { createAttachmentFromFile } from "./Entry"; import { listenToCkeditor } from "../Ckeditor/Event"; import { getTabMenu } from "../Message/MessageTabMenu"; +import Sortable from "sortablejs"; +import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex"; +import { postObject } from "WoltLabSuite/Core/Api/PostObject"; function fileToAttachment(fileList: HTMLElement, file: WoltlabCoreFileElement, editor: HTMLElement): void { fileList.append(createAttachmentFromFile(file, editor)); @@ -44,6 +47,36 @@ export function setup(editorId: string): void { uploadButton.insertAdjacentElement("afterend", fileList); } + new Sortable(fileList, { + direction: "vertical", + dragClass: ".fileList__item", + ghostClass: "fileList__item--ghost", + handle: ".fileList__item__file", + animation: 150, + fallbackOnBody: true, + onChange(event) { + const file = event.item.querySelector("woltlab-core-file")!; + const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); + if (thumbnail !== undefined) { + file.thumbnail = thumbnail; + } else if (file.link) { + file.previewUrl = file.link; + } + }, + onEnd: promiseMutex(async (event) => { + if (event.oldIndex === event.newIndex) { + return; + } + + const attachmentIDs = Array.from(fileList.querySelectorAll("woltlab-core-file")) + .map((file) => file.data?.attachmentID) + .filter((attachmentID) => attachmentID !== undefined); + const context = JSON.parse(uploadButton.dataset.context!); + + await postObject(`${window.WSC_RPC_API_URL}core/attachments/show-order`, { ...context, attachmentIDs }); + }), + }); + let showOrder = -1; uploadButton.addEventListener("uploadStart", (event: CustomEvent) => { fileToAttachment(fileList, event.detail, editor); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js index 41da2da7fe6..2c89d0369b0 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js @@ -1,7 +1,8 @@ -define(["require", "exports", "./Entry", "../Ckeditor/Event", "../Message/MessageTabMenu"], function (require, exports, Entry_1, Event_1, MessageTabMenu_1) { +define(["require", "exports", "tslib", "./Entry", "../Ckeditor/Event", "../Message/MessageTabMenu", "sortablejs", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Api/PostObject"], function (require, exports, tslib_1, Entry_1, Event_1, MessageTabMenu_1, sortablejs_1, PromiseMutex_1, PostObject_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; + sortablejs_1 = tslib_1.__importDefault(sortablejs_1); function fileToAttachment(fileList, file, editor) { fileList.append((0, Entry_1.createAttachmentFromFile)(file, editor)); } @@ -32,6 +33,34 @@ define(["require", "exports", "./Entry", "../Ckeditor/Event", "../Message/Messag fileList.classList.add("fileList"); uploadButton.insertAdjacentElement("afterend", fileList); } + new sortablejs_1.default(fileList, { + direction: "vertical", + dragClass: ".fileList__item", + ghostClass: "fileList__item--ghost", + handle: ".fileList__item__file", + animation: 150, + fallbackOnBody: true, + onChange(event) { + const file = event.item.querySelector("woltlab-core-file"); + const thumbnail = file.thumbnails.find((thumbnail) => thumbnail.identifier === "tiny"); + if (thumbnail !== undefined) { + file.thumbnail = thumbnail; + } + else if (file.link) { + file.previewUrl = file.link; + } + }, + onEnd: (0, PromiseMutex_1.promiseMutex)(async (event) => { + if (event.oldIndex === event.newIndex) { + return; + } + const attachmentIDs = Array.from(fileList.querySelectorAll("woltlab-core-file")) + .map((file) => file.data?.attachmentID) + .filter((attachmentID) => attachmentID !== undefined); + const context = JSON.parse(uploadButton.dataset.context); + await (0, PostObject_1.postObject)(`${window.WSC_RPC_API_URL}core/attachments/show-order`, { ...context, attachmentIDs }); + }), + }); let showOrder = -1; uploadButton.addEventListener("uploadStart", (event) => { fileToAttachment(fileList, event.detail, editor); diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 69e00d6e778..4f233f8e7c5 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -238,6 +238,7 @@ static function (\wcf\event\endpoint\ControllerCollecting $event) { $event->register(new \wcf\system\endpoint\controller\core\smilies\ChangeShowOrder()); $event->register(new \wcf\system\endpoint\controller\core\smilies\categories\GetSmileyShowOrder()); $event->register(new \wcf\system\endpoint\controller\core\smilies\categories\ChangeSmileyShowOrder()); + $event->register(new \wcf\system\endpoint\controller\core\attachments\ChangeShowOrder()); } ); diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/attachments/ChangeShowOrder.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/attachments/ChangeShowOrder.class.php new file mode 100644 index 00000000000..f987e535b31 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/attachments/ChangeShowOrder.class.php @@ -0,0 +1,100 @@ + + * @since 6.2 + */ +#[PostRequest('/core/attachments/show-order')] +final class ChangeShowOrder implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $parameters = Helper::mapApiParameters($request, ChangeShowOrderParameters::class); + + $attachmentHandler = new AttachmentHandler( + $parameters->objectType, + $parameters->objectID, + $parameters->tmpHash, + $parameters->parentObjectID, + ); + + $this->assertAttachmentsCanBeSorted($attachmentHandler, $parameters->attachmentIDs); + + $this->saveShowOrder($parameters->attachmentIDs); + + return new JsonResponse([]); + } + + /** + * @param list $attachmentIDs + */ + private function assertAttachmentsCanBeSorted(AttachmentHandler $attachmentHandler, array $attachmentIDs): void + { + if (!$attachmentHandler->canUpload()) { + throw new PermissionDeniedException(); + } + + $attachmentList = $attachmentHandler->getAttachmentList(); + foreach ($attachmentIDs as $attachmentID) { + if (!\in_array($attachmentID, $attachmentList->getObjectIDs(), true)) { + throw new PermissionDeniedException(); + } + } + } + + /** + * @param list $attachmentIDs + */ + private function saveShowOrder(array $attachmentIDs): void + { + WCF::getDB()->beginTransaction(); + + $sql = "UPDATE wcf1_attachment + SET showOrder = ? + WHERE attachmentID = ?"; + $statement = WCF::getDB()->prepare($sql); + + foreach ($attachmentIDs as $showOrder => $attachmentID) { + $statement->execute([ + $showOrder + 1, + $attachmentID, + ]); + } + + WCF::getDB()->commitTransaction(); + } +} + +/** @internal */ +final class ChangeShowOrderParameters +{ + public function __construct( + /** @var non-empty-string */ + public readonly string $objectType, + /** @var non-negative-int */ + public readonly int $objectID, + /** @var non-negative-int */ + public readonly int $parentObjectID, + public readonly string $tmpHash, + /** @var list */ + public readonly array $attachmentIDs, + ) { + } +} diff --git a/wcfsetup/install/files/style/ui/fileList.scss b/wcfsetup/install/files/style/ui/fileList.scss index ccd0a1067f2..ab177a4e076 100644 --- a/wcfsetup/install/files/style/ui/fileList.scss +++ b/wcfsetup/install/files/style/ui/fileList.scss @@ -39,6 +39,10 @@ color: var(--wcfStatusErrorText); } +.fileList__item--ghost { + opacity: 0.54; +} + .fileList__item .innerError { grid-area: error; } @@ -106,3 +110,9 @@ woltlab-core-file img { .woltlabCoreFileUpload__input::-webkit-file-upload-button { cursor: pointer; } + +@media (hover: hover) { + .fileList__item__file:hover { + cursor: move; + } +}