Skip to content

Commit 2778563

Browse files
Merge pull request #5304 from habibayman/refactor/RTE-performance
refactor(texteditor): lazy load heavy extensions and general optimization
2 parents 7d178bb + d0c08f2 commit 2778563

File tree

9 files changed

+357
-89
lines changed

9 files changed

+357
-89
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
import translator from '../../translator';
128128
import { updateAnswersToQuestionType, assessmentItemKey } from '../../utils';
129129
import { AssessmentItemTypeLabels } from '../../constants';
130+
import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService';
130131
import {
131132
ContentModalities,
132133
AssessmentItemTypes,
@@ -136,7 +137,6 @@
136137
import ErrorList from 'shared/views/ErrorList/ErrorList';
137138
import DropdownWrapper from 'shared/views/form/DropdownWrapper';
138139
import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue';
139-
import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService';
140140
141141
export default {
142142
name: 'AssessmentItemEditor',

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ const MESSAGES = {
299299
message: 'Special Characters',
300300
context: 'Title for the menu containing special characters and mathematical symbols.',
301301
},
302+
loadingFormulas: {
303+
message: 'Loading math editor',
304+
context: 'Text displayed while the math editor is being loaded.',
305+
},
302306

303307
// Error Messages
304308
errorUploadingImage: {

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

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,13 @@
135135
<!-- More dropdown - only visible when there are overflow categories -->
136136
<div
137137
v-if="overflowCategories.length > 0"
138+
ref="moreDropdownContainer"
138139
class="more-dropdown-container"
139140
role="group"
140141
:aria-label="'More options'"
141142
>
142143
<ToolbarButton
144+
ref="moreButton"
143145
:title="'More options'"
144146
:icon="require('../../assets/icon-chevron-down.svg')"
145147
:is-active="isMoreDropdownOpen"
@@ -150,8 +152,9 @@
150152
/>
151153

152154
<div
153-
v-if="isMoreDropdownOpen"
155+
v-show="isMoreDropdownOpen"
154156
id="more-options-menu"
157+
ref="moreDropdown"
155158
class="more-dropdown"
156159
role="menu"
157160
:aria-label="'Additional formatting options'"
@@ -302,6 +305,9 @@
302305
},
303306
setup(props, { emit }) {
304307
const toolbarRef = ref(null);
308+
const moreButton = ref(null);
309+
const moreDropdown = ref(null);
310+
const moreDropdownContainer = ref(null);
305311
const isMoreDropdownOpen = ref(false);
306312
const toolbarWidth = ref(0);
307313
@@ -360,20 +366,24 @@
360366
361367
const updateToolbarWidth = () => {
362368
if (toolbarRef.value) {
363-
toolbarWidth.value = toolbarRef.value.offsetWidth;
369+
// Batch layout reads in next frame
370+
requestAnimationFrame(() => {
371+
toolbarWidth.value = toolbarRef.value.offsetWidth;
372+
});
364373
}
365374
};
366375
367376
const handleResize = entries => {
368-
setTimeout(() => {
377+
// Use ResizeObserver data directly - no DOM reading
378+
requestAnimationFrame(() => {
369379
for (const entry of entries) {
370380
toolbarWidth.value = entry.contentRect.width;
371381
}
372-
}, 0);
382+
});
373383
};
374384
375385
const handleWindowResize = () => {
376-
setTimeout(updateToolbarWidth, 0);
386+
requestAnimationFrame(updateToolbarWidth);
377387
};
378388
379389
const onToolClick = (tool, event) => {
@@ -405,14 +415,11 @@
405415
isMoreDropdownOpen.value = false;
406416
// Return focus to the more button
407417
await nextTick();
408-
const moreButton = toolbarRef.value?.querySelector(
409-
'.more-dropdown-container [role="button"]',
410-
);
411-
moreButton?.focus();
418+
moreButton.value?.$el?.focus();
412419
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
413420
event.preventDefault();
414421
const menuItems = Array.from(
415-
event.currentTarget.querySelectorAll('[role="menuitem"]:not(:disabled)'),
422+
moreDropdown.value?.querySelectorAll('[role="menuitem"]:not(:disabled)') || [],
416423
);
417424
const currentIndex = menuItems.indexOf(document.activeElement);
418425
@@ -429,28 +436,29 @@
429436
430437
// Close dropdown when clicking outside
431438
const handleClickOutside = event => {
432-
const dropdown = event.target.closest('.more-dropdown-container');
433-
if (!dropdown) {
439+
if (moreDropdownContainer.value && !moreDropdownContainer.value.contains(event.target)) {
434440
isMoreDropdownOpen.value = false;
435441
}
436442
};
437443
438444
onMounted(async () => {
439445
await nextTick();
440446
441-
// Initial width measurement
442-
updateToolbarWidth();
447+
// Initial width measurement in next frame
448+
requestAnimationFrame(() => {
449+
updateToolbarWidth();
450+
});
443451
444452
// Set up resize observer
445453
if (toolbarRef.value && window.ResizeObserver) {
446454
resizeObserver = new ResizeObserver(handleResize);
447455
resizeObserver.observe(toolbarRef.value);
448456
} else {
449-
// Fallback to window resize listener
450-
window.addEventListener('resize', handleWindowResize);
457+
// Fallback to window resize listener with passive flag
458+
window.addEventListener('resize', handleWindowResize, { passive: true });
451459
}
452460
453-
document.addEventListener('click', handleClickOutside);
461+
document.addEventListener('click', handleClickOutside, { passive: true });
454462
});
455463
456464
onUnmounted(() => {
@@ -464,6 +472,9 @@
464472
465473
return {
466474
toolbarRef,
475+
moreButton,
476+
moreDropdown,
477+
moreDropdownContainer,
467478
isMoreDropdownOpen,
468479
visibleCategories,
469480
overflowCategories,

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

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<NodeViewWrapper class="image-node-wrapper">
44
<div
5+
ref="containerRef"
56
class="image-node-view"
67
:class="{ 'is-selected': selected && editor.isEditable }"
78
:style="{ width: styleWidth }"
@@ -47,6 +48,7 @@
4748

4849
<div
4950
v-if="editor.isEditable"
51+
ref="resizeHandleRef"
5052
class="resize-handle"
5153
tabindex="0"
5254
role="slider"
@@ -74,13 +76,18 @@
7476
components: {
7577
NodeViewWrapper,
7678
},
79+
7780
setup(props) {
7881
const width = ref(props.node.attrs.width || null);
7982
const height = ref(props.node.attrs.height || null);
8083
const imageRef = ref(null);
84+
const containerRef = ref(null);
85+
const resizeHandleRef = ref(null);
8186
const naturalAspectRatio = ref(null);
8287
const minWidth = 50;
8388
const compactThreshold = 200;
89+
const debounceTimer = null;
90+
let resizeListeners = null;
8491
8592
// Create debounced version of saveSize function
8693
const debouncedSaveSize = _.debounce(() => {
@@ -149,6 +156,7 @@
149156
};
150157
151158
const isRtl = computed(() => {
159+
// Cache the RTL check result to avoid repeated DOM traversal
152160
return props.editor.view.dom.closest('[dir="rtl"]') !== null;
153161
});
154162
@@ -176,7 +184,7 @@
176184
177185
const onResizeStart = startEvent => {
178186
const startX = startEvent.clientX;
179-
const startWidth = width.value || startEvent.target.parentElement.offsetWidth;
187+
const startWidth = width.value || containerRef.value.offsetWidth;
180188
181189
const onMouseMove = moveEvent => {
182190
const deltaX = moveEvent.clientX - startX;
@@ -186,23 +194,39 @@
186194
: startWidth + deltaX; // In LTR, moving right should increase width
187195
188196
const clampedWidth = Math.max(minWidth, newWidth);
189-
width.value = clampedWidth;
190-
height.value = calculateProportionalHeight(clampedWidth);
197+
198+
// Batch DOM updates
199+
requestAnimationFrame(() => {
200+
width.value = clampedWidth;
201+
height.value = calculateProportionalHeight(clampedWidth);
202+
});
191203
};
192204
193205
const onMouseUp = () => {
194-
document.removeEventListener('mousemove', onMouseMove);
195-
document.removeEventListener('mouseup', onMouseUp);
206+
// Clean up listeners
207+
if (resizeListeners) {
208+
document.removeEventListener('mousemove', resizeListeners.move);
209+
document.removeEventListener('mouseup', resizeListeners.up);
210+
resizeListeners = null;
211+
}
196212
saveSize();
197213
};
198214
199-
document.addEventListener('mousemove', onMouseMove);
200-
document.addEventListener('mouseup', onMouseUp);
215+
// Store listeners for proper cleanup
216+
resizeListeners = {
217+
move: onMouseMove,
218+
up: onMouseUp,
219+
};
220+
221+
// Use passive listeners where possible
222+
document.addEventListener('mousemove', onMouseMove, { passive: true });
223+
document.addEventListener('mouseup', onMouseUp, { passive: true });
201224
};
202225
203226
const onResizeKeyDown = event => {
204227
const step = 10;
205-
const currentWidth = width.value || event.target.parentElement.offsetWidth;
228+
// Use ref instead of DOM query
229+
const currentWidth = width.value || containerRef.value.offsetWidth;
206230
let newWidth = currentWidth;
207231
208232
// Invert keyboard controls for RTL
@@ -214,7 +238,8 @@
214238
} else if (event.key === leftKey) {
215239
newWidth = currentWidth - step;
216240
} else if (event.key === 'Escape' || event.key === 'Enter') {
217-
event.target.blur();
241+
// Use ref instead of DOM query
242+
resizeHandleRef.value?.blur();
218243
const endPosition = props.getPos() + props.node.nodeSize;
219244
props.editor.chain().focus().insertContentAt(endPosition, { type: 'paragraph' }).run();
220245
return;
@@ -223,8 +248,12 @@
223248
}
224249
225250
const clampedWidth = Math.max(minWidth, newWidth);
226-
width.value = clampedWidth;
227-
height.value = calculateProportionalHeight(clampedWidth);
251+
252+
// Batch DOM updates
253+
requestAnimationFrame(() => {
254+
width.value = clampedWidth;
255+
height.value = calculateProportionalHeight(clampedWidth);
256+
});
228257
229258
debouncedSaveSize();
230259
};
@@ -251,13 +280,21 @@
251280
});
252281
253282
onUnmounted(() => {
254-
// Cancel any pending debounced calls
255-
debouncedSaveSize.cancel();
283+
clearTimeout(debounceTimer);
284+
285+
// Clean up any remaining resize listeners
286+
if (resizeListeners) {
287+
document.removeEventListener('mousemove', resizeListeners.move);
288+
document.removeEventListener('mouseup', resizeListeners.up);
289+
resizeListeners = null;
290+
}
256291
});
257292
258293
return {
259294
width,
260295
imageRef,
296+
containerRef,
297+
resizeHandleRef,
261298
styleWidth,
262299
onResizeStart,
263300
removeImage,

0 commit comments

Comments
 (0)