2 * Handles editing media files via dialog.
4 * @author Matthias Schmidt
5 * @copyright 2001-2021 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Media/Editor
10 import * as Core from "../Core";
11 import { Media, MediaEditorCallbackObject } from "./Data";
12 import { AjaxCallbackObject, AjaxCallbackSetup } from "../Ajax/Data";
13 import * as UiNotification from "../Ui/Notification";
14 import * as UiDialog from "../Ui/Dialog";
15 import { DialogCallbackObject } from "../Ui/Dialog/Data";
16 import * as LanguageChooser from "../Language/Chooser";
17 import * as LanguageInput from "../Language/Input";
18 import * as DomUtil from "../Dom/Util";
19 import * as DomTraverse from "../Dom/Traverse";
20 import DomChangeListener from "../Dom/Change/Listener";
21 import * as Language from "../Language";
22 import * as Ajax from "../Ajax";
23 import MediaReplace from "./Replace";
24 import { I18nValues } from "../Language/Input";
26 interface InitEditorData {
28 availableLanguageCount: number;
29 categoryIDs: number[];
34 class MediaEditor implements AjaxCallbackObject {
35 protected _availableLanguageCount = 1;
36 protected _categoryIds: number[] = [];
37 protected _dialogs = new Map<string, DialogCallbackObject>();
38 protected readonly _callbackObject: MediaEditorCallbackObject;
39 protected _media: Media | null = null;
40 protected _oldCategoryId = 0;
42 constructor(callbackObject: MediaEditorCallbackObject) {
43 this._callbackObject = callbackObject || {};
45 if (this._callbackObject._editorClose && typeof this._callbackObject._editorClose !== "function") {
46 throw new TypeError("Callback object has no function '_editorClose'.");
48 if (this._callbackObject._editorSuccess && typeof this._callbackObject._editorSuccess !== "function") {
49 throw new TypeError("Callback object has no function '_editorSuccess'.");
53 public _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
57 className: "wcf\\data\\media\\MediaAction",
62 public _ajaxSuccess(): void {
63 UiNotification.show();
65 if (this._callbackObject._editorSuccess) {
66 this._callbackObject._editorSuccess(this._media, this._oldCategoryId);
67 this._oldCategoryId = 0;
70 UiDialog.close(`mediaEditor_${this._media!.mediaID}`);
76 * Is called if an editor is manually closed by the user.
78 protected _close(): void {
81 if (this._callbackObject._editorClose) {
82 this._callbackObject._editorClose();
87 * Initializes the editor dialog.
91 protected _initEditor(content: HTMLElement, data: InitEditorData): void {
92 this._availableLanguageCount = ~~data.returnValues.availableLanguageCount;
93 this._categoryIds = data.returnValues.categoryIDs.map((number) => ~~number);
95 if (data.returnValues.mediaData) {
96 this._media = data.returnValues.mediaData;
98 const mediaId = this._media!.mediaID;
100 // make sure that the language chooser is initialized first
102 if (this._availableLanguageCount > 1) {
103 LanguageChooser.setLanguageId(
104 `mediaEditor_${mediaId}_languageID`,
105 this._media!.languageID || window.LANGUAGE_ID,
109 if (this._categoryIds.length) {
110 const categoryID = content.querySelector("select[name=categoryID]") as HTMLSelectElement;
111 if (this._media!.categoryID) {
112 categoryID.value = this._media!.categoryID.toString();
114 categoryID.value = "0";
118 const title = content.querySelector("input[name=title]") as HTMLInputElement;
119 const altText = content.querySelector("input[name=altText]") as HTMLInputElement;
120 const caption = content.querySelector("textarea[name=caption]") as HTMLInputElement;
122 if (this._availableLanguageCount > 1 && this._media!.isMultilingual) {
123 if (document.getElementById(`altText_${mediaId}`)) {
124 LanguageInput.setValues(`altText_${mediaId}`, (this._media!.altText || {}) as I18nValues);
127 if (document.getElementById(`caption_${mediaId}`)) {
128 LanguageInput.setValues(`caption_${mediaId}`, (this._media!.caption || {}) as I18nValues);
131 LanguageInput.setValues(`title_${mediaId}`, (this._media!.title || {}) as I18nValues);
133 title.value = this._media?.title[this._media!.languageID || window.LANGUAGE_ID] || "";
135 altText.value = this._media?.altText[this._media!.languageID || window.LANGUAGE_ID] || "";
138 caption.value = this._media?.caption[this._media!.languageID || window.LANGUAGE_ID] || "";
142 if (this._availableLanguageCount > 1) {
143 const isMultilingual = content.querySelector("input[name=isMultilingual]") as HTMLInputElement;
144 isMultilingual.addEventListener("change", (ev) => this._updateLanguageFields(ev));
146 this._updateLanguageFields(null, isMultilingual);
150 altText.addEventListener("keypress", (ev) => this._keyPress(ev));
152 title.addEventListener("keypress", (ev) => this._keyPress(ev));
154 content.querySelector("button[data-type=submit]")!.addEventListener("click", () => this._saveData());
156 // remove focus from input elements and scroll dialog to top
157 (document.activeElement! as HTMLElement).blur();
158 (document.getElementById(`mediaEditor_${mediaId}`)!.parentNode as HTMLElement).scrollTop = 0;
160 // Initialize button to replace media file.
161 const uploadButton = content.querySelector(".mediaManagerMediaReplaceButton")!;
162 let target = content.querySelector(".mediaThumbnail");
164 target = document.createElement("div");
165 content.appendChild(target);
169 DomUtil.identify(uploadButton),
170 // Pass an anonymous element for non-images which is required internally
171 // but not needed in this case.
172 DomUtil.identify(target),
178 DomChangeListener.trigger();
183 * Handles the `[ENTER]` key to submit the form.
185 protected _keyPress(event: KeyboardEvent): void {
186 if (event.key === "Enter") {
187 event.preventDefault();
194 * Saves the data of the currently edited media.
196 protected _saveData(): void {
197 const content = UiDialog.getDialog(`mediaEditor_${this._media!.mediaID}`)!.content;
199 const categoryId = content.querySelector("select[name=categoryID]") as HTMLSelectElement;
200 const altText = content.querySelector("input[name=altText]") as HTMLInputElement;
201 const caption = content.querySelector("textarea[name=caption]") as HTMLTextAreaElement;
202 const captionEnableHtml = content.querySelector("input[name=captionEnableHtml]") as HTMLInputElement;
203 const title = content.querySelector("input[name=title]") as HTMLInputElement;
205 let hasError = false;
206 const altTextError = altText ? DomTraverse.childByClass(altText.parentNode! as HTMLElement, "innerError") : false;
207 const captionError = caption ? DomTraverse.childByClass(caption.parentNode! as HTMLElement, "innerError") : false;
208 const titleError = DomTraverse.childByClass(title.parentNode! as HTMLElement, "innerError");
211 this._oldCategoryId = this._media!.categoryID;
212 if (this._categoryIds.length) {
213 this._media!.categoryID = ~~categoryId.value;
215 // if the selected category id not valid (manipulated DOM), ignore
216 if (this._categoryIds.indexOf(this._media!.categoryID) === -1) {
217 this._media!.categoryID = 0;
221 // language and multilingualism
222 if (this._availableLanguageCount > 1) {
223 const isMultilingual = content.querySelector("input[name=isMultilingual]") as HTMLInputElement;
224 this._media!.isMultilingual = ~~isMultilingual.checked;
225 this._media!.languageID = this._media!.isMultilingual
227 : LanguageChooser.getLanguageId(`mediaEditor_${this._media!.mediaID}_languageID`);
229 this._media!.languageID = window.LANGUAGE_ID;
232 // altText, caption and title
233 this._media!.altText = {};
234 this._media!.caption = {};
235 this._media!.title = {};
236 if (this._availableLanguageCount > 1 && this._media!.isMultilingual) {
237 if (altText && !LanguageInput.validate(altText.id, true)) {
240 DomUtil.innerError(altText, Language.get("wcf.global.form.error.multilingual"));
243 if (caption && !LanguageInput.validate(caption.id, true)) {
246 DomUtil.innerError(caption, Language.get("wcf.global.form.error.multilingual"));
249 if (!LanguageInput.validate(title.id, true)) {
252 DomUtil.innerError(title, Language.get("wcf.global.form.error.multilingual"));
256 this._media!.altText = altText ? this.mapToI18nValues(LanguageInput.getValues(altText.id)) : "";
257 this._media!.caption = caption ? this.mapToI18nValues(LanguageInput.getValues(caption.id)) : "";
258 this._media!.title = this.mapToI18nValues(LanguageInput.getValues(title.id));
260 this._media!.altText[this._media!.languageID!] = altText ? altText.value : "";
261 this._media!.caption[this._media!.languageID!] = caption ? caption.value : "";
262 this._media!.title[this._media!.languageID!] = title.value;
266 if (captionEnableHtml) {
267 this._media!.captionEnableHtml = ~~captionEnableHtml.checked;
269 this._media!.captionEnableHtml = 0;
273 allowAll: ~~(document.getElementById(`mediaEditor_${this._media!.mediaID}_aclAllowAll`)! as HTMLInputElement)
276 content.querySelectorAll(`input[name="mediaEditor_${this._media!.mediaID}_aclValues[group][]"]`),
277 ).map((aclGroup: HTMLInputElement) => ~~aclGroup.value),
279 content.querySelectorAll(`input[name="mediaEditor_${this._media!.mediaID}_aclValues[user][]"]`),
280 ).map((aclUser: HTMLInputElement) => ~~aclUser.value),
285 altTextError.remove();
288 captionError.remove();
295 actionName: "update",
296 objectIDs: [this._media!.mediaID],
298 aclValues: aclValues,
299 altText: this._media!.altText,
300 caption: this._media!.caption,
302 captionEnableHtml: this._media!.captionEnableHtml,
303 categoryID: this._media!.categoryID,
304 isMultilingual: this._media!.isMultilingual,
305 languageID: this._media!.languageID,
307 title: this._media!.title,
313 private mapToI18nValues(values: Map<number, string>): I18nValues {
315 values.forEach((value, key) => (obj[key] = value));
321 * Updates language-related input fields depending on whether multilingualis is enabled.
323 protected _updateLanguageFields(event: Event | null, element?: HTMLInputElement): void {
325 element = event.currentTarget as HTMLInputElement;
328 const mediaId = this._media!.mediaID;
329 const languageChooserContainer = document.getElementById(`mediaEditor_${mediaId}_languageIDContainer`)!
330 .parentNode! as HTMLElement;
332 if (element!.checked) {
333 LanguageInput.enable(`title_${mediaId}`);
334 if (document.getElementById(`caption_${mediaId}`)) {
335 LanguageInput.enable(`caption_${mediaId}`);
337 if (document.getElementById(`altText_${mediaId}`)) {
338 LanguageInput.enable(`altText_${mediaId}`);
341 DomUtil.hide(languageChooserContainer);
343 LanguageInput.disable(`title_${mediaId}`);
344 if (document.getElementById(`caption_${mediaId}`)) {
345 LanguageInput.disable(`caption_${mediaId}`);
347 if (document.getElementById(`altText_${mediaId}`)) {
348 LanguageInput.disable(`altText_${mediaId}`);
351 DomUtil.show(languageChooserContainer);
356 * Edits the media with the given data or id.
358 public edit(editedMedia: Media | number): void {
361 if (typeof editedMedia === "object") {
363 mediaId = media.mediaID;
366 mediaID: editedMedia,
368 mediaId = editedMedia;
371 if (this._media !== null) {
372 throw new Error(`Cannot edit media with id ${mediaId} while editing media with id '${this._media.mediaID}'.`);
377 if (!this._dialogs.has(`mediaEditor_${mediaId}`)) {
378 this._dialogs.set(`mediaEditor_${mediaId}`, {
379 _dialogSetup: () => {
381 id: `mediaEditor_${mediaId}`,
383 backdropCloseOnClick: false,
384 onClose: () => this._close(),
385 title: Language.get("wcf.media.edit"),
388 after: (content: HTMLElement, responseData: InitEditorData) => this._initEditor(content, responseData),
390 actionName: "getEditorDialog",
391 className: "wcf\\data\\media\\MediaAction",
392 objectIDs: [mediaId],
400 UiDialog.open(this._dialogs.get(`mediaEditor_${mediaId}`)!);
404 * Updates the data of the currently edited media file.
406 public updateData(media: Media): void {
407 if (this._callbackObject._editorSuccess) {
408 this._callbackObject._editorSuccess(media);
413 Core.enableLegacyInheritance(MediaEditor);
415 export = MediaEditor;