'{unsafe:$field->getPrefixedId()|encodeJS}',
{if $field->isSingleFileUpload()}true{else}false{/if},
{if $field->isBigPreview()}true{else}false{/if},
+ {if $field->isSimpleReplace()}true{else}false{/if},
+ {if $field->isHideDeleteButton()}true{else}false{/if},
[{implode from=$actionButtons item=actionButton}{
title: '{unsafe:$actionButton['title']|encodeJS}',
icon: {if $actionButton['icon'] === null}undefined{else}'{unsafe:$actionButton['icon']->toHtml()|encodeJS}'{/if},
void upload(element, resizedFile);
})
.catch((e) => {
+ element.dispatchEvent(new CustomEvent("cancel"));
+
if (e === undefined) {
// User closed the dialog.
return;
};
window.addEventListener("resize", resize, { passive: true });
- this.dialog.addEventListener(
- "afterClose",
- () => {
- window.removeEventListener("resize", resize);
- },
- {
- once: true,
- },
- );
return new Promise<File>((resolve, reject) => {
+ let callReject = true;
+ this.dialog!.addEventListener("afterClose", () => {
+ window.removeEventListener("resize", resize);
+
+ // If the dialog is closed without confirming, reject the promise to trigger a cancel event.
+ if (callReject) {
+ reject();
+ }
+ });
+
this.dialog!.addEventListener("primary", () => {
+ callReject = false;
+
void this.getCanvas()
.then((canvas) => {
this.resizer
readonly #fileInput: HTMLInputElement;
readonly #useBigPreview: boolean;
readonly #singleFileUpload: boolean;
+ readonly #simpleReplace: boolean;
+ readonly #hideDeleteButton: boolean;
readonly #extraButtons: ExtraButton[];
#uploadResolve: undefined | (() => void);
fieldId: string,
singleFileUpload: boolean = false,
useBigPreview: boolean = false,
+ simpleReplace: boolean = false,
+ hideDeleteButton: boolean = false,
extraButtons: ExtraButton[] = [],
) {
this.#fieldId = fieldId;
this.#useBigPreview = useBigPreview;
this.#singleFileUpload = singleFileUpload;
+ this.#simpleReplace = simpleReplace;
+ this.#hideDeleteButton = hideDeleteButton;
this.#extraButtons = extraButtons;
this.#container = document.getElementById(fieldId + "Container")!;
}
this.#uploadButton = this.#container.querySelector("woltlab-core-file-upload") as WoltlabCoreFileUploadElement;
+
+ if (this.#simpleReplace) {
+ this.#uploadButton.addEventListener("shouldUpload", () => {
+ const file = this.#uploadButton.parentElement!.querySelector("woltlab-core-file");
+ if (!file) {
+ return;
+ }
+
+ this.#simpleFileReplace(file);
+ });
+ }
+
this.#uploadButton.addEventListener("uploadStart", (event: CustomEvent<WoltlabCoreFileElement>) => {
if (this.#uploadResolve !== undefined) {
this.#uploadResolve();
buttons.classList.add("buttonList");
buttons.classList.add(this.classPrefix + "item__buttons");
- let listItem = document.createElement("li");
- listItem.append(this.getDeleteButton(element));
- buttons.append(listItem);
+ if (!this.#hideDeleteButton) {
+ const listItem = document.createElement("li");
+ listItem.append(this.getDeleteButton(element));
+ buttons.append(listItem);
+ }
- if (this.#singleFileUpload) {
- listItem = document.createElement("li");
+ if (this.#singleFileUpload && !this.#simpleReplace) {
+ const listItem = document.createElement("li");
listItem.append(this.getReplaceButton(element));
buttons.append(listItem);
}
container.append(buttons);
}
+ protected getReplaceButton(element: WoltlabCoreFileElement): HTMLButtonElement {
+ const replaceButton = document.createElement("button");
+ replaceButton.type = "button";
+ replaceButton.classList.add("button", "small");
+ replaceButton.textContent = getPhrase("wcf.global.button.replace");
+ replaceButton.addEventListener("click", () => {
+ const oldContext = this.#startReplaceFile(element);
+
+ clearPreviousErrors(this.#uploadButton);
+
+ const changeEventListener = () => {
+ this.#fileInput.removeEventListener("cancel", cancelEventListener);
+
+ // Wait until the upload starts,
+ // the request to the server is not synchronized with the end of the `change` event.
+ // Otherwise, we would swap the context too soon.
+ void new Promise<void>((resolve) => {
+ this.#uploadResolve = resolve;
+ }).then(() => {
+ this.#uploadResolve = undefined;
+ this.#uploadButton.dataset.context = oldContext;
+ });
+ };
+ const cancelEventListener = () => {
+ this.#uploadButton.dataset.context = oldContext;
+ this.#registerFile(this.#replaceElement!);
+ this.#replaceElement = undefined;
+ this.#fileInput.removeEventListener("change", changeEventListener);
+ };
+
+ this.#fileInput.addEventListener("cancel", cancelEventListener, { once: true });
+ this.#fileInput.addEventListener("change", changeEventListener, { once: true });
+
+ this.#fileInput.click();
+ });
+
+ return replaceButton;
+ }
+
#markElementUploadHasFailed(container: HTMLElement, element: WoltlabCoreFileElement, reason: unknown): void {
fileInitializationFailed(container, element, reason);
return deleteButton;
}
- protected getReplaceButton(element: WoltlabCoreFileElement): HTMLButtonElement {
- const replaceButton = document.createElement("button");
- replaceButton.type = "button";
- replaceButton.classList.add("button", "small");
- replaceButton.textContent = getPhrase("wcf.global.button.replace");
- replaceButton.addEventListener("click", () => {
- // Add to context an extra attribute that the replace button is clicked.
- // After the dialog is closed or the file is selected, the context will be reset to his old value.
- // This is necessary as the serverside validation will otherwise fail.
- const oldContext = this.#uploadButton.dataset.context!;
- const context = JSON.parse(oldContext);
- context.__replace = true;
- this.#uploadButton.dataset.context = JSON.stringify(context);
+ #simpleFileReplace(oldFile: WoltlabCoreFileElement) {
+ const oldContext = this.#startReplaceFile(oldFile);
- this.#replaceElement = element;
- this.#unregisterFile(element);
+ const cropCancelledEvent = () => {
+ this.#uploadButton.dataset.context = oldContext;
+ this.#registerFile(this.#replaceElement!);
+ this.#replaceElement = undefined;
+ };
- clearPreviousErrors(this.#uploadButton);
+ this.#uploadButton.addEventListener("cancel", cropCancelledEvent, { once: true });
- const changeEventListener = () => {
- this.#fileInput.removeEventListener("cancel", cancelEventListener);
+ void new Promise<void>((resolve) => {
+ this.#uploadResolve = resolve;
+ }).then(() => {
+ this.#uploadResolve = undefined;
+ this.#uploadButton.dataset.context = oldContext;
+ this.#fileInput.removeEventListener("cancel", cropCancelledEvent);
+ });
+ }
- // Wait until the upload starts,
- // the request to the server is not synchronized with the end of the `change` event.
- // Otherwise, we would swap the context too soon.
- void new Promise<void>((resolve) => {
- this.#uploadResolve = resolve;
- }).then(() => {
- this.#uploadResolve = undefined;
- this.#uploadButton.dataset.context = oldContext;
- });
- };
- const cancelEventListener = () => {
- this.#uploadButton.dataset.context = oldContext;
- this.#registerFile(this.#replaceElement!);
- this.#replaceElement = undefined;
- this.#fileInput.removeEventListener("change", changeEventListener);
- };
+ #startReplaceFile(element: WoltlabCoreFileElement): string {
+ // Add to context an extra attribute that the replace button is clicked.
+ // After the dialog is closed or the file is selected, the context will be reset to his old value.
+ // This is necessary as the serverside validation will otherwise fail.
+ const oldContext = this.#uploadButton.dataset.context!;
+ const context = JSON.parse(oldContext);
+ context.__replace = true;
+ this.#uploadButton.dataset.context = JSON.stringify(context);
- this.#fileInput.addEventListener("cancel", cancelEventListener, { once: true });
- this.#fileInput.addEventListener("change", changeEventListener, { once: true });
- this.#fileInput.click();
- });
+ this.#replaceElement = element;
+ this.#unregisterFile(element);
- return replaceButton;
+ return oldContext;
}
#unregisterFile(element: WoltlabCoreFileElement): void {
void upload(element, resizedFile);
})
.catch((e) => {
+ element.dispatchEvent(new CustomEvent("cancel"));
if (e === undefined) {
// User closed the dialog.
return;
this.centerSelection();
};
window.addEventListener("resize", resize, { passive: true });
- this.dialog.addEventListener("afterClose", () => {
- window.removeEventListener("resize", resize);
- }, {
- once: true,
- });
return new Promise((resolve, reject) => {
+ let callReject = true;
+ this.dialog.addEventListener("afterClose", () => {
+ window.removeEventListener("resize", resize);
+ // If the dialog is closed without confirming, reject the promise to trigger a cancel event.
+ if (callReject) {
+ reject();
+ }
+ });
this.dialog.addEventListener("primary", () => {
+ callReject = false;
void this.getCanvas()
.then((canvas) => {
this.resizer
#fileInput;
#useBigPreview;
#singleFileUpload;
+ #simpleReplace;
+ #hideDeleteButton;
#extraButtons;
#uploadResolve;
- constructor(fieldId, singleFileUpload = false, useBigPreview = false, extraButtons = []) {
+ constructor(fieldId, singleFileUpload = false, useBigPreview = false, simpleReplace = false, hideDeleteButton = false, extraButtons = []) {
this.#fieldId = fieldId;
this.#useBigPreview = useBigPreview;
this.#singleFileUpload = singleFileUpload;
+ this.#simpleReplace = simpleReplace;
+ this.#hideDeleteButton = hideDeleteButton;
this.#extraButtons = extraButtons;
this.#container = document.getElementById(fieldId + "Container");
if (this.#container === null) {
throw new Error("Unknown field with id '" + fieldId + "'");
}
this.#uploadButton = this.#container.querySelector("woltlab-core-file-upload");
+ if (this.#simpleReplace) {
+ this.#uploadButton.addEventListener("shouldUpload", () => {
+ const file = this.#uploadButton.parentElement.querySelector("woltlab-core-file");
+ if (!file) {
+ return;
+ }
+ this.#simpleFileReplace(file);
+ });
+ }
this.#uploadButton.addEventListener("uploadStart", (event) => {
if (this.#uploadResolve !== undefined) {
this.#uploadResolve();
const buttons = document.createElement("ul");
buttons.classList.add("buttonList");
buttons.classList.add(this.classPrefix + "item__buttons");
- let listItem = document.createElement("li");
- listItem.append(this.getDeleteButton(element));
- buttons.append(listItem);
- if (this.#singleFileUpload) {
- listItem = document.createElement("li");
+ if (!this.#hideDeleteButton) {
+ const listItem = document.createElement("li");
+ listItem.append(this.getDeleteButton(element));
+ buttons.append(listItem);
+ }
+ if (this.#singleFileUpload && !this.#simpleReplace) {
+ const listItem = document.createElement("li");
listItem.append(this.getReplaceButton(element));
buttons.append(listItem);
}
});
container.append(buttons);
}
- #markElementUploadHasFailed(container, element, reason) {
- (0, Helper_1.fileInitializationFailed)(container, element, reason);
- container.classList.add("innerError");
- }
- getDeleteButton(element) {
- const deleteButton = document.createElement("button");
- deleteButton.type = "button";
- deleteButton.classList.add("button", "small");
- deleteButton.textContent = (0, Language_1.getPhrase)("wcf.global.button.delete");
- deleteButton.addEventListener("click", async () => {
- const result = await (0, DeleteFile_1.deleteFile)(element.fileId);
- if (result.ok) {
- this.#unregisterFile(element);
- }
- else {
- let container = element;
- if (!this.#useBigPreview) {
- container = container.parentElement;
- }
- if (result.error.code === "permission_denied") {
- (0, Util_1.innerError)(container, (0, Language_1.getPhrase)("wcf.upload.error.delete.permissionDenied"), true);
- }
- else {
- (0, Util_1.innerError)(container, result.error.message ?? (0, Language_1.getPhrase)("wcf.upload.error.delete.unknownError"));
- }
- }
- });
- return deleteButton;
- }
getReplaceButton(element) {
const replaceButton = document.createElement("button");
replaceButton.type = "button";
replaceButton.classList.add("button", "small");
replaceButton.textContent = (0, Language_1.getPhrase)("wcf.global.button.replace");
replaceButton.addEventListener("click", () => {
- // Add to context an extra attribute that the replace button is clicked.
- // After the dialog is closed or the file is selected, the context will be reset to his old value.
- // This is necessary as the serverside validation will otherwise fail.
- const oldContext = this.#uploadButton.dataset.context;
- const context = JSON.parse(oldContext);
- context.__replace = true;
- this.#uploadButton.dataset.context = JSON.stringify(context);
- this.#replaceElement = element;
- this.#unregisterFile(element);
+ const oldContext = this.#startReplaceFile(element);
(0, Upload_1.clearPreviousErrors)(this.#uploadButton);
const changeEventListener = () => {
this.#fileInput.removeEventListener("cancel", cancelEventListener);
});
return replaceButton;
}
+ #markElementUploadHasFailed(container, element, reason) {
+ (0, Helper_1.fileInitializationFailed)(container, element, reason);
+ container.classList.add("innerError");
+ }
+ getDeleteButton(element) {
+ const deleteButton = document.createElement("button");
+ deleteButton.type = "button";
+ deleteButton.classList.add("button", "small");
+ deleteButton.textContent = (0, Language_1.getPhrase)("wcf.global.button.delete");
+ deleteButton.addEventListener("click", async () => {
+ const result = await (0, DeleteFile_1.deleteFile)(element.fileId);
+ if (result.ok) {
+ this.#unregisterFile(element);
+ }
+ else {
+ let container = element;
+ if (!this.#useBigPreview) {
+ container = container.parentElement;
+ }
+ if (result.error.code === "permission_denied") {
+ (0, Util_1.innerError)(container, (0, Language_1.getPhrase)("wcf.upload.error.delete.permissionDenied"), true);
+ }
+ else {
+ (0, Util_1.innerError)(container, result.error.message ?? (0, Language_1.getPhrase)("wcf.upload.error.delete.unknownError"));
+ }
+ }
+ });
+ return deleteButton;
+ }
+ #simpleFileReplace(oldFile) {
+ const oldContext = this.#startReplaceFile(oldFile);
+ const cropCancelledEvent = () => {
+ this.#uploadButton.dataset.context = oldContext;
+ this.#registerFile(this.#replaceElement);
+ this.#replaceElement = undefined;
+ };
+ this.#uploadButton.addEventListener("cancel", cropCancelledEvent, { once: true });
+ void new Promise((resolve) => {
+ this.#uploadResolve = resolve;
+ }).then(() => {
+ this.#uploadResolve = undefined;
+ this.#uploadButton.dataset.context = oldContext;
+ this.#fileInput.removeEventListener("cancel", cropCancelledEvent);
+ });
+ }
+ #startReplaceFile(element) {
+ // Add to context an extra attribute that the replace button is clicked.
+ // After the dialog is closed or the file is selected, the context will be reset to his old value.
+ // This is necessary as the serverside validation will otherwise fail.
+ const oldContext = this.#uploadButton.dataset.context;
+ const context = JSON.parse(oldContext);
+ context.__replace = true;
+ this.#uploadButton.dataset.context = JSON.stringify(context);
+ this.#replaceElement = element;
+ this.#unregisterFile(element);
+ return oldContext;
+ }
#unregisterFile(element) {
if (this.#useBigPreview) {
element.parentElement.innerHTML = "";
->required()
->singleFileUpload()
->bigPreview()
+ ->simpleReplace()
+ ->hideDeleteButton()
->addDependency(
ValueFormFieldDependency::create('avatarType')
->fieldId('avatarType')
private array $files = [];
private bool $singleFileUpload = false;
private bool $bigPreview = false;
+ private bool $simpleReplace = false;
+ private bool $hideDeleteButton = false;
private array $actionButtons = [];
#[\Override]
),
'maxUploads' => $this->getFileProcessor()->getMaximumCount($this->context),
'actionButtons' => $this->actionButtons,
+ 'simpleReplace' => $this->simpleReplace,
];
}
);
}
+ if (!$singleFileUpload && $this->simpleReplace) {
+ throw new \InvalidArgumentException(
+ "Single file upload can't be disabled if the simple replace is enabled for the field '{$this->getId()}'."
+ );
+ }
+
$this->singleFileUpload = $singleFileUpload;
return $this;
return $this;
}
+
+ /**
+ * Returns whether the simple replace is enabled.
+ */
+ public function isSimpleReplace(): bool
+ {
+ return $this->simpleReplace;
+ }
+
+ /**
+ * Sets whether the simple replace is enabled.
+ * Simple replace can only be enabled if single file upload is true.
+ * If enabled, there is no replace button and the existing file is replaced when a new file is uploaded.
+ */
+ public function simpleReplace(bool $simpleReplace = true): self
+ {
+ if ($simpleReplace && !$this->singleFileUpload) {
+ throw new \InvalidArgumentException(
+ "Simple replace can only be enabled for single file uploads for the field '{$this->getId()}'."
+ );
+ }
+
+ $this->simpleReplace = $simpleReplace;
+
+ return $this;
+ }
+
+ /**
+ * Sets whether the delete button should be hidden.
+ */
+ public function hideDeleteButton(bool $hideDeleteButton = true): self
+ {
+ $this->hideDeleteButton = $hideDeleteButton;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether the delete button is hidden.
+ */
+ public function isHideDeleteButton(): bool
+ {
+ return $this->hideDeleteButton;
+ }
}