Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
import translator from '../../translator';
import { updateAnswersToQuestionType, assessmentItemKey } from '../../utils';
import { AssessmentItemTypeLabels } from '../../constants';
import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService';
import {
ContentModalities,
AssessmentItemTypes,
Expand All @@ -136,7 +137,6 @@
import ErrorList from 'shared/views/ErrorList/ErrorList';
import DropdownWrapper from 'shared/views/form/DropdownWrapper';
import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue';
import EditorImageProcessor from 'shared/views/TipTapEditor/TipTapEditor/services/imageService';

export default {
name: 'AssessmentItemEditor',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ const MESSAGES = {
message: 'Special Characters',
context: 'Title for the menu containing special characters and mathematical symbols.',
},
loadingFormulas: {
message: 'Loading math editor',
context: 'Text displayed while the math editor is being loaded.',
},

// Error Messages
errorUploadingImage: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,13 @@
<!-- More dropdown - only visible when there are overflow categories -->
<div
v-if="overflowCategories.length > 0"
ref="moreDropdownContainer"
class="more-dropdown-container"
role="group"
:aria-label="'More options'"
>
<ToolbarButton
ref="moreButton"
:title="'More options'"
:icon="require('../../assets/icon-chevron-down.svg')"
:is-active="isMoreDropdownOpen"
Expand All @@ -150,8 +152,9 @@
/>

<div
v-if="isMoreDropdownOpen"
v-show="isMoreDropdownOpen"
id="more-options-menu"
ref="moreDropdown"
class="more-dropdown"
role="menu"
:aria-label="'Additional formatting options'"
Expand Down Expand Up @@ -302,6 +305,9 @@
},
setup(props, { emit }) {
const toolbarRef = ref(null);
const moreButton = ref(null);
const moreDropdown = ref(null);
const moreDropdownContainer = ref(null);
const isMoreDropdownOpen = ref(false);
const toolbarWidth = ref(0);

Expand Down Expand Up @@ -360,20 +366,24 @@

const updateToolbarWidth = () => {
if (toolbarRef.value) {
toolbarWidth.value = toolbarRef.value.offsetWidth;
// Batch layout reads in next frame
requestAnimationFrame(() => {
toolbarWidth.value = toolbarRef.value.offsetWidth;
});
}
};

const handleResize = entries => {
setTimeout(() => {
// Use ResizeObserver data directly - no DOM reading
requestAnimationFrame(() => {
for (const entry of entries) {
toolbarWidth.value = entry.contentRect.width;
}
}, 0);
});
};

const handleWindowResize = () => {
setTimeout(updateToolbarWidth, 0);
requestAnimationFrame(updateToolbarWidth);
};

const onToolClick = (tool, event) => {
Expand Down Expand Up @@ -405,14 +415,11 @@
isMoreDropdownOpen.value = false;
// Return focus to the more button
await nextTick();
const moreButton = toolbarRef.value?.querySelector(
'.more-dropdown-container [role="button"]',
);
moreButton?.focus();
moreButton.value?.$el?.focus();
} else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
const menuItems = Array.from(
event.currentTarget.querySelectorAll('[role="menuitem"]:not(:disabled)'),
moreDropdown.value?.querySelectorAll('[role="menuitem"]:not(:disabled)') || [],
);
const currentIndex = menuItems.indexOf(document.activeElement);

Expand All @@ -429,28 +436,29 @@

// Close dropdown when clicking outside
const handleClickOutside = event => {
const dropdown = event.target.closest('.more-dropdown-container');
if (!dropdown) {
if (moreDropdownContainer.value && !moreDropdownContainer.value.contains(event.target)) {
isMoreDropdownOpen.value = false;
}
};

onMounted(async () => {
await nextTick();

// Initial width measurement
updateToolbarWidth();
// Initial width measurement in next frame
requestAnimationFrame(() => {
updateToolbarWidth();
});

// Set up resize observer
if (toolbarRef.value && window.ResizeObserver) {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(toolbarRef.value);
} else {
// Fallback to window resize listener
window.addEventListener('resize', handleWindowResize);
// Fallback to window resize listener with passive flag
window.addEventListener('resize', handleWindowResize, { passive: true });
}

document.addEventListener('click', handleClickOutside);
document.addEventListener('click', handleClickOutside, { passive: true });
});

onUnmounted(() => {
Expand All @@ -464,6 +472,9 @@

return {
toolbarRef,
moreButton,
moreDropdown,
moreDropdownContainer,
isMoreDropdownOpen,
visibleCategories,
overflowCategories,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<NodeViewWrapper class="image-node-wrapper">
<div
ref="containerRef"
class="image-node-view"
:class="{ 'is-selected': selected && editor.isEditable }"
:style="{ width: styleWidth }"
Expand Down Expand Up @@ -47,6 +48,7 @@

<div
v-if="editor.isEditable"
ref="resizeHandleRef"
class="resize-handle"
tabindex="0"
role="slider"
Expand Down Expand Up @@ -74,13 +76,18 @@
components: {
NodeViewWrapper,
},

setup(props) {
const width = ref(props.node.attrs.width || null);
const height = ref(props.node.attrs.height || null);
const imageRef = ref(null);
const containerRef = ref(null);
const resizeHandleRef = ref(null);
const naturalAspectRatio = ref(null);
const minWidth = 50;
const compactThreshold = 200;
const debounceTimer = null;
let resizeListeners = null;

// Create debounced version of saveSize function
const debouncedSaveSize = _.debounce(() => {
Expand Down Expand Up @@ -149,6 +156,7 @@
};

const isRtl = computed(() => {
// Cache the RTL check result to avoid repeated DOM traversal
return props.editor.view.dom.closest('[dir="rtl"]') !== null;
});

Expand Down Expand Up @@ -176,7 +184,7 @@

const onResizeStart = startEvent => {
const startX = startEvent.clientX;
const startWidth = width.value || startEvent.target.parentElement.offsetWidth;
const startWidth = width.value || containerRef.value.offsetWidth;

const onMouseMove = moveEvent => {
const deltaX = moveEvent.clientX - startX;
Expand All @@ -186,23 +194,39 @@
: startWidth + deltaX; // In LTR, moving right should increase width

const clampedWidth = Math.max(minWidth, newWidth);
width.value = clampedWidth;
height.value = calculateProportionalHeight(clampedWidth);

// Batch DOM updates
requestAnimationFrame(() => {
width.value = clampedWidth;
height.value = calculateProportionalHeight(clampedWidth);
});
};

const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Clean up listeners
if (resizeListeners) {
document.removeEventListener('mousemove', resizeListeners.move);
document.removeEventListener('mouseup', resizeListeners.up);
resizeListeners = null;
}
saveSize();
};

document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
// Store listeners for proper cleanup
resizeListeners = {
move: onMouseMove,
up: onMouseUp,
};

// Use passive listeners where possible
document.addEventListener('mousemove', onMouseMove, { passive: true });
document.addEventListener('mouseup', onMouseUp, { passive: true });
};

const onResizeKeyDown = event => {
const step = 10;
const currentWidth = width.value || event.target.parentElement.offsetWidth;
// Use ref instead of DOM query
const currentWidth = width.value || containerRef.value.offsetWidth;
let newWidth = currentWidth;

// Invert keyboard controls for RTL
Expand All @@ -214,7 +238,8 @@
} else if (event.key === leftKey) {
newWidth = currentWidth - step;
} else if (event.key === 'Escape' || event.key === 'Enter') {
event.target.blur();
// Use ref instead of DOM query
resizeHandleRef.value?.blur();
const endPosition = props.getPos() + props.node.nodeSize;
props.editor.chain().focus().insertContentAt(endPosition, { type: 'paragraph' }).run();
return;
Expand All @@ -223,8 +248,12 @@
}

const clampedWidth = Math.max(minWidth, newWidth);
width.value = clampedWidth;
height.value = calculateProportionalHeight(clampedWidth);

// Batch DOM updates
requestAnimationFrame(() => {
width.value = clampedWidth;
height.value = calculateProportionalHeight(clampedWidth);
});

debouncedSaveSize();
};
Expand All @@ -251,13 +280,21 @@
});

onUnmounted(() => {
// Cancel any pending debounced calls
debouncedSaveSize.cancel();
clearTimeout(debounceTimer);

// Clean up any remaining resize listeners
if (resizeListeners) {
document.removeEventListener('mousemove', resizeListeners.move);
document.removeEventListener('mouseup', resizeListeners.up);
resizeListeners = null;
}
});

return {
width,
imageRef,
containerRef,
resizeHandleRef,
styleWidth,
onResizeStart,
removeImage,
Expand Down
Loading