Skip to content

Commit 7d178bb

Browse files
Merge pull request #5271 from habibayman/refactor/RTE-image-upload
refactor(texteditor): replace image data urls with server uploads
2 parents f85dd61 + ab7d534 commit 7d178bb

File tree

12 files changed

+308
-235
lines changed

12 files changed

+308
-235
lines changed

contentcuration/contentcuration/frontend/channelEdit/components/AnswersEditor/AnswersEditor.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
v-model="answer.answer"
7979
class="editor"
8080
:mode="isAnswerOpen(answerIdx) ? 'edit' : 'view'"
81+
:imageProcessor="EditorImageProcessor"
8182
@update="updateAnswerText($event, answerIdx)"
8283
@minimize="emitClose"
8384
@open-editor="emitOpen(answerIdx)"
@@ -127,6 +128,7 @@
127128
import { AssessmentItemTypes } from 'shared/constants';
128129
import { swapElements } from 'shared/utils/helpers';
129130
import Checkbox from 'shared/views/form/Checkbox';
131+
import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService';
130132
131133
import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue';
132134
@@ -169,6 +171,7 @@
169171
},
170172
data() {
171173
return {
174+
EditorImageProcessor, // Make it available in the template
172175
correctAnswersIndices: getCorrectAnswersIndices(this.questionKind, this.answers),
173176
numericRule: val => floatOrIntRegex.test(val) || this.$tr('numberFieldErrorLabel'),
174177
};

contentcuration/contentcuration/frontend/channelEdit/components/AssessmentItemEditor/AssessmentItemEditor.vue

Lines changed: 112 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,119 @@
11
<template>
22

3-
<Uploader
4-
ref="uploader"
5-
:presetID="imagePreset"
6-
>
7-
<template #default="{ handleFiles }">
8-
<VLayout>
9-
<DropdownWrapper
10-
component="VFlex"
11-
xs7
12-
lg5
13-
>
14-
<template #default="{ attach, menuProps }">
15-
<VSelect
16-
:key="kindSelectKey"
17-
:items="kindSelectItems"
18-
:value="kind"
19-
:label="$tr('questionTypeLabel')"
20-
data-test="kindSelect"
21-
:menu-props="menuProps"
22-
:attach="attach"
23-
box
24-
@input="onKindUpdate"
25-
/>
26-
</template>
27-
</DropdownWrapper>
28-
</VLayout>
29-
30-
<VLayout>
31-
<VFlex>
32-
<ErrorList
33-
:errors="questionErrorMessages"
34-
data-test="questionErrors"
3+
<div>
4+
<VLayout>
5+
<DropdownWrapper
6+
component="VFlex"
7+
xs7
8+
lg5
9+
>
10+
<template #default="{ attach, menuProps }">
11+
<VSelect
12+
:key="kindSelectKey"
13+
:items="kindSelectItems"
14+
:value="kind"
15+
:label="$tr('questionTypeLabel')"
16+
data-test="kindSelect"
17+
:menu-props="menuProps"
18+
:attach="attach"
19+
box
20+
@input="onKindUpdate"
3521
/>
22+
</template>
23+
</DropdownWrapper>
24+
</VLayout>
25+
26+
<VLayout>
27+
<VFlex>
28+
<ErrorList
29+
:errors="questionErrorMessages"
30+
data-test="questionErrors"
31+
/>
32+
33+
<div class="grey--text mb-1 text--darken-2">
34+
{{ $tr('questionLabel') }}
35+
</div>
36+
37+
<transition name="fade">
38+
<keep-alive include="TipTapEditor">
39+
<!--analyticsLabel="Question"-->
40+
<TipTapEditor
41+
v-if="isQuestionOpen"
42+
v-model="question"
43+
mode="edit"
44+
:imageProcessor="EditorImageProcessor"
45+
@update="onQuestionUpdate"
46+
@minimize="closeQuestion"
47+
/>
3648

37-
<div class="grey--text mb-1 text--darken-2">
38-
{{ $tr('questionLabel') }}
39-
</div>
40-
41-
<transition name="fade">
42-
<keep-alive include="TipTapEditor">
43-
<!--analyticsLabel="Question"-->
44-
<TipTapEditor
45-
v-if="isQuestionOpen"
46-
v-model="question"
47-
mode="edit"
48-
@update="onQuestionUpdate"
49-
@minimize="closeQuestion"
50-
/>
51-
52-
<div
53-
v-else
54-
class="pb-3 pl-2 pr-2 pt-3 question-text"
55-
data-test="questionText"
56-
@click="openQuestion"
49+
<div
50+
v-else
51+
class="pb-3 pl-2 pr-2 pt-3 question-text"
52+
data-test="questionText"
53+
@click="openQuestion"
54+
>
55+
<VLayout
56+
align-start
57+
justify-space-between
5758
>
58-
<VLayout
59-
align-start
60-
justify-space-between
61-
>
62-
<VFlex grow>
63-
<TipTapEditor
64-
v-model="question"
65-
mode="view"
66-
tabindex="-1"
59+
<VFlex grow>
60+
<TipTapEditor
61+
v-model="question"
62+
mode="view"
63+
tabindex="-1"
64+
/>
65+
</VFlex>
66+
67+
<VFlex shrink>
68+
<button
69+
class="v-btn v-btn--flat v-btn--icon v-size--default"
70+
data-test="editQuestionButton"
71+
@click.stop="openQuestion"
72+
>
73+
<Icon
74+
:color="$themePalette.grey.v_800"
75+
icon="edit"
76+
class="mr-2"
6777
/>
68-
</VFlex>
69-
70-
<VFlex shrink>
71-
<button
72-
class="v-btn v-btn--flat v-btn--icon v-size--default"
73-
data-test="editQuestionButton"
74-
@click.stop="openQuestion"
75-
>
76-
<Icon
77-
:color="$themePalette.grey.v_800"
78-
icon="edit"
79-
class="mr-2"
80-
/>
81-
</button>
82-
</VFlex>
83-
</VLayout>
84-
</div>
85-
</keep-alive>
86-
</transition>
87-
</VFlex>
88-
</VLayout>
89-
90-
<VLayout
91-
v-if="kind !== AssessmentItemTypes.FREE_RESPONSE"
92-
mt-4
93-
>
94-
<VFlex>
95-
<ErrorList
96-
:errors="answersErrorMessages"
97-
data-test="answersErrors"
98-
/>
99-
100-
<AnswersEditor
101-
:questionKind="kind"
102-
:answers="answers"
103-
:openAnswerIdx="openAnswerIdx"
104-
@update="onAnswersUpdate"
105-
@open="openAnswer"
106-
@close="closeAnswer"
107-
/>
108-
109-
<HintsEditor
110-
class="mt-4"
111-
:hints="hints"
112-
:openHintIdx="openHintIdx"
113-
:handleFileUpload="handleFiles"
114-
@update="onHintsUpdate"
115-
@open="openHint"
116-
@close="closeHint"
117-
/>
118-
</VFlex>
119-
</VLayout>
120-
</template>
121-
</Uploader>
78+
</button>
79+
</VFlex>
80+
</VLayout>
81+
</div>
82+
</keep-alive>
83+
</transition>
84+
</VFlex>
85+
</VLayout>
86+
87+
<VLayout
88+
v-if="kind !== AssessmentItemTypes.FREE_RESPONSE"
89+
mt-4
90+
>
91+
<VFlex>
92+
<ErrorList
93+
:errors="answersErrorMessages"
94+
data-test="answersErrors"
95+
/>
96+
97+
<AnswersEditor
98+
:questionKind="kind"
99+
:answers="answers"
100+
:openAnswerIdx="openAnswerIdx"
101+
@update="onAnswersUpdate"
102+
@open="openAnswer"
103+
@close="closeAnswer"
104+
/>
105+
106+
<HintsEditor
107+
class="mt-4"
108+
:hints="hints"
109+
:openHintIdx="openHintIdx"
110+
@update="onHintsUpdate"
111+
@open="openHint"
112+
@close="closeHint"
113+
/>
114+
</VFlex>
115+
</VLayout>
116+
</div>
122117

123118
</template>
124119

@@ -139,10 +134,9 @@
139134
FeatureFlagKeys,
140135
} from 'shared/constants';
141136
import ErrorList from 'shared/views/ErrorList/ErrorList';
142-
import Uploader from 'shared/views/files/Uploader';
143-
import { FormatPresetsNames } from 'shared/leUtils/FormatPresets';
144137
import DropdownWrapper from 'shared/views/form/DropdownWrapper';
145138
import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue';
139+
import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService';
146140
147141
export default {
148142
name: 'AssessmentItemEditor',
@@ -151,7 +145,6 @@
151145
ErrorList,
152146
AnswersEditor,
153147
HintsEditor,
154-
Uploader,
155148
TipTapEditor,
156149
},
157150
model: {
@@ -212,6 +205,7 @@
212205
openAnswerIdx: null,
213206
kindSelectKey: 0,
214207
AssessmentItemTypes,
208+
EditorImageProcessor,
215209
};
216210
},
217211
computed: {
@@ -224,9 +218,6 @@
224218
225219
return this.item.question;
226220
},
227-
imagePreset() {
228-
return FormatPresetsNames.EXERCISE_IMAGE;
229-
},
230221
modality() {
231222
return this.getContentNode(this.nodeId)?.extra_fields?.options?.modality;
232223
},

contentcuration/contentcuration/frontend/channelEdit/components/HintsEditor/HintsEditor.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<TipTapEditor
3939
v-model="hint.hint"
4040
:mode="isHintOpen(hintIdx) ? 'edit' : 'view'"
41+
:image-processor="EditorImageProcessor"
4142
@update="updateHintText($event, hintIdx)"
4243
@minimize="emitClose"
4344
@open-editor="emitOpen(answerIdx)"
@@ -82,6 +83,7 @@
8283
import AssessmentItemToolbar from '../AssessmentItemToolbar';
8384
import { AssessmentItemToolbarActions } from '../../constants';
8485
import { swapElements } from 'shared/utils/helpers';
86+
import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService';
8587
8688
import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue';
8789
@@ -121,6 +123,7 @@
121123
AssessmentItemToolbarActions.MOVE_ITEM_DOWN,
122124
AssessmentItemToolbarActions.DELETE_ITEM,
123125
],
126+
EditorImageProcessor,
124127
};
125128
},
126129
methods: {

contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@
157157
const { isMobile } = useBreakpoint();
158158
159159
const imageHandler = useImageHandling(editor);
160+
provide('imageProcessor', props.imageProcessor);
160161
161162
const sharedEventHandlers = computed(() => ({
162163
'insert-image': target => imageHandler.openCreateModal({ targetElement: target }),
@@ -294,6 +295,10 @@
294295
type: [String, Number],
295296
default: 0,
296297
},
298+
imageProcessor: {
299+
type: Object,
300+
default: () => ({}),
301+
},
297302
},
298303
emits: ['update', 'minimize', 'open-editor'],
299304
});

contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ const MESSAGES = {
301301
},
302302

303303
// Error Messages
304+
errorUploadingImage: {
305+
message: 'Error uploading image',
306+
context: 'Title for the error modal when an image upload fails.',
307+
},
304308
noFileProvided: {
305309
message: 'No file provided.',
306310
context: 'Error message when no file is provided for upload',
@@ -321,6 +325,10 @@ const MESSAGES = {
321325
message: 'Failed to process the image file.',
322326
context: 'Error message when image processing fails',
323327
},
328+
noEnoughStorageSpace: {
329+
message: 'Not enough storage space available. File size exceeds remaining storage.',
330+
context: 'Error message when there is insufficient storage space for the file',
331+
},
324332

325333
// for MobileFormattingBar
326334
collapseFormattingBar: {

contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/image/ImageNodeView.vue

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,19 @@
122122
if (imageRef.value && imageRef.value.naturalWidth && imageRef.value.naturalHeight) {
123123
naturalAspectRatio.value = imageRef.value.naturalWidth / imageRef.value.naturalHeight;
124124
125-
// If no dimensions are set, use natural dimensions
125+
// If no dimensions are set, use natural dimensions but constrain to editor width
126126
if (!width.value && !height.value) {
127-
width.value = imageRef.value.naturalWidth;
128-
height.value = imageRef.value.naturalHeight;
127+
// Get the editor's actual container width
128+
const editorContainer = props.editor.view.dom.closest('.editor-container');
129+
const editorWidth = editorContainer
130+
? editorContainer.offsetWidth
131+
: window.innerWidth * 0.4; // fallback: 40% of viewport width
132+
133+
const maxWidth = Math.min(imageRef.value.naturalWidth, editorWidth);
134+
135+
width.value = maxWidth;
136+
height.value = Math.round(maxWidth / naturalAspectRatio.value);
137+
129138
saveSize();
130139
} else if (width.value && !height.value) {
131140
// If we have width but no height, calculate height

0 commit comments

Comments
 (0)