Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b19b72c
feat(texteditor): implement code block action
habibayman Jun 14, 2025
4bbc477
implement super/sub script actions
habibayman Jun 14, 2025
5e31d02
test - feat(texteditor): simplified image handling logic
habibayman Jun 16, 2025
90c872a
feat(texteditor): implement full image handling actions
habibayman Jun 17, 2025
776d017
feat(texteditor)[image]: enhance a11y
habibayman Jun 29, 2025
01e8fee
fix(texteditor)[image]: adjust insertion modal to a popover
habibayman Jun 29, 2025
c51cb1c
feat(teteditor)[image]: error handling multi-file drop
habibayman Jun 29, 2025
f30ce26
feat(texteditor)[code]: implement syntax highlighting
habibayman Jun 29, 2025
3a2c387
feat(texteditor)[unformat]: implement remove all format button
habibayman Jun 30, 2025
60db7f5
test - implement link handling
habibayman Jul 2, 2025
89d9a6e
feat(texteditor): wrap strings for i18n
habibayman Jul 3, 2025
ff3f86d
fix(tests): attempt to fix dependency issues
habibayman Jul 3, 2025
04fae14
feat(texteditor)[link]: improve link a11y
habibayman Jul 3, 2025
e1967b6
fix(texteditor): minor fixes
habibayman Jul 3, 2025
ecc5b28
fix(texteditor)[link]: edit link bug
habibayman Jul 6, 2025
3a266ee
fix(texteditor): replace bubblemenu extensions to solve dependency co…
habibayman Jul 6, 2025
5c0376e
config: add mathlive dependency
habibayman Jul 8, 2025
da858ef
feat(texteditor)[math]: initial implementation formula editor
habibayman Jul 8, 2025
207a0d8
fix: lint trailing white space
habibayman Jul 8, 2025
d577651
feat(texteditor)[math]: adjust menu opening position
habibayman Jul 8, 2025
a84511c
refactor(texteditor)[image]: no prefill alt text with img name
habibayman Jul 8, 2025
7c3083c
feat(texteditor)[math]: config mathlive for i18n
habibayman Jul 8, 2025
1538c74
feat(texteditor)[math]: add placeholders for some formulas
habibayman Jul 8, 2025
48d44ae
fix: lint trailing line
habibayman Jul 8, 2025
462843e
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jul 8, 2025
bfa49fb
fix(texteditor)[i18n]: translate image service errors
habibayman Jul 9, 2025
acd63af
fix(texteditor)[image]: resize handle for rtl
habibayman Jul 9, 2025
273ab33
Merge branch 'unstable' into feat/RTE-insertion
habibayman Jul 11, 2025
b5ea065
fix merge conflicts
habibayman Jul 11, 2025
0bb4568
fix(texteditor)[copy]: failure to copy math elements
habibayman Jul 14, 2025
e14e2da
feat(texteditor)[math]: translate formulas
habibayman Jul 14, 2025
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
@@ -1,32 +1,151 @@
<template>

<div class="editor-container">
<EditorToolbar />
<EditorContentWrapper />
<EditorToolbar
@insert-image="target => openCreateModal({ targetElement: target })"
@insert-link="linkHandler.openLinkEditor()"
@insert-math="target => mathHandler.openCreateMathModal({ targetElement: target })"
/>

<div
v-if="linkHandler.isBubbleMenuOpen.value"
:style="linkHandler.popoverStyle.value"
>
<LinkBubbleMenu
v-if="isReady"
:editor="editor"
/>
</div>

<div
v-if="linkHandler.isEditorOpen.value"
:style="linkHandler.popoverStyle.value"
>
<LinkEditor
:mode="linkHandler.editorMode.value"
:initial-state="linkHandler.editorInitialState.value"
@save="linkHandler.saveLink"
@remove="linkHandler.removeLink"
@close="linkHandler.closeLinkEditor"
/>
</div>

<div
v-if="modalMode"
class="image-upload-popover-wrapper"
:class="{ 'has-overlay': isModalCentered }"
@click.self="closeModal"
>
<ImageUploadModal
:style="popoverStyle"
:mode="modalMode"
:initial-data="modalInitialData"
@close="closeModal"
@insert="handleInsert"
@update="handleUpdate"
@remove="handleRemove"
/>
</div>

<div
v-if="mathHandler.isMathModalOpen.value"
class="math-modal-popover-wrapper"
:class="{ 'has-overlay': mathHandler.isModalCentered.value }"
@click.self="mathHandler.closeMathModal()"
>
<FormulasMenu
:style="mathHandler.popoverStyle.value"
:mode="mathHandler.mathModalMode.value"
:initial-latex="mathHandler.mathModalInitialLatex.value"
@save="mathHandler.handleSaveMath"
@close="mathHandler.closeMathModal"
/>
</div>

<EditorContentWrapper
@drop.native.prevent="handleDrop"
@dragover.native.prevent
/>
</div>

</template>


<script>

import { defineComponent, provide } from 'vue';
import { defineComponent, provide, onMounted } from 'vue';
import EditorToolbar from './components/EditorToolbar.vue';
import EditorContentWrapper from './components/EditorContentWrapper.vue';
import { useEditor } from './composables/useEditor';
import ImageUploadModal from './components/image/ImageUploadModal.vue';
import { useImageHandling } from './composables/useImageHandling';
import '../assets/styles/code-theme-dark.css';
import { useLinkHandling } from './composables/useLinkHandling';
import LinkBubbleMenu from './components/link/LinkBubbleMenu.vue';
import LinkEditor from './components/link/LinkEditor.vue';
import { useMathHandling } from './composables/useMathHandling';
import FormulasMenu from './components/math/FormulasMenu.vue';

export default defineComponent({
name: 'RichTextEditor',
components: {
EditorToolbar,
EditorContentWrapper,
ImageUploadModal,
LinkBubbleMenu,
LinkEditor,
FormulasMenu,
},
setup() {
const { editor, isReady } = useEditor();

// Provide editor instance to all child components
const { editor, isReady, initializeEditor } = useEditor();
provide('editor', editor);
provide('isReady', isReady);

const linkHandler = useLinkHandling(editor);
provide('linkHandler', linkHandler);

const mathHandler = useMathHandling(editor);
provide('mathHandler', mathHandler);

onMounted(() => {
initializeEditor();
});

const {
modalMode,
modalInitialData,
popoverStyle,
isModalCentered,
openCreateModal,
closeModal,
handleInsert,
handleUpdate,
handleRemove,
} = useImageHandling(editor);

const handleDrop = event => {
const file = event.dataTransfer.files[0];
if (file) {
openCreateModal(file);
}
};

return {
isReady,
modalMode,
modalInitialData,
popoverStyle,
openCreateModal,
closeModal,
handleInsert,
handleUpdate,
handleRemove,
handleDrop,
isModalCentered,
linkHandler,
editor,
mathHandler,
};
},
});

Expand All @@ -36,8 +155,9 @@
<style scoped>

.editor-container {
position: relative;
width: 1000px;
margin: 40px auto;
margin: 80px auto;
font-family:
'Noto Sans',
-apple-system,
Expand All @@ -51,6 +171,47 @@
border-radius: 8px;
}

.image-upload-popover-wrapper,
.math-modal-popover-wrapper {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
width: 100%;
height: 100%;
pointer-events: none;
}

.image-upload-popover-wrapper > *,
.math-modal-popover-wrapper > * {
pointer-events: auto;
}

/* Overlay for edit mode to allow clicking outside to close */
.image-upload-popover-wrapper.has-overlay,
.math-modal-popover-wrapper.has-overlay {
pointer-events: auto;
background: rgba(0, 0, 0, 0.5);
}

.image-upload-modal-content {
position: relative;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.close-modal {
position: absolute;
top: 10px;
right: 10px;
font-size: 1.5rem;
cursor: pointer;
background: transparent;
border: 0;
}

</style>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,167 @@ const MESSAGES = {
message: 'Clipboard access failed. Try copying again.',
context: 'Message shown when clipboard access fails during paste operation',
},

// Image Upload
editImage: {
message: 'Edit image',
context: 'Title for the image modal when editing an existing image.',
},
uploadImage: {
message: 'Upload image',
context: 'Title for the image modal when uploading a new image.',
},
closeModal: {
message: 'Close modal',
context: 'Accessibility label for the button that closes the modal window.',
},
cancelLoading: {
message: 'Cancel loading',
context: 'Button label to cancel an image upload that is in progress.',
},
cancel: {
message: 'Cancel',
context: 'Generic button label for a cancel action.',
},
multipleFilesDroppedWarning: {
message: 'Multiple files were dropped. Only the first file has been selected.',
context: 'Warning message shown when a user drops multiple files into the upload area.',
},
replaceFile: {
message: 'Replace file',
context: 'Button label to replace an existing image file.',
},
selectFile: {
message: 'Select file',
context: 'Button label to open a file picker to select an image.',
},
imagePreview: {
message: 'Image preview',
context: 'Alt text for the image preview in the upload modal.',
},
altTextLabel: {
message: 'Alt text (Optional)',
context: 'Label for the alt text input field.',
},
altTextPlaceholder: {
message: 'Describe your image...',
context: 'Placeholder text for the alt text input field.',
},
altTextDescription: {
message:
'Alt text is necessary to enable visually impaired learners to answer questions, and it also displays when the image fails to load',
context: 'Description of the purpose and importance of alt text.',
},
imageDropZoneText: {
message: 'Drag and drop an image here or upload manually',
context: 'Instructional text inside the image drop zone.',
},
selectFileToUpload: {
message: 'Select file to upload',
context: 'Accessibility label for the button that opens the file picker.',
},
supportedFileTypes: {
message: 'Supported file types: png, jpg, jpeg, svg, webp',
context: 'A list of supported image file formats.',
},
removeImage: {
message: 'Remove image',
context: 'Button label to remove an image from the editor.',
},
remove: {
message: 'Remove',
context: 'Generic button label for a remove action.',
},
saveChanges: {
message: 'Save changes',
context: 'Button label to save changes made to an image.',
},
save: {
message: 'Save',
context: 'Generic button label for a save action.',
},
insert: {
message: 'Insert',
context: 'Generic button label for an insert action.',
},
defaultImageName: {
message: 'Image',
context: 'Default name for an image when a file name is not available.',
},

// Link
goToLink: {
message: 'Go to link',
context: 'Text for a link that opens the URL in a new tab.',
},
copyLink: {
message: 'Copy link',
context: 'Button title to copy the link URL to the clipboard.',
},
editLink: {
message: 'Edit link',
context: 'Button title to open the link editor modal.',
},
edit: {
message: 'Edit',
context: 'Generic button label for an edit action.',
},
removeLink: {
message: 'Remove link',
context: 'Button title to remove the link from the text.',
},
linkActions: {
message: 'Link actions',
context: 'Accessibility label for the toolbar containing link-related actions.',
},
opensInNewTab: {
message: '(opens in new tab)',
context: 'Accessibility text indicating that a link will open in a new browser tab.',
},
addLink: {
message: 'Add link',
context: 'Title for the link editor modal.',
},
close: {
message: 'Close',
context: 'Generic button label or title for a close action.',
},
text: {
message: 'Text',
context: 'Label for the link text input field.',
},
link: {
message: 'Link',
context: 'Label for the link URL input field.',
},

// Math
formulasMenuTitle: {
message: 'Special Characters',
context: 'Title for the menu containing special characters and mathematical symbols.',
},

// Error Messages
noFileProvided: {
message: 'No file provided.',
context: 'Error message when no file is provided for upload',
},
invalidFileType: {
message: 'Invalid file type. Please use: ',
context: 'Error message when an unsupported file type is uploaded',
},
fileTooLarge: {
message: 'File is too large. Maximum size is ',
context: 'Error message when uploaded file exceeds size limit',
},
fileSizeUnit: {
message: 'MB.',
context: 'Unit for file size in megabytes',
},
failedToProcessImage: {
message: 'Failed to process the image file.',
context: 'Error message when image processing fails',
},
};

let TipTapEditorStrings = null;
Expand Down
Loading