![]()
import { nextTick, ref, useTemplateRef, watch } from "vue";
import Cropper from "cropperjs";
+import { useI18n } from "vue-i18n";
const props = defineProps({
image: {
@@ -153,6 +154,8 @@ const props = defineProps({
const emit = defineEmits(["newImage", "deleteImage"]);
+const { t } = useI18n();
+
const modalVisible = ref(false);
const isLoadingAction = ref(false);
const isSavingAction = ref(false);
@@ -161,6 +164,13 @@ const croppedImage = ref(null);
const cropper = ref();
const cropperImgRef = ref(null);
+const moveHandle = ref(null);
+const topLeftHandle = ref(null);
+const topRightHandle = ref(null);
+const bottomLeftHandle = ref(null);
+const bottomRightHandle = ref(null);
+const scaleMove = ref(1);
+
watch(
() => props.image,
() => {
@@ -204,6 +214,7 @@ async function resetFileUpload() {
function closeModal() {
if (cropper.value) {
+ clearKeyboardShortcuts();
cropper.value.destroy();
cropper.value = null;
}
@@ -236,6 +247,8 @@ async function onFileSelect(event) {
dragMode: "none",
ready: async function () {
isLoadingAction.value = false;
+
+ initKeyboardShortcuts();
},
});
};
@@ -253,4 +266,180 @@ async function undoDeleteImage() {
await nextTick();
document.querySelector("[data-test='delete-image-button']")?.focus();
}
+
+function clearKeyboardShortcuts() {
+ // Remove event listeners
+ moveHandle.value?.removeEventListener("keydown", moveEventListener);
+ topLeftHandle.value?.removeEventListener("keydown", topLeftEventListener);
+ topRightHandle.value?.removeEventListener("keydown", topRightEventListener);
+ bottomLeftHandle.value?.removeEventListener(
+ "keydown",
+ bottomLeftEventListener,
+ );
+ bottomRightHandle.value?.removeEventListener(
+ "keydown",
+ bottomRightEventListener,
+ );
+}
+
+function initKeyboardShortcuts() {
+ const dimensions = cropper.value.getData();
+ scaleMove.value = Math.min(dimensions.width, dimensions.height) / 100;
+
+ // Get all handles
+ moveHandle.value = document.getElementsByClassName("cropper-move")[0];
+ topLeftHandle.value = document.getElementsByClassName(
+ "cropper-point point-nw",
+ )[0];
+ topRightHandle.value = document.getElementsByClassName(
+ "cropper-point point-ne",
+ )[0];
+ bottomLeftHandle.value = document.getElementsByClassName(
+ "cropper-point point-sw",
+ )[0];
+ bottomRightHandle.value = document.getElementsByClassName(
+ "cropper-point point-se",
+ )[0];
+
+ // Add tabindex
+ moveHandle.value.setAttribute("tabindex", 0);
+ topLeftHandle.value.setAttribute("tabindex", 0);
+ topRightHandle.value.setAttribute("tabindex", 0);
+ bottomLeftHandle.value.setAttribute("tabindex", 0);
+ bottomRightHandle.value.setAttribute("tabindex", 0);
+
+ // Add aria-labels
+ moveHandle.value.setAttribute(
+ "aria-label",
+ t("admin.users.image.aria_crop_selection.move"),
+ );
+ topLeftHandle.value.setAttribute(
+ "aria-label",
+ t("admin.users.image.aria_crop_selection.top_left"),
+ );
+ topRightHandle.value.setAttribute(
+ "aria-label",
+ t("admin.users.image.aria_crop_selection.top_right"),
+ );
+ bottomLeftHandle.value.setAttribute(
+ "aria-label",
+ t("admin.users.image.aria_crop_selection.bottom_left"),
+ );
+ bottomRightHandle.value.setAttribute(
+ "aria-label",
+ t("admin.users.image.aria_crop_selection.bottom_right"),
+ );
+
+ // Add event listeners
+ moveHandle.value.addEventListener("keydown", moveEventListener);
+ topLeftHandle.value.addEventListener("keydown", topLeftEventListener);
+ topRightHandle.value.addEventListener("keydown", topRightEventListener);
+ bottomLeftHandle.value.addEventListener("keydown", bottomLeftEventListener);
+ bottomRightHandle.value.addEventListener("keydown", bottomRightEventListener);
+}
+
+function moveEventListener(e) {
+ const cropDimensions = cropper.value.getCropBoxData();
+
+ switch (e.key) {
+ case "ArrowUp":
+ cropDimensions.top -= scaleMove.value;
+ break;
+ case "ArrowDown":
+ cropDimensions.top += scaleMove.value;
+ break;
+ case "ArrowLeft":
+ cropDimensions.left -= scaleMove.value;
+ break;
+ case "ArrowRight":
+ cropDimensions.left += scaleMove.value;
+ break;
+ }
+
+ cropper.value.setCropBoxData(cropDimensions);
+}
+
+function topLeftEventListener(e) {
+ const cropDimensions = cropper.value.getCropBoxData();
+
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowLeft":
+ cropDimensions.top -= scaleMove.value;
+ cropDimensions.height += scaleMove.value;
+ cropDimensions.left -= scaleMove.value;
+ cropDimensions.width += scaleMove.value;
+ break;
+ case "ArrowDown":
+ case "ArrowRight":
+ cropDimensions.top += scaleMove.value;
+ cropDimensions.height -= scaleMove.value;
+ cropDimensions.left += scaleMove.value;
+ cropDimensions.width -= scaleMove.value;
+ break;
+ }
+
+ cropper.value.setCropBoxData(cropDimensions);
+}
+
+function topRightEventListener(e) {
+ const cropDimensions = cropper.value.getCropBoxData();
+
+ switch (e.key) {
+ case "ArrowUp":
+ case "ArrowRight":
+ cropDimensions.top -= scaleMove.value;
+ cropDimensions.height += scaleMove.value;
+ cropDimensions.width += scaleMove.value;
+ break;
+ case "ArrowDown":
+ case "ArrowLeft":
+ cropDimensions.top += scaleMove.value;
+ cropDimensions.height -= scaleMove.value;
+ cropDimensions.width -= scaleMove.value;
+ break;
+ }
+
+ cropper.value.setCropBoxData(cropDimensions);
+}
+
+function bottomLeftEventListener(e) {
+ const cropDimensions = cropper.value.getCropBoxData();
+
+ switch (e.key) {
+ case "ArrowDown":
+ case "ArrowLeft":
+ cropDimensions.height += scaleMove.value;
+ cropDimensions.left -= scaleMove.value;
+ cropDimensions.width += scaleMove.value;
+ break;
+ case "ArrowUp":
+ case "ArrowRight":
+ cropDimensions.height -= scaleMove.value;
+ cropDimensions.left += scaleMove.value;
+ cropDimensions.width -= scaleMove.value;
+ break;
+ }
+
+ cropper.value.setCropBoxData(cropDimensions);
+}
+
+function bottomRightEventListener(e) {
+ const cropDimensions = cropper.value.getCropBoxData();
+
+ switch (e.key) {
+ case "ArrowDown":
+ case "ArrowRight":
+ cropDimensions.height += scaleMove.value;
+ cropDimensions.width += scaleMove.value;
+ break;
+ case "ArrowUp":
+ case "ArrowLeft":
+ cropDimensions.height -= scaleMove.value;
+ cropDimensions.width -= scaleMove.value;
+ break;
+ }
+
+ cropper.value.setCropBoxData(cropDimensions);
+}
diff --git a/resources/js/components/UserTabProfile.vue b/resources/js/components/UserTabProfile.vue
index 51c74f685..59b1a6631 100644
--- a/resources/js/components/UserTabProfile.vue
+++ b/resources/js/components/UserTabProfile.vue
@@ -250,7 +250,7 @@ function save() {
formData.append("image", "");
} else if (croppedImageBlob.value != null) {
// cropped image
- formData.append("image", croppedImageBlob.value, "image.png");
+ formData.append("image", croppedImageBlob.value, "image.jpg");
}
formErrors.clear();
From 663d8dfb5dca78cf95b17e2a6aec51256c2f87d9 Mon Sep 17 00:00:00 2001
From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com>
Date: Mon, 20 Apr 2026 13:06:49 +0200
Subject: [PATCH 05/12] Fix locale key
---
resources/js/components/RoomTabFilesUploadButton.vue | 2 +-
resources/js/components/SettingsFileSelector.vue | 4 ++--
resources/js/components/SettingsImageSelector.vue | 4 ++--
resources/js/components/UserProfileImageSelector.vue | 6 +++---
tests/Frontend/e2e/RoomsViewFilesFileActions.cy.js | 2 +-
5 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/resources/js/components/RoomTabFilesUploadButton.vue b/resources/js/components/RoomTabFilesUploadButton.vue
index df00dcf86..06041959a 100644
--- a/resources/js/components/RoomTabFilesUploadButton.vue
+++ b/resources/js/components/RoomTabFilesUploadButton.vue
@@ -255,7 +255,7 @@ function uploadFile(file) {
reset();
if (error.response) {
if (error.response.status === env.HTTP_PAYLOAD_TOO_LARGE) {
- formErrors.set({ file: [t("app.validation.too_large")] });
+ formErrors.set({ file: [t("app.file.too_large")] });
return;
}
if (error.response.status === env.HTTP_UNPROCESSABLE_ENTITY) {
diff --git a/resources/js/components/SettingsFileSelector.vue b/resources/js/components/SettingsFileSelector.vue
index d23868545..b7f47705d 100644
--- a/resources/js/components/SettingsFileSelector.vue
+++ b/resources/js/components/SettingsFileSelector.vue
@@ -56,10 +56,10 @@
diff --git a/resources/js/components/SettingsImageSelector.vue b/resources/js/components/SettingsImageSelector.vue
index 10af59bfc..a2dd0210b 100644
--- a/resources/js/components/SettingsImageSelector.vue
+++ b/resources/js/components/SettingsImageSelector.vue
@@ -56,10 +56,10 @@