-
+
+ {unsafe:$gridView->render()}
-{if $objects|count}
-
-{else}
-
{lang}wcf.global.noItems{/lang}
-{/if}
+
{include file='footer'}
diff --git a/wcfsetup/install/files/acp/templates/mediaListItems.tpl b/wcfsetup/install/files/acp/templates/mediaListItems.tpl
deleted file mode 100644
index d645a7dc0b5..00000000000
--- a/wcfsetup/install/files/acp/templates/mediaListItems.tpl
+++ /dev/null
@@ -1,48 +0,0 @@
-{foreach from=$mediaList item=media}
-
-
- {unsafe:$media->getElementTag(144)}
-
-
- {assign var='__mediaTitle' value=$media->filename}
- {if $media->title}
- {assign var='__mediaTitle' value=$media->title}
- {/if}
-
-
-
-
-
-
-{/foreach}
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_mediaL10n.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_mediaL10n.php
new file mode 100644
index 00000000000..0ac3341ee7f
--- /dev/null
+++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.3_mediaL10n.php
@@ -0,0 +1,135 @@
+
+ */
+
+use wcf\system\language\LanguageFactory;
+use wcf\system\WCF;
+
+// This script owns the table's content at this point (idempotency on re-runs).
+WCF::getDB()->prepare("DELETE FROM wcf1_media_l10n")->execute();
+
+$sql = "INSERT INTO wcf1_media_l10n
+ (mediaID, languageID, title, caption, altText)
+ SELECT media_content.mediaID, media_content.languageID,
+ media_content.title, media_content.caption, media_content.altText
+ FROM wcf1_media_content media_content
+ INNER JOIN wcf1_media media
+ ON media.mediaID = media_content.mediaID
+ WHERE media.isMultilingual = ?
+ AND media_content.languageID IS NOT NULL";
+$statement = WCF::getDB()->prepare($sql);
+$statement->execute([1]);
+
+$defaultLanguageID = LanguageFactory::getInstance()->getDefaultLanguageID();
+
+$sql = "SELECT media_content.*, media.languageID AS mediaLanguageID
+ FROM wcf1_media_content media_content
+ INNER JOIN wcf1_media media
+ ON media.mediaID = media_content.mediaID
+ WHERE media.isMultilingual = ?
+ AND media_content.mediaID > ?
+ ORDER BY media_content.mediaID";
+$selectStatement = WCF::getDB()->prepare($sql, 1_000);
+
+$sql = "INSERT INTO wcf1_media_l10n
+ (mediaID, languageID, title, caption, altText)
+ VALUES (?, NULL, ?, ?, ?)";
+$insertStatement = WCF::getDB()->prepare($sql);
+
+/**
+ * Picks the content row that was displayed for a monolingual media file:
+ * its own language, a row without a language, the default language and
+ * finally the lowest language id.
+ *
+ * @param list
> $rows
+ * @return array
+ */
+$pickRow = static function (array $rows) use ($defaultLanguageID): array {
+ $mediaLanguageID = $rows[0]['mediaLanguageID'];
+
+ $preferred = [];
+ foreach ($rows as $row) {
+ $preferred[] = match (true) {
+ $mediaLanguageID !== null && $row['languageID'] === $mediaLanguageID => -3,
+ $row['languageID'] === null => -2,
+ $row['languageID'] === $defaultLanguageID => -1,
+ default => $row['languageID'],
+ };
+ }
+
+ return $rows[\array_search(\min($preferred), $preferred, true)];
+};
+
+$lastMediaID = 0;
+do {
+ $selectStatement->execute([0, $lastMediaID]);
+
+ $rowsByMedia = [];
+ while ($row = $selectStatement->fetchArray()) {
+ $rowsByMedia[$row['mediaID']][] = $row;
+ }
+
+ if ($rowsByMedia === []) {
+ break;
+ }
+
+ // The last media file may be incomplete due to the limit, it is read
+ // again in the next iteration unless it is the only one.
+ $mediaIDs = \array_keys($rowsByMedia);
+ if (\count($mediaIDs) > 1) {
+ unset($rowsByMedia[\end($mediaIDs)]);
+ }
+
+ WCF::getDB()->beginTransaction();
+ foreach ($rowsByMedia as $mediaID => $rows) {
+ $row = $pickRow($rows);
+
+ $insertStatement->execute([
+ $mediaID,
+ $row['title'],
+ $row['caption'],
+ $row['altText'],
+ ]);
+
+ $lastMediaID = $mediaID;
+ }
+ WCF::getDB()->commitTransaction();
+} while (true);
+
+// Media files are either assigned to a single language or multilingual.
+$sql = "UPDATE wcf1_media
+ SET languageID = NULL
+ WHERE isMultilingual = ?";
+$statement = WCF::getDB()->prepare($sql);
+$statement->execute([1]);
+
+// Media files that have never been edited or whose language has been deleted
+// have no language.
+$sql = "UPDATE wcf1_media
+ SET languageID = ?
+ WHERE isMultilingual = ?
+ AND languageID IS NULL";
+$statement = WCF::getDB()->prepare($sql);
+$statement->execute([
+ $defaultLanguageID,
+ 0,
+]);
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Component/Media/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Component/Media/List.js
new file mode 100644
index 00000000000..b31a8b87c8b
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Component/Media/List.js
@@ -0,0 +1,33 @@
+/**
+ * Reloads the grid view of media files after new files have been uploaded. A
+ * single uploaded file is opened in the edit dialog, multiple uploaded files
+ * are confirmed by a notification, because they might not be visible on the
+ * current page of the grid view.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.3
+ * @woltlabExcludeBundle all
+ */
+define(["require", "exports", "WoltLabSuite/Core/Component/Media/Upload", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Language"], function (require, exports, Upload_1, Snackbar_1, Language_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ function setup(uploadButton, container, editEndpoint) {
+ (0, Upload_1.setupMediaUpload)(uploadButton, {
+ // Reload the grid view only once after a batch of files has been uploaded.
+ allUploadsCompleted: async (mediaIds) => {
+ container.dispatchEvent(new CustomEvent("interaction:invalidate-all"));
+ if (mediaIds.length > 1) {
+ (0, Snackbar_1.showSuccessSnackbar)((0, Language_1.getPhrase)("wcf.media.upload.success.multiple", { count: mediaIds.length }));
+ return;
+ }
+ // A single uploaded file is edited right away.
+ if (mediaIds.length === 1 && (await (0, Upload_1.editMedia)(editEndpoint, mediaIds[0]))) {
+ container.dispatchEvent(new CustomEvent("interaction:invalidate-all"));
+ }
+ },
+ });
+ }
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Media.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Media.js
index 9eb6e05b142..a9ac45d06e0 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Media.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Ui/CodeMirror/Media.js
@@ -1,30 +1,36 @@
-define(["require", "exports", "tslib", "../../../Media/Manager/Editor"], function (require, exports, tslib_1, Editor_1) {
+/**
+ * Inserts media files into a CodeMirror editor.
+ *
+ * @author Matthias Schmidt, Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ */
+define(["require", "exports", "../../../Component/Media/Manager", "../../../Component/Media/Insert", "../../../Helper/PromiseMutex"], function (require, exports, Manager_1, Insert_1, PromiseMutex_1) {
"use strict";
- Editor_1 = tslib_1.__importDefault(Editor_1);
class AcpUiCodeMirrorMedia {
element;
constructor(elementId) {
this.element = document.getElementById(elementId);
const button = document.getElementById(`codemirror-${elementId}-media`);
- button.classList.add(button.id);
- new Editor_1.default({
- buttonClass: button.id,
- callbackInsert: (media, insertType, thumbnailSize) => this.insert(media, insertType, thumbnailSize),
- });
+ button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(() => this.insert()));
}
- insert(mediaList, insertType, thumbnailSize) {
- switch (insertType) {
- case "separate" /* MediaInsertType.Separate */: {
- let sizeArgument = "";
- if (thumbnailSize) {
- sizeArgument = ` size="${thumbnailSize}"`;
- }
- const content = Array.from(mediaList.values())
- .map((item) => `{{ media="${item.mediaID}"${sizeArgument} }}`)
- .join("");
- this.element.codemirror.replaceSelection(content);
- }
+ async insert() {
+ const media = await (0, Manager_1.openMediaManager)({ mode: "editor" });
+ if (media.length === 0) {
+ return;
}
+ const size = await (0, Insert_1.chooseImageSize)(media);
+ if (size === undefined) {
+ return;
+ }
+ const content = media
+ .map((item) => {
+ const effectiveSize = item.isImage ? (0, Insert_1.getEffectiveSize)(item, size) : "original";
+ const sizeArgument = effectiveSize !== "original" ? ` size="${effectiveSize}"` : "";
+ return `{{ media="${item.mediaID}"${sizeArgument} }}`;
+ })
+ .join("");
+ this.element.codemirror.replaceSelection(content);
}
}
return AcpUiCodeMirrorMedia;
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Media/GetMedia.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Media/GetMedia.js
new file mode 100644
index 00000000000..23e6399d5cf
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Media/GetMedia.js
@@ -0,0 +1,19 @@
+/**
+ * Gets the data of a media file that is required to insert or select it.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.3
+ */
+define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.getMedia = getMedia;
+ async function getMedia(mediaId) {
+ const url = new URL(`${window.WSC_RPC_API_URL}core/media/${mediaId}`);
+ return (0, Result_1.fromInfallibleApiRequest)(() => {
+ return (0, Backend_1.prepareRequest)(url).get().fetchAsJson();
+ });
+ }
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
index fdf796d2a5d..08d1b177890 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
@@ -157,18 +157,21 @@ define(["require", "exports", "tslib", "./Core", "./Date/Picker", "./Devtools",
(0, LazyLoader_1.whenFirstSeen)(".userTrophyOverlayList", () => {
void new Promise((resolve_12, reject_12) => { require(["./Component/User/TrophyList"], resolve_12, reject_12); }).then(tslib_1.__importStar).then(({ setup }) => setup());
});
+ (0, LazyLoader_1.whenFirstSeen)(".mediaGallery", () => {
+ void new Promise((resolve_13, reject_13) => { require(["./Component/Media/Gallery"], resolve_13, reject_13); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+ });
(0, LazyLoader_1.whenFirstSeen)("[data-fancybox]", () => {
- void new Promise((resolve_13, reject_13) => { require(["./Component/Image/Viewer"], resolve_13, reject_13); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+ void new Promise((resolve_14, reject_14) => { require(["./Component/Image/Viewer"], resolve_14, reject_14); }).then(tslib_1.__importStar).then(({ setup }) => setup());
});
(0, LazyLoader_1.whenFirstSeen)(".jsImageViewer", () => {
console.warn("The class `jsImageViewer` is deprecated. Use the attribute `data-fancybox` instead.");
- void new Promise((resolve_14, reject_14) => { require(["./Component/Image/Viewer"], resolve_14, reject_14); }).then(tslib_1.__importStar).then(({ setupLegacy }) => setupLegacy());
+ void new Promise((resolve_15, reject_15) => { require(["./Component/Image/Viewer"], resolve_15, reject_15); }).then(tslib_1.__importStar).then(({ setupLegacy }) => setupLegacy());
});
(0, LazyLoader_1.whenFirstSeen)(".jsEnablesOptions", () => {
- void new Promise((resolve_15, reject_15) => { require(["./Component/Option/Enable"], resolve_15, reject_15); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+ void new Promise((resolve_16, reject_16) => { require(["./Component/Option/Enable"], resolve_16, reject_16); }).then(tslib_1.__importStar).then(({ setup }) => setup());
});
(0, LazyLoader_1.whenFirstSeen)("[data-edit-cover-photo]", () => {
- void new Promise((resolve_16, reject_16) => { require(["./Component/User/CoverPhoto"], resolve_16, reject_16); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+ void new Promise((resolve_17, reject_17) => { require(["./Component/User/CoverPhoto"], resolve_17, reject_17); }).then(tslib_1.__importStar).then(({ setup }) => setup());
});
// Move the reCAPTCHA widget overlay to the `pageOverlayContainer`
// when widget form elements are placed in a dialog.
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Media.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Media.js
index 70dacb8a29c..ed6f131bbc4 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Media.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Ckeditor/Media.js
@@ -1,46 +1,13 @@
/**
* Forwards upload requests from the editor to the media system.
*
- * @author Alexander Ebert
- * @copyright 2001-2023 WoltLab GmbH
+ * @author Alexander Ebert, Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License
* @since 6.0
* @woltlabExcludeBundle tiny
*/
-var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- var desc = Object.getOwnPropertyDescriptor(m, k);
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
- desc = { enumerable: true, get: function() { return m[k]; } };
- }
- Object.defineProperty(o, k2, desc);
-}) : (function(o, m, k, k2) {
- if (k2 === undefined) k2 = k;
- o[k2] = m[k];
-}));
-var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
- Object.defineProperty(o, "default", { enumerable: true, value: v });
-}) : function(o, v) {
- o["default"] = v;
-});
-var __importStar = (this && this.__importStar) || (function () {
- var ownKeys = function(o) {
- ownKeys = Object.getOwnPropertyNames || function (o) {
- var ar = [];
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
- return ar;
- };
- return ownKeys(o);
- };
- return function (mod) {
- if (mod && mod.__esModule) return mod;
- var result = {};
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
- __setModuleDefault(result, mod);
- return result;
- };
-})();
-define(["require", "exports", "./Event", "./Filename"], function (require, exports, Event_1, Filename_1) {
+define(["require", "exports", "./Event", "./Filename", "../Media/Manager", "../Media/Insert", "../../Helper/PromiseMutex"], function (require, exports, Event_1, Filename_1, Manager_1, Insert_1, PromiseMutex_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.setup = setup;
@@ -55,6 +22,22 @@ define(["require", "exports", "./Event", "./Filename"], function (require, expor
// stop caring about the file so that we regain control.
return Promise.reject();
}
+ /**
+ * Opens the media manager and inserts the chosen media files. A file that was
+ * pasted or dropped into the editor is uploaded right away and inserted once
+ * the upload has been completed.
+ */
+ async function insertMedia(ckeditor, file) {
+ const media = await (0, Manager_1.openMediaManager)({ mode: "editor", file });
+ if (media.length === 0) {
+ return;
+ }
+ const options = await (0, Insert_1.chooseInsertOptions)(media);
+ if (options === undefined) {
+ return;
+ }
+ (0, Insert_1.insertIntoCkeditor)(ckeditor, media, options.size, options.gallery);
+ }
function setup(element) {
(0, Event_1.listenToCkeditor)(element)
.setupConfiguration(({ configuration, features }) => {
@@ -71,11 +54,20 @@ define(["require", "exports", "./Event", "./Filename"], function (require, expor
if (!ckeditor.features.media) {
return;
}
- void new Promise((resolve_1, reject_1) => { require(["../../Media/Manager/Editor"], resolve_1, reject_1); }).then(__importStar).then(({ MediaManagerEditor }) => {
- new MediaManagerEditor({
- ckeditor,
- });
+ // The media manager is loaded asynchronously, repeated clicks must not open it twice.
+ const openManager = (0, PromiseMutex_1.promiseMutex)(() => insertMedia(ckeditor));
+ (0, Event_1.listenToCkeditor)(ckeditor.sourceElement).bbcode(({ bbcode }) => {
+ if (bbcode !== "media") {
+ return false;
+ }
+ openManager();
+ return true;
});
+ if (!ckeditor.features.attachment) {
+ (0, Event_1.listenToCkeditor)(ckeditor.sourceElement).uploadMedia((payload) => {
+ void insertMedia(ckeditor, payload.file);
+ });
+ }
});
}
});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js
index fff4e66f45b..b24b52b24ec 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView.js
@@ -104,7 +104,7 @@ define(["require", "exports", "tslib", "./ListView/State", "../Dom/Change/Listen
this.#listViewParameters.set(key, event.detail[key]);
}
}
- void this.#loadItems(0 /* StateChangeCause.Change */);
+ this.#state.resetPageNo();
});
}
#setupState(viewId, pageNo, baseUrl, sortField, sortOrder, defaultSortField, defaultSortOrder) {
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js
index 08dd745e986..9f40914855e 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/Setup.js
@@ -42,7 +42,7 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend"], function (requi
Object.defineProperty(exports, "__esModule", { value: true });
exports.ListViewSetup = void 0;
class ListViewSetup {
- async fromPreset(title, listViewClass, listViewParameters, filters, sortField = "", sortOrder = "ASC", pageNo = 1) {
+ async fromPreset(title, listViewClass, listViewParameters, filters, sortField = "", sortOrder = "ASC", pageNo = 1, promptOptions) {
const url = new URL(`${window.WSC_RPC_API_URL}core/list-views/render`);
url.searchParams.set("listView", listViewClass);
url.searchParams.set("pageNo", pageNo.toString());
@@ -61,7 +61,9 @@ define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend"], function (requi
const json = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson());
// Prevents a circular dependency.
const { dialogFactory } = await new Promise((resolve_1, reject_1) => { require(["../Dialog"], resolve_1, reject_1); }).then(__importStar);
- const dialog = dialogFactory().fromHtml(json.listView).withoutControls();
+ // The dialog controls are only shown if the list view is used as a prompt.
+ const controls = dialogFactory().fromHtml(json.listView);
+ const dialog = promptOptions === undefined ? controls.withoutControls() : controls.asPrompt(promptOptions);
dialog.show(title);
return dialog;
}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/State.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/State.js
index 21ae1bfb6cf..83985c4e1e9 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/State.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/ListView/State.js
@@ -67,6 +67,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "./F
getPageNo() {
return this.#pageNo;
}
+ /**
+ * Reloads the list starting from the first page, the previous page may
+ * no longer exist after the parameters of the list view have changed.
+ */
+ resetPageNo() {
+ this.#switchPage(1, 0 /* StateChangeCause.Change */);
+ }
getSortField() {
return this.#sorting.getSortField();
}
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Gallery.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Gallery.js
new file mode 100644
index 00000000000..efd7516b6dd
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Gallery.js
@@ -0,0 +1,78 @@
+/**
+ * Adds the controls to navigate between the images of media galleries.
+ *
+ * The gallery itself is a horizontally scrollable track that snaps to the
+ * images and remains usable without these controls.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.3
+ */
+define(["require", "exports", "WoltLabSuite/Core/Helper/Selector"], function (require, exports, Selector_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ function getStep(track, slides) {
+ if (slides.length < 2) {
+ return track.clientWidth;
+ }
+ return Math.abs(slides[1].offsetLeft - slides[0].offsetLeft);
+ }
+ function initGallery(gallery) {
+ const track = gallery.querySelector(".mediaGallery__track");
+ const controls = gallery.querySelector(".mediaGallery__controls");
+ const previousButton = controls.querySelector(".mediaGallery__button--previous");
+ const nextButton = controls.querySelector(".mediaGallery__button--next");
+ const counter = controls.querySelector(".mediaGallery__counter");
+ const slides = Array.from(track.querySelectorAll(".mediaGallery__slide"));
+ // The scroll position is negative for right-to-left languages.
+ const isRtl = () => window.getComputedStyle(track).direction === "rtl";
+ const update = () => {
+ const isScrollable = track.scrollWidth > track.clientWidth + 1;
+ controls.hidden = !isScrollable;
+ if (!isScrollable) {
+ return;
+ }
+ const scrollPosition = Math.abs(track.scrollLeft);
+ const index = Math.min(Math.round(scrollPosition / getStep(track, slides)), slides.length - 1);
+ counter.textContent = `${index + 1} / ${slides.length}`;
+ previousButton.disabled = scrollPosition <= 1;
+ nextButton.disabled = scrollPosition + track.clientWidth >= track.scrollWidth - 1;
+ };
+ const scrollBySlides = (count) => {
+ track.scrollBy({ left: count * getStep(track, slides) });
+ };
+ previousButton.addEventListener("click", () => scrollBySlides(isRtl() ? 1 : -1));
+ nextButton.addEventListener("click", () => scrollBySlides(isRtl() ? -1 : 1));
+ track.addEventListener("keydown", (event) => {
+ if (event.target !== track) {
+ return;
+ }
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollBySlides(-1);
+ }
+ else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollBySlides(1);
+ }
+ });
+ let pendingUpdate = false;
+ track.addEventListener("scroll", () => {
+ if (pendingUpdate) {
+ return;
+ }
+ pendingUpdate = true;
+ window.requestAnimationFrame(() => {
+ pendingUpdate = false;
+ update();
+ });
+ }, { passive: true });
+ new ResizeObserver(() => update()).observe(track);
+ update();
+ }
+ function setup() {
+ (0, Selector_1.wheneverFirstSeen)(".mediaGallery", (gallery) => initGallery(gallery));
+ }
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Insert.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Insert.js
new file mode 100644
index 00000000000..64bf3b27ea1
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Insert.js
@@ -0,0 +1,122 @@
+/**
+ * Helper functions to insert media files into editors.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.3
+ * @woltlabExcludeBundle tiny
+ */
+define(["require", "exports", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/StringUtil"], function (require, exports, Dialog_1, Language_1, StringUtil_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.chooseImageSize = chooseImageSize;
+ exports.chooseInsertOptions = chooseInsertOptions;
+ exports.getEffectiveSize = getEffectiveSize;
+ exports.insertIntoCkeditor = insertIntoCkeditor;
+ const sizes = ["small", "medium", "large", "original"];
+ function getAvailableSizes(media) {
+ return sizes.filter((size) => media.some((item) => item.isImage && item.editorLinks[size]));
+ }
+ function renderSizeSelect(availableSizes) {
+ const options = availableSizes
+ .map((size) => `${(0, Language_1.getPhrase)(`wcf.media.insert.imageSize.${size}`)} `)
+ .join("");
+ return `
+ ${(0, Language_1.getPhrase)("wcf.media.insert.imageSize")}
+ ${options}
+ `;
+ }
+ /**
+ * Asks the user for the size in which images are inserted. Resolves with
+ * `original` without asking if there is no image or no image offers a
+ * thumbnail and with `undefined` if the user canceled the dialog.
+ */
+ async function chooseImageSize(media) {
+ const options = await chooseInsertOptions(media, false);
+ return options?.size;
+ }
+ /**
+ * Asks the user for the size in which images are inserted and whether
+ * multiple images are inserted as a gallery. Resolves without asking if
+ * there is nothing to choose and with `undefined` if the user canceled the
+ * dialog.
+ */
+ async function chooseInsertOptions(media, allowGallery = true) {
+ const availableSizes = getAvailableSizes(media);
+ const canInsertGallery = allowGallery && media.filter((item) => item.isImage).length >= 2;
+ if (availableSizes.length < 2 && !canInsertGallery) {
+ return { size: "original", gallery: false };
+ }
+ let html = "";
+ if (canInsertGallery) {
+ html += `
+ ${(0, Language_1.getPhrase)("wcf.media.insert.type")}
+
+ ${(0, Language_1.getPhrase)("wcf.media.insert.type.separate")}
+ ${(0, Language_1.getPhrase)("wcf.media.insert.type.gallery")}
+
+ `;
+ }
+ if (availableSizes.length >= 2) {
+ html += renderSizeSelect(availableSizes);
+ }
+ const dialog = (0, Dialog_1.dialogFactory)()
+ .fromHtml(html)
+ .asPrompt({ primary: (0, Language_1.getPhrase)("wcf.global.button.insert") });
+ const sizeSelect = dialog.content.querySelector("select");
+ const isGallery = () => dialog.content.querySelector('input[name="mediaInsertType"]:checked')?.value === "gallery";
+ // Galleries always use the same thumbnail size.
+ dialog.content.querySelectorAll('input[name="mediaInsertType"]').forEach((input) => {
+ input.addEventListener("change", () => {
+ if (sizeSelect !== null) {
+ sizeSelect.disabled = isGallery();
+ }
+ });
+ });
+ return new Promise((resolve) => {
+ dialog.addEventListener("primary", () => {
+ resolve({
+ size: sizeSelect !== null ? sizeSelect.value : "original",
+ gallery: isGallery(),
+ });
+ });
+ // Covers cancel, Escape and backdrop clicks. No-op after `primary` resolved.
+ dialog.addEventListener("afterClose", () => {
+ resolve(undefined);
+ });
+ dialog.show((0, Language_1.getPhrase)("wcf.media.insert"));
+ });
+ }
+ /**
+ * Returns the largest size of the image that does not exceed the requested size.
+ */
+ function getEffectiveSize(media, size) {
+ for (let i = sizes.indexOf(size); i >= 0; i--) {
+ if (media.editorLinks[sizes[i]]) {
+ return sizes[i];
+ }
+ }
+ return "original";
+ }
+ function insertIntoCkeditor(ckeditor, media, size, gallery = false) {
+ if (gallery) {
+ const imageIds = media.filter((item) => item.isImage).map((item) => item.mediaID);
+ if (imageIds.length > 0) {
+ ckeditor.insertText(`[wsmg='${imageIds.join(",")}'][/wsmg]`);
+ }
+ }
+ for (const item of media) {
+ if (item.isImage) {
+ if (gallery) {
+ continue;
+ }
+ const effectiveSize = getEffectiveSize(item, size);
+ ckeditor.insertHtml(` `);
+ }
+ else {
+ ckeditor.insertText(`[wsm='${item.mediaID}'][/wsm]`);
+ }
+ }
+ }
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Manager.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Manager.js
new file mode 100644
index 00000000000..2907765515f
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Manager.js
@@ -0,0 +1,422 @@
+/**
+ * Shows the media manager in a dialog to insert or select media files.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.3
+ * @woltlabExcludeBundle tiny
+ */
+define(["require", "exports", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Api/Media/GetMedia", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/StringUtil", "../Ckeditor/Filename", "./Upload"], function (require, exports, Dialog_1, GetMedia_1, Language_1, StringUtil_1, Filename_1, Upload_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.openMediaManager = openMediaManager;
+ /**
+ * Keeps track of the selected media files, which survives the reloading of
+ * the list, and reflects it in the tiles and the footer of the dialog.
+ */
+ class MediaSelection {
+ #dialog;
+ #listViewItems;
+ #multiple;
+ #selected = new Map();
+ #selectionInfo;
+ #focusedId = undefined;
+ constructor(dialog, listViewItems, multiple) {
+ this.#dialog = dialog;
+ this.#listViewItems = listViewItems;
+ this.#multiple = multiple;
+ this.#selectionInfo = document.createElement("div");
+ this.#selectionInfo.classList.add("mediaManager__selectionInfo");
+ dialog.querySelector("woltlab-core-dialog-control").append(this.#selectionInfo);
+ listViewItems.setAttribute("role", "listbox");
+ if (multiple) {
+ listViewItems.setAttribute("aria-multiselectable", "true");
+ }
+ // The tiles are replaced whenever the list is reloaded.
+ new MutationObserver(() => this.render()).observe(listViewItems, { childList: true });
+ this.#initEventListeners();
+ this.render();
+ }
+ getSelectedIds() {
+ return Array.from(this.#selected.keys());
+ }
+ toggle(tile) {
+ const mediaId = parseInt(tile.dataset.objectId);
+ if (this.#selected.has(mediaId)) {
+ this.#selected.delete(mediaId);
+ this.#focusedId = mediaId;
+ this.render();
+ }
+ else {
+ this.select(tile);
+ }
+ }
+ select(tile) {
+ if (!this.#multiple) {
+ this.#selected.clear();
+ }
+ const mediaId = parseInt(tile.dataset.objectId);
+ this.#selected.set(mediaId, {
+ filename: tile.dataset.filename,
+ details: tile.dataset.details,
+ });
+ this.#focusedId = mediaId;
+ this.render();
+ }
+ getTiles() {
+ return Array.from(this.#listViewItems.querySelectorAll(".mediaManager__tile"));
+ }
+ getTile(mediaId) {
+ return this.#listViewItems.querySelector(`.mediaManager__tile[data-object-id="${mediaId}"]`);
+ }
+ render() {
+ const tiles = this.getTiles();
+ let focusableTile = tiles.find((tile) => parseInt(tile.dataset.objectId) === this.#focusedId);
+ focusableTile ??= tiles.find((tile) => this.#selected.has(parseInt(tile.dataset.objectId)));
+ focusableTile ??= tiles[0];
+ for (const tile of tiles) {
+ const isSelected = this.#selected.has(parseInt(tile.dataset.objectId));
+ tile.classList.toggle("mediaManager__tile--selected", isSelected);
+ tile.setAttribute("aria-selected", isSelected ? "true" : "false");
+ tile.tabIndex = tile === focusableTile ? 0 : -1;
+ }
+ this.#renderSelectionInfo();
+ this.#dialog.incomplete = this.#selected.size === 0;
+ }
+ confirm() {
+ if (this.#selected.size === 0) {
+ return;
+ }
+ // The `disabled` state of the primary button is updated asynchronously
+ // when `incomplete` changes, `click()` would be a no-op immediately after
+ // selecting the first tile.
+ const button = this.#dialog.querySelector(".dialog__control__button--primary");
+ button.form.requestSubmit(button);
+ }
+ #renderSelectionInfo() {
+ this.#selectionInfo.classList.toggle("mediaManager__selectionInfo--empty", this.#selected.size === 0);
+ if (this.#selected.size === 0) {
+ this.#selectionInfo.textContent = (0, Language_1.getPhrase)("wcf.media.manager.selection.none");
+ }
+ else if (this.#selected.size === 1) {
+ const { filename, details } = Array.from(this.#selected.values())[0];
+ this.#selectionInfo.innerHTML = `${(0, StringUtil_1.escapeHTML)(filename)} · ${(0, StringUtil_1.escapeHTML)(details)}`;
+ }
+ else {
+ this.#selectionInfo.textContent = (0, Language_1.getPhrase)("wcf.media.manager.selection.multiple", {
+ count: this.#selected.size,
+ });
+ }
+ this.#selectionInfo.title = this.#selectionInfo.textContent || "";
+ }
+ #initEventListeners() {
+ this.#listViewItems.addEventListener("click", (event) => {
+ const tile = this.#getTileFromEvent(event);
+ if (tile !== null) {
+ this.toggle(tile);
+ }
+ });
+ // Deleted media files must not remain part of the selection.
+ this.#listViewItems.addEventListener("interaction:remove", (event) => {
+ const mediaId = parseInt(event.target.dataset.objectId);
+ if (this.#selected.delete(mediaId)) {
+ this.render();
+ }
+ });
+ this.#listViewItems.addEventListener("dblclick", (event) => {
+ const tile = this.#getTileFromEvent(event);
+ if (tile !== null) {
+ this.select(tile);
+ this.confirm();
+ }
+ });
+ this.#listViewItems.addEventListener("keydown", (event) => {
+ const tile = this.#getTileFromEvent(event);
+ if (tile === null || event.target !== tile) {
+ return;
+ }
+ switch (event.key) {
+ case " ":
+ event.preventDefault();
+ this.toggle(tile);
+ break;
+ case "Enter":
+ event.preventDefault();
+ this.select(tile);
+ this.confirm();
+ break;
+ case "ArrowLeft":
+ case "ArrowRight":
+ case "ArrowUp":
+ case "ArrowDown":
+ case "Home":
+ case "End":
+ event.preventDefault();
+ this.#moveFocus(tile, event.key);
+ break;
+ }
+ });
+ }
+ #getTileFromEvent(event) {
+ const target = event.target;
+ // The context menu must not change the selection.
+ if (target.closest(".mediaManager__tile__menu") !== null) {
+ return null;
+ }
+ return target.closest(".mediaManager__tile");
+ }
+ #moveFocus(tile, key) {
+ const tiles = this.getTiles();
+ const index = tiles.indexOf(tile);
+ // The number of columns is derived from the tiles in the first row.
+ const columns = Math.max(1, tiles.filter((element) => element.offsetTop === tiles[0].offsetTop).length);
+ let newIndex = index;
+ switch (key) {
+ case "ArrowLeft":
+ newIndex = index - 1;
+ break;
+ case "ArrowRight":
+ newIndex = index + 1;
+ break;
+ case "ArrowUp":
+ newIndex = index - columns;
+ break;
+ case "ArrowDown":
+ newIndex = index + columns;
+ break;
+ case "Home":
+ newIndex = 0;
+ break;
+ case "End":
+ newIndex = tiles.length - 1;
+ break;
+ }
+ const newTile = tiles[newIndex];
+ if (newTile === undefined) {
+ return;
+ }
+ this.#focusedId = parseInt(newTile.dataset.objectId);
+ this.render();
+ newTile.focus();
+ }
+ }
+ /**
+ * Resolves with the tiles of the given media files once they have been
+ * rendered after a reload of the list.
+ */
+ function waitForTiles(selection, listViewItems, mediaIds) {
+ const getTiles = () => mediaIds.map((mediaId) => selection.getTile(mediaId)).filter((tile) => tile !== null);
+ return new Promise((resolve) => {
+ // The list view replaces all items at once, the new media files may not
+ // be part of the current page or match the current search though.
+ const observer = new MutationObserver(() => {
+ observer.disconnect();
+ window.clearTimeout(timeout);
+ resolve(getTiles());
+ });
+ observer.observe(listViewItems, { childList: true });
+ // An empty list that remains empty does not cause any mutation.
+ const timeout = window.setTimeout(() => {
+ observer.disconnect();
+ resolve(getTiles());
+ }, 5_000);
+ });
+ }
+ function scrollToTile(content, tile) {
+ // The toolbar of the list view sticks to the top of the scrolling container.
+ const toolbarHeight = content.querySelector(".listView__header")?.offsetHeight ?? 0;
+ const offset = tile.getBoundingClientRect().top - content.getBoundingClientRect().top;
+ content.scrollTop = Math.max(0, content.scrollTop + offset - toolbarHeight - 10);
+ }
+ function setupSearch(dialog, listViewItems) {
+ const input = dialog.content.querySelector(".mediaManager__searchInput");
+ if (input === null) {
+ return;
+ }
+ let timeout = undefined;
+ input.addEventListener("input", () => {
+ window.clearTimeout(timeout);
+ timeout = window.setTimeout(() => {
+ listViewItems.dispatchEvent(new CustomEvent("interaction:set-parameters", {
+ detail: { searchString: input.value.trim() },
+ }));
+ }, 200);
+ });
+ // Prevent Enter from submitting the prompt form of the dialog.
+ input.addEventListener("keydown", (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ }
+ });
+ }
+ function setupPaste(dialog, uploadButton) {
+ dialog.addEventListener("paste", (event) => {
+ const files = Array.from(event.clipboardData?.files ?? []);
+ if (files.length === 0) {
+ // Text is pasted as usual, e.g. into the search field.
+ return;
+ }
+ event.preventDefault();
+ uploadButton.uploadFiles(files.map((file) => (0, Filename_1.normalizePastedFilename)(file)));
+ });
+ }
+ function setupDropZone(dialog, uploadButton) {
+ const dropZone = document.createElement("div");
+ dropZone.classList.add("mediaManager__dropZone");
+ dropZone.innerHTML = `
+
+
+ ${(0, Language_1.getPhrase)("wcf.media.manager.dropZone")}
+ ${(0, Language_1.getPhrase)("wcf.media.manager.dropZone.select")}
+
+ `;
+ dropZone.querySelector("button").addEventListener("click", () => {
+ uploadButton.click();
+ });
+ dialog.content.querySelector(".listView__itemContainer").before(dropZone);
+ // The whole content area accepts the files while dragging them over the dialog.
+ const content = dialog.content;
+ const hasFiles = (event) => event.dataTransfer?.types.includes("Files") === true;
+ let dragCounter = 0;
+ content.addEventListener("dragenter", (event) => {
+ if (!hasFiles(event)) {
+ return;
+ }
+ event.preventDefault();
+ dragCounter++;
+ content.classList.add("mediaManager--dragOver");
+ });
+ content.addEventListener("dragover", (event) => {
+ if (hasFiles(event)) {
+ event.preventDefault();
+ }
+ });
+ content.addEventListener("dragleave", () => {
+ dragCounter = Math.max(0, dragCounter - 1);
+ if (dragCounter === 0) {
+ content.classList.remove("mediaManager--dragOver");
+ }
+ });
+ content.addEventListener("drop", (event) => {
+ if (!hasFiles(event)) {
+ return;
+ }
+ event.preventDefault();
+ dragCounter = 0;
+ content.classList.remove("mediaManager--dragOver");
+ const files = Array.from(event.dataTransfer.files);
+ if (files.length > 0) {
+ uploadButton.uploadFiles(files);
+ }
+ });
+ }
+ /**
+ * Opens the media manager and resolves with the media files that the user has
+ * inserted or selected, an empty list is returned if the dialog was closed.
+ */
+ async function openMediaManager(options) {
+ const multiple = options.mode === "editor";
+ const dialog = await (0, Dialog_1.dialogFactory)()
+ .usingListView()
+ .fromPreset(options.title || (0, Language_1.getPhrase)(multiple ? "wcf.media.manager" : "wcf.media.chooseFile"), "wcf\\system\\listView\\user\\MediaManagerListView", new Map([
+ ["mode", options.mode],
+ ["imagesOnly", options.imagesOnly ? "1" : "0"],
+ ]), undefined, "uploadTime", "DESC", 1, {
+ primary: (0, Language_1.getPhrase)(multiple ? "wcf.global.button.insert" : "wcf.media.button.select"),
+ });
+ dialog.dialog.classList.add("dialog--mediaManager");
+ const listViewItems = dialog.content.querySelector(".listView__items");
+ const uploadButton = dialog.content.querySelector("woltlab-core-file-upload");
+ const selection = new MediaSelection(dialog, listViewItems, multiple);
+ setupSearch(dialog, listViewItems);
+ return new Promise((resolve) => {
+ let completed = false;
+ const complete = async (mediaIds) => {
+ if (completed) {
+ return;
+ }
+ completed = true;
+ // Media files may have been deleted in the meantime, the remaining ones
+ // are still inserted.
+ const results = await Promise.allSettled(mediaIds.map((mediaId) => (0, GetMedia_1.getMedia)(mediaId)));
+ const media = results
+ .filter((result) => result.status === "fulfilled")
+ .map((result) => result.value);
+ // Settle first, callers lock their buttons until the promise resolves.
+ if (dialog.isConnected) {
+ dialog.close();
+ }
+ resolve(media);
+ const rejected = results.find((result) => result.status === "rejected");
+ if (media.length === 0 && rejected !== undefined) {
+ throw rejected.reason;
+ }
+ };
+ dialog.addEventListener("primary", () => {
+ void complete(selection.getSelectedIds());
+ });
+ if (uploadButton !== null) {
+ setupDropZone(dialog, uploadButton);
+ setupPaste(dialog, uploadButton);
+ // The upload element is hidden and triggered by the primary button of the list view.
+ const proxyButton = dialog.content.querySelector(".mediaManager__uploadButton");
+ if (proxyButton !== null) {
+ proxyButton.addEventListener("click", () => {
+ uploadButton.click();
+ });
+ uploadButton.addEventListener("uploadStart", () => {
+ proxyButton.disabled = true;
+ });
+ }
+ (0, Upload_1.setupMediaUpload)(uploadButton, {
+ uploadCompleted: (mediaId) => {
+ if (options.file !== undefined) {
+ void complete([mediaId]);
+ }
+ },
+ allUploadsCompleted: async (mediaIds) => {
+ if (proxyButton !== null) {
+ proxyButton.disabled = false;
+ }
+ if (!listViewItems.isConnected || options.file !== undefined || mediaIds.length === 0) {
+ return;
+ }
+ // Reload the list only once after a batch of files has been uploaded.
+ const tilesPromise = waitForTiles(selection, listViewItems, mediaIds);
+ listViewItems.dispatchEvent(new CustomEvent("interaction:invalidate-all"));
+ // Select the uploaded media files, a single selection selects the last one.
+ const tiles = await tilesPromise;
+ for (const tile of multiple ? tiles : tiles.slice(-1)) {
+ selection.select(tile);
+ }
+ if (tiles.length > 0) {
+ scrollToTile(dialog.content, tiles[0]);
+ }
+ // A single uploaded file is edited right away.
+ const editEndpoint = uploadButton.closest(".mediaManager__upload")?.dataset.editEndpoint;
+ if (mediaIds.length !== 1 || editEndpoint === undefined) {
+ return;
+ }
+ if ((await (0, Upload_1.editMedia)(editEndpoint, mediaIds[0])) && listViewItems.isConnected) {
+ listViewItems.dispatchEvent(new CustomEvent("interaction:invalidate-all"));
+ }
+ },
+ });
+ const file = options.file;
+ if (file !== undefined) {
+ // The upload button is initialized by a mutation observer.
+ window.setTimeout(() => {
+ uploadButton.uploadFiles([file]);
+ }, 0);
+ }
+ }
+ dialog.addEventListener("afterClose", () => {
+ if (!completed) {
+ completed = true;
+ resolve([]);
+ }
+ });
+ });
+ }
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Select.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Select.js
new file mode 100644
index 00000000000..0e6d27032ac
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Select.js
@@ -0,0 +1,90 @@
+/**
+ * Selects a media file for an input element using the media manager.
+ *
+ * The button references the id of the hidden input that stores the id of the
+ * media file via `data-store` and optionally the id of the element that shows
+ * a preview of the selected media file via `data-display`.
+ *
+ * @author Matthias Schmidt, Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.3
+ * @woltlabExcludeBundle tiny
+ */
+define(["require", "exports", "tslib", "./Manager", "WoltLabSuite/Core/Helper/PromiseMutex", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/StringUtil", "WoltLabSuite/Core/FileUtil"], function (require, exports, tslib_1, Manager_1, PromiseMutex_1, Language_1, StringUtil_1, FileUtil) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.setup = setup;
+ FileUtil = tslib_1.__importStar(FileUtil);
+ function renderPreview(displayElement, media) {
+ if (media.isImage) {
+ const link = (0, StringUtil_1.escapeHTML)(media.smallThumbnailLink || media.link);
+ displayElement.innerHTML = ` `;
+ return;
+ }
+ const iconName = FileUtil.getIconNameByFilename(media.filename);
+ displayElement.innerHTML = `
+
+
+
+
${(0, StringUtil_1.escapeHTML)(media.filename)}
+
${(0, StringUtil_1.escapeHTML)(media.formattedFilesize)}
+
+
`;
+ }
+ function createRemoveButton(button, onRemove) {
+ const removeButton = document.createElement("button");
+ removeButton.type = "button";
+ removeButton.classList.add("button", "jsTooltip");
+ if (button.classList.contains("small")) {
+ removeButton.classList.add("small");
+ }
+ removeButton.title = (0, Language_1.getPhrase)("wcf.global.button.delete");
+ removeButton.innerHTML = ' ';
+ removeButton.addEventListener("click", () => onRemove());
+ // The buttons are usually part of a button group.
+ if (button.parentElement.tagName === "LI") {
+ const listItem = document.createElement("li");
+ listItem.append(removeButton);
+ button.parentElement.insertAdjacentElement("afterend", listItem);
+ return listItem;
+ }
+ button.insertAdjacentElement("afterend", removeButton);
+ return removeButton;
+ }
+ function setup(button, options = {}) {
+ const input = document.getElementById(button.dataset.store || "");
+ if (!(input instanceof HTMLInputElement)) {
+ throw new Error("The button must reference an input element via `data-store`.", { cause: { button } });
+ }
+ const getDisplayElement = () => button.dataset.display ? document.getElementById(button.dataset.display) : null;
+ const removeButton = createRemoveButton(button, () => {
+ input.value = "";
+ input.dispatchEvent(new Event("change", { bubbles: true }));
+ const displayElement = getDisplayElement();
+ if (displayElement !== null) {
+ displayElement.innerHTML = "";
+ }
+ removeButton.hidden = true;
+ });
+ removeButton.hidden = input.value === "";
+ button.addEventListener("click", (0, PromiseMutex_1.promiseMutex)(async (event) => {
+ event.preventDefault();
+ const [media] = await (0, Manager_1.openMediaManager)({
+ mode: "select",
+ imagesOnly: options.imagesOnly,
+ title: options.title,
+ });
+ if (media === undefined) {
+ return;
+ }
+ input.value = media.mediaID.toString();
+ input.dispatchEvent(new Event("change", { bubbles: true }));
+ const displayElement = getDisplayElement();
+ if (displayElement !== null) {
+ renderPreview(displayElement, media);
+ }
+ removeButton.hidden = false;
+ }));
+ }
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Upload.js
new file mode 100644
index 00000000000..40cda72b69f
--- /dev/null
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Media/Upload.js
@@ -0,0 +1,71 @@
+/**
+ * Notifies the caller once media uploads have been completed.
+ *
+ * @author Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
+ * @license GNU Lesser General Public License
+ * @since 6.3
+ * @woltlabExcludeBundle tiny
+ */
+define(["require", "exports", "WoltLabSuite/Core/Component/File/Helper", "WoltLabSuite/Core/Component/Dialog", "WoltLabSuite/Core/Component/Snackbar", "WoltLabSuite/Core/Language"], function (require, exports, Helper_1, Dialog_1, Snackbar_1, Language_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.editMedia = editMedia;
+ exports.setupMediaUpload = setupMediaUpload;
+ function showError(message) {
+ const dialog = (0, Dialog_1.dialogFactory)().fromHtml(`${message}
`).asAlert();
+ dialog.show((0, Language_1.getPhrase)("wcf.global.error.title"));
+ }
+ /**
+ * Opens the dialog to edit the media file with the given id. The endpoint
+ * contains `%s` as the placeholder for the id.
+ */
+ async function editMedia(endpoint, mediaId) {
+ const { ok } = await (0, Dialog_1.dialogFactory)().usingFormBuilder().fromEndpoint(endpoint.replace("%s", mediaId.toString()));
+ if (ok) {
+ (0, Snackbar_1.showDefaultSuccessSnackbar)();
+ }
+ return ok;
+ }
+ function setupMediaUpload(uploadButton, callbacks) {
+ let pendingUploads = 0;
+ let mediaIds = [];
+ // Errors that occur before the upload has started are shown in a dialog
+ // instead of below the upload button.
+ uploadButton.addEventListener("upload:error", (event) => {
+ event.preventDefault();
+ showError(event.detail.message);
+ });
+ uploadButton.addEventListener("uploadStart", (event) => {
+ pendingUploads++;
+ // The file element only tracks the state of the upload while it is part
+ // of the document, but there is no need to display it.
+ const file = event.detail;
+ file.hidden = true;
+ document.body.append(file);
+ void file.ready
+ .then(() => {
+ const mediaId = file.data?.mediaID;
+ if (typeof mediaId === "number") {
+ mediaIds.push(mediaId);
+ callbacks.uploadCompleted?.(mediaId);
+ }
+ })
+ .catch(() => {
+ if (file.apiError !== undefined) {
+ showError((0, Helper_1.getErrorMessageFromFile)(file));
+ }
+ })
+ .finally(() => {
+ file.remove();
+ pendingUploads--;
+ if (pendingUploads === 0) {
+ // All files of a selection start uploading before the first one is completed.
+ const completedMediaIds = mediaIds;
+ mediaIds = [];
+ callbacks.allUploadsCompleted?.(completedMediaIds);
+ }
+ });
+ });
+ }
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Media/List.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Media/List.js
deleted file mode 100644
index 3a40eaa3b13..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Media/List.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * Initializes modules required for media list view.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @woltlabExcludeBundle all
- */
-define(["require", "exports", "tslib", "../../Media/List/Upload", "../../Media/Clipboard", "../../Event/Handler", "../../Media/Editor", "../../Dom/Change/Listener"], function (require, exports, tslib_1, Upload_1, MediaClipboard, EventHandler, Editor_1, DomChangeListener) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.init = init;
- Upload_1 = tslib_1.__importDefault(Upload_1);
- MediaClipboard = tslib_1.__importStar(MediaClipboard);
- EventHandler = tslib_1.__importStar(EventHandler);
- Editor_1 = tslib_1.__importDefault(Editor_1);
- DomChangeListener = tslib_1.__importStar(DomChangeListener);
- const _mediaEditor = new Editor_1.default({
- _editorSuccess: (media, oldCategoryId, closedEditorDialog = true) => {
- if (media.categoryID != oldCategoryId || closedEditorDialog) {
- window.setTimeout(() => {
- window.location.reload();
- }, 500);
- }
- },
- });
- const _tableBody = document.getElementById("mediaListTableBody");
- let _upload;
- function init(options) {
- options = options || {};
- _upload = new Upload_1.default("uploadButton", "mediaListTableBody", {
- categoryId: options.categoryId,
- multiple: true,
- elementTagSize: 48,
- });
- MediaClipboard.init("wcf\\acp\\page\\MediaListPage", options.hasMarkedItems || false, {
- clipboardDeleteMedia: (mediaIds) => clipboardDeleteMedia(mediaIds),
- });
- addButtonEventListeners();
- DomChangeListener.add("WoltLabSuite/Core/Controller/Media/List", () => addButtonEventListeners());
- EventHandler.add("com.woltlab.wcf.media.upload", "success", (data) => openEditorAfterUpload(data));
- }
- /**
- * Adds the `click` event listeners to the media edit icons in new media table rows.
- */
- function addButtonEventListeners() {
- Array.from(_tableBody.getElementsByClassName("jsMediaEditButton")).forEach((button) => {
- button.classList.remove("jsMediaEditButton");
- button.addEventListener("click", (ev) => edit(ev));
- });
- }
- /**
- * Is called when a media edit icon is clicked.
- */
- function edit(event) {
- _mediaEditor.edit(~~event.currentTarget.dataset.objectId);
- }
- /**
- * Opens the media editor after uploading a single file.
- */
- function openEditorAfterUpload(data) {
- if (data.upload === _upload && !data.isMultiFileUpload && !_upload.hasPendingUploads()) {
- const keys = Object.keys(data.media);
- if (keys.length) {
- _mediaEditor.edit(data.media[keys[0]]);
- }
- }
- }
- /**
- * Is called after the media files with the given ids have been deleted via clipboard.
- */
- function clipboardDeleteMedia(mediaIds) {
- Array.from(document.getElementsByClassName("jsMediaRow")).forEach((media) => {
- const mediaID = ~~media.querySelector(".jsClipboardItem").dataset.objectId;
- if (mediaIds.indexOf(mediaID) !== -1) {
- media.remove();
- }
- });
- if (!document.getElementsByClassName("jsMediaRow").length) {
- window.location.reload();
- }
- }
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/SimpleAcl.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/SimpleAcl.js
index 91125d899e3..73bbb8f031d 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/SimpleAcl.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/SimpleAcl.js
@@ -5,8 +5,10 @@ define(["require", "exports", "tslib", "./Field", "WoltLabSuite/Core/Dom/Util"],
_getData() {
const groupIds = Array.from(document.querySelectorAll(`input[name="${(0, Util_1.escapeAttributeSelector)(this._fieldId)}[group][]"]`)).map((input) => input.value);
const usersIds = Array.from(document.querySelectorAll(`input[name="${(0, Util_1.escapeAttributeSelector)(this._fieldId)}[user][]"]`)).map((input) => input.value);
+ const allowAll = document.querySelector(`input[name="${(0, Util_1.escapeAttributeSelector)(this._fieldId)}[allowAll]"]:checked`);
return {
[this._fieldId]: {
+ allowAll: allowAll?.value ?? "1",
group: groupIds,
user: usersIds,
},
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/ValueI18n.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/ValueI18n.js
index 1a12a23f38b..7cd68026bb1 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/ValueI18n.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/ValueI18n.js
@@ -15,17 +15,17 @@ define(["require", "exports", "tslib", "./Field", "../../../Language/Input"], fu
_getData() {
const data = {};
const values = LanguageInput.getValues(this._fieldId);
- if (values.size > 1) {
- if (!Object.prototype.hasOwnProperty.call(data, this._fieldId + "_i18n")) {
- data[this._fieldId + "_i18n"] = {};
- }
+ if (values.size === 1 && values.has(0)) {
+ data[this._fieldId] = values.get(0);
+ }
+ else {
+ // Fields that require per-language values can hold the value of a
+ // single language, which must not be sent as a monolingual value.
+ data[this._fieldId + "_i18n"] = {};
values.forEach((value, key) => {
data[this._fieldId + "_i18n"][key] = value;
});
}
- else {
- data[this._fieldId] = values.get(0);
- }
return data;
}
destroy() {
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Clipboard.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Clipboard.js
deleted file mode 100644
index a7ace2b0e1e..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Clipboard.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * Initializes modules required for media clipboard.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @woltlabExcludeBundle tiny
- */
-define(["require", "exports", "tslib", "../Controller/Clipboard", "../Event/Handler", "../Language", "../Ajax", "WoltLabSuite/Core/Component/Dialog", "../Component/Snackbar"], function (require, exports, tslib_1, Clipboard, EventHandler, Language_1, Ajax, Dialog_1, Snackbar_1) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.init = init;
- exports.setMediaManager = setMediaManager;
- Clipboard = tslib_1.__importStar(Clipboard);
- EventHandler = tslib_1.__importStar(EventHandler);
- Ajax = tslib_1.__importStar(Ajax);
- let _mediaManager;
- let _didInit = false;
- class MediaClipboard {
- #dialog;
- _ajaxSetup() {
- return {
- data: {
- className: "wcf\\data\\media\\MediaAction",
- },
- };
- }
- _ajaxSuccess(data) {
- switch (data.actionName) {
- case "getSetCategoryDialog":
- this.#dialog = (0, Dialog_1.dialogFactory)().fromHtml(data.returnValues.template).asConfirmation();
- this.#dialog.show((0, Language_1.getPhrase)("wcf.media.setCategory"));
- this.#dialog.addEventListener("primary", () => {
- const category = this.#dialog.content.querySelector('select[name="categoryID"]');
- setCategory(~~category.value);
- });
- break;
- case "setCategory":
- this.#dialog?.close();
- (0, Snackbar_1.showDefaultSuccessSnackbar)();
- Clipboard.reload();
- break;
- }
- }
- }
- const ajax = new MediaClipboard();
- let clipboardObjectIds = [];
- /**
- * Handles successful clipboard actions.
- */
- function clipboardAction(actionData) {
- const mediaIds = actionData.data.parameters.objectIDs;
- switch (actionData.data.actionName) {
- case "com.woltlab.wcf.media.delete":
- // only consider events if the action has been executed
- if (actionData.responseData !== null) {
- _mediaManager.clipboardDeleteMedia(mediaIds);
- }
- break;
- case "com.woltlab.wcf.media.insert": {
- const mediaManagerEditor = _mediaManager;
- mediaManagerEditor.clipboardInsertMedia(mediaIds);
- break;
- }
- case "com.woltlab.wcf.media.setCategory":
- clipboardObjectIds = mediaIds;
- Ajax.api(ajax, {
- actionName: "getSetCategoryDialog",
- });
- break;
- }
- }
- /**
- * Sets the category of the marked media files.
- */
- function setCategory(categoryID) {
- Ajax.api(ajax, {
- actionName: "setCategory",
- objectIDs: clipboardObjectIds,
- parameters: {
- categoryID: categoryID,
- },
- });
- }
- function init(pageClassName, hasMarkedItems, mediaManager) {
- if (!_didInit) {
- Clipboard.setup({
- hasMarkedItems: hasMarkedItems,
- pageClassName: pageClassName,
- });
- EventHandler.add("com.woltlab.wcf.clipboard", "com.woltlab.wcf.media", (data) => clipboardAction(data));
- _didInit = true;
- }
- _mediaManager = mediaManager;
- }
- function setMediaManager(mediaManager) {
- _mediaManager = mediaManager;
- }
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Data.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Data.js
index 8b8ca08a70f..07ad1a66ff4 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Data.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Data.js
@@ -3,6 +3,7 @@
* @copyright 2001-2021 WoltLab GmbH
* @license GNU Lesser General Public License
* @woltlabExcludeBundle all
+ * @deprecated 6.3 Use `WoltLabSuite/Core/Api/Media/GetMedia` and `WoltLabSuite/Core/Component/Media/Manager` instead.
*/
define(["require", "exports"], function (require, exports) {
"use strict";
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js
deleted file mode 100644
index 39edae7a5ca..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js
+++ /dev/null
@@ -1,344 +0,0 @@
-/**
- * Handles editing media files via dialog.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @woltlabExcludeBundle tiny
- */
-define(["require", "exports", "tslib", "../Ui/Dialog", "../Language/Chooser", "../Language/Input", "../Dom/Util", "../Dom/Traverse", "../Dom/Change/Listener", "WoltLabSuite/Core/Language", "../Ajax", "./Replace", "../Component/Snackbar"], function (require, exports, tslib_1, UiDialog, LanguageChooser, LanguageInput, DomUtil, DomTraverse, Listener_1, Language_1, Ajax, Replace_1, Snackbar_1) {
- "use strict";
- UiDialog = tslib_1.__importStar(UiDialog);
- LanguageChooser = tslib_1.__importStar(LanguageChooser);
- LanguageInput = tslib_1.__importStar(LanguageInput);
- DomUtil = tslib_1.__importStar(DomUtil);
- DomTraverse = tslib_1.__importStar(DomTraverse);
- Listener_1 = tslib_1.__importDefault(Listener_1);
- Ajax = tslib_1.__importStar(Ajax);
- Replace_1 = tslib_1.__importDefault(Replace_1);
- class MediaEditor {
- _availableLanguageCount = 1;
- _categoryIds = [];
- _dialogs = new Map();
- _callbackObject;
- _media = null;
- _oldCategoryId = 0;
- constructor(callbackObject) {
- this._callbackObject = callbackObject || {};
- if (this._callbackObject._editorClose && typeof this._callbackObject._editorClose !== "function") {
- throw new TypeError("Callback object has no function '_editorClose'.");
- }
- if (this._callbackObject._editorSuccess && typeof this._callbackObject._editorSuccess !== "function") {
- throw new TypeError("Callback object has no function '_editorSuccess'.");
- }
- }
- _ajaxSetup() {
- return {
- data: {
- actionName: "update",
- className: "wcf\\data\\media\\MediaAction",
- },
- };
- }
- _ajaxSuccess() {
- (0, Snackbar_1.showDefaultSuccessSnackbar)();
- if (this._callbackObject._editorSuccess) {
- this._callbackObject._editorSuccess(this._media, this._oldCategoryId);
- this._oldCategoryId = 0;
- }
- UiDialog.close(`mediaEditor_${this._media.mediaID}`);
- this._media = null;
- }
- /**
- * Is called if an editor is manually closed by the user.
- */
- _close() {
- this._media = null;
- if (this._callbackObject._editorClose) {
- this._callbackObject._editorClose();
- }
- }
- /**
- * Initializes the editor dialog.
- *
- * @since 5.3
- */
- _initEditor(content, data) {
- this._availableLanguageCount = ~~data.returnValues.availableLanguageCount;
- this._categoryIds = data.returnValues.categoryIDs.map((number) => ~~number);
- if (data.returnValues.mediaData) {
- this._media = data.returnValues.mediaData;
- }
- const media = this._media;
- const mediaId = media.mediaID;
- // make sure that the language chooser is initialized first
- setTimeout(() => {
- if (this._availableLanguageCount > 1) {
- LanguageChooser.setLanguageId(`mediaEditor_${mediaId}_languageID`, media.languageID || window.LANGUAGE_ID);
- }
- if (this._categoryIds.length) {
- const categoryID = content.querySelector("select[name=categoryID]");
- if (media.categoryID) {
- categoryID.value = media.categoryID.toString();
- }
- else {
- categoryID.value = "0";
- }
- }
- const title = content.querySelector("input[name=title]");
- const altText = content.querySelector("input[name=altText]");
- const caption = content.querySelector("textarea[name=caption]");
- if (this._availableLanguageCount > 1 && media.isMultilingual) {
- if (document.getElementById(`altText_${mediaId}`)) {
- LanguageInput.setValues(`altText_${mediaId}`, (media.altText || {}));
- }
- if (document.getElementById(`caption_${mediaId}`)) {
- LanguageInput.setValues(`caption_${mediaId}`, (media.caption || {}));
- }
- LanguageInput.setValues(`title_${mediaId}`, (media.title || {}));
- }
- else {
- title.value = media.title ? media.title[media.languageID || window.LANGUAGE_ID] : "";
- if (altText) {
- altText.value = media.altText ? media.altText[media.languageID || window.LANGUAGE_ID] : "";
- }
- if (caption) {
- caption.value = media.caption ? media.caption[media.languageID || window.LANGUAGE_ID] : "";
- }
- }
- if (this._availableLanguageCount > 1) {
- const isMultilingual = content.querySelector("input[name=isMultilingual]");
- isMultilingual.addEventListener("change", (ev) => this._updateLanguageFields(ev));
- this._updateLanguageFields(null, isMultilingual);
- }
- if (altText) {
- altText.addEventListener("keypress", (ev) => this._keyPress(ev));
- }
- title.addEventListener("keypress", (ev) => this._keyPress(ev));
- content.querySelector("button[data-type=submit]").addEventListener("click", () => this._saveData());
- // remove focus from input elements and scroll dialog to top
- document.activeElement.blur();
- document.getElementById(`mediaEditor_${mediaId}`).parentNode.scrollTop = 0;
- // Initialize button to replace media file.
- const uploadButton = content.querySelector(".mediaManagerMediaReplaceButton");
- let target = content.querySelector(".mediaThumbnail");
- if (!target) {
- target = document.createElement("div");
- content.appendChild(target);
- }
- new Replace_1.default(mediaId, DomUtil.identify(uploadButton),
- // Pass an anonymous element for non-images which is required internally
- // but not needed in this case.
- DomUtil.identify(target), {
- mediaEditor: this,
- });
- Listener_1.default.trigger();
- }, 200);
- }
- /**
- * Handles the `[ENTER]` key to submit the form.
- */
- _keyPress(event) {
- if (event.key === "Enter") {
- event.preventDefault();
- this._saveData();
- }
- }
- /**
- * Saves the data of the currently edited media.
- */
- _saveData() {
- const content = UiDialog.getDialog(`mediaEditor_${this._media.mediaID}`).content;
- const categoryId = content.querySelector("select[name=categoryID]");
- const altText = content.querySelector("input[name=altText]");
- const caption = content.querySelector("textarea[name=caption]");
- const captionEnableHtml = content.querySelector("input[name=captionEnableHtml]");
- const title = content.querySelector("input[name=title]");
- let hasError = false;
- const altTextError = altText ? DomTraverse.childByClass(altText.parentNode, "innerError") : false;
- const captionError = caption ? DomTraverse.childByClass(caption.parentNode, "innerError") : false;
- const titleError = DomTraverse.childByClass(title.parentNode, "innerError");
- // category
- this._oldCategoryId = this._media.categoryID;
- if (this._categoryIds.length) {
- this._media.categoryID = ~~categoryId.value;
- // if the selected category id not valid (manipulated DOM), ignore
- if (this._categoryIds.indexOf(this._media.categoryID) === -1) {
- this._media.categoryID = 0;
- }
- }
- // language and multilingualism
- if (this._availableLanguageCount > 1) {
- const isMultilingual = content.querySelector("input[name=isMultilingual]");
- this._media.isMultilingual = ~~isMultilingual.checked;
- this._media.languageID = this._media.isMultilingual
- ? null
- : LanguageChooser.getLanguageId(`mediaEditor_${this._media.mediaID}_languageID`);
- }
- else {
- this._media.languageID = window.LANGUAGE_ID;
- }
- // altText, caption and title
- this._media.altText = {};
- this._media.caption = {};
- this._media.title = {};
- if (this._availableLanguageCount > 1 && this._media.isMultilingual) {
- if (altText && !LanguageInput.validate(altText.id, true)) {
- hasError = true;
- if (!altTextError) {
- DomUtil.innerError(altText, (0, Language_1.getPhrase)("wcf.global.form.error.multilingual"));
- }
- }
- if (caption && !LanguageInput.validate(caption.id, true)) {
- hasError = true;
- if (!captionError) {
- DomUtil.innerError(caption, (0, Language_1.getPhrase)("wcf.global.form.error.multilingual"));
- }
- }
- if (!LanguageInput.validate(title.id, true)) {
- hasError = true;
- if (!titleError) {
- DomUtil.innerError(title, (0, Language_1.getPhrase)("wcf.global.form.error.multilingual"));
- }
- }
- this._media.altText = altText ? this.mapToI18nValues(LanguageInput.getValues(altText.id)) : "";
- this._media.caption = caption ? this.mapToI18nValues(LanguageInput.getValues(caption.id)) : "";
- this._media.title = this.mapToI18nValues(LanguageInput.getValues(title.id));
- }
- else {
- this._media.altText[this._media.languageID] = altText ? altText.value : "";
- this._media.caption[this._media.languageID] = caption ? caption.value : "";
- this._media.title[this._media.languageID] = title.value;
- }
- // captionEnableHtml
- if (captionEnableHtml) {
- this._media.captionEnableHtml = ~~captionEnableHtml.checked;
- }
- else {
- this._media.captionEnableHtml = 0;
- }
- const aclValues = {
- allowAll: ~~document.getElementById(`mediaEditor_${this._media.mediaID}_aclAllowAll`)
- .checked,
- group: Array.from(content.querySelectorAll(`input[name="mediaEditor_${this._media.mediaID}_aclValues[group][]"]`)).map((aclGroup) => ~~aclGroup.value),
- user: Array.from(content.querySelectorAll(`input[name="mediaEditor_${this._media.mediaID}_aclValues[user][]"]`)).map((aclUser) => ~~aclUser.value),
- };
- if (!hasError) {
- if (altTextError) {
- altTextError.remove();
- }
- if (captionError) {
- captionError.remove();
- }
- if (titleError) {
- titleError.remove();
- }
- Ajax.api(this, {
- actionName: "update",
- objectIDs: [this._media.mediaID],
- parameters: {
- aclValues: aclValues,
- altText: this._media.altText,
- caption: this._media.caption,
- data: {
- captionEnableHtml: this._media.captionEnableHtml,
- categoryID: this._media.categoryID,
- isMultilingual: this._media.isMultilingual,
- languageID: this._media.languageID,
- },
- title: this._media.title,
- },
- });
- }
- }
- mapToI18nValues(values) {
- const obj = {};
- values.forEach((value, key) => (obj[key] = value));
- return obj;
- }
- /**
- * Updates language-related input fields depending on whether multilingualis is enabled.
- */
- _updateLanguageFields(event, element) {
- if (event) {
- element = event.currentTarget;
- }
- const mediaId = this._media.mediaID;
- const languageChooserContainer = document.getElementById(`mediaEditor_${mediaId}_languageIDContainer`)
- .parentNode;
- if (element.checked) {
- LanguageInput.enable(`title_${mediaId}`);
- if (document.getElementById(`caption_${mediaId}`)) {
- LanguageInput.enable(`caption_${mediaId}`);
- }
- if (document.getElementById(`altText_${mediaId}`)) {
- LanguageInput.enable(`altText_${mediaId}`);
- }
- DomUtil.hide(languageChooserContainer);
- }
- else {
- LanguageInput.disable(`title_${mediaId}`);
- if (document.getElementById(`caption_${mediaId}`)) {
- LanguageInput.disable(`caption_${mediaId}`);
- }
- if (document.getElementById(`altText_${mediaId}`)) {
- LanguageInput.disable(`altText_${mediaId}`);
- }
- DomUtil.show(languageChooserContainer);
- }
- }
- /**
- * Edits the media with the given data or id.
- */
- edit(editedMedia) {
- let media;
- let mediaId = 0;
- if (typeof editedMedia === "object") {
- media = editedMedia;
- mediaId = media.mediaID;
- }
- else {
- media = {
- mediaID: editedMedia,
- };
- mediaId = editedMedia;
- }
- if (this._media !== null) {
- throw new Error(`Cannot edit media with id ${mediaId} while editing media with id '${this._media.mediaID}'.`);
- }
- this._media = media;
- if (!this._dialogs.has(`mediaEditor_${mediaId}`)) {
- this._dialogs.set(`mediaEditor_${mediaId}`, {
- _dialogSetup: () => {
- return {
- id: `mediaEditor_${mediaId}`,
- options: {
- backdropCloseOnClick: false,
- onClose: () => this._close(),
- title: (0, Language_1.getPhrase)("wcf.media.edit"),
- },
- source: {
- after: (content, responseData) => this._initEditor(content, responseData),
- data: {
- actionName: "getEditorDialog",
- className: "wcf\\data\\media\\MediaAction",
- objectIDs: [mediaId],
- },
- },
- };
- },
- });
- }
- UiDialog.open(this._dialogs.get(`mediaEditor_${mediaId}`));
- }
- /**
- * Updates the data of the currently edited media file.
- */
- updateData(media) {
- if (this._callbackObject._editorSuccess) {
- this._callbackObject._editorSuccess(media, undefined, false);
- }
- }
- }
- return MediaEditor;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/List/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/List/Upload.js
deleted file mode 100644
index 51a28398cd3..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/List/Upload.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Uploads media files.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @woltlabExcludeBundle tiny
- */
-define(["require", "exports", "tslib", "../Upload", "../../Core"], function (require, exports, tslib_1, Upload_1, Core) {
- "use strict";
- Upload_1 = tslib_1.__importDefault(Upload_1);
- Core = tslib_1.__importStar(Core);
- class MediaListUpload extends Upload_1.default {
- _createButton() {
- super._createButton();
- const span = this._button.querySelector("span");
- const space = document.createTextNode(" ");
- span.insertBefore(space, span.childNodes[0]);
- const icon = document.createElement("fa-icon");
- icon.setIcon("upload");
- span.insertBefore(icon, span.childNodes[0]);
- }
- _getParameters() {
- if (this._options.categoryId) {
- return Core.extend(super._getParameters(), {
- categoryID: this._options.categoryId,
- });
- }
- return super._getParameters();
- }
- }
- return MediaListUpload;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js
deleted file mode 100644
index e0e92ee6ad3..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js
+++ /dev/null
@@ -1,449 +0,0 @@
-/**
- * Provides the media manager dialog.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2020 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @woltlabExcludeBundle tiny
- */
-define(["require", "exports", "tslib", "../../Core", "WoltLabSuite/Core/Language", "../../Permission", "../../Dom/Change/Listener", "../../Event/Handler", "../../Dom/Traverse", "../../Dom/Util", "../../Ui/Dialog", "../../Controller/Clipboard", "../../Ui/Pagination", "../../StringUtil", "./Search", "../Upload", "../Editor", "../Clipboard", "WoltLabSuite/Core/Component/Snackbar"], function (require, exports, tslib_1, Core, Language_1, Permission, DomChangeListener, EventHandler, DomTraverse, DomUtil, UiDialog, Clipboard, Pagination_1, StringUtil, Search_1, Upload_1, Editor_1, MediaClipboard, Snackbar_1) {
- "use strict";
- Core = tslib_1.__importStar(Core);
- Permission = tslib_1.__importStar(Permission);
- DomChangeListener = tslib_1.__importStar(DomChangeListener);
- EventHandler = tslib_1.__importStar(EventHandler);
- DomTraverse = tslib_1.__importStar(DomTraverse);
- DomUtil = tslib_1.__importStar(DomUtil);
- UiDialog = tslib_1.__importStar(UiDialog);
- Clipboard = tslib_1.__importStar(Clipboard);
- Pagination_1 = tslib_1.__importDefault(Pagination_1);
- StringUtil = tslib_1.__importStar(StringUtil);
- Search_1 = tslib_1.__importDefault(Search_1);
- Upload_1 = tslib_1.__importDefault(Upload_1);
- Editor_1 = tslib_1.__importDefault(Editor_1);
- MediaClipboard = tslib_1.__importStar(MediaClipboard);
- let mediaManagerCounter = 0;
- class MediaManager {
- _forceClipboard = false;
- _hadInitiallyMarkedItems = false;
- _id;
- _listItems = new Map();
- _media = new Map();
- _mediaCategorySelect;
- _mediaEditor = null;
- _mediaManagerMediaList = null;
- _pagination = null;
- _search = null;
- _upload = null;
- _options;
- constructor(options) {
- this._options = Core.extend({
- dialogTitle: (0, Language_1.getPhrase)("wcf.media.manager"),
- imagesOnly: false,
- minSearchLength: 3,
- }, options);
- this._id = `mediaManager${mediaManagerCounter++}`;
- if (Permission.get("admin.content.cms.canManageMedia")) {
- this._mediaEditor = new Editor_1.default(this);
- }
- DomChangeListener.add("WoltLabSuite/Core/Media/Manager", () => this._addButtonEventListeners());
- EventHandler.add("com.woltlab.wcf.media.upload", "success", (data) => this._openEditorAfterUpload(data));
- }
- /**
- * Adds click event listeners to media buttons.
- */
- _addButtonEventListeners() {
- if (!this._mediaManagerMediaList || !Permission.get("admin.content.cms.canManageMedia"))
- return;
- DomTraverse.childrenByTag(this._mediaManagerMediaList, "LI").forEach((listItem) => {
- const editIcon = listItem.querySelector(".jsMediaEditButton");
- if (editIcon) {
- editIcon.classList.remove("jsMediaEditButton");
- editIcon.addEventListener("click", (ev) => this._editMedia(ev));
- }
- });
- }
- /**
- * Is called when a new category is selected.
- */
- _categoryChange() {
- this._search.search();
- }
- /**
- * Handles clicks on the media manager button.
- */
- _click(event) {
- event?.preventDefault();
- UiDialog.open(this);
- }
- /**
- * Is called if the media manager dialog is closed.
- */
- _dialogClose() {
- // only show media clipboard if editor is open
- if (Permission.get("admin.content.cms.canManageMedia") || this._forceClipboard) {
- Clipboard.hideEditor("com.woltlab.wcf.media");
- }
- }
- /**
- * Initializes the dialog when first loaded.
- */
- _dialogInit(content, data) {
- // store media data locally
- Object.entries(data.returnValues.media || {}).forEach(([mediaId, media]) => {
- this._media.set(~~mediaId, media);
- });
- this._initPagination(~~data.returnValues.pageCount);
- this._hadInitiallyMarkedItems = data.returnValues.hasMarkedItems > 0;
- }
- /**
- * Returns all data to setup the media manager dialog.
- */
- _dialogSetup() {
- return {
- id: this._id,
- options: {
- onClose: () => this._dialogClose(),
- onShow: () => this._dialogShow(),
- title: this._options.dialogTitle,
- },
- source: {
- after: (content, data) => this._dialogInit(content, data),
- data: {
- actionName: "getManagementDialog",
- className: "wcf\\data\\media\\MediaAction",
- parameters: {
- mode: this.getMode(),
- imagesOnly: this._options.imagesOnly,
- },
- },
- },
- };
- }
- /**
- * Is called if the media manager dialog is shown.
- */
- _dialogShow() {
- if (!this._mediaManagerMediaList) {
- const dialog = this.getDialog();
- this._mediaManagerMediaList = dialog.querySelector(".mediaManagerMediaList");
- this._mediaCategorySelect = dialog.querySelector(".mediaManagerCategoryList > select");
- if (this._mediaCategorySelect) {
- this._mediaCategorySelect.addEventListener("change", () => this._categoryChange());
- }
- // store list items locally
- const listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, "LI");
- listItems.forEach((listItem) => {
- this._listItems.set(~~listItem.dataset.objectId, listItem);
- });
- if (Permission.get("admin.content.cms.canManageMedia")) {
- const uploadButton = UiDialog.getDialog(this).dialog.querySelector(".mediaManagerMediaUploadButton");
- this._upload = new Upload_1.default(DomUtil.identify(uploadButton), DomUtil.identify(this._mediaManagerMediaList), {
- mediaManager: this,
- });
- EventHandler.add("WoltLabSuite/Core/Ui/Object/Action", "delete", (data) => this.removeMedia(~~data.objectElement.dataset.objectId));
- }
- if (Permission.get("admin.content.cms.canManageMedia") || this._forceClipboard) {
- MediaClipboard.init("menuManagerDialog-" + this.getMode(), this._hadInitiallyMarkedItems ? true : false, this);
- }
- else {
- this._removeClipboardCheckboxes();
- }
- this._search = new Search_1.default(this);
- if (!listItems.length) {
- this._search.hideSearch();
- }
- }
- else {
- MediaClipboard.setMediaManager(this);
- }
- // only show media clipboard if editor is open
- if (Permission.get("admin.content.cms.canManageMedia") || this._forceClipboard) {
- Clipboard.showEditor();
- }
- }
- /**
- * Opens the media editor for a media file.
- */
- _editMedia(event) {
- if (!Permission.get("admin.content.cms.canManageMedia")) {
- throw new Error("You are not allowed to edit media files.");
- }
- UiDialog.close(this);
- const target = event.currentTarget;
- this._mediaEditor.edit(this._media.get(~~target.dataset.objectId));
- }
- /**
- * Re-opens the manager dialog after closing the editor dialog.
- */
- _editorClose() {
- UiDialog.open(this);
- }
- /**
- * Re-opens the manager dialog and updates the media data after successfully editing a media file.
- */
- _editorSuccess(media, oldCategoryId, closedEditorDialog = true) {
- // if the category changed of media changed and category
- // is selected, check if media list needs to be refreshed
- if (this._mediaCategorySelect) {
- const selectedCategoryId = ~~this._mediaCategorySelect.value;
- if (selectedCategoryId) {
- const newCategoryId = ~~media.categoryID;
- if (oldCategoryId != newCategoryId &&
- (oldCategoryId == selectedCategoryId || newCategoryId == selectedCategoryId)) {
- this._search.search();
- }
- }
- }
- if (closedEditorDialog) {
- UiDialog.open(this);
- }
- this._media.set(~~media.mediaID, media);
- const listItem = this._listItems.get(~~media.mediaID);
- const p = listItem.querySelector(".mediaTitle");
- if (media.isMultilingual) {
- if (media.title && media.title[window.LANGUAGE_ID]) {
- p.textContent = media.title[window.LANGUAGE_ID];
- }
- else {
- p.textContent = media.filename;
- }
- }
- else {
- if (media.title && media.title[media.languageID]) {
- p.textContent = media.title[media.languageID];
- }
- else {
- p.textContent = media.filename;
- }
- }
- const thumbnail = listItem.querySelector(".mediaThumbnail");
- thumbnail.innerHTML = media.elementTag;
- // Bust browser cache by adding additional parameter.
- const img = thumbnail.querySelector("img");
- if (img) {
- img.src += `&refresh=${Date.now()}`;
- }
- }
- /**
- * Initializes the dialog pagination.
- */
- _initPagination(pageCount, pageNo) {
- if (pageNo === undefined)
- pageNo = 1;
- if (pageCount > 1) {
- const newPagination = document.createElement("div");
- newPagination.className = "paginationBottom jsPagination";
- const oldPagination = UiDialog.getDialog(this).content.querySelector(".jsPagination");
- oldPagination.insertAdjacentElement("beforebegin", newPagination);
- oldPagination.remove();
- this._pagination = new Pagination_1.default(newPagination, {
- activePage: pageNo,
- callbackSwitch: (pageNo) => this._search.search(pageNo),
- maxPage: pageCount,
- });
- }
- else if (this._pagination) {
- DomUtil.hide(this._pagination.getElement());
- }
- }
- /**
- * Removes all media clipboard checkboxes.
- */
- _removeClipboardCheckboxes() {
- this._mediaManagerMediaList.querySelectorAll(".mediaCheckbox").forEach((el) => el.remove());
- }
- /**
- * Opens the media editor after uploading a single file.
- *
- * @since 5.2
- */
- _openEditorAfterUpload(data) {
- if (data.upload === this._upload && !data.isMultiFileUpload && !this._upload.hasPendingUploads()) {
- const keys = Object.keys(data.media);
- if (keys.length) {
- UiDialog.close(this);
- this._mediaEditor.edit(this._media.get(~~data.media[keys[0]].mediaID));
- }
- }
- }
- /**
- * Sets the displayed media (after a search).
- */
- _setMedia(media) {
- this._media = new Map(Object.entries(media).map(([mediaId, media]) => [~~mediaId, media]));
- let info = DomTraverse.nextByClass(this._mediaManagerMediaList, "info");
- if (this._media.size) {
- if (info) {
- DomUtil.hide(info);
- }
- }
- else {
- if (info === null) {
- info = document.createElement("p");
- info.className = "info";
- info.textContent = (0, Language_1.getPhrase)("wcf.media.search.noResults");
- }
- DomUtil.show(info);
- DomUtil.insertAfter(info, this._mediaManagerMediaList);
- }
- DomTraverse.childrenByTag(this._mediaManagerMediaList, "LI").forEach((listItem) => {
- if (!this._media.has(~~listItem.dataset.objectId)) {
- DomUtil.hide(listItem);
- }
- else {
- DomUtil.show(listItem);
- }
- });
- DomChangeListener.trigger();
- if (Permission.get("admin.content.cms.canManageMedia") || this._forceClipboard) {
- Clipboard.reload();
- }
- else {
- this._removeClipboardCheckboxes();
- }
- }
- /**
- * Adds a media file to the manager.
- */
- addMedia(media, listItem) {
- if (!media.languageID)
- media.isMultilingual = 1;
- this._media.set(~~media.mediaID, media);
- this._listItems.set(~~media.mediaID, listItem);
- if (this._listItems.size === 1) {
- this._search.showSearch();
- }
- }
- /**
- * Is called after the media files with the given ids have been deleted via clipboard.
- */
- clipboardDeleteMedia(mediaIds) {
- mediaIds.forEach((mediaId) => {
- this.removeMedia(~~mediaId);
- });
- (0, Snackbar_1.showDefaultSuccessSnackbar)();
- }
- /**
- * Returns the id of the currently selected category or `0` if no category is selected.
- */
- getCategoryId() {
- if (this._mediaCategorySelect) {
- return ~~this._mediaCategorySelect.value;
- }
- return 0;
- }
- /**
- * Returns the media manager dialog element.
- */
- getDialog() {
- return UiDialog.getDialog(this).dialog;
- }
- /**
- * Returns the mode of the media manager.
- */
- getMode() {
- return "";
- }
- /**
- * Returns the media manager option with the given name.
- */
- getOption(name) {
- if (this._options[name]) {
- return this._options[name];
- }
- return null;
- }
- /**
- * Removes a media file.
- */
- removeMedia(mediaId) {
- if (this._listItems.has(mediaId)) {
- // remove list item
- try {
- this._listItems.get(mediaId).remove();
- }
- catch {
- // ignore errors if item has already been removed by other code
- }
- this._listItems.delete(mediaId);
- this._media.delete(mediaId);
- }
- }
- /**
- * Changes the displayed media to the previously displayed media.
- */
- resetMedia() {
- // calling WoltLabSuite/Core/Media/Manager/Search.search() reloads the first page of the dialog
- this._search.search();
- }
- /**
- * Sets the media files currently displayed.
- */
- setMedia(media, template, additionalData) {
- const hasMedia = Object.entries(media).length > 0;
- if (hasMedia) {
- const ul = document.createElement("ul");
- ul.innerHTML = template;
- DomTraverse.childrenByTag(ul, "LI").forEach((listItem) => {
- if (!this._listItems.has(~~listItem.dataset.objectId)) {
- this._listItems.set(~~listItem.dataset.objectId, listItem);
- this._mediaManagerMediaList.appendChild(listItem);
- }
- });
- }
- this._initPagination(additionalData.pageCount, additionalData.pageNo);
- this._setMedia(media);
- }
- /**
- * Sets up a new media element.
- */
- setupMediaElement(media, mediaElement) {
- const mediaInformation = DomTraverse.childByClass(mediaElement, "mediaInformation");
- const buttonGroupNavigation = document.createElement("nav");
- buttonGroupNavigation.className = "jsMobileNavigation buttonGroupNavigation";
- mediaInformation.parentNode.appendChild(buttonGroupNavigation);
- const buttons = document.createElement("ul");
- buttons.className = "buttonList iconList";
- buttonGroupNavigation.appendChild(buttons);
- const listItem = document.createElement("li");
- listItem.className = "mediaCheckbox";
- buttons.appendChild(listItem);
- const a = document.createElement("a");
- listItem.appendChild(a);
- const label = document.createElement("label");
- a.appendChild(label);
- const checkbox = document.createElement("input");
- checkbox.className = "jsClipboardItem";
- checkbox.type = "checkbox";
- checkbox.dataset.objectId = media.mediaID.toString();
- label.appendChild(checkbox);
- if (Permission.get("admin.content.cms.canManageMedia")) {
- const editButton = document.createElement("li");
- editButton.className = "jsMediaEditButton";
- editButton.dataset.objectId = media.mediaID.toString();
- buttons.appendChild(editButton);
- editButton.innerHTML = `
-
-
- ${(0, Language_1.getPhrase)("wcf.global.button.edit")}
- `;
- const deleteButton = document.createElement("li");
- deleteButton.classList.add("jsObjectAction");
- deleteButton.dataset.objectAction = "delete";
- // use temporary title to not unescape html in filename
- const uuid = Core.getUuid();
- deleteButton.dataset.confirmMessage = StringUtil.unescapeHTML((0, Language_1.getPhrase)("wcf.media.delete.confirmMessage", {
- title: uuid,
- })).replace(uuid, StringUtil.escapeHTML(media.filename));
- buttons.appendChild(deleteButton);
- deleteButton.innerHTML = `
-
-
- ${(0, Language_1.getPhrase)("wcf.global.button.delete")}
- `;
- }
- }
- }
- return MediaManager;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Editor.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Editor.js
index df4abc188a6..67a1c19417f 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Editor.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Editor.js
@@ -1,305 +1,47 @@
/**
- * Provides the media manager dialog for selecting media for Redactor editors.
+ * Provides the media manager dialog for inserting media files.
*
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
+ * @author Matthias Schmidt, Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License
* @woltlabExcludeBundle tiny
+ * @deprecated 6.3 Use `WoltLabSuite/Core/Component/Media/Manager` instead.
*/
-define(["require", "exports", "tslib", "./Base", "../../Core", "../../Event/Handler", "../../Dom/Traverse", "WoltLabSuite/Core/Language", "../../Ui/Dialog", "../../Controller/Clipboard", "../../Dom/Util", "../../Component/Ckeditor/Event", "WoltLabSuite/Core/StringUtil"], function (require, exports, tslib_1, Base_1, Core, EventHandler, DomTraverse, Language_1, UiDialog, Clipboard, Util_1, Event_1, StringUtil_1) {
+define(["require", "exports", "../../Component/Media/Manager", "../../Component/Media/Insert", "../../Helper/PromiseMutex"], function (require, exports, Manager_1, Insert_1, PromiseMutex_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MediaManagerEditor = void 0;
- Base_1 = tslib_1.__importDefault(Base_1);
- Core = tslib_1.__importStar(Core);
- EventHandler = tslib_1.__importStar(EventHandler);
- DomTraverse = tslib_1.__importStar(DomTraverse);
- UiDialog = tslib_1.__importStar(UiDialog);
- Clipboard = tslib_1.__importStar(Clipboard);
- Util_1 = tslib_1.__importDefault(Util_1);
- class MediaManagerEditor extends Base_1.default {
- _mediaToInsert = new Map();
- _mediaToInsertByClipboard = false;
- _uploadData;
- _uploadId = null;
+ class MediaManagerEditor {
+ #options;
constructor(options) {
- options = Core.extend({
- callbackInsert: null,
- }, options);
- super(options);
- this._forceClipboard = true;
- if (this._options.ckeditor === undefined) {
- if (typeof this._options.buttonClass === "string") {
- document.querySelectorAll(`.${this._options.buttonClass}`).forEach((button) => {
- button.addEventListener("click", (event) => {
- this._click(event);
- });
- });
- }
- }
- else {
- const ckeditor = this._options.ckeditor;
- (0, Event_1.listenToCkeditor)(ckeditor.sourceElement).bbcode(({ bbcode }) => {
- if (bbcode !== "media") {
- return false;
- }
- this._click();
- return true;
- });
- if (!ckeditor.features.attachment) {
- (0, Event_1.listenToCkeditor)(ckeditor.sourceElement).uploadMedia((payload) => {
- this._editorUpload(payload);
- });
- }
- }
- }
- _addButtonEventListeners() {
- super._addButtonEventListeners();
- if (!this._mediaManagerMediaList) {
+ this.#options = options;
+ if (options.ckeditor !== undefined) {
+ // Editors are supported by `WoltLabSuite/Core/Component/Ckeditor/Media` out of the box.
return;
}
- DomTraverse.childrenByTag(this._mediaManagerMediaList, "LI").forEach((listItem) => {
- const insertIcon = listItem.querySelector(".jsMediaInsertButton");
- if (insertIcon) {
- insertIcon.classList.remove("jsMediaInsertButton");
- insertIcon.addEventListener("click", (ev) => this._openInsertDialog(ev));
- }
- });
- }
- /**
- * Builds the dialog to setup inserting media files.
- */
- _buildInsertDialog() {
- let thumbnailOptions = "";
- this._getThumbnailSizes().forEach((thumbnailSize) => {
- thumbnailOptions +=
- '' +
- (0, Language_1.getPhrase)("wcf.media.insert.imageSize." + thumbnailSize) +
- " ";
- });
- thumbnailOptions += '' + (0, Language_1.getPhrase)("wcf.media.insert.imageSize.original") + " ";
- const dialog = `
-
-
- ${(0, Language_1.getPhrase)("wcf.media.insert.imageSize")}
-
-
- ${thumbnailOptions}
-
-
-
-
-
- ${(0, Language_1.getPhrase)("wcf.global.button.insert")}
-
`;
- UiDialog.open({
- _dialogSetup: () => {
- return {
- id: this._getInsertDialogId(),
- options: {
- onClose: () => this._editorClose(),
- onSetup: (content) => {
- content.querySelector(".buttonPrimary").addEventListener("click", (ev) => this._insertMedia(ev));
- Util_1.default.show(content.querySelector(".thumbnailSizeSelection"));
- },
- title: (0, Language_1.getPhrase)("wcf.media.insert"),
- },
- source: dialog,
- };
- },
- });
- }
- _dialogShow() {
- super._dialogShow();
- // check if data needs to be uploaded
- if (this._uploadData) {
- if (this._upload !== null) {
- const uploadId = this._upload.uploadFile(this._uploadData.file);
- this._uploadData.promise = new Promise((resolve) => {
- const uuid = EventHandler.add("com.woltlab.wcf.media.upload", "success", (data) => {
- if (data.uploadId !== uploadId) {
- return;
- }
- EventHandler.remove("com.woltlab.wcf.media.upload", "success", uuid);
- resolve({
- mediaId: data.media[0].mediaID,
- mediaSize: "original",
- url: data.media[0].link,
- });
- });
+ if (typeof options.buttonClass === "string") {
+ const insert = (0, PromiseMutex_1.promiseMutex)(() => this.#insert());
+ document.querySelectorAll(`.${options.buttonClass}`).forEach((button) => {
+ button.addEventListener("click", () => {
+ insert();
});
- }
- this._uploadData = undefined;
- }
- }
- /**
- * Handles pasting and dragging and dropping files into the editor.
- */
- _editorUpload(data) {
- this._uploadData = data;
- UiDialog.open(this);
- }
- /**
- * Returns the id of the insert dialog based on the media files to be inserted.
- */
- _getInsertDialogId() {
- return [this._id + "Insert", ...this._mediaToInsert.keys()].join("-");
- }
- /**
- * Returns the supported thumbnail sizes (excluding `original`) for all media images to be inserted.
- */
- _getThumbnailSizes() {
- return ["small", "medium", "large"]
- .map((size) => {
- const sizeSupported = Array.from(this._mediaToInsert.values()).every((media) => {
- return media[size + "ThumbnailType"] !== null;
- });
- if (sizeSupported) {
- return size;
- }
- return null;
- })
- .filter((s) => s !== null);
- }
- /**
- * Inserts media files into the editor.
- */
- _insertMedia(event, thumbnailSize, closeEditor = false) {
- if (closeEditor === undefined)
- closeEditor = true;
- // update insert options with selected values if method is called by clicking on 'insert' button
- // in dialog
- if (event) {
- UiDialog.close(this._getInsertDialogId());
- const dialogContent = event.currentTarget.closest(".dialogContent");
- const thumbnailSizeSelect = dialogContent.querySelector("select[name=thumbnailSize]");
- thumbnailSize = thumbnailSizeSelect.value;
- }
- if (this._options.callbackInsert !== null) {
- this._options.callbackInsert(this._mediaToInsert, "separate" /* MediaInsertType.Separate */, thumbnailSize);
- }
- else {
- this._mediaToInsert.forEach((media) => this._insertMediaItem(thumbnailSize, media));
- }
- if (this._mediaToInsertByClipboard) {
- Clipboard.unmark("com.woltlab.wcf.media", Array.from(this._mediaToInsert.keys()));
- }
- this._mediaToInsert = new Map();
- this._mediaToInsertByClipboard = false;
- // close manager dialog
- if (closeEditor) {
- UiDialog.close(this);
- }
- }
- /**
- * Inserts a single media item into the editor.
- */
- _insertMediaItem(thumbnailSize, media) {
- const ckeditor = this._options.ckeditor;
- if (media.isImage) {
- let available = "";
- ["small", "medium", "large", "original"].some((size) => {
- if (media[size + "ThumbnailHeight"] != 0) {
- available = size;
- if (thumbnailSize == size) {
- return true;
- }
- }
- return false;
});
- thumbnailSize = available;
- if (!thumbnailSize) {
- thumbnailSize = "original";
- }
- let link = media.link;
- if (thumbnailSize !== "original") {
- link = media[thumbnailSize + "ThumbnailLink"];
- }
- ckeditor.insertHtml(` `);
- }
- else {
- ckeditor.insertText(`[wsm='${media.mediaID}'][/wsm]`);
}
}
- /**
- * Is called after media files are successfully uploaded to insert copied media.
- */
- _mediaUploaded(data) {
- if (this._uploadId !== null && this._upload === data.upload) {
- if (this._uploadId === data.uploadId ||
- (Array.isArray(this._uploadId) && this._uploadId.indexOf(data.uploadId) !== -1)) {
- this._mediaToInsert = new Map(data.media.entries());
- this._insertMedia(null, "medium", false);
- this._uploadId = null;
- }
- }
- }
- /**
- * Handles clicking on the insert button.
- */
- _openInsertDialog(event) {
- const target = event.currentTarget;
- this.insertMedia([~~target.dataset.objectId]);
- }
- /**
- * Is called to insert the media files with the given ids into an editor.
- */
- clipboardInsertMedia(mediaIds) {
- this.insertMedia(mediaIds, true);
- }
- /**
- * Prepares insertion of the media files with the given ids.
- */
- insertMedia(mediaIds, insertedByClipboard) {
- this._mediaToInsert = new Map();
- this._mediaToInsertByClipboard = insertedByClipboard || false;
- // open the insert dialog if all media files are images
- let imagesOnly = true;
- mediaIds.forEach((mediaId) => {
- const media = this._media.get(mediaId);
- this._mediaToInsert.set(media.mediaID, media);
- if (!media.isImage) {
- imagesOnly = false;
- }
+ async #insert() {
+ const media = await (0, Manager_1.openMediaManager)({
+ mode: "editor",
+ imagesOnly: this.#options.imagesOnly,
+ title: this.#options.dialogTitle,
});
- if (imagesOnly) {
- const thumbnailSizes = this._getThumbnailSizes();
- if (thumbnailSizes.length) {
- UiDialog.close(this);
- const dialogId = this._getInsertDialogId();
- if (UiDialog.getDialog(dialogId)) {
- UiDialog.openStatic(dialogId, null);
- }
- else {
- this._buildInsertDialog();
- }
- }
- else {
- this._insertMedia(undefined, "original");
- }
+ if (media.length === 0 || this.#options.callbackInsert === undefined) {
+ return;
}
- else {
- this._insertMedia();
+ const size = await (0, Insert_1.chooseImageSize)(media);
+ if (size === undefined) {
+ return;
}
- }
- getMode() {
- return "editor";
- }
- setupMediaElement(media, mediaElement) {
- super.setupMediaElement(media, mediaElement);
- // add media insertion icon
- const buttons = mediaElement.querySelector("nav.buttonGroupNavigation > ul");
- const listItem = document.createElement("li");
- listItem.className = "jsMediaInsertButton";
- listItem.dataset.objectId = media.mediaID.toString();
- buttons.appendChild(listItem);
- listItem.innerHTML = `
-
-
- ${(0, Language_1.getPhrase)("wcf.global.button.insert")}
- `;
+ this.#options.callbackInsert(new Map(media.map((item) => [item.mediaID, item])), "separate" /* MediaInsertType.Separate */, size === "original" ? undefined : size);
}
}
exports.MediaManagerEditor = MediaManagerEditor;
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Search.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Search.js
deleted file mode 100644
index 19ef3f1451b..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Search.js
+++ /dev/null
@@ -1,142 +0,0 @@
-/**
- * Provides the media search for the media manager.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @woltlabExcludeBundle tiny
- */
-define(["require", "exports", "tslib", "../../Dom/Traverse", "WoltLabSuite/Core/Language", "../../Ajax", "../../Dom/Util"], function (require, exports, tslib_1, DomTraverse, Language_1, Ajax, Util_1) {
- "use strict";
- DomTraverse = tslib_1.__importStar(DomTraverse);
- Ajax = tslib_1.__importStar(Ajax);
- Util_1 = tslib_1.__importDefault(Util_1);
- class MediaManagerSearch {
- _cancelButton;
- _input;
- _mediaManager;
- _searchContainer;
- _searchMode = false;
- constructor(mediaManager) {
- this._mediaManager = mediaManager;
- const dialog = mediaManager.getDialog();
- this._searchContainer = dialog.querySelector(".mediaManagerSearch");
- this._input = dialog.querySelector(".mediaManagerSearchField");
- this._input.addEventListener("keypress", (ev) => this._keyPress(ev));
- this._cancelButton = dialog.querySelector(".mediaManagerSearchCancelButton");
- this._cancelButton.addEventListener("click", () => this._cancelSearch());
- }
- _ajaxSetup() {
- return {
- data: {
- actionName: "getSearchResultList",
- className: "wcf\\data\\media\\MediaAction",
- interfaceName: "wcf\\data\\ISearchAction",
- },
- };
- }
- _ajaxSuccess(data) {
- this._mediaManager.setMedia(data.returnValues.media || ({}), data.returnValues.template || "", {
- pageCount: data.returnValues.pageCount || 0,
- pageNo: data.returnValues.pageNo || 0,
- });
- this._mediaManager.getDialog().querySelector(".dialogContent").scrollTop = 0;
- }
- /**
- * Cancels the search after clicking on the cancel search button.
- */
- _cancelSearch() {
- if (this._searchMode) {
- this._searchMode = false;
- this.resetSearch();
- this._mediaManager.resetMedia();
- }
- }
- /**
- * Hides the search string threshold error.
- */
- _hideStringThresholdError() {
- const innerInfo = DomTraverse.childByClass(this._input.parentNode.parentNode, "innerInfo");
- if (innerInfo) {
- Util_1.default.hide(innerInfo);
- }
- }
- /**
- * Handles the `[ENTER]` key to submit the form.
- */
- _keyPress(event) {
- if (event.key === "Enter") {
- event.preventDefault();
- if (this._input.value.length >= this._mediaManager.getOption("minSearchLength")) {
- this._hideStringThresholdError();
- this.search();
- }
- else {
- this._showStringThresholdError();
- }
- }
- }
- /**
- * Shows the search string threshold error.
- */
- _showStringThresholdError() {
- let innerInfo = DomTraverse.childByClass(this._input.parentNode.parentNode, "innerInfo");
- if (innerInfo) {
- Util_1.default.show(innerInfo);
- }
- else {
- innerInfo = document.createElement("p");
- innerInfo.className = "innerInfo";
- innerInfo.textContent = (0, Language_1.getPhrase)("wcf.media.search.info.searchStringThreshold", {
- minSearchLength: this._mediaManager.getOption("minSearchLength"),
- });
- this._input.parentNode.insertAdjacentElement("afterend", innerInfo);
- }
- }
- /**
- * Hides the media search.
- */
- hideSearch() {
- Util_1.default.hide(this._searchContainer);
- }
- /**
- * Resets the media search.
- */
- resetSearch() {
- this._input.value = "";
- }
- /**
- * Shows the media search.
- */
- showSearch() {
- Util_1.default.show(this._searchContainer);
- }
- /**
- * Sends an AJAX request to fetch search results.
- */
- search(pageNo) {
- if (typeof pageNo !== "number") {
- pageNo = 1;
- }
- let searchString = this._input.value;
- if (searchString && this._input.value.length < this._mediaManager.getOption("minSearchLength")) {
- this._showStringThresholdError();
- searchString = "";
- }
- else {
- this._hideStringThresholdError();
- }
- this._searchMode = true;
- Ajax.api(this, {
- parameters: {
- categoryID: this._mediaManager.getCategoryId(),
- imagesOnly: this._mediaManager.getOption("imagesOnly"),
- mode: this._mediaManager.getMode(),
- pageNo: pageNo,
- searchString: searchString,
- },
- });
- }
- }
- return MediaManagerSearch;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Select.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Select.js
index b96d171311f..5892b91ac92 100644
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Select.js
+++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Select.js
@@ -1,178 +1,25 @@
/**
* Provides the media manager dialog for selecting media for input elements.
*
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
+ * @author Matthias Schmidt, Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License
* @woltlabExcludeBundle tiny
+ * @deprecated 6.3 Use `WoltLabSuite/Core/Component/Media/Select` instead.
*/
-define(["require", "exports", "tslib", "./Base", "../../Core", "../../Dom/Traverse", "../../FileUtil", "WoltLabSuite/Core/Language", "../../Ui/Dialog", "WoltLabSuite/Core/StringUtil"], function (require, exports, tslib_1, Base_1, Core, DomTraverse, FileUtil, Language_1, UiDialog, StringUtil_1) {
+define(["require", "exports", "../../Component/Media/Select"], function (require, exports, Select_1) {
"use strict";
- Base_1 = tslib_1.__importDefault(Base_1);
- Core = tslib_1.__importStar(Core);
- DomTraverse = tslib_1.__importStar(DomTraverse);
- FileUtil = tslib_1.__importStar(FileUtil);
- UiDialog = tslib_1.__importStar(UiDialog);
- class MediaManagerSelect extends Base_1.default {
- _activeButton = null;
- _buttons;
- _storeElements = new WeakMap();
+ class MediaManagerSelect {
constructor(options) {
- super(options);
- this._buttons = document.getElementsByClassName(this._options.buttonClass || "jsMediaSelectButton");
- Array.from(this._buttons).forEach((button) => {
- // only consider buttons with a proper store specified
- const store = button.dataset.store;
- if (store) {
- const storeElement = document.getElementById(store);
- if (storeElement && storeElement.tagName === "INPUT") {
- button.addEventListener("click", (ev) => this._click(ev));
- this._storeElements.set(button, storeElement);
- const removeButton = document.createElement("button");
- removeButton.type = "button";
- removeButton.classList.add("button", "jsTooltip");
- removeButton.title = (0, Language_1.getPhrase)("wcf.global.button.delete");
- removeButton.innerHTML = ' ';
- if (button.parentElement.tagName === "LI") {
- const listItem = document.createElement("li");
- listItem.append(removeButton);
- button.parentElement.insertAdjacentElement("afterend", listItem);
- if (!storeElement.value) {
- listItem.hidden = true;
- }
- }
- else {
- button.insertAdjacentElement("afterend", removeButton);
- if (!storeElement.value) {
- removeButton.hidden = true;
- }
- }
- removeButton.addEventListener("click", () => this._removeMedia(button, removeButton));
- }
+ document.querySelectorAll(`.${options.buttonClass || "jsMediaSelectButton"}`).forEach((button) => {
+ if (button.dataset.store) {
+ (0, Select_1.setup)(button, {
+ imagesOnly: options.imagesOnly,
+ title: options.dialogTitle,
+ });
}
});
}
- _addButtonEventListeners() {
- super._addButtonEventListeners();
- if (!this._mediaManagerMediaList)
- return;
- DomTraverse.childrenByTag(this._mediaManagerMediaList, "LI").forEach((listItem) => {
- const chooseIcon = listItem.querySelector(".jsMediaSelectButton");
- if (chooseIcon) {
- chooseIcon.classList.remove("jsMediaSelectButton");
- chooseIcon.addEventListener("click", (ev) => this._chooseMedia(ev));
- }
- });
- }
- /**
- * Handles clicking on a media choose icon.
- */
- _chooseMedia(event) {
- if (this._activeButton === null) {
- throw new Error("Media cannot be chosen if no button is active.");
- }
- const target = event.currentTarget;
- const media = this._media.get(~~target.dataset.objectId);
- // save selected media in store element
- const input = document.getElementById(this._activeButton.dataset.store);
- input.value = media.mediaID.toString();
- Core.triggerEvent(input, "change");
- // display selected media
- const display = this._activeButton.dataset.display;
- if (display) {
- const displayElement = document.getElementById(display);
- if (displayElement) {
- if (media.isImage) {
- const thumbnailLink = (0, StringUtil_1.escapeHTML)(media.smallThumbnailLink ? media.smallThumbnailLink : media.link);
- const altText = (0, StringUtil_1.escapeHTML)(media.altText && media.altText[window.LANGUAGE_ID] ? media.altText[window.LANGUAGE_ID] : "");
- displayElement.innerHTML = ` `;
- }
- else {
- let fileIcon = FileUtil.getIconNameByFilename(media.filename);
- if (fileIcon) {
- fileIcon = "file-" + fileIcon;
- }
- else {
- fileIcon = "file";
- }
- displayElement.innerHTML = `
-
-
-
-
${(0, StringUtil_1.escapeHTML)(media.filename)}
-
${media.formattedFilesize}
-
-
`;
- }
- }
- }
- // show remove button
- if (this._activeButton.parentElement.tagName === "LI") {
- const removeButton = this._activeButton.parentElement.nextElementSibling;
- removeButton.hidden = false;
- }
- else {
- const removeButton = this._activeButton.nextElementSibling;
- removeButton.hidden = false;
- }
- UiDialog.close(this);
- }
- _click(event) {
- event.preventDefault();
- this._activeButton = event.currentTarget;
- super._click(event);
- if (!this._mediaManagerMediaList) {
- return;
- }
- const storeElement = this._storeElements.get(this._activeButton);
- DomTraverse.childrenByTag(this._mediaManagerMediaList, "LI").forEach((listItem) => {
- if (storeElement.value && storeElement.value == listItem.dataset.objectId) {
- listItem.classList.add("jsSelected");
- }
- else {
- listItem.classList.remove("jsSelected");
- }
- });
- }
- getMode() {
- return "select";
- }
- setupMediaElement(media, mediaElement) {
- super.setupMediaElement(media, mediaElement);
- // add media insertion icon
- const buttons = mediaElement.querySelector("nav.buttonGroupNavigation > ul");
- const listItem = document.createElement("li");
- listItem.className = "jsMediaSelectButton";
- listItem.dataset.objectId = media.mediaID.toString();
- buttons.appendChild(listItem);
- listItem.innerHTML = `
-
-
- ${(0, Language_1.getPhrase)("wcf.media.button.select")}
-
- `;
- }
- /**
- * Handles clicking on the remove button.
- */
- _removeMedia(selectButton, removeButton) {
- if (removeButton.parentElement.tagName === "LI") {
- removeButton.parentElement.hidden = true;
- }
- else {
- removeButton.hidden = true;
- }
- const input = document.getElementById(selectButton.dataset.store);
- input.value = "";
- Core.triggerEvent(input, "change");
- const display = selectButton.dataset.display;
- if (display) {
- const displayElement = document.getElementById(display);
- if (displayElement) {
- displayElement.innerHTML = "";
- }
- }
- }
}
return MediaManagerSelect;
});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Replace.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Replace.js
deleted file mode 100644
index 99ce02c5152..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Replace.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Uploads replacemnts for media files.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @since 5.3
- * @woltlabExcludeBundle tiny
- */
-define(["require", "exports", "tslib", "../Core", "./Upload", "WoltLabSuite/Core/Language", "../Dom/Util", "../Dom/Change/Listener", "../Component/Snackbar"], function (require, exports, tslib_1, Core, Upload_1, Language_1, Util_1, DomChangeListener, Snackbar_1) {
- "use strict";
- Core = tslib_1.__importStar(Core);
- Upload_1 = tslib_1.__importDefault(Upload_1);
- Util_1 = tslib_1.__importDefault(Util_1);
- DomChangeListener = tslib_1.__importStar(DomChangeListener);
- class MediaReplace extends Upload_1.default {
- _mediaID;
- constructor(mediaID, buttonContainerId, targetId, options) {
- super(buttonContainerId, targetId, Core.extend(options, {
- action: "replaceFile",
- }));
- this._mediaID = mediaID;
- }
- _createButton() {
- super._createButton();
- this._button.classList.add("small");
- this._button.querySelector("span").textContent = (0, Language_1.getPhrase)("wcf.media.button.replaceFile");
- }
- _createFileElement() {
- return this._target;
- }
- _getFormData() {
- return {
- objectIDs: [this._mediaID],
- };
- }
- _success(uploadId, data) {
- this._fileElements[uploadId].forEach((file) => {
- const internalFileId = file.dataset.internalFileId;
- const media = data.returnValues.media[internalFileId];
- if (media) {
- if (media.isImage) {
- this._target.innerHTML = media.smallThumbnailTag;
- }
- document.getElementById("mediaFilename").textContent = media.filename;
- document.getElementById("mediaFilesize").textContent = media.formattedFilesize;
- if (media.isImage) {
- document.getElementById("mediaImageDimensions").textContent = media.imageDimensions;
- }
- document.getElementById("mediaUploader").innerHTML = media.userLinkElement;
- this._options.mediaEditor.updateData(media);
- // Remove existing error messages.
- Util_1.default.innerError(this._buttonContainer, "");
- (0, Snackbar_1.showDefaultSuccessSnackbar)();
- }
- else {
- let error = data.returnValues.errors[internalFileId];
- if (!error) {
- error = {
- errorType: "uploadFailed",
- filename: file.dataset.filename,
- };
- }
- Util_1.default.innerError(this._buttonContainer, (0, Language_1.getPhrase)("wcf.media.upload.error." + error.errorType, {
- filename: error.filename,
- }));
- }
- DomChangeListener.trigger();
- });
- }
- }
- return MediaReplace;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Upload.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Upload.js
deleted file mode 100644
index 49bf762894b..00000000000
--- a/wcfsetup/install/files/js/WoltLabSuite/Core/Media/Upload.js
+++ /dev/null
@@ -1,257 +0,0 @@
-/**
- * Uploads media files.
- *
- * @author Matthias Schmidt
- * @copyright 2001-2021 WoltLab GmbH
- * @license GNU Lesser General Public License
- * @woltlabExcludeBundle tiny
- */
-define(["require", "exports", "tslib", "../Upload", "../Core", "../Dom/Util", "WoltLabSuite/Core/Language", "../User", "../Date/Util", "../FileUtil", "../Dom/Change/Listener", "../Event/Handler"], function (require, exports, tslib_1, Upload_1, Core, DomUtil, Language_1, User_1, DateUtil, FileUtil, DomChangeListener, EventHandler) {
- "use strict";
- Upload_1 = tslib_1.__importDefault(Upload_1);
- Core = tslib_1.__importStar(Core);
- DomUtil = tslib_1.__importStar(DomUtil);
- User_1 = tslib_1.__importDefault(User_1);
- DateUtil = tslib_1.__importStar(DateUtil);
- FileUtil = tslib_1.__importStar(FileUtil);
- DomChangeListener = tslib_1.__importStar(DomChangeListener);
- EventHandler = tslib_1.__importStar(EventHandler);
- class MediaUpload extends Upload_1.default {
- _categoryId = null;
- _elementTagSize;
- _mediaManager;
- constructor(buttonContainerId, targetId, options) {
- super(buttonContainerId, targetId, Core.extend({
- className: "wcf\\data\\media\\MediaAction",
- multiple: options.mediaManager ? true : false,
- singleFileRequests: true,
- }, options || {}));
- this._elementTagSize = 144;
- if (this._options.elementTagSize) {
- this._elementTagSize = this._options.elementTagSize;
- }
- this._mediaManager = null;
- if (this._options.mediaManager) {
- this._mediaManager = this._options.mediaManager;
- delete this._options.mediaManager;
- }
- }
- _createFileElement(file) {
- let fileElement;
- if (this._target.nodeName === "OL" || this._target.nodeName === "UL") {
- fileElement = document.createElement("li");
- }
- else if (this._target.nodeName === "TBODY") {
- const firstTr = this._target.getElementsByTagName("TR")[0];
- const tableContainer = this._target.parentNode.parentNode;
- if (tableContainer.style.getPropertyValue("display") === "none") {
- fileElement = firstTr;
- tableContainer.style.removeProperty("display");
- document.getElementById(this._target.dataset.noItemsInfo).remove();
- }
- else {
- fileElement = firstTr.cloneNode(true);
- // regenerate id of table row
- fileElement.removeAttribute("id");
- DomUtil.identify(fileElement);
- }
- Array.from(fileElement.getElementsByTagName("TD")).forEach((cell) => {
- if (cell.classList.contains("columnMark")) {
- cell.querySelectorAll("[data-object-id]").forEach((el) => DomUtil.hide(el));
- }
- else if (cell.classList.contains("columnIcon")) {
- cell.querySelectorAll("[data-object-id]").forEach((el) => DomUtil.hide(el));
- cell.querySelector(".mediaEditButton").classList.add("jsMediaEditButton");
- cell.querySelector(".jsObjectAction[data-object-action='delete']").dataset.confirmMessage =
- (0, Language_1.getPhrase)("wcf.media.delete.confirmMessage", {
- title: file.name,
- });
- }
- else if (cell.classList.contains("columnFilename")) {
- // replace copied image with spinner
- let image = cell.querySelector("img");
- if (!image) {
- image = cell.querySelector("fa-icon");
- }
- const spinner = document.createElement("span");
- spinner.innerHTML = ' ';
- spinner.classList.add("mediaThumbnail");
- DomUtil.replaceElement(image, spinner);
- // replace title and uploading user
- const ps = cell.querySelectorAll(".box48 > div > p");
- ps[0].textContent = file.name;
- let userLink = ps[1].getElementsByTagName("A")[0];
- if (!userLink) {
- userLink = document.createElement("a");
- ps[1].getElementsByTagName("SMALL")[0].appendChild(userLink);
- }
- userLink.setAttribute("href", User_1.default.getLink());
- userLink.textContent = User_1.default.username;
- }
- else if (cell.classList.contains("columnUploadTime")) {
- cell.innerHTML = "";
- cell.appendChild(DateUtil.getTimeElement(new Date()));
- }
- else if (cell.classList.contains("columnFilesize")) {
- cell.textContent = FileUtil.formatFilesize(file.size);
- }
- else if (cell.classList.contains("columnDownloads")) {
- cell.textContent = "0";
- }
- else {
- // empty the other cells
- cell.innerHTML = "";
- }
- });
- DomUtil.prepend(fileElement, this._target);
- return fileElement;
- }
- else {
- fileElement = document.createElement("p");
- }
- const thumbnail = document.createElement("div");
- thumbnail.classList.add("mediaThumbnail");
- fileElement.appendChild(thumbnail);
- const fileIcon = document.createElement("fa-icon");
- fileIcon.size = 144;
- fileIcon.setIcon("spinner");
- thumbnail.appendChild(fileIcon);
- const mediaInformation = document.createElement("div");
- mediaInformation.className = "mediaInformation";
- fileElement.appendChild(mediaInformation);
- const p = document.createElement("p");
- p.className = "mediaTitle";
- p.textContent = file.name;
- mediaInformation.appendChild(p);
- const progress = document.createElement("progress");
- progress.max = 100;
- mediaInformation.appendChild(progress);
- DomUtil.prepend(fileElement, this._target);
- DomChangeListener.trigger();
- return fileElement;
- }
- _getParameters() {
- const parameters = {
- elementTagSize: this._elementTagSize,
- };
- if (this._mediaManager) {
- parameters.imagesOnly = this._mediaManager.getOption("imagesOnly");
- const categoryId = this._mediaManager.getCategoryId();
- if (categoryId) {
- parameters.categoryID = categoryId;
- }
- }
- return Core.extend(super._getParameters(), parameters);
- }
- _replaceFileIcon(fileIcon, media, size) {
- if (media.elementTag) {
- fileIcon.outerHTML = media.elementTag;
- }
- else if (media.tinyThumbnailType) {
- const img = document.createElement("img");
- img.src = media.tinyThumbnailLink;
- img.alt = "";
- img.style.setProperty("width", `${size}px`);
- img.style.setProperty("height", `${size}px`);
- DomUtil.replaceElement(fileIcon, img);
- }
- else {
- let fileIconName = FileUtil.getIconNameByFilename(media.filename);
- if (fileIconName) {
- fileIconName = `file-${fileIconName}`;
- }
- else {
- fileIconName = "file";
- }
- fileIcon.setIcon(fileIconName, false);
- }
- }
- _success(uploadId, data) {
- const files = this._fileElements[uploadId];
- files.forEach((file) => {
- const internalFileId = file.dataset.internalFileId;
- const media = data.returnValues.media[internalFileId];
- if (file.tagName === "TR") {
- if (media) {
- // update object id
- file.dataset.objectId = media.mediaID.toString();
- file.querySelectorAll("[data-object-id]").forEach((el) => {
- el.dataset.objectId = media.mediaID.toString();
- el.style.removeProperty("display");
- });
- file.querySelector(".columnMediaID").textContent = media.mediaID.toString();
- // update icon
- this._replaceFileIcon(file.querySelector(".columnFilename fa-icon"), media, 48);
- }
- else {
- let error = data.returnValues.errors[internalFileId];
- if (!error) {
- error = {
- errorType: "uploadFailed",
- filename: file.dataset.filename,
- };
- }
- const deleteButton = document.createElement("button");
- deleteButton.type = "button";
- deleteButton.classList.add("jsTooltip");
- deleteButton.title = (0, Language_1.getPhrase)("wcf.global.button.delete");
- deleteButton.addEventListener("click", () => {
- deleteButton.closest(".mediaFile").remove();
- EventHandler.fire("com.woltlab.wcf.media.upload", "removedErroneousUploadRow");
- });
- const fileIcon = file.querySelector("fa-icon");
- fileIcon.setIcon("xmark");
- fileIcon.insertAdjacentElement("beforebegin", deleteButton);
- deleteButton.append(fileIcon);
- file.classList.add("uploadFailed");
- const p = file.querySelectorAll(".columnFilename .box48 > div > p")[1];
- DomUtil.innerError(p, (0, Language_1.getPhrase)(`wcf.media.upload.error.${error.errorType}`, {
- filename: error.filename,
- }));
- p.remove();
- }
- }
- else {
- file.querySelector(".mediaInformation progress").remove();
- if (media) {
- const fileIcon = file.querySelector(".mediaThumbnail fa-icon");
- this._replaceFileIcon(fileIcon, media, 144);
- file.classList.add("jsClipboardObject", "mediaFile", "jsObjectActionObject");
- file.dataset.objectId = media.mediaID.toString();
- if (this._mediaManager) {
- this._mediaManager.setupMediaElement(media, file);
- this._mediaManager.addMedia(media, file);
- }
- }
- else {
- let error = data.returnValues.errors[internalFileId];
- if (!error) {
- error = {
- errorType: "uploadFailed",
- filename: file.dataset.filename,
- };
- }
- const fileIcon = file.querySelector(".mediaThumbnail fa-icon");
- fileIcon.setIcon("xmark");
- file.classList.add("uploadFailed", "pointer", "jsTooltip");
- file.title = (0, Language_1.getPhrase)("wcf.global.button.delete");
- file.addEventListener("click", () => file.remove());
- const title = file.querySelector(".mediaInformation .mediaTitle");
- title.textContent = (0, Language_1.getPhrase)(`wcf.media.upload.error.${error.errorType}`, {
- filename: error.filename,
- });
- }
- }
- DomChangeListener.trigger();
- });
- EventHandler.fire("com.woltlab.wcf.media.upload", "success", {
- files: files,
- isMultiFileUpload: this._multiFileUploadIds.indexOf(uploadId) !== -1,
- media: data.returnValues.media,
- upload: this,
- uploadId: uploadId,
- });
- }
- }
- return MediaUpload;
-});
diff --git a/wcfsetup/install/files/lib/acp/action/MediaSetCategoryAction.class.php b/wcfsetup/install/files/lib/acp/action/MediaSetCategoryAction.class.php
new file mode 100644
index 00000000000..79b9fd6cb25
--- /dev/null
+++ b/wcfsetup/install/files/lib/acp/action/MediaSetCategoryAction.class.php
@@ -0,0 +1,109 @@
+
+ * @since 6.3
+ */
+final class MediaSetCategoryAction implements RequestHandlerInterface
+{
+ #[\Override]
+ public function handle(ServerRequestInterface $request): ResponseInterface
+ {
+ $media = $this->getMedia($request);
+
+ $form = $this->getForm();
+
+ if ($request->getMethod() === 'GET') {
+ return $form->toResponse();
+ } elseif ($request->getMethod() === 'POST') {
+ $response = $form->validateRequest($request);
+ if ($response !== null) {
+ return $response;
+ }
+
+ $categoryID = (int)$form->getData()['data']['categoryID'] ?: null;
+ foreach ($media as $mediaFile) {
+ (new UpdateMedia(
+ MediaBuilder::forUpdate($mediaFile)->setCategoryID($categoryID)
+ ))();
+ }
+
+ return new JsonResponse([]);
+ } else {
+ throw new \LogicException('Unreachable');
+ }
+ }
+
+ /**
+ * @return array
+ */
+ private function getMedia(ServerRequestInterface $request): array
+ {
+ try {
+ $parameters = Helper::mapQueryParameters(
+ $request->getQueryParams(),
+ <<<'EOT'
+ array {
+ ids: non-empty-list
+ }
+ EOT
+ );
+ } catch (MappingError) {
+ throw new IllegalLinkException();
+ }
+
+ $mediaIDs = \array_unique($parameters['ids']);
+
+ $mediaList = new MediaList();
+ $mediaList->setObjectIDs($mediaIDs);
+ $mediaList->readObjects();
+ $media = $mediaList->getObjects();
+ if (\count($media) !== \count($mediaIDs)) {
+ throw new IllegalLinkException();
+ }
+
+ foreach ($media as $mediaFile) {
+ if (!$mediaFile->canManage()) {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ return $media;
+ }
+
+ private function getForm(): Psr15DialogForm
+ {
+ $form = new Psr15DialogForm(
+ static::class,
+ WCF::getLanguage()->get('wcf.media.setCategory')
+ );
+ $form->appendChildren([
+ MediaCategoryFormField::create(),
+ ]);
+ $form->build();
+
+ return $form;
+ }
+}
diff --git a/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php b/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php
index 4c87343a619..2ba33a3e456 100644
--- a/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php
+++ b/wcfsetup/install/files/lib/acp/form/BoxAddForm.class.php
@@ -8,7 +8,7 @@
use wcf\data\box\BoxAction;
use wcf\data\box\BoxEditor;
use wcf\data\media\Media;
-use wcf\data\media\ViewableMediaList;
+use wcf\data\media\MediaList;
use wcf\data\object\type\ObjectType;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\page\Page;
@@ -428,7 +428,7 @@ protected function readBoxImages()
return;
}
- $mediaList = new ViewableMediaList();
+ $mediaList = new MediaList();
$mediaList->setObjectIDs(\array_values($imageIDs));
$mediaList->readObjects();
diff --git a/wcfsetup/install/files/lib/acp/page/MediaListPage.class.php b/wcfsetup/install/files/lib/acp/page/MediaListPage.class.php
index c9ea573628c..0ad4ce652ce 100644
--- a/wcfsetup/install/files/lib/acp/page/MediaListPage.class.php
+++ b/wcfsetup/install/files/lib/acp/page/MediaListPage.class.php
@@ -2,185 +2,52 @@
namespace wcf\acp\page;
-use wcf\data\category\CategoryNode;
-use wcf\data\category\CategoryNodeTree;
-use wcf\data\media\ViewableMediaList;
-use wcf\page\SortablePage;
-use wcf\system\clipboard\ClipboardHandler;
+use wcf\action\MediaEditAction;
+use wcf\page\AbstractGridViewPage;
+use wcf\system\file\processor\FileProcessor;
+use wcf\system\file\processor\MediaFileProcessor;
+use wcf\system\gridView\admin\MediaGridView;
use wcf\system\request\LinkHandler;
use wcf\system\WCF;
-use wcf\util\StringUtil;
/**
* Shows the list of media entries.
*
- * @author Matthias Schmidt
- * @copyright 2001-2019 WoltLab GmbH
+ * @author Matthias Schmidt, Marcel Werk
+ * @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License
*
- * @extends SortablePage
+ * @extends AbstractGridViewPage
*/
-class MediaListPage extends SortablePage
+class MediaListPage extends AbstractGridViewPage
{
/**
* @inheritDoc
*/
public $activeMenuItem = 'wcf.acp.menu.link.media.list';
- /**
- * id of the selected media category
- * @var int
- */
- public $categoryID = 0;
-
- /**
- * node tree with all available media categories
- * @var \RecursiveIteratorIterator|null
- */
- public $categoryList;
-
- /**
- * @inheritDoc
- */
- public $defaultSortField = 'uploadTime';
-
- /**
- * @inheritDoc
- */
- public $defaultSortOrder = 'DESC';
-
- /**
- * searched media query
- * @var string
- */
- public $query = '';
-
- /**
- * @inheritDoc
- */
- public $forceCanonicalURL = true;
-
/**
* @inheritDoc
*/
public $neededPermissions = ['admin.content.cms.canManageMedia'];
- /**
- * @inheritDoc
- */
- public $objectListClassName = ViewableMediaList::class;
-
- /**
- * name of the user who uploaded the searched media files
- * @var string
- */
- public $username = '';
-
- /**
- * @inheritDoc
- */
- public $validSortFields = [
- 'filename',
- 'filesize',
- 'mediaID',
- 'title',
- 'uploadTime',
- 'downloads',
- 'lastDownloadTime',
- ];
-
#[\Override]
- public function assignVariables()
+ protected function createGridView(): MediaGridView
{
- parent::assignVariables();
-
- WCF::getTPL()->assign([
- 'categoryID' => $this->categoryID,
- 'categoryList' => $this->categoryList,
- 'q' => $this->query,
- 'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media')),
- 'username' => $this->username,
- ]);
+ return new MediaGridView();
}
#[\Override]
- protected function initObjectList()
- {
- parent::initObjectList();
-
- if (WCF::getSession()->hasPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
- $this->objectList->getConditionBuilder()->add('media.userID = ?', [WCF::getUser()->userID]);
- }
-
- if ($this->categoryID !== 0) {
- if ($this->categoryID === -1) {
- $this->objectList->getConditionBuilder()->add('media.categoryID IS NULL');
- } else {
- $this->objectList->getConditionBuilder()->add('media.categoryID = ?', [$this->categoryID]);
- }
- }
- if ($this->query !== '') {
- $this->objectList->addSearchConditions($this->query);
- }
- if ($this->username !== '') {
- $this->objectList->getConditionBuilder()->add(
- 'media.username LIKE ?',
- ['%' . \addcslashes($this->username, '_%') . '%']
- );
- }
- }
-
- #[\Override]
- public function readData()
- {
- parent::readData();
-
- $this->categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
- $this->categoryList->setMaxDepth(0);
- }
-
- #[\Override]
- public function readParameters()
+ public function assignVariables()
{
- parent::readParameters();
-
- if (isset($_REQUEST['categoryID'])) {
- $this->categoryID = \intval($_REQUEST['categoryID']);
- }
- if (isset($_REQUEST['q'])) {
- $this->query = StringUtil::trim($_REQUEST['q']);
- }
- if (isset($_REQUEST['username'])) {
- $this->username = StringUtil::trim($_REQUEST['username']);
- }
-
- $parameters = [];
- if ($this->sortField !== '') {
- $parameters['sortField'] = $this->sortField;
- }
- if ($this->sortOrder !== '') {
- $parameters['sortOrder'] = $this->sortOrder;
- }
- if ($this->query !== '') {
- $parameters['q'] = $this->query;
- }
- if ($this->username !== '') {
- $parameters['username'] = $this->username;
- }
- if ($this->categoryID !== 0) {
- $parameters['categoryID'] = $this->categoryID;
- }
+ parent::assignVariables();
- $this->canonicalURL = LinkHandler::getInstance()->getControllerLink(MediaListPage::class, $parameters);
- }
+ $processor = FileProcessor::getInstance()->getProcessorByName('com.woltlab.wcf.media');
+ \assert($processor instanceof MediaFileProcessor);
- #[\Override]
- protected function readObjects()
- {
- if ($this->sqlOrderBy !== '' && $this->sortField === 'mediaID') {
- $this->sqlOrderBy = 'media.' . $this->sortField . ' ' . $this->sortOrder;
- }
-
- parent::readObjects();
+ WCF::getTPL()->assign([
+ 'uploadButton' => $processor->toHtmlElement(),
+ 'editEndpoint' => LinkHandler::getInstance()->getControllerLink(MediaEditAction::class, ['id' => '%s']),
+ ]);
}
}
diff --git a/wcfsetup/install/files/lib/acp/page/MediaPage.class.php b/wcfsetup/install/files/lib/acp/page/MediaPage.class.php
index f37638b807d..97b2d478b28 100644
--- a/wcfsetup/install/files/lib/acp/page/MediaPage.class.php
+++ b/wcfsetup/install/files/lib/acp/page/MediaPage.class.php
@@ -12,6 +12,7 @@
* @author Matthias Schmidt
* @copyright 2001-2019 WoltLab GmbH
* @license GNU Lesser General Public License
+ * @deprecated 6.3 Only kept to resolve existing links, use `Media::getLink()` or `Media::getThumbnailLink()` instead.
*/
class MediaPage extends AbstractPage
{
diff --git a/wcfsetup/install/files/lib/action/FileDownloadAction.class.php b/wcfsetup/install/files/lib/action/FileDownloadAction.class.php
index 0b84b56006c..d8eec92a2de 100644
--- a/wcfsetup/install/files/lib/action/FileDownloadAction.class.php
+++ b/wcfsetup/install/files/lib/action/FileDownloadAction.class.php
@@ -64,9 +64,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface
throw new PermissionDeniedException();
}
- $processor->trackDownload($file);
- EventHandler::getInstance()->fire(new DownloadStarting($file));
-
$filename = $file->getPathname();
$response = new Response(
new Stream($filename),
@@ -102,6 +99,14 @@ public function handle(ServerRequestInterface $request): ResponseInterface
->withHeader('content-range', "bytes */{$file->fileSize}");
}
+ // Media players and download managers request the remaining parts of
+ // a file through additional range requests, which must not be counted
+ // as separate downloads.
+ if ($startByte === 0) {
+ $processor->trackDownload($file);
+ EventHandler::getInstance()->fire(new DownloadStarting($file));
+ }
+
if ($startByte > 0 || $endByte < $file->fileSize - 1) {
$response = $response
->withStatus(206)
@@ -113,8 +118,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface
// Prevent