Merge branch '5.3'
authorMatthias Schmidt <gravatronics@live.com>
Wed, 12 May 2021 08:41:26 +0000 (10:41 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Wed, 12 May 2021 08:41:26 +0000 (10:41 +0200)
1  2 
ts/WoltLabSuite/Core/Controller/Media/List.ts
ts/WoltLabSuite/Core/Media/Data.ts
ts/WoltLabSuite/Core/Media/Editor.ts
ts/WoltLabSuite/Core/Media/Manager/Base.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Media/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Editor.js
wcfsetup/install/files/js/WoltLabSuite/Core/Media/Manager/Base.js
wcfsetup/install/files/lib/data/media/MediaAction.class.php

index ba49f8e17d8b3be73e0fb7d5149e308cf823a8b9,0000000000000000000000000000000000000000..b64cd5846235ba7b899c3665930c9a2ab1f8dd42
mode 100644,000000..100644
--- /dev/null
@@@ -1,101 -1,0 +1,101 @@@
-   _editorSuccess: (media: Media, oldCategoryId: number) => {
-     if (media.categoryID != oldCategoryId) {
 +/**
 + * Initializes modules required for media list view.
 + *
 + * @author  Matthias Schmidt
 + * @copyright 2001-2021 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  WoltLabSuite/Core/Controller/Media/List
 + */
 +
 +import MediaListUpload from "../../Media/List/Upload";
 +import * as MediaClipboard from "../../Media/Clipboard";
 +import * as EventHandler from "../../Event/Handler";
 +import MediaEditor from "../../Media/Editor";
 +import * as DomChangeListener from "../../Dom/Change/Listener";
 +import { Media, MediaUploadSuccessEventData } from "../../Media/Data";
 +import MediaManager from "../../Media/Manager/Base";
 +
 +const _mediaEditor = new MediaEditor({
++  _editorSuccess: (media: Media, oldCategoryId: number, closedEditorDialog = true) => {
++    if (media.categoryID != oldCategoryId || closedEditorDialog) {
 +      window.setTimeout(() => {
 +        window.location.reload();
 +      }, 500);
 +    }
 +  },
 +});
 +const _tableBody = document.getElementById("mediaListTableBody")!;
 +let _upload: MediaListUpload;
 +
 +interface MediaListOptions {
 +  categoryId?: number;
 +  hasMarkedItems?: boolean;
 +}
 +
 +export function init(options: MediaListOptions): void {
 +  options = options || {};
 +  _upload = new MediaListUpload("uploadButton", "mediaListTableBody", {
 +    categoryId: options.categoryId,
 +    multiple: true,
 +    elementTagSize: 48,
 +  });
 +
 +  MediaClipboard.init("wcf\\acp\\page\\MediaListPage", options.hasMarkedItems || false, {
 +    clipboardDeleteMedia: (mediaIds: number[]) => clipboardDeleteMedia(mediaIds),
 +  } as MediaManager);
 +
 +  addButtonEventListeners();
 +
 +  DomChangeListener.add("WoltLabSuite/Core/Controller/Media/List", () => addButtonEventListeners());
 +
 +  EventHandler.add("com.woltlab.wcf.media.upload", "success", (data: MediaUploadSuccessEventData) =>
 +    openEditorAfterUpload(data),
 +  );
 +}
 +
 +/**
 + * Adds the `click` event listeners to the media edit icons in new media table rows.
 + */
 +function addButtonEventListeners(): void {
 +  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: Event): void {
 +  _mediaEditor.edit(~~(event.currentTarget as HTMLElement).dataset.objectId!);
 +}
 +
 +/**
 + * Opens the media editor after uploading a single file.
 + */
 +function openEditorAfterUpload(data: MediaUploadSuccessEventData) {
 +  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: number[]) {
 +  Array.from(document.getElementsByClassName("jsMediaRow")).forEach((media) => {
 +    const mediaID = ~~(media.querySelector(".jsClipboardItem") as HTMLElement).dataset.objectId!;
 +
 +    if (mediaIds.indexOf(mediaID) !== -1) {
 +      media.remove();
 +    }
 +  });
 +
 +  if (!document.getElementsByClassName("jsMediaRow").length) {
 +    window.location.reload();
 +  }
 +}
index 24ad9739232d990782e2bd91469ce8fe38e867d3,0000000000000000000000000000000000000000..52e346fdf2658778e82ce79686556972786b31f5
mode 100644,000000..100644
--- /dev/null
@@@ -1,88 -1,0 +1,88 @@@
-   _editorSuccess?: (Media, number?) => void;
 +/**
 + * @author  Matthias Schmidt
 + * @copyright 2001-2021 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  WoltLabSuite/Core/Media/Data
 + */
 +
 +import MediaUpload from "./Upload";
 +import { FileElements, UploadOptions } from "../Upload/Data";
 +import MediaEditor from "./Editor";
 +import MediaManager from "./Manager/Base";
 +import { RedactorEditor } from "../Ui/Redactor/Editor";
 +import { I18nValues } from "../Language/Input";
 +
 +export interface Media {
 +  altText: I18nValues | string;
 +  caption: I18nValues | string;
 +  categoryID: number;
 +  elementTag: string;
 +  captionEnableHtml: number;
 +  filename: string;
 +  formattedFilesize: string;
 +  languageID: number | null;
 +  isImage: number;
 +  isMultilingual: number;
 +  link: string;
 +  mediaID: number;
 +  smallThumbnailLink: string;
 +  smallThumbnailType: string;
 +  tinyThumbnailLink: string;
 +  tinyThumbnailType: string;
 +  title: I18nValues | string;
 +}
 +
 +export interface MediaManagerOptions {
 +  dialogTitle: string;
 +  imagesOnly: boolean;
 +  minSearchLength: number;
 +}
 +
 +export const enum MediaInsertType {
 +  Separate = "separate",
 +}
 +
 +export interface MediaManagerEditorOptions extends MediaManagerOptions {
 +  buttonClass?: string;
 +  callbackInsert: (media: Map<number, Media>, insertType: MediaInsertType, thumbnailSize?: string) => void;
 +  editor?: RedactorEditor;
 +}
 +
 +export interface MediaManagerSelectOptions extends MediaManagerOptions {
 +  buttonClass?: string;
 +}
 +
 +export interface MediaEditorCallbackObject {
 +  _editorClose?: () => void;
++  _editorSuccess?: (Media, number?, boolean?) => void;
 +}
 +
 +export interface MediaUploadSuccessEventData {
 +  files: FileElements;
 +  isMultiFileUpload: boolean;
 +  media: Media[];
 +  upload: MediaUpload;
 +  uploadId: number;
 +}
 +
 +export interface MediaUploadOptions extends UploadOptions {
 +  elementTagSize: number;
 +  mediaEditor?: MediaEditor;
 +  mediaManager?: MediaManager;
 +}
 +
 +export interface MediaListUploadOptions extends MediaUploadOptions {
 +  categoryId?: number;
 +}
 +
 +export interface MediaUploadAjaxResponseData {
 +  returnValues: {
 +    errors: MediaUploadError[];
 +    media: Media[];
 +  };
 +}
 +
 +export interface MediaUploadError {
 +  errorType: string;
 +  filename: string;
 +}
index f4254165c3d1bd1ea637afe50d46920e763416c3,0000000000000000000000000000000000000000..ba5c2d8f0b95f9944b1f3851132b81ba96c6b5fe
mode 100644,000000..100644
--- /dev/null
@@@ -1,413 -1,0 +1,413 @@@
-       this._callbackObject._editorSuccess(media);
 +/**
 + * Handles editing media files via dialog.
 + *
 + * @author  Matthias Schmidt
 + * @copyright 2001-2021 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  WoltLabSuite/Core/Media/Editor
 + */
 +
 +import * as Core from "../Core";
 +import { Media, MediaEditorCallbackObject } from "./Data";
 +import { AjaxCallbackObject, AjaxCallbackSetup } from "../Ajax/Data";
 +import * as UiNotification from "../Ui/Notification";
 +import * as UiDialog from "../Ui/Dialog";
 +import { DialogCallbackObject } from "../Ui/Dialog/Data";
 +import * as LanguageChooser from "../Language/Chooser";
 +import * as LanguageInput from "../Language/Input";
 +import * as DomUtil from "../Dom/Util";
 +import * as DomTraverse from "../Dom/Traverse";
 +import DomChangeListener from "../Dom/Change/Listener";
 +import * as Language from "../Language";
 +import * as Ajax from "../Ajax";
 +import MediaReplace from "./Replace";
 +import { I18nValues } from "../Language/Input";
 +
 +interface InitEditorData {
 +  returnValues: {
 +    availableLanguageCount: number;
 +    categoryIDs: number[];
 +    mediaData?: Media;
 +  };
 +}
 +
 +class MediaEditor implements AjaxCallbackObject {
 +  protected _availableLanguageCount = 1;
 +  protected _categoryIds: number[] = [];
 +  protected _dialogs = new Map<string, DialogCallbackObject>();
 +  protected readonly _callbackObject: MediaEditorCallbackObject;
 +  protected _media: Media | null = null;
 +  protected _oldCategoryId = 0;
 +
 +  constructor(callbackObject: MediaEditorCallbackObject) {
 +    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'.");
 +    }
 +  }
 +
 +  public _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
 +    return {
 +      data: {
 +        actionName: "update",
 +        className: "wcf\\data\\media\\MediaAction",
 +      },
 +    };
 +  }
 +
 +  public _ajaxSuccess(): void {
 +    UiNotification.show();
 +
 +    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.
 +   */
 +  protected _close(): void {
 +    this._media = null;
 +
 +    if (this._callbackObject._editorClose) {
 +      this._callbackObject._editorClose();
 +    }
 +  }
 +
 +  /**
 +   * Initializes the editor dialog.
 +   *
 +   * @since 5.3
 +   */
 +  protected _initEditor(content: HTMLElement, data: InitEditorData): void {
 +    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]") as HTMLSelectElement;
 +        if (media.categoryID) {
 +          categoryID.value = media.categoryID.toString();
 +        } else {
 +          categoryID.value = "0";
 +        }
 +      }
 +
 +      const title = content.querySelector("input[name=title]") as HTMLInputElement;
 +      const altText = content.querySelector("input[name=altText]") as HTMLInputElement;
 +      const caption = content.querySelector("textarea[name=caption]") as HTMLInputElement;
 +
 +      if (this._availableLanguageCount > 1 && media.isMultilingual) {
 +        if (document.getElementById(`altText_${mediaId}`)) {
 +          LanguageInput.setValues(`altText_${mediaId}`, (media.altText || {}) as I18nValues);
 +        }
 +
 +        if (document.getElementById(`caption_${mediaId}`)) {
 +          LanguageInput.setValues(`caption_${mediaId}`, (media.caption || {}) as I18nValues);
 +        }
 +
 +        LanguageInput.setValues(`title_${mediaId}`, (media.title || {}) as I18nValues);
 +      } 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]") as HTMLInputElement;
 +        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! as HTMLElement).blur();
 +      (document.getElementById(`mediaEditor_${mediaId}`)!.parentNode as HTMLElement).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 MediaReplace(
 +        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,
 +        },
 +      );
 +
 +      DomChangeListener.trigger();
 +    }, 200);
 +  }
 +
 +  /**
 +   * Handles the `[ENTER]` key to submit the form.
 +   */
 +  protected _keyPress(event: KeyboardEvent): void {
 +    if (event.key === "Enter") {
 +      event.preventDefault();
 +
 +      this._saveData();
 +    }
 +  }
 +
 +  /**
 +   * Saves the data of the currently edited media.
 +   */
 +  protected _saveData(): void {
 +    const content = UiDialog.getDialog(`mediaEditor_${this._media!.mediaID}`)!.content;
 +
 +    const categoryId = content.querySelector("select[name=categoryID]") as HTMLSelectElement;
 +    const altText = content.querySelector("input[name=altText]") as HTMLInputElement;
 +    const caption = content.querySelector("textarea[name=caption]") as HTMLTextAreaElement;
 +    const captionEnableHtml = content.querySelector("input[name=captionEnableHtml]") as HTMLInputElement;
 +    const title = content.querySelector("input[name=title]") as HTMLInputElement;
 +
 +    let hasError = false;
 +    const altTextError = altText ? DomTraverse.childByClass(altText.parentNode! as HTMLElement, "innerError") : false;
 +    const captionError = caption ? DomTraverse.childByClass(caption.parentNode! as HTMLElement, "innerError") : false;
 +    const titleError = DomTraverse.childByClass(title.parentNode! as HTMLElement, "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]") as HTMLInputElement;
 +      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, Language.get("wcf.global.form.error.multilingual"));
 +        }
 +      }
 +      if (caption && !LanguageInput.validate(caption.id, true)) {
 +        hasError = true;
 +        if (!captionError) {
 +          DomUtil.innerError(caption, Language.get("wcf.global.form.error.multilingual"));
 +        }
 +      }
 +      if (!LanguageInput.validate(title.id, true)) {
 +        hasError = true;
 +        if (!titleError) {
 +          DomUtil.innerError(title, Language.get("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`)! as HTMLInputElement)
 +        .checked,
 +      group: Array.from(
 +        content.querySelectorAll(`input[name="mediaEditor_${this._media!.mediaID}_aclValues[group][]"]`),
 +      ).map((aclGroup: HTMLInputElement) => ~~aclGroup.value),
 +      user: Array.from(
 +        content.querySelectorAll(`input[name="mediaEditor_${this._media!.mediaID}_aclValues[user][]"]`),
 +      ).map((aclUser: HTMLInputElement) => ~~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,
 +        },
 +      });
 +    }
 +  }
 +
 +  private mapToI18nValues(values: Map<number, string>): I18nValues {
 +    const obj = {};
 +    values.forEach((value, key) => (obj[key] = value));
 +
 +    return obj;
 +  }
 +
 +  /**
 +   * Updates language-related input fields depending on whether multilingualis is enabled.
 +   */
 +  protected _updateLanguageFields(event: Event | null, element?: HTMLInputElement): void {
 +    if (event) {
 +      element = event.currentTarget as HTMLInputElement;
 +    }
 +
 +    const mediaId = this._media!.mediaID;
 +    const languageChooserContainer = document.getElementById(`mediaEditor_${mediaId}_languageIDContainer`)!
 +      .parentNode! as HTMLElement;
 +
 +    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.
 +   */
 +  public edit(editedMedia: Media | number): void {
 +    let media: Media;
 +    let mediaId = 0;
 +    if (typeof editedMedia === "object") {
 +      media = editedMedia;
 +      mediaId = media.mediaID;
 +    } else {
 +      media = {
 +        mediaID: editedMedia,
 +      } as Media;
 +      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: Language.get("wcf.media.edit"),
 +            },
 +            source: {
 +              after: (content: HTMLElement, responseData: InitEditorData) => 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.
 +   */
 +  public updateData(media: Media): void {
 +    if (this._callbackObject._editorSuccess) {
++      this._callbackObject._editorSuccess(media, undefined, false);
 +    }
 +  }
 +}
 +
 +Core.enableLegacyInheritance(MediaEditor);
 +
 +export = MediaEditor;
index 057ca6993497976a71ee434ce4057b2e9dcc1d70,0000000000000000000000000000000000000000..929d2b20446f9686306566e64d81dbaa65761e9f
mode 100644,000000..100644
--- /dev/null
@@@ -1,549 -1,0 +1,551 @@@
-   _editorSuccess(media: Media, oldCategoryId?: number): void {
 +/**
 + * Provides the media manager dialog.
 + *
 + * @author  Matthias Schmidt
 + * @copyright 2001-2020 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  WoltLabSuite/Core/Media/Manager/Base
 + */
 +
 +import * as Core from "../../Core";
 +import { Media, MediaManagerOptions, MediaEditorCallbackObject, MediaUploadSuccessEventData } from "../Data";
 +import * as Language from "../../Language";
 +import * as Permission from "../../Permission";
 +import * as DomChangeListener from "../../Dom/Change/Listener";
 +import * as EventHandler from "../../Event/Handler";
 +import * as DomTraverse from "../../Dom/Traverse";
 +import * as DomUtil from "../../Dom/Util";
 +import * as UiDialog from "../../Ui/Dialog";
 +import { DialogCallbackSetup, DialogCallbackObject } from "../../Ui/Dialog/Data";
 +import * as Clipboard from "../../Controller/Clipboard";
 +import UiPagination from "../../Ui/Pagination";
 +import * as UiNotification from "../../Ui/Notification";
 +import * as StringUtil from "../../StringUtil";
 +import MediaManagerSearch from "./Search";
 +import MediaUpload from "../Upload";
 +import MediaEditor from "../Editor";
 +import * as MediaClipboard from "../Clipboard";
 +import { ObjectActionData } from "../../Ui/Object/Data";
 +
 +let mediaManagerCounter = 0;
 +
 +interface DialogInitAjaxResponseData {
 +  returnValues: {
 +    hasMarkedItems: number;
 +    media: object;
 +    pageCount: number;
 +  };
 +}
 +
 +interface SetMediaAdditionalData {
 +  pageCount: number;
 +  pageNo: number;
 +}
 +
 +abstract class MediaManager<TOptions extends MediaManagerOptions = MediaManagerOptions>
 +  implements DialogCallbackObject, MediaEditorCallbackObject {
 +  protected _forceClipboard = false;
 +  protected _hadInitiallyMarkedItems = false;
 +  protected readonly _id;
 +  protected readonly _listItems = new Map<number, HTMLLIElement>();
 +  protected _media = new Map<number, Media>();
 +  protected _mediaCategorySelect: HTMLSelectElement | null;
 +  protected readonly _mediaEditor: MediaEditor | null = null;
 +  protected _mediaManagerMediaList: HTMLElement | null = null;
 +  protected _pagination: UiPagination | null = null;
 +  protected _search: MediaManagerSearch | null = null;
 +  protected _upload: any = null;
 +  protected readonly _options: TOptions;
 +
 +  constructor(options: Partial<TOptions>) {
 +    this._options = Core.extend(
 +      {
 +        dialogTitle: Language.get("wcf.media.manager"),
 +        imagesOnly: false,
 +        minSearchLength: 3,
 +      },
 +      options,
 +    ) as TOptions;
 +
 +    this._id = `mediaManager${mediaManagerCounter++}`;
 +
 +    if (Permission.get("admin.content.cms.canManageMedia")) {
 +      this._mediaEditor = new MediaEditor(this);
 +    }
 +
 +    DomChangeListener.add("WoltLabSuite/Core/Media/Manager", () => this._addButtonEventListeners());
 +
 +    EventHandler.add("com.woltlab.wcf.media.upload", "success", (data: MediaUploadSuccessEventData) =>
 +      this._openEditorAfterUpload(data),
 +    );
 +  }
 +
 +  /**
 +   * Adds click event listeners to media buttons.
 +   */
 +  protected _addButtonEventListeners(): void {
 +    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.
 +   */
 +  protected _categoryChange(): void {
 +    this._search!.search();
 +  }
 +
 +  /**
 +   * Handles clicks on the media manager button.
 +   */
 +  protected _click(event: Event): void {
 +    event.preventDefault();
 +
 +    UiDialog.open(this);
 +  }
 +
 +  /**
 +   * Is called if the media manager dialog is closed.
 +   */
 +  protected _dialogClose(): void {
 +    // 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.
 +   */
 +  protected _dialogInit(content: HTMLElement, data: DialogInitAjaxResponseData): void {
 +    // 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.
 +   */
 +  public _dialogSetup(): ReturnType<DialogCallbackSetup> {
 +    return {
 +      id: this._id,
 +      options: {
 +        onClose: () => this._dialogClose(),
 +        onShow: () => this._dialogShow(),
 +        title: this._options.dialogTitle,
 +      },
 +      source: {
 +        after: (content: HTMLElement, data: DialogInitAjaxResponseData) => 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.
 +   */
 +  protected _dialogShow(): void {
 +    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: HTMLLIElement) => {
 +        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 MediaUpload(DomUtil.identify(uploadButton), DomUtil.identify(this._mediaManagerMediaList!), {
 +          mediaManager: this,
 +        });
 +
 +        EventHandler.add("WoltLabSuite/Core/Ui/Object/Action", "delete", (data: ObjectActionData) =>
 +          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 MediaManagerSearch(this);
 +
 +      if (!listItems.length) {
 +        this._search.hideSearch();
 +      }
 +    }
 +
 +    // 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.
 +   */
 +  protected _editMedia(event: Event): void {
 +    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 as HTMLElement;
 +
 +    this._mediaEditor!.edit(this._media.get(~~target.dataset.objectId!)!);
 +  }
 +
 +  /**
 +   * Re-opens the manager dialog after closing the editor dialog.
 +   */
 +  _editorClose(): void {
 +    UiDialog.open(this);
 +  }
 +
 +  /**
 +   * Re-opens the manager dialog and updates the media data after successfully editing a media file.
 +   */
-     UiDialog.open(this);
++  _editorSuccess(media: Media, oldCategoryId?: number, closedEditorDialog = true): void {
 +    // 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.
 +   */
 +  protected _initPagination(pageCount: number, pageNo?: number): void {
 +    if (pageNo === undefined) pageNo = 1;
 +
 +    if (pageCount > 1) {
 +      const newPagination = document.createElement("div");
 +      newPagination.className = "paginationBottom jsPagination";
 +      DomUtil.replaceElement(
 +        UiDialog.getDialog(this)!.content.querySelector(".jsPagination") as HTMLElement,
 +        newPagination,
 +      );
 +
 +      this._pagination = new UiPagination(newPagination, {
 +        activePage: pageNo,
 +        callbackSwitch: (pageNo: number) => this._search!.search(pageNo),
 +        maxPage: pageCount,
 +      });
 +    } else if (this._pagination) {
 +      DomUtil.hide(this._pagination.getElement());
 +    }
 +  }
 +
 +  /**
 +   * Removes all media clipboard checkboxes.
 +   */
 +  _removeClipboardCheckboxes(): void {
 +    this._mediaManagerMediaList!.querySelectorAll(".mediaCheckbox").forEach((el) => el.remove());
 +  }
 +
 +  /**
 +   * Opens the media editor after uploading a single file.
 +   *
 +   * @since 5.2
 +   */
 +  _openEditorAfterUpload(data: MediaUploadSuccessEventData): void {
 +    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: object): void {
 +    this._media = new Map<number, Media>(Object.entries(media).map(([mediaId, media]) => [~~mediaId, media]));
 +
 +    let info = DomTraverse.nextByClass(this._mediaManagerMediaList!, "info") as HTMLElement;
 +
 +    if (this._media.size) {
 +      if (info) {
 +        DomUtil.hide(info);
 +      }
 +    } else {
 +      if (info === null) {
 +        info = document.createElement("p");
 +        info.className = "info";
 +        info.textContent = Language.get("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.
 +   */
 +  public addMedia(media: Media, listItem: HTMLLIElement): void {
 +    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.
 +   */
 +  public clipboardDeleteMedia(mediaIds: number[]): void {
 +    mediaIds.forEach((mediaId) => {
 +      this.removeMedia(~~mediaId);
 +    });
 +
 +    UiNotification.show();
 +  }
 +
 +  /**
 +   * Returns the id of the currently selected category or `0` if no category is selected.
 +   */
 +  public getCategoryId(): number {
 +    if (this._mediaCategorySelect) {
 +      return ~~this._mediaCategorySelect.value;
 +    }
 +
 +    return 0;
 +  }
 +
 +  /**
 +   * Returns the media manager dialog element.
 +   */
 +  getDialog(): HTMLElement {
 +    return UiDialog.getDialog(this)!.dialog;
 +  }
 +
 +  /**
 +   * Returns the mode of the media manager.
 +   */
 +  public getMode(): string {
 +    return "";
 +  }
 +
 +  /**
 +   * Returns the media manager option with the given name.
 +   */
 +  public getOption(name: string): any {
 +    if (this._options[name]) {
 +      return this._options[name];
 +    }
 +
 +    return null;
 +  }
 +
 +  /**
 +   * Removes a media file.
 +   */
 +  public removeMedia(mediaId: number): void {
 +    if (this._listItems.has(mediaId)) {
 +      // remove list item
 +      try {
 +        this._listItems.get(mediaId)!.remove();
 +      } catch (e) {
 +        // 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.
 +   */
 +  public resetMedia(): void {
 +    // 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: object, template: string, additionalData: SetMediaAdditionalData): void {
 +    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.
 +   */
 +  public setupMediaElement(media: Media, mediaElement: HTMLElement): void {
 +    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 = `
 +        <a>
 +          <span class="icon icon16 fa-pencil jsTooltip" title="${Language.get("wcf.global.button.edit")}"></span>
 +          <span class="invisible">${Language.get("wcf.global.button.edit")}</span>
 +        </a>`;
 +
 +      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(
 +        Language.get("wcf.media.delete.confirmMessage", {
 +          title: uuid,
 +        }),
 +      ).replace(uuid, StringUtil.escapeHTML(media.filename));
 +      buttons.appendChild(deleteButton);
 +
 +      deleteButton.innerHTML = `
 +        <a>
 +          <span class="icon icon16 fa-times jsTooltip" title="${Language.get("wcf.global.button.delete")}"></span>
 +          <span class="invisible">${Language.get("wcf.global.button.delete")}</span>
 +        </a>`;
 +    }
 +  }
 +}
 +
 +Core.enableLegacyInheritance(MediaManager);
 +
 +export = MediaManager;
index 48bfbfe5428d43620d12133a6f369f33adf72aa1,acd351382cef58ca073016ed96c423f97b1577e6..09b9fad80ce168685f5cdff35510daf86adf0e3e
  /**
   * Initializes modules required for media list view.
   *
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @module    WoltLabSuite/Core/Controller/Media/List
 + * @author  Matthias Schmidt
 + * @copyright 2001-2021 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  WoltLabSuite/Core/Controller/Media/List
   */
 -define([
 -              'Dom/ChangeListener',
 -              'EventHandler',
 -              'WoltLabSuite/Core/Controller/Clipboard',
 -              'WoltLabSuite/Core/Media/Clipboard',
 -              'WoltLabSuite/Core/Media/Editor',
 -              'WoltLabSuite/Core/Media/List/Upload'
 -      ],
 -      function(
 -              DomChangeListener,
 -              EventHandler,
 -              Clipboard,
 -              MediaClipboard,
 -              MediaEditor,
 -              MediaListUpload
 -      ) {
 -      "use strict";
 -      
 -      if (!COMPILER_TARGET_DEFAULT) {
 -              var Fake = function() {};
 -              Fake.prototype = {
 -                      init: function() {},
 -                      _addButtonEventListeners: function() {},
 -                      _deleteCallback: function() {},
 -                      _deleteMedia: function(mediaIds) {},
 -                      _edit: function() {}
 -              };
 -              return Fake;
 -      }
 -      
 -      var _mediaEditor;
 -      var _tableBody = elById('mediaListTableBody');
 -      var _clipboardObjectIds = [];
 -      var _upload;
 -      
 -      /**
 -       * @exports     WoltLabSuite/Core/Controller/Media/List
 -       */
 -      return {
 -              init: function(options) {
 -                      options = options || {};
 -                      _upload = new MediaListUpload('uploadButton', 'mediaListTableBody', {
 -                              categoryId: options.categoryId,
 -                              multiple: true,
 -                              elementTagSize: 48
 -                      });
 -                      
 -                      MediaClipboard.init(
 -                              'wcf\\acp\\page\\MediaListPage',
 -                              options.hasMarkedItems || false,
 -                              this
 -                      );
 -                      
 -                      EventHandler.add('com.woltlab.wcf.media.upload', 'removedErroneousUploadRow', this._deleteCallback.bind(this));
 -                      
 -                      var deleteAction = new WCF.Action.Delete('wcf\\data\\media\\MediaAction', '.jsMediaRow');
 -                      deleteAction.setCallback(this._deleteCallback);
 -                      
 -                      _mediaEditor = new MediaEditor({
 -                              _editorSuccess: function(media, oldCategoryId, closedEditorDialog = true) {
 -                                      if (media.categoryID != oldCategoryId || closedEditorDialog) {
 -                                              window.setTimeout(function() {
 -                                                      window.location.reload();
 -                                              }, 500);
 -                                      }
 -                              }
 -                      });
 -                      
 -                      this._addButtonEventListeners();
 -                      
 -                      DomChangeListener.add('WoltLabSuite/Core/Controller/Media/List', this._addButtonEventListeners.bind(this));
 -                      
 -                      EventHandler.add('com.woltlab.wcf.media.upload', 'success', this._openEditorAfterUpload.bind(this));
 -              },
 -              
 -              /**
 -               * Adds the `click` event listeners to the media edit icons in
 -               * new media table rows.
 -               */
 -              _addButtonEventListeners: function() {
 -                      var buttons = elByClass('jsMediaEditButton', _tableBody), button;
 -                      while (buttons.length) {
 -                              button = buttons[0];
 -                              button.classList.remove('jsMediaEditButton');
 -                              button.addEventListener(WCF_CLICK_EVENT, this._edit.bind(this));
 -                      }
 -              },
 -              
 -              /**
 -               * Is triggered after media files have been deleted using the delete icon.
 -               * 
 -               * @param       {int[]?}        objectIds
 -               */
 -              _deleteCallback: function(objectIds) {
 -                      var tableRowCount = elByTag('tr', _tableBody).length;
 -                      if (objectIds.length === undefined) {
 -                              if (!tableRowCount) {
 -                                      window.location.reload();
 -                              }
 -                      }
 -                      else if (objectIds.length === tableRowCount) {
 -                              // table is empty, reload page
 -                              window.location.reload();
 -                      }
 -                      else {
 -                              Clipboard.reload.bind(Clipboard)
 -                      }
 -              },
 -              
 -              /**
 -               * Is called when a media edit icon is clicked.
 -               * 
 -               * @param       {Event}         event
 -               */
 -              _edit: function(event) {
 -                      _mediaEditor.edit(elData(event.currentTarget, 'object-id'));
 -              },
 -              
 -              /**
 -               * Opens the media editor after uploading a single file.
 -               *
 -               * @param       {object}        data    upload event data
 -               * @since       5.2
 -               */
 -              _openEditorAfterUpload: function(data) {
 -                      if (data.upload === _upload && !data.isMultiFileUpload && !_upload.hasPendingUploads()) {
 -                              var 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.
 -               * 
 -               * @param       {int[]}         mediaIds        ids of deleted media files
 -               */
 -              clipboardDeleteMedia: function(mediaIds) {
 -                      var mediaRows = elByClass('jsMediaRow');
 -                      for (var i = 0; i < mediaRows.length; i++) {
 -                              var media = mediaRows[i];
 -                              var mediaID = ~~elData(elByClass('jsClipboardItem', media)[0], 'object-id');
 -                              
 -                              if (mediaIds.indexOf(mediaID) !== -1) {
 -                                      elRemove(media);
 -                                      i--;
 -                              }
 -                      }
 -                      
 -                      if (!mediaRows.length) {
 -                              window.location.reload();
 -                      }
 -              }
 -      }
 -});
 +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 = void 0;
 +    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) => {
-             if (media.categoryID != oldCategoryId) {
++        _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));
 +    }
 +    exports.init = init;
 +    /**
 +     * 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();
 +        }
 +    }
 +});
index 32c912021ca422088b3a518aa6533873af56365c,1f7f7a32930324931e8f43355db7bbde3fffa977..328c6974780bb9dbc88d9ebc47ea5b2f670b38d7
  /**
   * Handles editing media files via dialog.
 - * 
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @module    WoltLabSuite/Core/Media/Editor
 + *
 + * @author  Matthias Schmidt
 + * @copyright 2001-2021 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  WoltLabSuite/Core/Media/Editor
   */
 -define(
 -      [
 -              'Ajax',
 -              'Core',
 -              'Dictionary',
 -              'Dom/ChangeListener',
 -              'Dom/Traverse',
 -              'Dom/Util',
 -              'Language',
 -              'Ui/Dialog',
 -              'Ui/Notification',
 -              'WoltLabSuite/Core/Language/Chooser',
 -              'WoltLabSuite/Core/Language/Input',
 -              'EventKey',
 -              'WoltLabSuite/Core/Media/Replace'
 -      ],
 -      function(
 -              Ajax,
 -              Core,
 -              Dictionary,
 -              DomChangeListener,
 -              DomTraverse,
 -              DomUtil,
 -              Language,
 -              UiDialog,
 -              UiNotification,
 -              LanguageChooser,
 -              LanguageInput,
 -              EventKey,
 -              MediaReplace
 -      )
 -{
 -      "use strict";
 -      
 -      if (!COMPILER_TARGET_DEFAULT) {
 -              var Fake = function() {};
 -              Fake.prototype = {
 -                      _ajaxSetup: function() {},
 -                      _ajaxSuccess: function() {},
 -                      _close: function() {},
 -                      _keyPress: function() {},
 -                      _saveData: function() {},
 -                      _updateLanguageFields: function() {},
 -                      edit: function() {}
 -              };
 -              return Fake;
 -      }
 -      
 -      /**
 -       * @constructor
 -       */
 -      function MediaEditor(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'.");
 -              }
 -              
 -              this._media = null;
 -              this._availableLanguageCount = 1;
 -              this._categoryIds = [];
 -              this._oldCategoryId = 0;
 -              
 -              this._dialogs = new Dictionary();
 -      }
 -      MediaEditor.prototype = {
 -              /**
 -               * Returns the data for Ajax to setup the Ajax/Request object.
 -               * 
 -               * @return      {object}        setup data for Ajax/Request object
 -               */
 -              _ajaxSetup: function() {
 -                      return {
 -                              data: {
 -                                      actionName: 'update',
 -                                      className: 'wcf\\data\\media\\MediaAction'
 -                              }
 -                      };
 -              },
 -              
 -              /**
 -               * Handles successful AJAX requests.
 -               * 
 -               * @param       {object}        data    response data
 -               */
 -              _ajaxSuccess: function(data) {
 -                      UiNotification.show();
 -                      
 -                      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: function() {
 -                      this._media = null;
 -                      
 -                      if (this._callbackObject._editorClose) {
 -                              this._callbackObject._editorClose();
 -                      }
 -              },
 -              
 -              /**
 -               * Initializes the editor dialog.
 -               * 
 -               * @param       {HTMLElement}           content
 -               * @param       {object}                data
 -               * @since       5.3
 -               */
 -              _initEditor: function(content, data) {
 -                      this._availableLanguageCount = ~~data.returnValues.availableLanguageCount;
 -                      this._categoryIds = data.returnValues.categoryIDs.map(function(number) {
 -                              return ~~number;
 -                      });
 -                      
 -                      var didLoadMediaData = false;
 -                      if (data.returnValues.mediaData) {
 -                              this._media = data.returnValues.mediaData;
 -                              
 -                              didLoadMediaData = true;
 -                      }
 -                      
 -                      // make sure that the language chooser is initialized first
 -                      setTimeout(function() {
 -                              if (this._availableLanguageCount > 1) {
 -                                      LanguageChooser.setLanguageId('mediaEditor_' + this._media.mediaID + '_languageID', this._media.languageID || LANGUAGE_ID);
 -                              }
 -                              
 -                              if (this._categoryIds.length) {
 -                                      elBySel('select[name=categoryID]', content).value = ~~this._media.categoryID;
 -                              }
 -                              
 -                              var title = elBySel('input[name=title]', content);
 -                              var altText = elBySel('input[name=altText]', content);
 -                              var caption = elBySel('textarea[name=caption]', content);
 -                              
 -                              if (this._availableLanguageCount > 1 && this._media.isMultilingual) {
 -                                      if (elById('altText_' + this._media.mediaID)) LanguageInput.setValues('altText_' + this._media.mediaID, Dictionary.fromObject(this._media.altText || { }));
 -                                      if (elById('caption_' + this._media.mediaID)) LanguageInput.setValues('caption_' + this._media.mediaID, Dictionary.fromObject(this._media.caption || { }));
 -                                      LanguageInput.setValues('title_' + this._media.mediaID, Dictionary.fromObject(this._media.title || { }));
 -                              }
 -                              else {
 -                                      title.value = this._media.title ? this._media.title[this._media.languageID || LANGUAGE_ID] : '';
 -                                      if (altText) altText.value = this._media.altText ? this._media.altText[this._media.languageID || LANGUAGE_ID] : '';
 -                                      if (caption) caption.value = this._media.caption ? this._media.caption[this._media.languageID || LANGUAGE_ID] : '';
 -                              }
 -                              
 -                              if (this._availableLanguageCount > 1) {
 -                                      var isMultilingual = elBySel('input[name=isMultilingual]', content);
 -                                      isMultilingual.addEventListener('change', this._updateLanguageFields.bind(this));
 -                                      
 -                                      this._updateLanguageFields(null, isMultilingual);
 -                              }
 -                              
 -                              var keyPress = this._keyPress.bind(this);
 -                              if (altText) altText.addEventListener('keypress', keyPress);
 -                              title.addEventListener('keypress', keyPress);
 -                              
 -                              elBySel('button[data-type=submit]', content).addEventListener(WCF_CLICK_EVENT, this._saveData.bind(this));
 -                              
 -                              // remove focus from input elements and scroll dialog to top
 -                              document.activeElement.blur();
 -                              elById('mediaEditor_' + this._media.mediaID).parentNode.scrollTop = 0;
 -                              
 -                              // Initialize button to replace media file.
 -                              var uploadButton = elByClass('mediaManagerMediaReplaceButton', content)[0];
 -                              var target = elByClass('mediaThumbnail', content)[0];
 -                              if (!target) {
 -                                      target = elCreate('div');
 -                                      content.appendChild(target);
 -                              }
 -                              new MediaReplace(
 -                                      this._media.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
 -                                      }
 -                              );
 -                              
 -                              DomChangeListener.trigger();
 -                      }.bind(this), 200);
 -              },
 -              
 -              /**
 -               * Handles the `[ENTER]` key to submit the form.
 -               * 
 -               * @param       {object}        event           event object
 -               */
 -              _keyPress: function(event) {
 -                      if (EventKey.Enter(event)) {
 -                              event.preventDefault();
 -                              
 -                              this._saveData();
 -                      }
 -              },
 -              
 -              /**
 -               * Saves the data of the currently edited media.
 -               */
 -              _saveData: function() {
 -                      var content = UiDialog.getDialog('mediaEditor_' + this._media.mediaID).content;
 -                      
 -                      var categoryId = elBySel('select[name=categoryID]', content);
 -                      var altText = elBySel('input[name=altText]', content);
 -                      var caption = elBySel('textarea[name=caption]', content);
 -                      var captionEnableHtml = elBySel('input[name=captionEnableHtml]', content);
 -                      var title = elBySel('input[name=title]', content);
 -                      
 -                      var hasError = false;
 -                      var altTextError = (altText ? DomTraverse.childByClass(altText.parentNode.parentNode, 'innerError') : false);
 -                      var captionError = (caption ? DomTraverse.childByClass(caption.parentNode.parentNode, 'innerError') : false);
 -                      var titleError = DomTraverse.childByClass(title.parentNode.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) {
 -                              this._media.isMultilingual = ~~elBySel('input[name=isMultilingual]', content).checked;
 -                              this._media.languageID = this._media.isMultilingual ? null : LanguageChooser.getLanguageId('mediaEditor_' + this._media.mediaID + '_languageID');
 -                      }
 -                      else {
 -                              this._media.languageID = LANGUAGE_ID;
 -                      }
 -                      
 -                      // altText, caption and title
 -                      this._media.altText = {};
 -                      this._media.caption = {};
 -                      this._media.title = {};
 -                      if (this._availableLanguageCount > 1 && this._media.isMultilingual) {
 -                              if (elById('altText_' + this._media.mediaID) && !LanguageInput.validate('altText_' + this._media.mediaID, true)) {
 -                                      hasError = true;
 -                                      if (!altTextError) {
 -                                              var error = elCreate('small');
 -                                              error.className = 'innerError';
 -                                              error.textContent = Language.get('wcf.global.form.error.multilingual');
 -                                              altText.parentNode.parentNode.appendChild(error);
 -                                      }
 -                              }
 -                              if (elById('caption_' + this._media.mediaID) && !LanguageInput.validate('caption_' + this._media.mediaID, true)) {
 -                                      hasError = true;
 -                                      if (!captionError) {
 -                                              var error = elCreate('small');
 -                                              error.className = 'innerError';
 -                                              error.textContent = Language.get('wcf.global.form.error.multilingual');
 -                                              caption.parentNode.parentNode.appendChild(error);
 -                                      }
 -                              }
 -                              if (!LanguageInput.validate('title_' + this._media.mediaID, true)) {
 -                                      hasError = true;
 -                                      if (!titleError) {
 -                                              var error = elCreate('small');
 -                                              error.className = 'innerError';
 -                                              error.textContent = Language.get('wcf.global.form.error.multilingual');
 -                                              title.parentNode.parentNode.appendChild(error);
 -                                      }
 -                              }
 -                              
 -                              this._media.altText = (elById('altText_' + this._media.mediaID) ? LanguageInput.getValues('altText_' + this._media.mediaID).toObject() : '');
 -                              this._media.caption = (elById('caption_' + this._media.mediaID) ? LanguageInput.getValues('caption_' + this._media.mediaID).toObject() : '');
 -                              this._media.title = LanguageInput.getValues('title_' + this._media.mediaID).toObject();
 -                      }
 -                      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;
 -                      
 -                      var aclValues = {
 -                              allowAll: ~~elById('mediaEditor_' + this._media.mediaID + '_aclAllowAll').checked,
 -                              group: [],
 -                              user: []
 -                      };
 -                      
 -                      var aclGroups = elBySelAll('input[name="mediaEditor_' + this._media.mediaID + '_aclValues[group][]"]', content);
 -                      for (var i = 0, length = aclGroups.length; i < length; i++) {
 -                              aclValues.group.push(~~aclGroups[i].value);
 -                      }
 -                      
 -                      var aclUsers = elBySelAll('input[name="mediaEditor_' + this._media.mediaID + '_aclValues[user][]"]', content);
 -                      for (var i = 0, length = aclUsers.length; i < length; i++) {
 -                              aclValues.user.push(~~aclUsers[i].value);
 -                      }
 -                      
 -                      if (!hasError) {
 -                              if (altTextError) elRemove(altTextError);
 -                              if (captionError) elRemove(captionError);
 -                              if (titleError) elRemove(titleError);
 -                              
 -                              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
 -                                      }
 -                              });
 -                      }
 -              },
 -              
 -              /**
 -               * Updates language-related input fields depending on whether multilingualism
 -               * is enabled.
 -               */
 -              _updateLanguageFields: function(event, element) {
 -                      if (event) element = event.currentTarget;
 -                      
 -                      var languageChooserContainer = elById('mediaEditor_' + this._media.mediaID + '_languageIDContainer').parentNode;
 -                      
 -                      if (element.checked) {
 -                              LanguageInput.enable('title_' + this._media.mediaID);
 -                              if (elById('caption_' + this._media.mediaID)) LanguageInput.enable('caption_' + this._media.mediaID);
 -                              if (elById('altText_' + this._media.mediaID)) LanguageInput.enable('altText_' + this._media.mediaID);
 -                              
 -                              elHide(languageChooserContainer);
 -                      }
 -                      else {
 -                              LanguageInput.disable('title_' + this._media.mediaID);
 -                              if (elById('caption_' + this._media.mediaID)) LanguageInput.disable('caption_' + this._media.mediaID);
 -                              if (elById('altText_' + this._media.mediaID)) LanguageInput.disable('altText_' + this._media.mediaID);
 -                              
 -                              elShow(languageChooserContainer);
 -                      }
 -              },
 -              
 -              /**
 -               * Edits the media with the given data.
 -               * 
 -               * @param       {object|integer}        media           data of the edited media or media id for which the data will be loaded
 -               */
 -              edit: function(media) {
 -                      if (typeof media !== 'object') {
 -                              media = {
 -                                      mediaID: ~~media
 -                              };
 -                      }
 -                      
 -                      if (this._media !== null) {
 -                              throw new Error("Cannot edit media with id '" + media.mediaID + "' while editing media with id '" + this._media.mediaID + "'");
 -                      }
 -                      
 -                      this._media = media;
 -                      
 -                      if (!this._dialogs.has('mediaEditor_' + media.mediaID)) {
 -                              this._dialogs.set('mediaEditor_' + media.mediaID, {
 -                                      _dialogSetup: function() {
 -                                              return {
 -                                                      id: 'mediaEditor_' + media.mediaID,
 -                                                      options: {
 -                                                              backdropCloseOnClick: false,
 -                                                              onClose: this._close.bind(this),
 -                                                              title: Language.get('wcf.media.edit')
 -                                                      },
 -                                                      source: {
 -                                                              after: this._initEditor.bind(this),
 -                                                              data: {
 -                                                                      actionName: 'getEditorDialog',
 -                                                                      className: 'wcf\\data\\media\\MediaAction',
 -                                                                      objectIDs: [media.mediaID]
 -                                                              }
 -                                                      }
 -                                              };
 -                                      }.bind(this)
 -                              });
 -                      }
 -                      
 -                      UiDialog.open(this._dialogs.get('mediaEditor_' + media.mediaID));
 -              },
 -              
 -              /**
 -               * Updates the data of the currently edited media file.
 -               * 
 -               * @param       {object}        data
 -               * @since       5.3
 -               */
 -              updateData: function(data) {
 -                      if (this._callbackObject._editorSuccess) {
 -                              this._callbackObject._editorSuccess(data, undefined, false);
 -                      }
 -              }
 -      };
 -      
 -      return MediaEditor;
 +define(["require", "exports", "tslib", "../Core", "../Ui/Notification", "../Ui/Dialog", "../Language/Chooser", "../Language/Input", "../Dom/Util", "../Dom/Traverse", "../Dom/Change/Listener", "../Language", "../Ajax", "./Replace"], function (require, exports, tslib_1, Core, UiNotification, UiDialog, LanguageChooser, LanguageInput, DomUtil, DomTraverse, Listener_1, Language, Ajax, Replace_1) {
 +    "use strict";
 +    Core = tslib_1.__importStar(Core);
 +    UiNotification = tslib_1.__importStar(UiNotification);
 +    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);
 +    Language = tslib_1.__importStar(Language);
 +    Ajax = tslib_1.__importStar(Ajax);
 +    Replace_1 = tslib_1.__importDefault(Replace_1);
 +    class MediaEditor {
 +        constructor(callbackObject) {
 +            this._availableLanguageCount = 1;
 +            this._categoryIds = [];
 +            this._dialogs = new Map();
 +            this._media = null;
 +            this._oldCategoryId = 0;
 +            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() {
 +            UiNotification.show();
 +            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, Language.get("wcf.global.form.error.multilingual"));
 +                    }
 +                }
 +                if (caption && !LanguageInput.validate(caption.id, true)) {
 +                    hasError = true;
 +                    if (!captionError) {
 +                        DomUtil.innerError(caption, Language.get("wcf.global.form.error.multilingual"));
 +                    }
 +                }
 +                if (!LanguageInput.validate(title.id, true)) {
 +                    hasError = true;
 +                    if (!titleError) {
 +                        DomUtil.innerError(title, Language.get("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: Language.get("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);
++                this._callbackObject._editorSuccess(media, undefined, false);
 +            }
 +        }
 +    }
 +    Core.enableLegacyInheritance(MediaEditor);
 +    return MediaEditor;
  });
index 2f1858f746394b77de1720892a760cfe0bf56fd6,7e05686744d495ec55ce5455e6de84ff2b52aa62..23701abede8cec5f6ff86933e6a47958ec98795c
  /**
   * Provides the media manager dialog.
 - * 
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @module    WoltLabSuite/Core/Media/Manager/Base
 + *
 + * @author  Matthias Schmidt
 + * @copyright 2001-2020 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module  WoltLabSuite/Core/Media/Manager/Base
   */
 -define(
 -      [
 -              'Core',                     'Dictionary',               'Dom/ChangeListener',              'Dom/Traverse',
 -              'Dom/Util',                 'EventHandler',             'Language',                        'List',
 -              'Permission',               'Ui/Dialog',                'Ui/Notification',                 'WoltLabSuite/Core/Controller/Clipboard',
 -              'WoltLabSuite/Core/Media/Editor', 'WoltLabSuite/Core/Media/Upload', 'WoltLabSuite/Core/Media/Manager/Search', 'StringUtil',
 -              'WoltLabSuite/Core/Ui/Pagination',
 -              'WoltLabSuite/Core/Media/Clipboard'
 -      ],
 -      function(
 -              Core,                        Dictionary,                 DomChangeListener,                 DomTraverse,
 -              DomUtil,                     EventHandler,               Language,                          List,
 -              Permission,                  UiDialog,                   UiNotification,                    Clipboard,
 -              MediaEditor,                 MediaUpload,                MediaManagerSearch,                StringUtil,
 -              UiPagination,
 -              MediaClipboard
 -      )
 -{
 -      "use strict";
 -      
 -      if (!COMPILER_TARGET_DEFAULT) {
 -              var Fake = function() {};
 -              Fake.prototype = {
 -                      _addButtonEventListeners: function() {},
 -                      _click: function() {},
 -                      _dialogClose: function() {},
 -                      _dialogInit: function() {},
 -                      _dialogSetup: function() {},
 -                      _dialogShow: function() {},
 -                      _editMedia: function() {},
 -                      _editorClose: function() {},
 -                      _editorSuccess: function() {},
 -                      _removeClipboardCheckboxes: function() {},
 -                      _setMedia: function() {},
 -                      addMedia: function() {},
 -                      clipboardDeleteMedia: function() {},
 -                      getDialog: function() {},
 -                      getMode: function() {},
 -                      getOption: function() {},
 -                      removeMedia: function() {},
 -                      resetMedia: function() {},
 -                      setMedia: function() {},
 -                      setupMediaElement: function() {}
 -              };
 -              return Fake;
 -      }
 -      
 -      var _mediaManagerCounter = 0;
 -      
 -      /**
 -       * @constructor
 -       */
 -      function MediaManagerBase(options) {
 -              this._options = Core.extend({
 -                      dialogTitle: Language.get('wcf.media.manager'),
 -                      imagesOnly: false,
 -                      minSearchLength: 3
 -              }, options);
 -              
 -              this._id = 'mediaManager' + _mediaManagerCounter++;
 -              this._listItems = new Dictionary();
 -              this._media = new Dictionary();
 -              this._mediaManagerMediaList = null;
 -              this._search = null;
 -              this._upload = null;
 -              this._forceClipboard = false;
 -              this._hadInitiallyMarkedItems = false;
 -              this._pagination = null;
 -              
 -              if (Permission.get('admin.content.cms.canManageMedia')) {
 -                      this._mediaEditor = new MediaEditor(this);
 -              }
 -              
 -              DomChangeListener.add('WoltLabSuite/Core/Media/Manager', this._addButtonEventListeners.bind(this));
 -              
 -              EventHandler.add('com.woltlab.wcf.media.upload', 'success', this._openEditorAfterUpload.bind(this));
 -      }
 -      MediaManagerBase.prototype = {
 -              /**
 -               * Adds click event listeners to media buttons.
 -               */
 -              _addButtonEventListeners: function() {
 -                      if (!this._mediaManagerMediaList) return;
 -                      
 -                      var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
 -                      for (var i = 0, length = listItems.length; i < length; i++) {
 -                              var listItem = listItems[i];
 -                              
 -                              if (Permission.get('admin.content.cms.canManageMedia')) {
 -                                      var editIcon = elByClass('jsMediaEditButton', listItem)[0];
 -                                      if (editIcon) {
 -                                              editIcon.classList.remove('jsMediaEditButton');
 -                                              editIcon.addEventListener(WCF_CLICK_EVENT, this._editMedia.bind(this));
 -                                      }
 -                              }
 -                      }
 -              },
 -              
 -              /**
 -               * Is called when a new category is selected.
 -               */
 -              _categoryChange: function() {
 -                      this._search.search();
 -              },
 -              
 -              /**
 -               * Handles clicks on the media manager button.
 -               * 
 -               * @param       {object}        event   event object
 -               */
 -              _click: function(event) {
 -                      event.preventDefault();
 -                      
 -                      UiDialog.open(this);
 -              },
 -              
 -              /**
 -               * Is called if the media manager dialog is closed.
 -               */
 -              _dialogClose: function() {
 -                      // 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.
 -               *
 -               * @param       {string}        content         dialog content
 -               * @param       {object}        data            AJAX request's response data
 -               */
 -              _dialogInit: function(content, data) {
 -                      // store media data locally
 -                      var media = data.returnValues.media || { };
 -                      for (var mediaId in media) {
 -                              if (objOwns(media, mediaId)) {
 -                                      this._media.set(~~mediaId, media[mediaId]);
 -                              }
 -                      }
 -                      
 -                      this._initPagination(~~data.returnValues.pageCount);
 -                      
 -                      this._hadInitiallyMarkedItems = data.returnValues.hasMarkedItems;
 -              },
 -              
 -              /**
 -               * Returns all data to setup the media manager dialog.
 -               * 
 -               * @return      {object}        dialog setup data
 -               */
 -              _dialogSetup: function() {
 -                      return {
 -                              id: this._id,
 -                              options: {
 -                                      onClose: this._dialogClose.bind(this),
 -                                      onShow: this._dialogShow.bind(this),
 -                                      title: this._options.dialogTitle
 -                              },
 -                              source: {
 -                                      after: this._dialogInit.bind(this),
 -                                      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: function() {
 -                      if (!this._mediaManagerMediaList) {
 -                              var dialog = this.getDialog();
 -                              
 -                              this._mediaManagerMediaList = elByClass('mediaManagerMediaList', dialog)[0];
 -                              
 -                              this._mediaCategorySelect = elBySel('.mediaManagerCategoryList > select', dialog);
 -                              if (this._mediaCategorySelect) {
 -                                      this._mediaCategorySelect.addEventListener('change', this._categoryChange.bind(this));
 -                              }
 -                              
 -                              // store list items locally
 -                              var listItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
 -                              for (var i = 0, length = listItems.length; i < length; i++) {
 -                                      var listItem = listItems[i];
 -                                      
 -                                      this._listItems.set(~~elData(listItem, 'object-id'), listItem);
 -                              }
 -                              
 -                              if (Permission.get('admin.content.cms.canManageMedia')) {
 -                                      var uploadButton = elByClass('mediaManagerMediaUploadButton', UiDialog.getDialog(this).dialog)[0];
 -                                      this._upload = new MediaUpload(DomUtil.identify(uploadButton), DomUtil.identify(this._mediaManagerMediaList), {
 -                                              mediaManager: this
 -                                      });
 -                                      
 -                                      var deleteAction = new WCF.Action.Delete('wcf\\data\\media\\MediaAction', '.mediaFile');
 -                                      deleteAction._didTriggerEffect = function(element) {
 -                                              this.removeMedia(elData(element[0], 'object-id'));
 -                                      }.bind(this);
 -                              }
 -                              
 -                              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 MediaManagerSearch(this);
 -                              
 -                              if (!listItems.length) {
 -                                      this._search.hideSearch();
 -                              }
 -                      }
 -                      
 -                      // only show media clipboard if editor is open
 -                      if (Permission.get('admin.content.cms.canManageMedia') || this._forceClipboard) {
 -                              Clipboard.showEditor('com.woltlab.wcf.media');
 -                      }
 -              },
 -              
 -              /**
 -               * Opens the media editor for a media file.
 -               * 
 -               * @param       {Event}         event           event object for clicks on edit icons
 -               */
 -              _editMedia: function(event) {
 -                      if (!Permission.get('admin.content.cms.canManageMedia')) {
 -                              throw new Error("You are not allowed to edit media files.");
 -                      }
 -                      
 -                      UiDialog.close(this);
 -                      
 -                      this._mediaEditor.edit(this._media.get(~~elData(event.currentTarget, 'object-id')));
 -              },
 -              
 -              /**
 -               * Re-opens the manager dialog after closing the editor dialog.
 -               */
 -              _editorClose: function() {
 -                      UiDialog.open(this);
 -              },
 -              
 -              /**
 -               * Re-opens the manager dialog and updates the media data after
 -               * successfully editing a media file.
 -               * 
 -               * @param       {object}        media           updated media file data
 -               * @param       {integer}       oldCategoryId   old category id
 -               * @param       {boolean}       closedEditorDialog
 -               */
 -              _editorSuccess: function(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) {
 -                              var selectedCategoryId = ~~this._mediaCategorySelect.value;
 -                              
 -                              if (selectedCategoryId) {
 -                                      var newCategoryId = ~~media.categoryID;
 -                                      
 -                                      if (oldCategoryId != newCategoryId && (oldCategoryId == selectedCategoryId || newCategoryId == selectedCategoryId)) {
 -                                              this._search.search();
 -                                      }
 -                              }
 -                      }
 -                      
 -                      if (closedEditorDialog) {
 -                              UiDialog.open(this);
 -                      }
 -                      
 -                      this._media.set(~~media.mediaID, media);
 -                      
 -                      var listItem = this._listItems.get(~~media.mediaID);
 -                      var p = elByClass('mediaTitle', listItem)[0];
 -                      if (media.isMultilingual) {
 -                              if (media.title && media.title[LANGUAGE_ID]) {
 -                                      p.textContent = media.title[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;
 -                              }
 -                      }
 -                      
 -                      var thumbnail = elByClass('mediaThumbnail', listItem)[0];
 -                      thumbnail.innerHTML = media.elementTag;
 -                      // Bust browser cache by adding additional parameter.
 -                      var imgs = elByTag('img', thumbnail);
 -                      if (imgs.length) {
 -                              imgs[0].src += '&refresh=' + Date.now();
 -                      }
 -              },
 -              
 -              /**
 -               * Initializes the dialog pagination.
 -               *
 -               * @param       {integer}       pageCount
 -               * @param       {integer}       pageNo
 -               */
 -              _initPagination: function(pageCount, pageNo) {
 -                      if (pageNo === undefined) pageNo = 1;
 -                      
 -                      if (pageCount > 1) {
 -                              var newPagination = elCreate('div');
 -                              newPagination.className = 'paginationBottom jsPagination';
 -                              DomUtil.replaceElement(elBySel('.jsPagination', UiDialog.getDialog(this).content), newPagination);
 -                              
 -                              this._pagination = new UiPagination(newPagination, {
 -                                      activePage: pageNo,
 -                                      callbackSwitch: this._search.search.bind(this._search),
 -                                      maxPage: pageCount
 -                              });
 -                      }
 -                      else if (this._pagination) {
 -                              elHide(this._pagination.getElement());
 -                      }
 -              },
 -              
 -              /**
 -               * Removes all media clipboard checkboxes.
 -               */
 -              _removeClipboardCheckboxes: function() {
 -                      var checkboxes = elByClass('mediaCheckbox', this._mediaManagerMediaList);
 -                      while (checkboxes.length) {
 -                              elRemove(checkboxes[0]);
 -                      }
 -              },
 -              
 -              /**
 -               * Opens the media editor after uploading a single file.
 -               * 
 -               * @param       {object}        data    upload event data
 -               * @since       5.2
 -               */
 -              _openEditorAfterUpload: function(data) {
 -                      if (data.upload === this._upload && !data.isMultiFileUpload && !this._upload.hasPendingUploads()) {
 -                              var 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).
 -               * 
 -               * @param       {Dictionary}    media           media to be set as active
 -               */
 -              _setMedia: function(media) {
 -                      if (Core.isPlainObject(media)) {
 -                              this._media = Dictionary.fromObject(media);
 -                      }
 -                      else {
 -                              this._media = media;
 -                      }
 -                      
 -                      var info = DomTraverse.nextByClass(this._mediaManagerMediaList, 'info');
 -                      
 -                      if (this._media.size) {
 -                              if (info) {
 -                                      elHide(info);
 -                              }
 -                      }
 -                      else {
 -                              if (info === null) {
 -                                      info = elCreate('p');
 -                                      info.className = 'info';
 -                                      info.textContent = Language.get('wcf.media.search.noResults');
 -                              }
 -                              
 -                              elShow(info);
 -                              DomUtil.insertAfter(info, this._mediaManagerMediaList);
 -                      }
 -                      
 -                      var mediaListItems = DomTraverse.childrenByTag(this._mediaManagerMediaList, 'LI');
 -                      for (var i = 0, length = mediaListItems.length; i < length; i++) {
 -                              var listItem = mediaListItems[i];
 -                              
 -                              if (!this._media.has(elData(listItem, 'object-id'))) {
 -                                      elHide(listItem);
 -                              }
 -                              else {
 -                                      elShow(listItem);
 -                              }
 -                      }
 -                      
 -                      DomChangeListener.trigger();
 -                      
 -                      if (Permission.get('admin.content.cms.canManageMedia') || this._forceClipboard) {
 -                              Clipboard.reload();
 -                      }
 -                      else {
 -                              this._removeClipboardCheckboxes();
 -                      }
 -              },
 -              
 -              /**
 -               * Adds a media file to the manager.
 -               * 
 -               * @param       {object}        media           data of the media file
 -               * @param       {Element}       listItem        list item representing the file
 -               */
 -              addMedia: function(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.
 -               * 
 -               * @param       {int[]}         mediaIds        ids of deleted media files
 -               */
 -              clipboardDeleteMedia: function(mediaIds) {
 -                      for (var i = 0, length = mediaIds.length; i < length; i++) {
 -                              this.removeMedia(~~mediaIds[i], true);
 -                      }
 -                      
 -                      UiNotification.show();
 -              },
 -              
 -              /**
 -               * Returns the id of the currently selected category or `0` if no category is selected.
 -               * 
 -               * @return      {integer}
 -               */
 -              getCategoryId: function() {
 -                      if (this._mediaCategorySelect) {
 -                              return this._mediaCategorySelect.value;
 -                      }
 -                      
 -                      return 0;
 -              },
 -              
 -              /**
 -               * Returns the media manager dialog element.
 -               * 
 -               * @return      {Element}       media manager dialog
 -               */
 -              getDialog: function() {
 -                      return UiDialog.getDialog(this).dialog;
 -              },
 -              
 -              /**
 -               * Returns the mode of the media manager.
 -               *
 -               * @return      {string}
 -               */
 -              getMode: function() {
 -                      return '';
 -              },
 -              
 -              /**
 -               * Returns the media manager option with the given name.
 -               * 
 -               * @param       {string}        name            option name
 -               * @return      {mixed}         option value or null
 -               */
 -              getOption: function(name) {
 -                      if (this._options[name]) {
 -                              return this._options[name];
 -                      }
 -                      
 -                      return null;
 -              },
 -              
 -              /**
 -               * Removes a media file.
 -               *
 -               * @param       {int}                   mediaId         id of the removed media file
 -               */
 -              removeMedia: function(mediaId) {
 -                      if (this._listItems.has(mediaId)) {
 -                              // remove list item
 -                              try {
 -                                      elRemove(this._listItems.get(mediaId));
 -                              }
 -                              catch (e) {
 -                                      // ignore errors if item has already been removed like by WCF.Action.Delete
 -                              }
 -                              
 -                              this._listItems.delete(mediaId);
 -                              this._media.delete(mediaId);
 -                      }
 -              },
 -              
 -              /**
 -               * Changes the displayed media to the previously displayed media.
 -               */
 -              resetMedia: function() {
 -                      // calling WoltLabSuite/Core/Media/Manager/Search.search() reloads the first page of the dialog
 -                      this._search.search();
 -              },
 -              
 -              /**
 -               * Sets the media files currently displayed.
 -               * 
 -               * @param       {object}        media           media data
 -               * @param       {string}        template        
 -               * @param       {object}        additionalData
 -               */
 -              setMedia: function(media, template, additionalData) {
 -                      var hasMedia = false;
 -                      for (var mediaId in media) {
 -                              if (objOwns(media, mediaId)) {
 -                                      hasMedia = true;
 -                              }
 -                      }
 -                      
 -                      var newListItems = [];
 -                      if (hasMedia) {
 -                              var ul = elCreate('ul');
 -                              ul.innerHTML = template;
 -                              
 -                              var listItems = DomTraverse.childrenByTag(ul, 'LI');
 -                              for (var i = 0, length = listItems.length; i < length; i++) {
 -                                      var listItem = listItems[i];
 -                                      if (!this._listItems.has(~~elData(listItem, 'object-id'))) {
 -                                              this._listItems.set(elData(listItem, 'object-id'), listItem);
 -                                              
 -                                              this._mediaManagerMediaList.appendChild(listItem);
 -                                      }
 -                              }
 -                      }
 -                      
 -                      this._initPagination(additionalData.pageCount, additionalData.pageNo);
 -                      
 -                      this._setMedia(media);
 -              },
 -              
 -              /**
 -               * Sets up a new media element.
 -               * 
 -               * @param       {object}        media           data of the media file
 -               * @param       {HTMLElement}   mediaElement    element representing the media file
 -               */
 -              setupMediaElement: function(media, mediaElement) {
 -                      var mediaInformation = DomTraverse.childByClass(mediaElement, 'mediaInformation');
 -                      
 -                      var buttonGroupNavigation = elCreate('nav');
 -                      buttonGroupNavigation.className = 'jsMobileNavigation buttonGroupNavigation';
 -                      mediaInformation.parentNode.appendChild(buttonGroupNavigation);
 -                      
 -                      var buttons = elCreate('ul');
 -                      buttons.className = 'buttonList iconList';
 -                      buttonGroupNavigation.appendChild(buttons);
 -                      
 -                      var listItem = elCreate('li');
 -                      listItem.className = 'mediaCheckbox';
 -                      buttons.appendChild(listItem);
 -                      
 -                      var a = elCreate('a');
 -                      listItem.appendChild(a);
 -                      
 -                      var label = elCreate('label');
 -                      a.appendChild(label);
 -                      
 -                      var checkbox = elCreate('input');
 -                      checkbox.className = 'jsClipboardItem';
 -                      elAttr(checkbox, 'type', 'checkbox');
 -                      elData(checkbox, 'object-id', media.mediaID);
 -                      label.appendChild(checkbox);
 -                      
 -                      if (Permission.get('admin.content.cms.canManageMedia')) {
 -                              listItem = elCreate('li');
 -                              listItem.className = 'jsMediaEditButton';
 -                              elData(listItem, 'object-id', media.mediaID);
 -                              buttons.appendChild(listItem);
 -                              
 -                              listItem.innerHTML = '<a><span class="icon icon16 fa-pencil jsTooltip" title="' + Language.get('wcf.global.button.edit') + '"></span> <span class="invisible">' + Language.get('wcf.global.button.edit') + '</span></a>';
 -                              
 -                              listItem = elCreate('li');
 -                              listItem.className = 'jsDeleteButton';
 -                              elData(listItem, 'object-id', media.mediaID);
 -                              
 -                              // use temporary title to not unescape html in filename
 -                              var uuid = Core.getUuid();
 -                              elData(listItem, 'confirm-message-html', StringUtil.unescapeHTML(Language.get('wcf.media.delete.confirmMessage', {
 -                                      title: uuid
 -                              })).replace(uuid, StringUtil.escapeHTML(media.filename)));
 -                              buttons.appendChild(listItem);
 -                              
 -                              listItem.innerHTML = '<a><span class="icon icon16 fa-times jsTooltip" title="' + Language.get('wcf.global.button.delete') + '"></span> <span class="invisible">' + Language.get('wcf.global.button.delete') + '</span></a>';
 -                      }
 -              }
 -      };
 -      
 -      return MediaManagerBase;
 +define(["require", "exports", "tslib", "../../Core", "../../Language", "../../Permission", "../../Dom/Change/Listener", "../../Event/Handler", "../../Dom/Traverse", "../../Dom/Util", "../../Ui/Dialog", "../../Controller/Clipboard", "../../Ui/Pagination", "../../Ui/Notification", "../../StringUtil", "./Search", "../Upload", "../Editor", "../Clipboard"], function (require, exports, tslib_1, Core, Language, Permission, DomChangeListener, EventHandler, DomTraverse, DomUtil, UiDialog, Clipboard, Pagination_1, UiNotification, StringUtil, Search_1, Upload_1, Editor_1, MediaClipboard) {
 +    "use strict";
 +    Core = tslib_1.__importStar(Core);
 +    Language = tslib_1.__importStar(Language);
 +    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);
 +    UiNotification = tslib_1.__importStar(UiNotification);
 +    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 {
 +        constructor(options) {
 +            this._forceClipboard = false;
 +            this._hadInitiallyMarkedItems = false;
 +            this._listItems = new Map();
 +            this._media = new Map();
 +            this._mediaEditor = null;
 +            this._mediaManagerMediaList = null;
 +            this._pagination = null;
 +            this._search = null;
 +            this._upload = null;
 +            this._options = Core.extend({
 +                dialogTitle: Language.get("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();
 +                }
 +            }
 +            // 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) {
++        _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();
 +                    }
 +                }
 +            }
-             UiDialog.open(this);
++            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";
 +                DomUtil.replaceElement(UiDialog.getDialog(this).content.querySelector(".jsPagination"), newPagination);
 +                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 = Language.get("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);
 +            });
 +            UiNotification.show();
 +        }
 +        /**
 +         * 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 (e) {
 +                    // 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 = `
 +        <a>
 +          <span class="icon icon16 fa-pencil jsTooltip" title="${Language.get("wcf.global.button.edit")}"></span>
 +          <span class="invisible">${Language.get("wcf.global.button.edit")}</span>
 +        </a>`;
 +                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(Language.get("wcf.media.delete.confirmMessage", {
 +                    title: uuid,
 +                })).replace(uuid, StringUtil.escapeHTML(media.filename));
 +                buttons.appendChild(deleteButton);
 +                deleteButton.innerHTML = `
 +        <a>
 +          <span class="icon icon16 fa-times jsTooltip" title="${Language.get("wcf.global.button.delete")}"></span>
 +          <span class="invisible">${Language.get("wcf.global.button.delete")}</span>
 +        </a>`;
 +            }
 +        }
 +    }
 +    Core.enableLegacyInheritance(MediaManager);
 +    return MediaManager;
  });
index 8569c59f946997689bdcded90b3879e3874c23d4,b8c7e935f5da8bb559daa3ea3345afa11164c3b4..78f042aa5269da372db183230b85ba7e597eeb2b
@@@ -25,795 -23,755 +25,799 @@@ use wcf\util\FileUtil
  
  /**
   * Executes media file-related actions.
 - * 
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\Data\Media
 - * @since     3.0
 - * 
 - * @method    Media           create()
 - * @method    MediaEditor[]   getObjects()
 - * @method    MediaEditor     getSingleObject()
 + *
 + * @author  Matthias Schmidt
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\Data\Media
 + * @since   3.0
 + *
 + * @method  Media       create()
 + * @method  MediaEditor[]   getObjects()
 + * @method  MediaEditor getSingleObject()
   */
 -class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, IUploadAction {
 -      /**
 -       * number of media files per media manager dialog page
 -       */
 -      const ITEMS_PER_MANAGER_DIALOG_PAGE = 50;
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function validateUpload() {
 -              WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 -              
 -              $this->readBoolean('imagesOnly', true);
 -              $this->readInteger('categoryID', true);
 -              
 -              /** @noinspection PhpUndefinedMethodInspection */
 -              $this->parameters['__files']->validateFiles(new MediaUploadFileValidationStrategy($this->parameters['imagesOnly']));
 -              
 -              if ($this->parameters['categoryID']) {
 -                      $category = CategoryHandler::getInstance()->getCategory($this->parameters['categoryID']);
 -                      if ($category === null || $category->getObjectType()->objectType !== 'com.woltlab.wcf.media.category') {
 -                              throw new UserInputException('categoryID');
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function upload() {
 -              $additionalData = ['username' => WCF::getUser()->username];
 -              if ($this->parameters['categoryID']) {
 -                      $additionalData['categoryID'] = $this->parameters['categoryID'];
 -              }
 -              
 -              // save files
 -              $saveStrategy = new DefaultUploadFileSaveStrategy(self::class, [
 -                      'generateThumbnails' => true,
 -                      'rotateImages' => true
 -              ], $additionalData);
 -              
 -              /** @noinspection PhpUndefinedMethodInspection */
 -              $this->parameters['__files']->saveFiles($saveStrategy);
 -              
 -              /** @var Media[] $mediaFiles */
 -              $mediaFiles = $saveStrategy->getObjects();
 -              
 -              $result = [
 -                      'errors' => [],
 -                      'media' => []
 -              ];
 -              
 -              if (!empty($mediaFiles)) {
 -                      $mediaIDs = $mediaToFileID = [];
 -                      foreach ($mediaFiles as $internalFileID => $media) {
 -                              $mediaIDs[] = $media->mediaID;
 -                              $mediaToFileID[$media->mediaID] = $internalFileID;
 -                      }
 -                      
 -                      // fetch media objects from database
 -                      $mediaList = new ViewableMediaList();
 -                      $mediaList->setObjectIDs($mediaIDs);
 -                      $mediaList->readObjects();
 -                      
 -                      foreach ($mediaList as $media) {
 -                              $result['media'][$mediaToFileID[$media->mediaID]] = $this->getMediaData($media);
 -                      }
 -              }
 -              
 -              /** @var UploadFile[] $files */
 -              /** @noinspection PhpUndefinedMethodInspection */
 -              $files = $this->parameters['__files']->getFiles();
 -              foreach ($files as $file) {
 -                      if ($file->getValidationErrorType()) {
 -                              $result['errors'][$file->getInternalFileID()] = [
 -                                      'filename' => $file->getFilename(),
 -                                      'filesize' => $file->getFilesize(),
 -                                      'errorType' => $file->getValidationErrorType()
 -                              ];
 -                      }
 -              }
 -              
 -              return $result;
 -      }
 -      
 -      /**
 -       * Generates thumbnails.
 -       */
 -      public function generateThumbnails() {
 -              if (empty($this->objects)) {
 -                      $this->readObjects();
 -              }
 -              
 -              $saveStrategy = new DefaultUploadFileSaveStrategy(self::class);
 -              
 -              foreach ($this->getObjects() as $mediaEditor) {
 -                      if ($mediaEditor->getDecoratedObject()->isImage) {
 -                              $saveStrategy->generateThumbnails($mediaEditor->getDecoratedObject());
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * Returns the data of the media file to be returned by AJAX requests.
 -       * 
 -       * @param       Media|ViewableMedia     $media          media files whose data will be returned
 -       * @return      string[]
 -       */
 -      protected function getMediaData($media) {
 -              return [
 -                      'altText' => $media instanceof ViewableMedia ? $media->altText : [],
 -                      'caption' => $media instanceof ViewableMedia ? $media->caption : [],
 -                      'captionEnableHtml' => $media->captionEnableHtml,
 -                      'categoryID' => $media->categoryID,
 -                      'elementTag' => $media instanceof ViewableMedia ? $media->getElementTag($this->parameters['elementTagSize'] ?? 144) : '',
 -                      'elementTag48' => $media instanceof ViewableMedia ? $media->getElementTag(48) : '',
 -                      'fileHash' => $media->fileHash,
 -                      'filename' => $media->filename,
 -                      'filesize' => $media->filesize,
 -                      'formattedFilesize' => FileUtil::formatFilesize($media->filesize),
 -                      'fileType' => $media->fileType,
 -                      'height' => $media->height,
 -                      'languageID' => $media->languageID,
 -                      'imageDimensions' => $media->isImage ? WCF::getLanguage()->getDynamicVariable('wcf.media.imageDimensions.value', [
 -                              'media' => $media,
 -                      ]) : '',
 -                      'isImage' => $media->isImage,
 -                      'isMultilingual' => $media->isMultilingual,
 -                      'largeThumbnailHeight' => $media->largeThumbnailHeight,
 -                      'largeThumbnailLink' => $media->largeThumbnailType ? $media->getThumbnailLink('large') : '',
 -                      'largeThumbnailType' => $media->largeThumbnailType,
 -                      'largeThumbnailWidth' => $media->largeThumbnailWidth,
 -                      'link' => $media->getLink(),
 -                      'mediaID' => $media->mediaID,
 -                      'mediumThumbnailHeight' => $media->mediumThumbnailHeight,
 -                      'mediumThumbnailLink' => $media->mediumThumbnailType ? $media->getThumbnailLink('medium') : '',
 -                      'mediumThumbnailType' => $media->mediumThumbnailType,
 -                      'mediumThumbnailWidth' => $media->mediumThumbnailWidth,
 -                      'smallThumbnailHeight' => $media->smallThumbnailHeight,
 -                      'smallThumbnailLink' => $media->smallThumbnailType ? $media->getThumbnailLink('small') : '',
 -                      'smallThumbnailTag' => $media->smallThumbnailType ? $media->getThumbnailTag('small') : '',
 -                      'smallThumbnailType' => $media->smallThumbnailType,
 -                      'smallThumbnailWidth' => $media->smallThumbnailWidth,
 -                      'tinyThumbnailHeight' => $media->tinyThumbnailHeight,
 -                      'tinyThumbnailLink' => $media->tinyThumbnailType ? $media->getThumbnailLink('tiny') : '',
 -                      'tinyThumbnailType' => $media->tinyThumbnailType,
 -                      'tinyThumbnailWidth' => $media->tinyThumbnailWidth,
 -                      'title' => $media instanceof ViewableMedia ? $media->title : [],
 -                      'uploadTime' => $media->uploadTime,
 -                      'userID' => $media->userID,
 -                      'userLink' => $media->userID ? LinkHandler::getInstance()->getLink('User', [
 -                              'id' => $media->userID,
 -                              'title' => $media->username
 -                      ]) : '',
 -                      'userLinkElement' => $media instanceof ViewableMedia ? WCF::getTPL()->fetchString(
 -                              WCF::getTPL()->getCompiler()->compileString('userLink', '{user object=$userProfile}')['template'],
 -                              ['userProfile' => $media->getUserProfile()]
 -                      ) : '',
 -                      'username' => $media->username,
 -                      'width' => $media->width
 -              ];
 -      }
 -      
 -      /**
 -       * Validates the 'getManagementDialog' action.
 -       */
 -      public function validateGetManagementDialog() {
 -              if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia') && !WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
 -                      throw new PermissionDeniedException();
 -              }
 -              
 -              $this->readBoolean('imagesOnly', true);
 -              
 -              $this->readString('mode');
 -              if ($this->parameters['mode'] != 'editor' && $this->parameters['mode'] != 'select') {
 -                      throw new UserInputException('mode');
 -              }
 -      }
 -      
 -      /**
 -       * Returns the dialog to manage media.
 -       * 
 -       * @return      string[]
 -       */
 -      public function getManagementDialog() {
 -              $mediaList = new ViewableMediaList();
 -              if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 -                      $mediaList->getConditionBuilder()->add('media.userID = ?', [WCF::getUser()->userID]);
 -              }
 -              if ($this->parameters['imagesOnly']) {
 -                      $mediaList->getConditionBuilder()->add('media.isImage = ?', [1]);
 -              }
 -              $mediaList->sqlOrderBy = 'media.uploadTime DESC, media.mediaID DESC';
 -              $mediaList->sqlLimit = static::ITEMS_PER_MANAGER_DIALOG_PAGE;
 -              $mediaList->readObjects();
 -              
 -              $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
 -              $categoryList->setMaxDepth(0);
 -              
 -              return [
 -                      'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media')),
 -                      'media' => $this->getI18nMediaData($mediaList),
 -                      'pageCount' => ceil($mediaList->countObjects() / static::ITEMS_PER_MANAGER_DIALOG_PAGE),
 -                      'template' => WCF::getTPL()->fetch('mediaManager', 'wcf', [
 -                              'categoryList' => $categoryList,
 -                              'mediaList' => $mediaList,
 -                              'mode' => $this->parameters['mode']
 -                      ])
 -              ];
 -      }
 -      
 -      /**
 -       * Returns the complete i18n data of the media files in the given list.
 -       * 
 -       * @param       MediaList       $mediaList
 -       * @return      array
 -       */
 -      protected function getI18nMediaData(MediaList $mediaList) {
 -              if (!count($mediaList)) return [];
 -              
 -              $conditionBuilder = new PreparedStatementConditionBuilder();
 -              $conditionBuilder->add('mediaID IN (?)', [$mediaList->getObjectIDs()]);
 -              
 -              $sql = "SELECT  *
 -                      FROM    wcf".WCF_N."_media_content
 -                      ".$conditionBuilder;
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute($conditionBuilder->getParameters());
 -              
 -              $mediaData = [];
 -              while ($row = $statement->fetchArray()) {
 -                      if (!isset($mediaData[$row['mediaID']])) {
 -                              $mediaData[$row['mediaID']] = [
 -                                      'altText' => [],
 -                                      'caption' => [],
 -                                      'title' => []
 -                              ];
 -                      }
 -                      
 -                      $mediaData[$row['mediaID']]['altText'][intval($row['languageID'])] = $row['altText'];
 -                      $mediaData[$row['mediaID']]['caption'][intval($row['languageID'])] = $row['caption'];
 -                      $mediaData[$row['mediaID']]['title'][intval($row['languageID'])] = $row['title'];
 -              }
 -              
 -              $i18nMediaData = [];
 -              foreach ($mediaList as $media) {
 -                      if (!isset($mediaData[$media->mediaID])) {
 -                              $mediaData[$media->mediaID] = [];
 -                      }
 -                      
 -                      $i18nMediaData[$media->mediaID] = array_merge($this->getMediaData($media), $mediaData[$media->mediaID]);
 -              }
 -              
 -              return $i18nMediaData;
 -      }
 -      
 -      /**
 -       * Validates the 'getEditorDialog' action.
 -       */
 -      public function validateGetEditorDialog() {
 -              WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 -              
 -              $this->getSingleObject();
 -              
 -              if (!$this->getSingleObject()->canManage()) {
 -                      throw new PermissionDeniedException();
 -              }
 -      }
 -      
 -      /**
 -       * Returns the template for the media editor.
 -       * 
 -       * @return      string[]
 -       */
 -      public function getEditorDialog() {
 -              $mediaList = new ViewableMediaList();
 -              $mediaList->setObjectIDs([$this->getSingleObject()->mediaID]);
 -              $mediaList->readObjects();
 -              $media = $mediaList->search($this->getSingleObject()->mediaID);
 -              
 -              I18nHandler::getInstance()->register('title_' . $media->mediaID);
 -              I18nHandler::getInstance()->register('caption_' . $media->mediaID);
 -              I18nHandler::getInstance()->register('altText_' . $media->mediaID);
 -              I18nHandler::getInstance()->assignVariables();
 -              
 -              $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
 -              $categoryList->setMaxDepth(0);
 -              
 -              return [
 -                      'availableLanguageCount' => count(LanguageFactory::getInstance()->getLanguages()),
 -                      'categoryIDs' => array_keys(CategoryHandler::getInstance()->getCategories('com.woltlab.wcf.media.category')),
 -                      'mediaData' => $this->getI18nMediaData($mediaList)[$this->getSingleObject()->mediaID],
 -                      'template' => WCF::getTPL()->fetch('mediaEditor', 'wcf', [
 -                              '__aclSimplePrefix' => 'mediaEditor_' . $media->mediaID . '_',
 -                              '__aclInputName' => 'mediaEditor_' . $media->mediaID . '_aclValues',
 -                              '__languageChooserPrefix' => 'mediaEditor_' . $media->mediaID . '_',
 -                              'aclValues' => SimpleAclHandler::getInstance()->getValues('com.woltlab.wcf.media', $media->mediaID),
 -                              'availableLanguages' => LanguageFactory::getInstance()->getLanguages(),
 -                              'categoryList' => $categoryList,
 -                              'languageID' => WCF::getUser()->languageID,
 -                              'languages' => LanguageFactory::getInstance()->getLanguages(),
 -                              'media' => $media
 -                      ])
 -              ];
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function validateUpdate() {
 -              WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 -              
 -              if (empty($this->objects)) {
 -                      $this->readObjects();
 -                      
 -                      if (empty($this->objects)) {
 -                              throw new UserInputException('objectIDs');
 -                      }
 -              }
 -              
 -              if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 -                      foreach ($this->getObjects() as $media) {
 -                              if ($media->userID != WCF::getUser()->userID) {
 -                                      throw new PermissionDeniedException();
 -                              }
 -                      }
 -              }
 -              
 -              $this->readInteger('categoryID', true, 'data');
 -              $this->readInteger('languageID', true, 'data');
 -              $this->readBoolean('isMultilingual', true, 'data');
 -              $this->readInteger('captionEnableHtml', true, 'data');
 -              
 -              if (count(LanguageFactory::getInstance()->getLanguages()) > 1) {
 -                      // languageID: convert zero to null
 -                      if (!$this->parameters['data']['languageID']) $this->parameters['data']['languageID'] = null;
 -                      
 -                      // isMultilingual: convert boolean to integer
 -                      $this->parameters['data']['isMultilingual'] = intval($this->parameters['data']['isMultilingual']);
 -              }
 -              else {
 -                      $this->parameters['data']['isMultilingual'] = 0;
 -                      $this->parameters['data']['languageID'] = WCF::getLanguage()->languageID;
 -              }
 -              
 -              // if data is not multilingual, a language id has to be given
 -              if (!$this->parameters['data']['isMultilingual'] && !$this->parameters['data']['languageID']) {
 -                      throw new UserInputException('languageID');
 -              }
 -              
 -              // check language id
 -              if ($this->parameters['data']['languageID'] && !LanguageFactory::getInstance()->getLanguage($this->parameters['data']['languageID'])) {
 -                      throw new UserInputException('languageID');
 -              }
 -              
 -              // check category id
 -              if ($this->parameters['data']['categoryID']) {
 -                      $category = CategoryHandler::getInstance()->getCategory($this->parameters['data']['categoryID']);
 -                      if ($category === null || $category->getObjectType()->objectType !== 'com.woltlab.wcf.media.category') {
 -                              throw new UserInputException('categoryID');
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function update() {
 -              if (isset($this->parameters['data']['categoryID']) && $this->parameters['data']['categoryID'] === 0) {
 -                      $this->parameters['data']['categoryID'] = null;
 -              }
 -              
 -              if (empty($this->objects)) {
 -                      $this->readObjects();
 -              }
 -              
 -              parent::update();
 -              
 -              if (count($this->objects) == 1 && (isset($this->parameters['title']) || isset($this->parameters['caption']) || isset($this->parameters['altText']))) {
 -                      $media = reset($this->objects);
 -                      
 -                      $isMultilingual = $media->isMultilingual;
 -                      if (isset($this->parameters['data']['isMultilingual'])) {
 -                              $isMultilingual = $this->parameters['data']['isMultilingual'];
 -                      }
 -                      
 -                      $sql = "DELETE FROM     wcf".WCF_N."_media_content
 -                              WHERE           mediaID = ?";
 -                      $statement = WCF::getDB()->prepareStatement($sql);
 -                      $statement->execute([$media->mediaID]);
 -                      
 -                      $sql = "INSERT INTO     wcf".WCF_N."_media_content
 -                                              (mediaID, languageID, title, caption, altText)
 -                              VALUES          (?, ?, ?, ?, ?)";
 -                      $statement = WCF::getDB()->prepareStatement($sql);
 -                      
 -                      if (!$isMultilingual) {
 -                              $languageID = $media->languageID;
 -                              if (isset($this->parameters['data']['languageID'])) {
 -                                      $languageID = $this->parameters['data']['languageID'];
 -                              }
 -                              $statement->execute([
 -                                      $media->mediaID,
 -                                      $languageID,
 -                                      isset($this->parameters['title'][$languageID]) ? mb_substr($this->parameters['title'][$languageID], 0, 255) : '',
 -                                      isset($this->parameters['caption'][$languageID]) ? $this->parameters['caption'][$languageID] : '',
 -                                      isset($this->parameters['altText'][$languageID]) ? mb_substr($this->parameters['altText'][$languageID], 0, 255) : ''
 -                              ]);
 -                      }
 -                      else {
 -                              $languages = LanguageFactory::getInstance()->getLanguages();
 -                              foreach ($languages as $language) {
 -                                      $title = $caption = $altText = '';
 -                                      foreach (['title', 'caption', 'altText'] as $type) {
 -                                              if (isset($this->parameters[$type])) {
 -                                                      if (is_array($this->parameters[$type])) {
 -                                                              if (isset($this->parameters[$type][$language->languageID])) {
 -                                                                      /** @noinspection PhpVariableVariableInspection */
 -                                                                      $$type = $this->parameters[$type][$language->languageID];
 -                                                              }
 -                                                      }
 -                                                      else {
 -                                                              /** @noinspection PhpVariableVariableInspection */
 -                                                              $$type = $this->parameters[$type];
 -                                                      }
 -                                              }
 -                                      }
 -                                      
 -                                      $statement->execute([
 -                                              $media->mediaID,
 -                                              $language->languageID,
 -                                              mb_substr($title, 0, 255),
 -                                              $caption,
 -                                              mb_substr($altText, 0, 255)
 -                                      ]);
 -                              }
 -                      }
 -                      
 -                      if (!empty($this->parameters['aclValues'])) {
 -                              SimpleAclHandler::getInstance()->setValues('com.woltlab.wcf.media', $media->mediaID, $this->parameters['aclValues']);
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function validateGetSearchResultList() {
 -              if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia') && !WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
 -                      throw new PermissionDeniedException();
 -              }
 -              
 -              $this->readString('searchString', true);
 -              $this->readInteger('categoryID', true);
 -              
 -              $this->readBoolean('imagesOnly', true);
 -              
 -              $this->readString('mode');
 -              if ($this->parameters['mode'] != 'editor' && $this->parameters['mode'] != 'select') {
 -                      throw new UserInputException('mode');
 -              }
 -              
 -              $this->readInteger('pageNo', true);
 -              if (!$this->parameters['pageNo']) $this->parameters['pageNo'] = 1;
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function getSearchResultList() {
 -              $mediaList = new MediaList();
 -              $mediaList->addSearchConditions($this->parameters['searchString']);
 -              if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 -                      $mediaList->getConditionBuilder()->add('media.userID = ?', [WCF::getUser()->userID]);
 -              }
 -              if ($this->parameters['imagesOnly']) {
 -                      $mediaList->getConditionBuilder()->add('media.isImage = ?', [1]);
 -              }
 -              if ($this->parameters['categoryID']) {
 -                      $mediaList->getConditionBuilder()->add('media.categoryID = ?', [$this->parameters['categoryID']]);
 -              }
 -              $mediaList->sqlOrderBy = 'media.uploadTime DESC, media.mediaID DESC';
 -              $mediaList->sqlLimit = static::ITEMS_PER_MANAGER_DIALOG_PAGE;
 -              $mediaList->sqlOffset = ($this->parameters['pageNo'] - 1) * static::ITEMS_PER_MANAGER_DIALOG_PAGE;
 -              $mediaList->readObjectIDs();
 -              
 -              if (empty($mediaList->getObjectIDs())) {
 -                      // check if page is requested that might have existed but does not exist anymore due to deleted
 -                      // media files
 -                      if ($this->parameters['pageNo'] > 1 && $this->parameters['searchString'] === '' && !$this->parameters['categoryID']) {
 -                              // request media dialog page with highest page number 
 -                              $parameters = $this->parameters;
 -                              $parameters['pageNo'] = ceil($mediaList->countObjects() / static::ITEMS_PER_MANAGER_DIALOG_PAGE);
 -                              
 -                              return (new MediaAction($this->objects, 'getSearchResultList', $parameters))->executeAction()['returnValues'];
 -                      }
 -                      
 -                      return [
 -                              'template' => WCF::getLanguage()->getDynamicVariable('wcf.media.search.noResults')
 -                      ];
 -              }
 -              
 -              $viewableMediaList = new ViewableMediaList();
 -              $viewableMediaList->setObjectIDs($mediaList->getObjectIDs());
 -              $viewableMediaList->readObjects();
 -              
 -              return [
 -                      'media' => $this->getI18nMediaData($viewableMediaList),
 -                      'pageCount' => ceil($mediaList->countObjects() / static::ITEMS_PER_MANAGER_DIALOG_PAGE),
 -                      'pageNo' => $this->parameters['pageNo'],
 -                      'template' => WCF::getTPL()->fetch('mediaListItems', 'wcf', [
 -                              'mediaList' => $viewableMediaList,
 -                              'mode' => $this->parameters['mode']
 -                      ])
 -              ];
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function validateDelete() {
 -              WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 -              
 -              if (empty($this->objects)) {
 -                      $this->readObjects();
 -                      
 -                      if (empty($this->objects)) {
 -                              throw new UserInputException('objectIDs');
 -                      }
 -              }
 -              
 -              if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 -                      foreach ($this->getObjects() as $media) {
 -                              if ($media->userID != WCF::getUser()->userID) {
 -                                      throw new PermissionDeniedException();
 -                              }
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function delete() {
 -              if (empty($this->objects)) {
 -                      $this->readObjects();
 -              }
 -              
 -              foreach ($this->getObjects() as $mediaEditor) {
 -                      $mediaEditor->deleteFiles();
 -              }
 -              
 -              parent::delete();
 -              
 -              $this->unmarkItems();
 -      }
 -      
 -      /**
 -       * Unmarks the media files with the given ids. If no media ids are given,
 -       * all media files currently loaded are unmarked.
 -       * 
 -       * @param       integer[]       $mediaIDs       ids of the media files to be unmarked
 -       */
 -      protected function unmarkItems(array $mediaIDs = []) {
 -              if (empty($mediaIDs)) {
 -                      foreach ($this->getObjects() as $media) {
 -                              $mediaIDs[] = $media->mediaID;
 -                      }
 -              }
 -              
 -              if (!empty($mediaIDs)) {
 -                      ClipboardHandler::getInstance()->unmark($mediaIDs, ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media'));
 -              }
 -      }
 -      
 -      /**
 -       * Validates the `getSetCategoryDialog` action.
 -       * 
 -       * @throws      PermissionDeniedException       if user is not allowed to set category of media files
 -       * @throws      IllegalLinkException            if no media file categories exist
 -       */
 -      public function validateGetSetCategoryDialog() {
 -              if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia')) {
 -                      throw new PermissionDeniedException();
 -              }
 -              
 -              if (empty(CategoryHandler::getInstance()->getCategories('com.woltlab.wcf.media.category'))) {
 -                      throw new IllegalLinkException();
 -              }
 -      }
 -      
 -      /**
 -       * Returns the dialog to set the category of multiple media files.
 -       * 
 -       * @return      string[]
 -       */
 -      public function getSetCategoryDialog() {
 -              $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
 -              $categoryList->setMaxDepth(0);
 -              
 -              return [
 -                      'template' => WCF::getTPL()->fetch('__mediaSetCategoryDialog', 'wcf', [
 -                              'categoryList' => $categoryList
 -                      ])
 -              ];
 -      }
 -      
 -      /**
 -       * Validates the `setCategory` action.
 -       * 
 -       * @throws      PermissionDeniedException       if user is not allowed to edit a requested media file
 -       * @throws      UserInputException              if no object ids are given
 -       */
 -      public function validateSetCategory() {
 -              $this->validateGetSetCategoryDialog();
 -              
 -              if (empty($this->objects)) {
 -                      $this->readObjects();
 -                      
 -                      if (empty($this->objects)) {
 -                              throw new UserInputException('objectIDs');
 -                      }
 -              }
 -              
 -              if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 -                      foreach ($this->getObjects() as $media) {
 -                              if ($media->userID != WCF::getUser()->userID) {
 -                                      throw new PermissionDeniedException();
 -                              }
 -                      }
 -              }
 -              
 -              $this->readInteger('categoryID', true);
 -      }
 -      
 -      /**
 -       * Sets the category of multiple media files.
 -       */
 -      public function setCategory() {
 -              $conditionBuilder = new PreparedStatementConditionBuilder();
 -              $conditionBuilder->add('mediaID IN (?)', [$this->objectIDs]);
 -              
 -              $sql = "UPDATE  wcf" . WCF_N . "_media
 -                      SET     categoryID = ?
 -                      " . $conditionBuilder;
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute(array_merge(
 -                      [$this->parameters['categoryID'] ?: null],
 -                      $conditionBuilder->getParameters()
 -              ));
 -              
 -              $this->unmarkItems();
 -      }
 -      
 -      /**
 -       * Validates the `replaceFile` action.
 -       * 
 -       * @since       5.3
 -       */
 -      public function validateReplaceFile() {
 -              WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 -              
 -              $this->getSingleObject();
 -              
 -              /** @noinspection PhpUndefinedMethodInspection */
 -              $this->parameters['__files']->validateFiles(
 -                      new MediaReplaceUploadFileValidationStrategy($this->getSingleObject()->getDecoratedObject())
 -              );
 -      }
 -      
 -      /**
 -       * Replaces the actual file of a media file.
 -       * 
 -       * @return      array
 -       * @since       5.3
 -       */
 -      public function replaceFile() {
 -              $saveStrategy = new DefaultUploadFileSaveStrategy(static::class, [
 -                      'action' => 'update',
 -                      'generateThumbnails' => true,
 -                      'object' => $this->getSingleObject()->getDecoratedObject(),
 -                      'rotateImages' => true,
 -              ], [
 -                      'fileUpdateTime' => TIME_NOW,
 -                      'userID' => $this->getSingleObject()->userID,
 -                      'username' => $this->getSingleObject()->username,
 -              ]);
 -              
 -              /** @noinspection PhpUndefinedMethodInspection */
 -              $this->parameters['__files']->saveFiles($saveStrategy);
 -              
 -              /** @var Media[] $mediaFiles */
 -              $mediaFiles = $saveStrategy->getObjects();
 -              
 -              $result = [
 -                      'errors' => [],
 -                      'media' => []
 -              ];
 -              
 -              if (!empty($mediaFiles)) {
 -                      $mediaIDs = $mediaToFileID = [];
 -                      foreach ($mediaFiles as $internalFileID => $media) {
 -                              $mediaIDs[] = $media->mediaID;
 -                              $mediaToFileID[$media->mediaID] = $internalFileID;
 -                      }
 -                      
 -                      // fetch media objects from database
 -                      $mediaList = new ViewableMediaList();
 -                      $mediaList->setObjectIDs($mediaIDs);
 -                      $mediaList->readObjects();
 -                      
 -                      foreach ($mediaList as $media) {
 -                              $result['media'][$mediaToFileID[$media->mediaID]] = $this->getMediaData($media);
 -                      }
 -              }
 -              
 -              /** @var UploadFile[] $files */
 -              /** @noinspection PhpUndefinedMethodInspection */
 -              $files = $this->parameters['__files']->getFiles();
 -              foreach ($files as $file) {
 -                      if ($file->getValidationErrorType()) {
 -                              $result['errors'][$file->getInternalFileID()] = [
 -                                      'filename' => $file->getFilename(),
 -                                      'filesize' => $file->getFilesize(),
 -                                      'errorType' => $file->getValidationErrorType()
 -                              ];
 -                      }
 -              }
 -              
 -              $outdatedMediaFile = $this->getSingleObject();
 -              $updatedMediaFile = new Media($this->getSingleObject()->mediaID);
 -              
 -              // Delete *old* files using the non-updated local media editor object if the new file is
 -              // stored in a different location.
 -              if (empty($result['errors']) && $updatedMediaFile->getLocation() !== $outdatedMediaFile->getLocation()) {
 -                      $outdatedMediaFile->deleteFiles();
 -              }
 -              
 -              return $result;
 -      }
 +class MediaAction extends AbstractDatabaseObjectAction implements ISearchAction, IUploadAction
 +{
 +    /**
 +     * number of media files per media manager dialog page
 +     */
 +    const ITEMS_PER_MANAGER_DIALOG_PAGE = 50;
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function validateUpload()
 +    {
 +        WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 +
 +        $this->readBoolean('imagesOnly', true);
 +        $this->readInteger('categoryID', true);
 +
 +        /** @noinspection PhpUndefinedMethodInspection */
 +        $this->parameters['__files']->validateFiles(new MediaUploadFileValidationStrategy($this->parameters['imagesOnly']));
 +
 +        if ($this->parameters['categoryID']) {
 +            $category = CategoryHandler::getInstance()->getCategory($this->parameters['categoryID']);
 +            if ($category === null || $category->getObjectType()->objectType !== 'com.woltlab.wcf.media.category') {
 +                throw new UserInputException('categoryID');
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function upload()
 +    {
 +        $additionalData = ['username' => WCF::getUser()->username];
 +        if ($this->parameters['categoryID']) {
 +            $additionalData['categoryID'] = $this->parameters['categoryID'];
 +        }
 +
 +        // save files
 +        $saveStrategy = new DefaultUploadFileSaveStrategy(self::class, [
 +            'generateThumbnails' => true,
 +            'rotateImages' => true,
 +        ], $additionalData);
 +
 +        /** @noinspection PhpUndefinedMethodInspection */
 +        $this->parameters['__files']->saveFiles($saveStrategy);
 +
 +        /** @var Media[] $mediaFiles */
 +        $mediaFiles = $saveStrategy->getObjects();
 +
 +        $result = [
 +            'errors' => [],
 +            'media' => [],
 +        ];
 +
 +        if (!empty($mediaFiles)) {
 +            $mediaIDs = $mediaToFileID = [];
 +            foreach ($mediaFiles as $internalFileID => $media) {
 +                $mediaIDs[] = $media->mediaID;
 +                $mediaToFileID[$media->mediaID] = $internalFileID;
 +            }
 +
 +            // fetch media objects from database
 +            $mediaList = new ViewableMediaList();
 +            $mediaList->setObjectIDs($mediaIDs);
 +            $mediaList->readObjects();
 +
 +            foreach ($mediaList as $media) {
 +                $result['media'][$mediaToFileID[$media->mediaID]] = $this->getMediaData($media);
 +            }
 +        }
 +
 +        /** @var UploadFile[] $files */
 +        /** @noinspection PhpUndefinedMethodInspection */
 +        $files = $this->parameters['__files']->getFiles();
 +        foreach ($files as $file) {
 +            if ($file->getValidationErrorType()) {
 +                $result['errors'][$file->getInternalFileID()] = [
 +                    'filename' => $file->getFilename(),
 +                    'filesize' => $file->getFilesize(),
 +                    'errorType' => $file->getValidationErrorType(),
 +                ];
 +            }
 +        }
 +
 +        return $result;
 +    }
 +
 +    /**
 +     * Generates thumbnails.
 +     */
 +    public function generateThumbnails()
 +    {
 +        if (empty($this->objects)) {
 +            $this->readObjects();
 +        }
 +
 +        $saveStrategy = new DefaultUploadFileSaveStrategy(self::class);
 +
 +        foreach ($this->getObjects() as $mediaEditor) {
 +            if ($mediaEditor->getDecoratedObject()->isImage) {
 +                $saveStrategy->generateThumbnails($mediaEditor->getDecoratedObject());
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Returns the data of the media file to be returned by AJAX requests.
 +     *
 +     * @param Media|ViewableMedia $media media files whose data will be returned
 +     * @return  string[]
 +     */
 +    protected function getMediaData($media)
 +    {
 +        return [
 +            'altText' => $media instanceof ViewableMedia ? $media->altText : [],
 +            'caption' => $media instanceof ViewableMedia ? $media->caption : [],
 +            'captionEnableHtml' => $media->captionEnableHtml,
 +            'categoryID' => $media->categoryID,
 +            'elementTag' => $media instanceof ViewableMedia ? $media->getElementTag($this->parameters['elementTagSize'] ?? 144) : '',
 +            'elementTag48' => $media instanceof ViewableMedia ? $media->getElementTag(48) : '',
 +            'fileHash' => $media->fileHash,
 +            'filename' => $media->filename,
 +            'filesize' => $media->filesize,
 +            'formattedFilesize' => FileUtil::formatFilesize($media->filesize),
 +            'fileType' => $media->fileType,
 +            'height' => $media->height,
 +            'languageID' => $media->languageID,
 +            'imageDimensions' => $media->isImage ? WCF::getLanguage()->getDynamicVariable(
 +                'wcf.media.imageDimensions.value',
 +                [
 +                    'media' => $media,
 +                ]
 +            ) : '',
 +            'isImage' => $media->isImage,
 +            'isMultilingual' => $media->isMultilingual,
 +            'largeThumbnailHeight' => $media->largeThumbnailHeight,
 +            'largeThumbnailLink' => $media->largeThumbnailType ? $media->getThumbnailLink('large') : '',
 +            'largeThumbnailType' => $media->largeThumbnailType,
 +            'largeThumbnailWidth' => $media->largeThumbnailWidth,
 +            'link' => $media->getLink(),
 +            'mediaID' => $media->mediaID,
 +            'mediumThumbnailHeight' => $media->mediumThumbnailHeight,
 +            'mediumThumbnailLink' => $media->mediumThumbnailType ? $media->getThumbnailLink('medium') : '',
 +            'mediumThumbnailType' => $media->mediumThumbnailType,
 +            'mediumThumbnailWidth' => $media->mediumThumbnailWidth,
 +            'smallThumbnailHeight' => $media->smallThumbnailHeight,
 +            'smallThumbnailLink' => $media->smallThumbnailType ? $media->getThumbnailLink('small') : '',
 +            'smallThumbnailTag' => $media->smallThumbnailType ? $media->getThumbnailTag('small') : '',
 +            'smallThumbnailType' => $media->smallThumbnailType,
 +            'smallThumbnailWidth' => $media->smallThumbnailWidth,
 +            'tinyThumbnailHeight' => $media->tinyThumbnailHeight,
 +            'tinyThumbnailLink' => $media->tinyThumbnailType ? $media->getThumbnailLink('tiny') : '',
 +            'tinyThumbnailType' => $media->tinyThumbnailType,
 +            'tinyThumbnailWidth' => $media->tinyThumbnailWidth,
 +            'title' => $media instanceof ViewableMedia ? $media->title : [],
 +            'uploadTime' => $media->uploadTime,
 +            'userID' => $media->userID,
 +            'userLink' => $media->userID ? LinkHandler::getInstance()->getLink('User', [
 +                'id' => $media->userID,
 +                'title' => $media->username,
 +            ]) : '',
 +            'userLinkElement' => $media instanceof ViewableMedia ? WCF::getTPL()->fetchString(
 +                WCF::getTPL()->getCompiler()->compileString('userLink', '{user object=$userProfile}')['template'],
 +                ['userProfile' => $media->getUserProfile()]
 +            ) : '',
 +            'username' => $media->username,
 +            'width' => $media->width,
 +        ];
 +    }
 +
 +    /**
 +     * Validates the 'getManagementDialog' action.
 +     */
 +    public function validateGetManagementDialog()
 +    {
 +        if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia') && !WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
 +            throw new PermissionDeniedException();
 +        }
 +
 +        $this->readBoolean('imagesOnly', true);
 +
 +        $this->readString('mode');
 +        if ($this->parameters['mode'] != 'editor' && $this->parameters['mode'] != 'select') {
 +            throw new UserInputException('mode');
 +        }
 +    }
 +
 +    /**
 +     * Returns the dialog to manage media.
 +     *
 +     * @return  string[]
 +     */
 +    public function getManagementDialog()
 +    {
 +        $mediaList = new ViewableMediaList();
 +        if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 +            $mediaList->getConditionBuilder()->add('media.userID = ?', [WCF::getUser()->userID]);
 +        }
 +        if ($this->parameters['imagesOnly']) {
 +            $mediaList->getConditionBuilder()->add('media.isImage = ?', [1]);
 +        }
 +        $mediaList->sqlOrderBy = 'media.uploadTime DESC, media.mediaID DESC';
 +        $mediaList->sqlLimit = static::ITEMS_PER_MANAGER_DIALOG_PAGE;
 +        $mediaList->readObjects();
 +
 +        $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
 +        $categoryList->setMaxDepth(0);
 +
 +        return [
 +            'hasMarkedItems' => ClipboardHandler::getInstance()->hasMarkedItems(ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media')),
 +            'media' => $this->getI18nMediaData($mediaList),
 +            'pageCount' => \ceil($mediaList->countObjects() / static::ITEMS_PER_MANAGER_DIALOG_PAGE),
 +            'template' => WCF::getTPL()->fetch('mediaManager', 'wcf', [
 +                'categoryList' => $categoryList,
 +                'mediaList' => $mediaList,
 +                'mode' => $this->parameters['mode'],
 +            ]),
 +        ];
 +    }
 +
 +    /**
 +     * Returns the complete i18n data of the media files in the given list.
 +     *
 +     * @param MediaList $mediaList
 +     * @return  array
 +     */
 +    protected function getI18nMediaData(MediaList $mediaList)
 +    {
 +        if (!\count($mediaList)) {
 +            return [];
 +        }
 +
 +        $conditionBuilder = new PreparedStatementConditionBuilder();
 +        $conditionBuilder->add('mediaID IN (?)', [$mediaList->getObjectIDs()]);
 +
 +        $sql = "SELECT  *
 +                FROM    wcf" . WCF_N . "_media_content
 +                " . $conditionBuilder;
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute($conditionBuilder->getParameters());
 +
 +        $mediaData = [];
 +        while ($row = $statement->fetchArray()) {
 +            if (!isset($mediaData[$row['mediaID']])) {
 +                $mediaData[$row['mediaID']] = [
 +                    'altText' => [],
 +                    'caption' => [],
 +                    'title' => [],
 +                ];
 +            }
 +
 +            $mediaData[$row['mediaID']]['altText'][\intval($row['languageID'])] = $row['altText'];
 +            $mediaData[$row['mediaID']]['caption'][\intval($row['languageID'])] = $row['caption'];
 +            $mediaData[$row['mediaID']]['title'][\intval($row['languageID'])] = $row['title'];
 +        }
 +
 +        $i18nMediaData = [];
 +        foreach ($mediaList as $media) {
 +            if (!isset($mediaData[$media->mediaID])) {
 +                $mediaData[$media->mediaID] = [];
 +            }
 +
 +            $i18nMediaData[$media->mediaID] = \array_merge($this->getMediaData($media), $mediaData[$media->mediaID]);
 +        }
 +
 +        return $i18nMediaData;
 +    }
 +
 +    /**
 +     * Validates the 'getEditorDialog' action.
 +     */
 +    public function validateGetEditorDialog()
 +    {
 +        WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 +
 +        $this->getSingleObject();
 +
 +        if (!$this->getSingleObject()->canManage()) {
 +            throw new PermissionDeniedException();
 +        }
 +    }
 +
 +    /**
 +     * Returns the template for the media editor.
 +     *
 +     * @return  string[]
 +     */
 +    public function getEditorDialog()
 +    {
 +        $mediaList = new ViewableMediaList();
 +        $mediaList->setObjectIDs([$this->getSingleObject()->mediaID]);
 +        $mediaList->readObjects();
 +        $media = $mediaList->search($this->getSingleObject()->mediaID);
 +
 +        I18nHandler::getInstance()->register('title_' . $media->mediaID);
 +        I18nHandler::getInstance()->register('caption_' . $media->mediaID);
 +        I18nHandler::getInstance()->register('altText_' . $media->mediaID);
 +        I18nHandler::getInstance()->assignVariables();
 +
 +        $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
 +        $categoryList->setMaxDepth(0);
 +
 +        return [
 +            'availableLanguageCount' => \count(LanguageFactory::getInstance()->getLanguages()),
 +            'categoryIDs' => \array_keys(CategoryHandler::getInstance()->getCategories('com.woltlab.wcf.media.category')),
 +            'mediaData' => $this->getI18nMediaData($mediaList)[$this->getSingleObject()->mediaID],
 +            'template' => WCF::getTPL()->fetch('mediaEditor', 'wcf', [
 +                '__aclSimplePrefix' => 'mediaEditor_' . $media->mediaID . '_',
 +                '__aclInputName' => 'mediaEditor_' . $media->mediaID . '_aclValues',
 +                '__languageChooserPrefix' => 'mediaEditor_' . $media->mediaID . '_',
 +                'aclValues' => SimpleAclHandler::getInstance()->getValues('com.woltlab.wcf.media', $media->mediaID),
 +                'availableLanguages' => LanguageFactory::getInstance()->getLanguages(),
 +                'categoryList' => $categoryList,
 +                'languageID' => WCF::getUser()->languageID,
 +                'languages' => LanguageFactory::getInstance()->getLanguages(),
 +                'media' => $media,
 +            ]),
 +        ];
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function validateUpdate()
 +    {
 +        WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 +
 +        if (empty($this->objects)) {
 +            $this->readObjects();
 +
 +            if (empty($this->objects)) {
 +                throw new UserInputException('objectIDs');
 +            }
 +        }
 +
 +        if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 +            foreach ($this->getObjects() as $media) {
 +                if ($media->userID != WCF::getUser()->userID) {
 +                    throw new PermissionDeniedException();
 +                }
 +            }
 +        }
 +
 +        $this->readInteger('categoryID', true, 'data');
 +        $this->readInteger('languageID', true, 'data');
 +        $this->readBoolean('isMultilingual', true, 'data');
 +        $this->readInteger('captionEnableHtml', true, 'data');
 +
 +        if (\count(LanguageFactory::getInstance()->getLanguages()) > 1) {
 +            // languageID: convert zero to null
 +            if (!$this->parameters['data']['languageID']) {
 +                $this->parameters['data']['languageID'] = null;
 +            }
 +
 +            // isMultilingual: convert boolean to integer
 +            $this->parameters['data']['isMultilingual'] = \intval($this->parameters['data']['isMultilingual']);
 +        } else {
 +            $this->parameters['data']['isMultilingual'] = 0;
 +            $this->parameters['data']['languageID'] = WCF::getLanguage()->languageID;
 +        }
 +
 +        // if data is not multilingual, a language id has to be given
 +        if (!$this->parameters['data']['isMultilingual'] && !$this->parameters['data']['languageID']) {
 +            throw new UserInputException('languageID');
 +        }
 +
 +        // check language id
 +        if ($this->parameters['data']['languageID'] && !LanguageFactory::getInstance()->getLanguage($this->parameters['data']['languageID'])) {
 +            throw new UserInputException('languageID');
 +        }
 +
 +        // check category id
 +        if ($this->parameters['data']['categoryID']) {
 +            $category = CategoryHandler::getInstance()->getCategory($this->parameters['data']['categoryID']);
 +            if ($category === null || $category->getObjectType()->objectType !== 'com.woltlab.wcf.media.category') {
 +                throw new UserInputException('categoryID');
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function update()
 +    {
 +        if (isset($this->parameters['data']['categoryID']) && $this->parameters['data']['categoryID'] === 0) {
 +            $this->parameters['data']['categoryID'] = null;
 +        }
 +
 +        if (empty($this->objects)) {
 +            $this->readObjects();
 +        }
 +
 +        parent::update();
 +
 +        if (\count($this->objects) == 1 && (isset($this->parameters['title']) || isset($this->parameters['caption']) || isset($this->parameters['altText']))) {
 +            $media = \reset($this->objects);
 +
 +            $isMultilingual = $media->isMultilingual;
 +            if (isset($this->parameters['data']['isMultilingual'])) {
 +                $isMultilingual = $this->parameters['data']['isMultilingual'];
 +            }
 +
 +            $sql = "DELETE FROM wcf" . WCF_N . "_media_content
 +                    WHERE       mediaID = ?";
 +            $statement = WCF::getDB()->prepareStatement($sql);
 +            $statement->execute([$media->mediaID]);
 +
 +            $sql = "INSERT INTO wcf" . WCF_N . "_media_content
 +                                (mediaID, languageID, title, caption, altText)
 +                    VALUES      (?, ?, ?, ?, ?)";
 +            $statement = WCF::getDB()->prepareStatement($sql);
 +
 +            if (!$isMultilingual) {
 +                $languageID = $media->languageID;
 +                if (isset($this->parameters['data']['languageID'])) {
 +                    $languageID = $this->parameters['data']['languageID'];
 +                }
 +                $statement->execute([
 +                    $media->mediaID,
 +                    $languageID,
 +                    isset($this->parameters['title'][$languageID]) ? \mb_substr(
 +                        $this->parameters['title'][$languageID],
 +                        0,
 +                        255
 +                    ) : '',
 +                    $this->parameters['caption'][$languageID] ?? '',
 +                    isset($this->parameters['altText'][$languageID]) ? \mb_substr(
 +                        $this->parameters['altText'][$languageID],
 +                        0,
 +                        255
 +                    ) : '',
 +                ]);
 +            } else {
 +                $languages = LanguageFactory::getInstance()->getLanguages();
 +                foreach ($languages as $language) {
 +                    $title = $caption = $altText = '';
 +                    foreach (['title', 'caption', 'altText'] as $type) {
 +                        if (isset($this->parameters[$type])) {
 +                            if (\is_array($this->parameters[$type])) {
 +                                if (isset($this->parameters[$type][$language->languageID])) {
 +                                    /** @noinspection PhpVariableVariableInspection */
 +                                    ${$type} = $this->parameters[$type][$language->languageID];
 +                                }
 +                            } else {
 +                                /** @noinspection PhpVariableVariableInspection */
 +                                ${$type} = $this->parameters[$type];
 +                            }
 +                        }
 +                    }
 +
 +                    $statement->execute([
 +                        $media->mediaID,
 +                        $language->languageID,
 +                        \mb_substr($title, 0, 255),
 +                        $caption,
 +                        \mb_substr($altText, 0, 255),
 +                    ]);
 +                }
 +            }
 +
 +            if (!empty($this->parameters['aclValues'])) {
 +                SimpleAclHandler::getInstance()->setValues(
 +                    'com.woltlab.wcf.media',
 +                    $media->mediaID,
 +                    $this->parameters['aclValues']
 +                );
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function validateGetSearchResultList()
 +    {
 +        if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia') && !WCF::getSession()->getPermission('admin.content.cms.canUseMedia')) {
 +            throw new PermissionDeniedException();
 +        }
 +
 +        $this->readString('searchString', true);
 +        $this->readInteger('categoryID', true);
 +
 +        $this->readBoolean('imagesOnly', true);
 +
 +        $this->readString('mode');
 +        if ($this->parameters['mode'] != 'editor' && $this->parameters['mode'] != 'select') {
 +            throw new UserInputException('mode');
 +        }
 +
 +        $this->readInteger('pageNo', true);
 +        if (!$this->parameters['pageNo']) {
 +            $this->parameters['pageNo'] = 1;
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function getSearchResultList()
 +    {
 +        $mediaList = new MediaList();
 +        $mediaList->addSearchConditions($this->parameters['searchString']);
 +        if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 +            $mediaList->getConditionBuilder()->add('media.userID = ?', [WCF::getUser()->userID]);
 +        }
 +        if ($this->parameters['imagesOnly']) {
 +            $mediaList->getConditionBuilder()->add('media.isImage = ?', [1]);
 +        }
 +        if ($this->parameters['categoryID']) {
 +            $mediaList->getConditionBuilder()->add('media.categoryID = ?', [$this->parameters['categoryID']]);
 +        }
 +        $mediaList->sqlOrderBy = 'media.uploadTime DESC, media.mediaID DESC';
 +        $mediaList->sqlLimit = static::ITEMS_PER_MANAGER_DIALOG_PAGE;
 +        $mediaList->sqlOffset = ($this->parameters['pageNo'] - 1) * static::ITEMS_PER_MANAGER_DIALOG_PAGE;
 +        $mediaList->readObjectIDs();
 +
 +        if (empty($mediaList->getObjectIDs())) {
 +            // check if page is requested that might have existed but does not exist anymore due to deleted
 +            // media files
 +            if ($this->parameters['pageNo'] > 1 && $this->parameters['searchString'] === '' && !$this->parameters['categoryID']) {
 +                // request media dialog page with highest page number
 +                $parameters = $this->parameters;
 +                $parameters['pageNo'] = \ceil($mediaList->countObjects() / static::ITEMS_PER_MANAGER_DIALOG_PAGE);
 +
 +                return (new self($this->objects, 'getSearchResultList', $parameters))->executeAction()['returnValues'];
 +            }
 +
 +            return [
 +                'template' => WCF::getLanguage()->getDynamicVariable('wcf.media.search.noResults'),
 +            ];
 +        }
 +
 +        $viewableMediaList = new ViewableMediaList();
 +        $viewableMediaList->setObjectIDs($mediaList->getObjectIDs());
 +        $viewableMediaList->readObjects();
 +
 +        return [
 +            'media' => $this->getI18nMediaData($viewableMediaList),
 +            'pageCount' => \ceil($mediaList->countObjects() / static::ITEMS_PER_MANAGER_DIALOG_PAGE),
 +            'pageNo' => $this->parameters['pageNo'],
 +            'template' => WCF::getTPL()->fetch('mediaListItems', 'wcf', [
 +                'mediaList' => $viewableMediaList,
 +                'mode' => $this->parameters['mode'],
 +            ]),
 +        ];
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function validateDelete()
 +    {
 +        WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 +
 +        if (empty($this->objects)) {
 +            $this->readObjects();
 +
 +            if (empty($this->objects)) {
 +                throw new UserInputException('objectIDs');
 +            }
 +        }
 +
 +        if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 +            foreach ($this->getObjects() as $media) {
 +                if ($media->userID != WCF::getUser()->userID) {
 +                    throw new PermissionDeniedException();
 +                }
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function delete()
 +    {
 +        if (empty($this->objects)) {
 +            $this->readObjects();
 +        }
 +
 +        foreach ($this->getObjects() as $mediaEditor) {
 +            $mediaEditor->deleteFiles();
 +        }
 +
 +        parent::delete();
 +
 +        $this->unmarkItems();
 +    }
 +
 +    /**
 +     * Unmarks the media files with the given ids. If no media ids are given,
 +     * all media files currently loaded are unmarked.
 +     *
 +     * @param int[] $mediaIDs ids of the media files to be unmarked
 +     */
 +    protected function unmarkItems(array $mediaIDs = [])
 +    {
 +        if (empty($mediaIDs)) {
 +            foreach ($this->getObjects() as $media) {
 +                $mediaIDs[] = $media->mediaID;
 +            }
 +        }
 +
 +        if (!empty($mediaIDs)) {
 +            ClipboardHandler::getInstance()->unmark(
 +                $mediaIDs,
 +                ClipboardHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.media')
 +            );
 +        }
 +    }
 +
 +    /**
 +     * Validates the `getSetCategoryDialog` action.
 +     *
 +     * @throws  PermissionDeniedException   if user is not allowed to set category of media files
 +     * @throws  IllegalLinkException        if no media file categories exist
 +     */
 +    public function validateGetSetCategoryDialog()
 +    {
 +        if (!WCF::getSession()->getPermission('admin.content.cms.canManageMedia')) {
 +            throw new PermissionDeniedException();
 +        }
 +
 +        if (empty(CategoryHandler::getInstance()->getCategories('com.woltlab.wcf.media.category'))) {
 +            throw new IllegalLinkException();
 +        }
 +    }
 +
 +    /**
 +     * Returns the dialog to set the category of multiple media files.
 +     *
 +     * @return  string[]
 +     */
 +    public function getSetCategoryDialog()
 +    {
 +        $categoryList = (new CategoryNodeTree('com.woltlab.wcf.media.category'))->getIterator();
 +        $categoryList->setMaxDepth(0);
 +
 +        return [
 +            'template' => WCF::getTPL()->fetch('__mediaSetCategoryDialog', 'wcf', [
 +                'categoryList' => $categoryList,
 +            ]),
 +        ];
 +    }
 +
 +    /**
 +     * Validates the `setCategory` action.
 +     *
 +     * @throws  PermissionDeniedException   if user is not allowed to edit a requested media file
 +     * @throws  UserInputException      if no object ids are given
 +     */
 +    public function validateSetCategory()
 +    {
 +        $this->validateGetSetCategoryDialog();
 +
 +        if (empty($this->objects)) {
 +            $this->readObjects();
 +
 +            if (empty($this->objects)) {
 +                throw new UserInputException('objectIDs');
 +            }
 +        }
 +
 +        if (WCF::getSession()->getPermission('admin.content.cms.canOnlyAccessOwnMedia')) {
 +            foreach ($this->getObjects() as $media) {
 +                if ($media->userID != WCF::getUser()->userID) {
 +                    throw new PermissionDeniedException();
 +                }
 +            }
 +        }
 +
 +        $this->readInteger('categoryID', true);
 +    }
 +
 +    /**
 +     * Sets the category of multiple media files.
 +     */
 +    public function setCategory()
 +    {
 +        $conditionBuilder = new PreparedStatementConditionBuilder();
 +        $conditionBuilder->add('mediaID IN (?)', [$this->objectIDs]);
 +
 +        $sql = "UPDATE  wcf" . WCF_N . "_media
 +                SET     categoryID = ?
 +                " . $conditionBuilder;
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute(\array_merge(
 +            [$this->parameters['categoryID'] ?: null],
 +            $conditionBuilder->getParameters()
 +        ));
 +
 +        $this->unmarkItems();
 +    }
 +
 +    /**
 +     * Validates the `replaceFile` action.
 +     *
 +     * @since       5.3
 +     */
 +    public function validateReplaceFile()
 +    {
 +        WCF::getSession()->checkPermissions(['admin.content.cms.canManageMedia']);
 +
 +        $this->getSingleObject();
 +
 +        /** @noinspection PhpUndefinedMethodInspection */
 +        $this->parameters['__files']->validateFiles(
 +            new MediaReplaceUploadFileValidationStrategy($this->getSingleObject()->getDecoratedObject())
 +        );
 +    }
 +
 +    /**
 +     * Replaces the actual file of a media file.
 +     *
 +     * @return      array
 +     * @since       5.3
 +     */
 +    public function replaceFile()
 +    {
 +        $saveStrategy = new DefaultUploadFileSaveStrategy(static::class, [
 +            'action' => 'update',
 +            'generateThumbnails' => true,
 +            'object' => $this->getSingleObject()->getDecoratedObject(),
 +            'rotateImages' => true,
 +        ], [
 +            'fileUpdateTime' => TIME_NOW,
 +            'userID' => $this->getSingleObject()->userID,
 +            'username' => $this->getSingleObject()->username,
 +        ]);
 +
 +        /** @noinspection PhpUndefinedMethodInspection */
 +        $this->parameters['__files']->saveFiles($saveStrategy);
 +
 +        /** @var Media[] $mediaFiles */
 +        $mediaFiles = $saveStrategy->getObjects();
 +
 +        $result = [
 +            'errors' => [],
 +            'media' => [],
 +        ];
 +
 +        if (!empty($mediaFiles)) {
 +            $mediaIDs = $mediaToFileID = [];
 +            foreach ($mediaFiles as $internalFileID => $media) {
 +                $mediaIDs[] = $media->mediaID;
 +                $mediaToFileID[$media->mediaID] = $internalFileID;
 +            }
 +
 +            // fetch media objects from database
 +            $mediaList = new ViewableMediaList();
 +            $mediaList->setObjectIDs($mediaIDs);
 +            $mediaList->readObjects();
 +
 +            foreach ($mediaList as $media) {
 +                $result['media'][$mediaToFileID[$media->mediaID]] = $this->getMediaData($media);
 +            }
 +        }
 +
 +        /** @var UploadFile[] $files */
 +        /** @noinspection PhpUndefinedMethodInspection */
 +        $files = $this->parameters['__files']->getFiles();
 +        foreach ($files as $file) {
 +            if ($file->getValidationErrorType()) {
 +                $result['errors'][$file->getInternalFileID()] = [
 +                    'filename' => $file->getFilename(),
 +                    'filesize' => $file->getFilesize(),
 +                    'errorType' => $file->getValidationErrorType(),
 +                ];
 +            }
 +        }
 +
-         // Delete *old* files using the non-updated local media editor object.
-         if (empty($result['errors'])) {
-             $this->getSingleObject()->deleteFiles();
++        $outdatedMediaFile = $this->getSingleObject();
++        $updatedMediaFile = new Media($this->getSingleObject()->mediaID);
++
++        // Delete *old* files using the non-updated local media editor object if the new file is
++        // stored in a different location.
++        if (empty($result['errors']) && $updatedMediaFile->getLocation() !== $outdatedMediaFile->getLocation()) {
++            $outdatedMediaFile->deleteFiles();
 +        }
 +
 +        return $result;
 +    }
  }