Skip to content

Commit

Permalink
images in options feature
Browse files Browse the repository at this point in the history
  • Loading branch information
deepansh96 committed Jul 30, 2024
1 parent 73eeaf0 commit 94f4d01
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 24 deletions.
78 changes: 75 additions & 3 deletions src/components/Editor/ItemEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
:titleConfig="addImageButtonTitleConfig"
:buttonClass="addImageButtonClass"
:isDisabled="isInteractionDisabled"
@click="showImageUploaderBox"
@click="showImageUploaderBoxForQuestion"
data-test="questionImage"
></icon-button>
</span>
Expand Down Expand Up @@ -207,6 +207,31 @@
data-test="option"
:ref="`optionText_index_${optionIndex}`"
></input-text>

<!-- add image to option text button -->

<!-- change tooltip text depending on whether an image is present or not for that option -->
<span
v-tooltip="{
content:
selectedItemOptionImagesMappedArray[optionIndex] === null
? addImageToOptionButtonTooltip
: updateImageToOptionButtonTooltip,
placement: 'left'
}"
class="p-2"
>
<icon-button
class="rounded-md w-12 h-12 disabled:opacity-50 my-auto group border pt-1"
orientation="vertical"
:iconConfig="addImageButtonIconConfig"
:titleConfig="selectedItemOptionImagesMappedArray[optionIndex] === null ? addImageToOptionButtonTitleConfig : updateImageToOptionButtonTitleConfig"
:buttonClass="addImageButtonClass"
:isDisabled="isInteractionDisabled"
@click="showImageUploaderBoxForOption(optionIndex)"
data-test="optionImage"
></icon-button>
</span>
<!-- add math to option text button -->
<span
v-tooltip="{ content: addMathButtonTooltip, placement: 'left' }"
Expand Down Expand Up @@ -496,9 +521,13 @@ export default {
this.showMathEditorPopup = true;
},
getImageSource: GenericUtilities.getImageSource,
showImageUploaderBox() {
showImageUploaderBoxForQuestion() {
// to show or hide the image uploader dialog box
this.$emit("show-image-uploader", "question");
},
showImageUploaderBoxForOption(optionIndex) {
// to show or hide the image uploader dialog box
this.$emit("show-image-uploader");
this.$emit("show-image-uploader", "option", optionIndex);
},
maxCharLimitInputKeypress(event) {
// invoked when a key is pressed in the input area for setting max limit
Expand Down Expand Up @@ -647,6 +676,27 @@ export default {
},
computed: {
// this returns an array which tells us if an image is present for the option or not. Length
// of this array is equal to the number of options. If an image is present for an option, the
// value at that index is the image url, else it is null.
selectedItemOptionImagesMappedArray() {
// a new array of the size of the options array, filled with nulls by default
// then we check if that index of the option is present as a key in this.selectedItemDetail.option_images object.
// if it is, then we pickup the value of that key and put it in the new array at that index.
if (this.selectedItemDetail.option_images == null) {
return this.selectedItemDetail.options.map(() => 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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand Down
110 changes: 101 additions & 9 deletions src/components/Items/Question/Body.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
<!-- loading spinner when question image is loading -->
<div
:class="questionImageAreaClass"
class="flex justify-center"
v-if="isImageLoading"
>
<inline-svg
Expand Down Expand Up @@ -48,24 +47,47 @@
>
<!-- each option is defined here -->
<!-- adding <label> so that touch input is just not limited to the radio/checkbox button -->
<label :class="labelClass(option)">
<label :class="labelClass(option, optionIndex)">
<!-- understand the meaning of the keys here:
https://www.w3schools.com/tags/att_input_type_radio.asp -->
<input
:type="optionInputType"
:value="option"
class="place-self-center text-primary focus:ring-0"
class="place-self-center text-primary focus:ring-0 mb-auto"
style="box-shadow: none"
@click="selectOption(optionIndex)"
:checked="isOptionMarked(optionIndex)"
:disabled="isAnswerSubmitted || previewMode"
:data-test="`optionSelector-${optionIndex}`"
/>
<div
class="ml-2 h-full place-self-center leading-tight"
class="ml-2 h-full place-self-center leading-tight w-full"
:data-test="`option-${optionIndex}`"
>
<span v-html="latexFormattedOptionText[optionIndex]"></span>

<!-- OPTION IMAGE AREA -->
<!-- loading spinner when question image is loading -->
<div
:class="optionImageAreaClass"
v-if="areOptionImagesLoading[optionIndex]"
>
<inline-svg
:src="require('@/assets/images/spinner-solid.svg')"
class="animate-spin h-4 object-scale-down"
></inline-svg>
</div>
<!-- option image container -->
<div :class="optionImagesContainerClass[optionIndex]" v-if="optionImagesPresentList[optionIndex]" >
<img
:src="optionImages[optionIndex].url"
class="object-contain h-full w-full"
:alt="optionImages[optionIndex].alt_text"
:ref="`optionImage_${optionIndex}`"
:class="{ invisible: areOptionImagesLoading[optionIndex] }"
@load="specificOptionImageLoaded(optionIndex)"
/>
</div>
</div>
</label>
</div>
Expand Down Expand Up @@ -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: {
Expand All @@ -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: {
Expand All @@ -159,6 +183,10 @@ export default {
default: () => [],
type: Array,
},
optionImages: {
default: null,
type: Object,
},
correctAnswer: {
default: null,
type: [Number, Array],
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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\\)?".
Expand Down Expand Up @@ -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
Expand All @@ -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 [
Expand Down
6 changes: 6 additions & 0 deletions src/components/Player/ItemModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<item-question-body
:questionText="questionText"
:options="questionOptions"
:optionImages="questionOptionImages"
:correctAnswer="questionCorrectAnswer"
:isAnswerSubmitted="isAnswerSubmitted"
:isSurveyQuestion="isSelectedItemSurveyQuestion"
Expand Down Expand Up @@ -269,6 +270,11 @@ export default {
if (this.currentItemDetails == undefined) return null;
return this.currentItemDetails["options"];
},
questionOptionImages() {
if (this.currentItemDetails == undefined) return null;
if (Object.keys(this.currentItemDetails).length == 0) return null;
return this.currentItemDetails["option_images"];
},
/** correct answer for the question */
questionCorrectAnswer() {
if (this.currentItemDetails == undefined) return null;
Expand Down
Loading

0 comments on commit 94f4d01

Please sign in to comment.