Refactor QuillyEditor and Quill Mention Extension for Improved Functionality

- Updated `QuillyEditor.vue` to enhance the handling of model updates and internal changes, ensuring better synchronization between the editor and the model value. Introduced a new `pasteHTML` function to manage HTML content pasting more effectively.
- Refactored `quillMentionExtension.js` to improve the processing of pasted content, ensuring that only valid mentions are retained and transforming flat mention structures into nested ones for better data integrity.
- Enhanced the SCSS styles in `app.scss` to apply list styles to ordered and unordered lists within the `.field-help` class, improving the visual presentation of help text.

These changes aim to improve the overall functionality, maintainability, and user experience of the QuillyEditor component and its associated mention extension.
This commit is contained in:
JhumanJ 2025-05-12 13:08:58 +02:00
parent 0b3011b4ee
commit 1a8af6257a
3 changed files with 140 additions and 96 deletions

View File

@ -5,12 +5,12 @@
/>
</template>
<script setup>
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
<script setup>
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
const props = defineProps({
const props = defineProps({
modelValue: {
type: String,
default: null
@ -19,9 +19,9 @@
type: Object,
default: () => ({})
}
})
})
const emit = defineEmits([
const emit = defineEmits([
'update:modelValue',
'text-change',
'selection-change',
@ -29,22 +29,17 @@
'blur',
'focus',
'ready'
])
])
let quillInstance = null
const container = ref(null)
let isInternalChange = false
let quillInstance = null
const container = ref(null)
const model = ref(props.modelValue)
const setContents = (content) => {
if (!quillInstance) return
const pasteHTML = (instance) => {
instance.clipboard.dangerouslyPasteHTML(props.modelValue || '', 'silent')
}
isInternalChange = true
quillInstance.root.innerHTML = content
quillInstance.update()
isInternalChange = false
}
const initializeQuill = () => {
const initializeQuill = () => {
if (container.value) {
quillInstance = new Quill(container.value, props.options)
@ -58,11 +53,9 @@
})
quillInstance.on('text-change', (delta, oldContents, source) => {
if (!isInternalChange) {
const html = quillInstance.root.innerHTML
// Update local model only on user input
model.value = quillInstance.getSemanticHTML()
emit('text-change', { delta, oldContents, source })
emit('update:modelValue', html)
}
})
quillInstance.on('editor-change', (eventName, ...args) => {
@ -70,29 +63,49 @@
})
if (props.modelValue) {
setContents(props.modelValue)
pasteHTML(quillInstance)
model.value = quillInstance.getSemanticHTML()
}
emit('ready', quillInstance)
}
}
}
onMounted(() => {
onMounted(() => {
initializeQuill()
})
})
watch(() => props.modelValue, (newValue) => {
if (quillInstance && newValue !== quillInstance.root.innerHTML) {
setContents(newValue || '')
// Watch modelValue and paste HTML if has changes
watch(
() => props.modelValue,
(newValue) => {
if (!quillInstance) return
if (newValue && newValue !== model.value) {
pasteHTML(quillInstance)
model.value = quillInstance.getSemanticHTML()
} else if (!newValue) {
quillInstance.setContents([])
model.value = ''
}
}, { immediate: true })
}
)
onBeforeUnmount(() => {
// Watch model and update modelValue if has changes
watch(model, (newValue, oldValue) => {
if (!quillInstance) return
if (newValue && newValue !== oldValue) {
emit('update:modelValue', newValue)
} else if (!newValue) {
quillInstance.setContents([])
}
})
onBeforeUnmount(() => {
if (quillInstance) {
quillInstance.off('selection-change')
quillInstance.off('text-change')
quillInstance.off('editor-change')
}
quillInstance = null
})
</script>
})
</script>

View File

@ -1,7 +1,6 @@
import { reactive } from 'vue'
import Quill from 'quill'
const Inline = Quill.import('blots/inline')
const Delta = Quill.import('delta')
const Clipboard = Quill.import('modules/clipboard')
export default function registerMentionExtension(Quill) {
@ -9,17 +8,42 @@ export default function registerMentionExtension(Quill) {
class MentionClipboard extends Clipboard {
convert(html) {
const delta = super.convert(html)
// Remove any mention formatting from pasted content
return delta.reduce((newDelta, op) => {
const processedDelta = delta.ops.reduce((newDelta, op) => {
if (op.attributes && op.attributes.mention) {
// Only keep mentions that have valid field IDs
if (!op.attributes.mention['mention-field-id']) {
const mentionData = op.attributes.mention
let isValid = false
// Check for nested structure
if (
mentionData &&
typeof mentionData === 'object' &&
mentionData.field &&
typeof mentionData.field === 'object' &&
mentionData.field.id
) {
isValid = true
} else if (
mentionData &&
typeof mentionData === 'object' &&
mentionData['mention-field-id']
) {
// Transform flat structure to nested structure
op.attributes.mention = {
field: {
id: mentionData['mention-field-id'],
name: mentionData['mention-field-name'] || '',
},
fallback: mentionData['mention-fallback'] || '',
}
isValid = true
}
if (!isValid) {
delete op.attributes.mention
}
}
newDelta.push(op)
return newDelta
}, new Delta())
}, [])
return processedDelta
}
}
Quill.register('modules/clipboard', MentionClipboard, true)
@ -43,7 +67,6 @@ export default function registerMentionExtension(Quill) {
if (!data || !data.field || !data.field.id) {
return
}
node.setAttribute('contenteditable', 'false')
node.setAttribute('mention', 'true')
node.setAttribute('mention-field-id', data.field.id || '')
@ -70,7 +93,7 @@ export default function registerMentionExtension(Quill) {
formats() {
let formats = super.formats()
formats['mention'] = MentionBlot.formats(this.domNode)
formats['mention'] = MentionBlot.value(this.domNode)
return formats
}

10
client/scss/app.scss vendored
View File

@ -52,9 +52,17 @@ body.dark * {
}
.field-help {
p {
p, ol, ul, li {
@apply text-gray-400 dark:text-gray-500;
}
ol {
@apply list-decimal list-inside;
}
ul {
@apply list-disc list-inside;
}
}
.public-page {