From 94f4d01775b81690bc5bf39ac4b92ae8d134be81 Mon Sep 17 00:00:00 2001 From: Deepansh Mathur Date: Tue, 30 Jul 2024 22:51:59 +0530 Subject: [PATCH] images in options feature --- src/components/Editor/ItemEditor.vue | 78 +++++++++++++++++- src/components/Items/Question/Body.vue | 110 +++++++++++++++++++++++-- src/components/Player/ItemModal.vue | 6 ++ src/pages/Editor.vue | 106 +++++++++++++++++++++--- 4 files changed, 276 insertions(+), 24 deletions(-) diff --git a/src/components/Editor/ItemEditor.vue b/src/components/Editor/ItemEditor.vue index 1a4738a1..dbd6ff56 100644 --- a/src/components/Editor/ItemEditor.vue +++ b/src/components/Editor/ItemEditor.vue @@ -151,7 +151,7 @@ :titleConfig="addImageButtonTitleConfig" :buttonClass="addImageButtonClass" :isDisabled="isInteractionDisabled" - @click="showImageUploaderBox" + @click="showImageUploaderBoxForQuestion" data-test="questionImage" > @@ -207,6 +207,31 @@ data-test="option" :ref="`optionText_index_${optionIndex}`" > + + + + + + + null); + } else { + return this.selectedItemDetail.options.map((option, index) => { + if (index in this.selectedItemDetail.option_images) { + return this.selectedItemDetail.option_images[index]; + } else { + return null; + } + }); + } + + }, textToSendToMathField() { // returns the text to be sent to the math field if (this.mathEditorTarget == "questionText") return this.questionText; @@ -702,6 +752,16 @@ export default { ? this.$t("tooltip.editor.item_editor.buttons.update_image.disabled") : this.$t("tooltip.editor.item_editor.buttons.update_image.enabled"); }, + addImageToOptionButtonTooltip() { + return this.isInteractionDisabled + ? this.$t("tooltip.editor.item_editor.buttons.add_image.disabled") + : this.$t("tooltip.editor.item_editor.buttons.add_image.enabled"); + }, + updateImageToOptionButtonTooltip() { + return this.isInteractionDisabled + ? this.$t("tooltip.editor.item_editor.buttons.update_image.disabled") + : this.$t("tooltip.editor.item_editor.buttons.update_image.enabled"); + }, addImageButtonTitleConfig() { // title config for the add image button return { @@ -712,6 +772,18 @@ export default { "text-xs group-hover:text-white group-disabled:text-black text-black font-normal", }; }, + addImageToOptionButtonTitleConfig() { + return { + value: this.$t("editor.item_editor.image_upload.add_image"), + class: "text-xs group-hover:text-white group-disabled:text-black text-black font-normal", + } + }, + updateImageToOptionButtonTitleConfig() { + return { + value: this.$t("editor.item_editor.image_upload.edit_image"), + class: "text-xs group-hover:text-white group-disabled:text-black text-black font-normal", + } + }, isQuestionImagePresent() { // if the current selected item has an image present return this.selectedItemDetail.image != null; diff --git a/src/components/Items/Question/Body.vue b/src/components/Items/Question/Body.vue index 81dab1af..e731b7ba 100644 --- a/src/components/Items/Question/Body.vue +++ b/src/components/Items/Question/Body.vue @@ -12,7 +12,6 @@
-
@@ -124,6 +146,7 @@ export default { surveyAnswerClass: "bg-gray-200", correctOptionClass: "text-white bg-green-500", wrongOptionClass: "text-white bg-red-500", + areOptionImagesLoading: new Array(this.options.length).fill(false), // an array mapped to number of options, tells us the loading state of each option image }; }, watch: { @@ -149,6 +172,7 @@ export default { async created() { this.subjectiveAnswer = this.defaultAnswer; if (this.isQuestionImagePresent) this.startImageLoading(); + if (this.optionImagesPresentList.includes(true)) this.startOptionImagesLoading(); }, props: { questionText: { @@ -159,6 +183,10 @@ export default { default: () => [], type: Array, }, + optionImages: { + default: null, + type: Object, + }, correctAnswer: { default: null, type: [Number, Array], @@ -221,14 +249,38 @@ export default { // stop the loading spinner when the image has been loaded this.isImageLoading = false; }, + startOptionImagesLoading() { + this.optionImagesPresentList.forEach((option, index) => { + if (option) { + this.areOptionImagesLoading[index] = true; + } + }); + }, + specificOptionImageLoaded(optionIndex) { + this.areOptionImagesLoading[optionIndex] = false; + }, checkCharLimit(event) { // checks if character limit is reached in case it is set if (!this.hasCharLimit) return; if (!this.charactersLeft) event.preventDefault(); }, - - labelClass(optionText) { - return [{ "h-4 sm:h-5": optionText == "" }, "flex content-center"]; + isImagePresentAtOptionIndex(optionIndex) { + if (this.optionImages == null) return false; + if (!(optionIndex in this.optionImages)) return false; + return this.optionImages[optionIndex] != null; + }, + labelClass( + optionText, + optionIndex + ) { + return [ + { + "h-4 sm:h-5": optionText == "" && !this.isImagePresentAtOptionIndex(optionIndex), + "h-4 sm:h-5": optionText != "" && !this.isImagePresentAtOptionIndex(optionIndex), + "h-full": this.isImagePresentAtOptionIndex(optionIndex), + }, + "flex content-center" + ]; }, selectOption(optionIndex) { // invoked when an option is selected @@ -261,6 +313,18 @@ export default { }, }, computed: { + // an array of booleans which tells us the loading state of each option image + optionImagesLoadingState() { + return this.options.map((option, index) => { + return this.isImagePresentAtOptionIndex(index); + }); + }, + // an array of booleans which tells us whether the image is present at each option index + optionImagesPresentList() { + return this.options.map((option, index) => { + return this.isImagePresentAtOptionIndex(index); + }); + }, latexFormattedQuestionText() { // we're getting a prop called "questionText". This is a string which may contain latex code and // might look like this - "What is the value of \\(x\\) in the equation \\(x^2 + 2x + 1 = 0\\)?". @@ -333,13 +397,29 @@ export default { }, questionImageAreaClass() { // styling class for the question image and loading spinner containers - return { + return [ + { "h-56 mb-4": !this.previewMode && this.isPortrait, "h-28 sm:h-36 md:h-48 lg:h-56 xl:h-80 w-1/2": !this.isPortrait && !this.previewMode, "h-20 bp-360:h-24 bp-420:h-28 bp-500:h-36 sm:h-48 md:h-24 lg:h-32 xl:h-40 w-1/2": this .previewMode, - }; + }, + "flex justify-center items-center" + ] + }, + optionImageAreaClass() { + // styling class for the option image and loading spinner containers + return [ + { + "h-56 mb-4": !this.previewMode && this.isPortrait, + "h-28 sm:h-36 md:h-48 lg:h-56 xl:h-80": + !this.isPortrait && !this.previewMode, + "h-20 bp-360:h-24 bp-420:h-28 bp-500:h-36 sm:h-48 md:h-24 lg:h-32 xl:h-40": this + .previewMode, + }, + "flex justify-center items-center" + ] }, questionImageContainerClass() { // styling class for the image container @@ -351,6 +431,18 @@ export default { "border rounded-md", ]; }, + optionImagesContainerClass() { + // styling class for the image containers of all options + return this.options.map((option, index) => { + return [ + this.questionImageAreaClass, + { + hidden: this.areOptionImagesLoading[index], + }, + "rounded-md w-full", + ]; + }); + }, orientationClass() { // styling class to decide orientation of image + options depending on portrait/landscape orientation return [ diff --git a/src/components/Player/ItemModal.vue b/src/components/Player/ItemModal.vue index a68e0be4..973d6e83 100644 --- a/src/components/Player/ItemModal.vue +++ b/src/components/Player/ItemModal.vue @@ -18,6 +18,7 @@ { - this.currentItemDetail.image = response.data; + if (this.imageUploaderMode == "question") { + this.currentItemDetail.image = response.data; + } else if (this.imageUploaderMode == "option") { + if (this.currentItemDetail.option_images == null) { + this.currentItemDetail.option_images = {}; + } + this.currentItemDetail.option_images[this.imageUploaderOptionIndex] = + response.data; + } this.stopLoading(); }); }, /** * toggles the visibility of the image uploader dialog box */ - toggleImageUploaderBox() { + toggleImageUploaderBox(mode = null, optionIndex = -1) { + if (this.isImageUploaderDialogShown) { + // pass + } else { + // dialog was hidden and now we wanna show it. + if (mode != null && mode == "question") { + this.imageUploaderMode = "question"; + this.imageUploaderOptionIndex = -1; + } else if (mode != null && mode == "option") { + this.imageUploaderMode = "option"; + this.imageUploaderOptionIndex = optionIndex; + } + } this.isImageUploaderDialogShown = !this.isImageUploaderDialogShown; }, /** @@ -1895,10 +1948,10 @@ export default { const seconds = Math.floor(timestamp % 60); const milliseconds = Math.floor((timestamp % 1) * 1000); - const formattedHours = String(hours).padStart(2, '0'); - const formattedMinutes = String(minutes).padStart(2, '0'); - const formattedSeconds = String(seconds).padStart(2, '0'); - const formattedMilliseconds = String(milliseconds).padStart(3, '0'); + const formattedHours = String(hours).padStart(2, "0"); + const formattedMinutes = String(minutes).padStart(2, "0"); + const formattedSeconds = String(seconds).padStart(2, "0"); + const formattedMilliseconds = String(milliseconds).padStart(3, "0"); if (hours > 0) { return `${formattedHours}:${formattedMinutes}:${formattedSeconds}:${formattedMilliseconds}`; @@ -1915,7 +1968,8 @@ export default { updatePlayerTimestamp(timestamp) { // custom updation of time shown on the plyr embed this.player.currentTime = timestamp; - if (this.$refs.videoPlayer != null) this.$refs.videoPlayer.timeDiv.innerHTML = this.formatTimestamp(timestamp); + if (this.$refs.videoPlayer != null) + this.$refs.videoPlayer.timeDiv.innerHTML = this.formatTimestamp(timestamp); }, /** * invoked when the time slider is updated @@ -2144,6 +2198,16 @@ export default { if ("image" in payload && payload["image"] != undefined) { payloadClone["image"] = payload["image"]["id"]; } + if ( + "option_images" in payload && + payload["option_images"] != null && + Object.keys(payload["option_images"]).length > 0 + ) { + payloadClone["option_images"] = {}; + for (const [key, value] of Object.entries(payload["option_images"])) { + payloadClone["option_images"][key] = value["id"]; + } + } await QuestionAPIService.updateQuestion(id, payloadClone); }, /** @@ -2231,12 +2295,30 @@ export default { deleteSelectedOption() { // delete the option this.currentItemDetail.options.splice(this.optionIndexToDelete, 1); + + // handle option image removal and re-indexing + if ( + this.currentItemDetail.option_images != null && + this.optionIndexToDelete in this.currentItemDetail.option_images + ) { + delete this.currentItemDetail.option_images[this.optionIndexToDelete]; + Object.entries(this.currentItemDetail.option_images).forEach( + ([key, value]) => { + if (key > this.optionIndexToDelete) { + this.currentItemDetail.option_images[key - 1] = value; + delete this.currentItemDetail.option_images[key]; + } + } + ); + } + if (this.isQuestionTypeMCQ) { if (this.optionIndexToDelete == this.correctAnswer) { // if the deleted option was the correct answer, reset the correct answer this.currentItemDetail.correct_answer = 0; - } else if (this.correctAnswer > this.optionIndexToDelete) + } else if (this.correctAnswer > this.optionIndexToDelete) { this.currentItemDetail.correct_answer -= 1; + } } else if (this.isQuestionTypeCheckbox) { if (this.correctAnswer.indexOf(this.optionIndexToDelete) != -1) { // remove the deleted option from the list of correct answers