diff --git a/client/components/open/forms/OpenCompleteForm.vue b/client/components/open/forms/OpenCompleteForm.vue
index 1eef8df7..701bc16b 100644
--- a/client/components/open/forms/OpenCompleteForm.vue
+++ b/client/components/open/forms/OpenCompleteForm.vue
@@ -343,7 +343,7 @@ const triggerSubmit = async () => {
submissionId: submissionId.value
}).then(result => {
if (result) {
- submittedData.value = result || {}
+ submittedData.value = formManager.form.data()
if (result?.submission_id) {
submissionId.value = result.submission_id
diff --git a/client/components/open/forms/components/form-components/FormInformation.vue b/client/components/open/forms/components/form-components/FormInformation.vue
index 6c69f520..54ff6632 100644
--- a/client/components/open/forms/components/form-components/FormInformation.vue
+++ b/client/components/open/forms/components/form-components/FormInformation.vue
@@ -35,7 +35,7 @@
:options="visibilityOptions"
/>
-
diff --git a/client/lib/quill/quillMentionExtension.js b/client/lib/quill/quillMentionExtension.js
index 875b207d..ba683c11 100644
--- a/client/lib/quill/quillMentionExtension.js
+++ b/client/lib/quill/quillMentionExtension.js
@@ -1,177 +1,198 @@
-import { reactive } from 'vue'
+import { reactive, nextTick } from 'vue'
import Quill from 'quill'
-const Inline = Quill.import('blots/inline')
-const Clipboard = Quill.import('modules/clipboard')
-export default function registerMentionExtension(Quill) {
- // Extend Clipboard to handle pasted content
- class MentionClipboard extends Clipboard {
- convert(html) {
- const delta = super.convert(html)
- const processedDelta = delta.ops.reduce((newDelta, op) => {
- if (op.attributes && op.attributes.mention) {
- 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
- }
+// Core imports
+const ParchmentEmbed = Quill.import('parchment').EmbedBlot
+const Delta = Quill.import('delta')
+const Parchment = Quill.import('parchment')
+
+/**
+ * Utility to remove BOM and other zero-width characters from a string.
+ */
+function cleanString(str) {
+ if (typeof str !== 'string') return ''
+ return str.replace(/\uFEFF/g, '').replace(/\s+/g, ' ').trim()
+}
+
+export default function registerMentionExtension(QuillInstance) {
+ /**
+ * MentionBlot - Embeds a mention as a non-editable element
+ */
+ if (!QuillInstance.imports['formats/mention']) {
+ class MentionBlot extends ParchmentEmbed {
+ static blotName = 'mention'
+ static tagName = 'SPAN'
+ static scope = Parchment.Scope.INLINE
+
+ // Match any span with a 'mention' attribute
+ static matches(domNode) {
+ return (
+ domNode instanceof HTMLElement &&
+ domNode.tagName === this.tagName &&
+ domNode.hasAttribute('mention')
+ )
+ }
+
+ static create(value) {
+ const node = document.createElement(this.tagName)
+ node.setAttribute('contenteditable', 'false')
+
+ const data = (typeof value === 'object' && value !== null) ? value : { field: {}, fallback: '' }
+ data.field = (typeof data.field === 'object' && data.field !== null) ? data.field : {}
+
+ const fieldName = cleanString(data.field.name)
+ const fallbackText = cleanString(data.fallback)
+ const displayText = fieldName || fallbackText || 'mention'
+
+ node.setAttribute('mention', 'true')
+ node.setAttribute('mention-field-id', data.field.id || '')
+ node.setAttribute('mention-field-name', fieldName)
+ node.setAttribute('mention-fallback', fallbackText)
+
+ const textNode = document.createTextNode(displayText)
+ node.appendChild(textNode)
+
+ return node
+ }
+
+ static value(domNode) {
+ return {
+ field: {
+ id: domNode.getAttribute('mention-field-id') || '',
+ name: domNode.getAttribute('mention-field-name') || ''
+ },
+ fallback: domNode.getAttribute('mention-fallback') || ''
}
- newDelta.push(op)
- return newDelta
- }, [])
- return processedDelta
- }
- }
- Quill.register('modules/clipboard', MentionClipboard, true)
-
- class MentionBlot extends Inline {
- static blotName = 'mention'
- static tagName = 'SPAN'
-
- static create(data) {
- // Only create mention if we have valid data
- if (!data || !data.field || !data.field.id) {
- return null
}
- let node = super.create()
- MentionBlot.setAttributes(node, data)
- return node
- }
- static setAttributes(node, data) {
- // Only set attributes if we have valid data
- if (!data || !data.field || !data.field.id) {
- return
+ static formats(domNode) {
+ return MentionBlot.value(domNode)
}
- node.setAttribute('contenteditable', 'false')
- node.setAttribute('mention', 'true')
- node.setAttribute('mention-field-id', data.field.id || '')
- node.setAttribute('mention-field-name', data.field.name || '')
- node.setAttribute('mention-fallback', data.fallback || '')
- node.textContent = data.field.name || ''
- }
- static formats(domNode) {
- return {
- 'mention-field-id': domNode.getAttribute('mention-field-id') || '',
- 'mention-field-name': domNode.getAttribute('mention-field-name') || '',
- 'mention-fallback': domNode.getAttribute('mention-fallback') || ''
+ length() {
+ return 1
}
}
- format(name, value) {
- if (name === 'mention' && value) {
- MentionBlot.setAttributes(this.domNode, value)
- } else {
- super.format(name, value)
- }
- }
-
- formats() {
- let formats = super.formats()
- formats['mention'] = MentionBlot.value(this.domNode)
- return formats
- }
-
- static value(domNode) {
- return {
- field: {
- id: domNode.getAttribute('mention-field-id') || '',
- name: domNode.getAttribute('mention-field-name') || ''
- },
- fallback: domNode.getAttribute('mention-fallback') || ''
- }
- }
-
- // Override attach to ensure contenteditable is always set
- attach() {
- super.attach()
- this.domNode.setAttribute('contenteditable', 'false')
- }
-
- length() {
- return 1
- }
+ // Register the blot with Quill
+ QuillInstance.register('formats/mention', MentionBlot)
}
- Quill.register(MentionBlot)
+ // Add clipboard matcher for handling mentions in pasted/loaded HTML
+ if (QuillInstance.clipboard && typeof QuillInstance.clipboard.addMatcher === 'function') {
+ QuillInstance.clipboard.addMatcher('span[mention]', (node, delta) => {
+ if (node.hasAttribute('mention')) {
+ const mentionData = {
+ field: {
+ id: node.getAttribute('mention-field-id') || '',
+ name: node.getAttribute('mention-field-name') || ''
+ },
+ fallback: node.getAttribute('mention-fallback') || ''
+ }
+
+ return new Delta().insert({ mention: mentionData })
+ }
+
+ return delta
+ })
+ }
- const mentionState = reactive({
- open: false,
- onInsert: null,
- onCancel: null,
- })
-
- class MentionModule {
- constructor(quill, options) {
- this.quill = quill
- this.options = options
-
- this.setupMentions()
- }
-
- setupMentions() {
- const toolbar = this.quill.getModule('toolbar')
- if (toolbar) {
- toolbar.addHandler('mention', () => {
- const range = this.quill.getSelection()
- if (range) {
- mentionState.open = true
- mentionState.onInsert = (mention) => {
- this.insertMention(mention, range.index)
+ /**
+ * MentionModule - Handles mention UI integration with Quill
+ */
+ if (!QuillInstance.imports['modules/mention']) {
+ class MentionModule {
+ constructor(quill, options = {}) {
+ this.quill = quill
+ this.options = options
+
+ // Reactive state for the UI component
+ this.state = reactive({
+ open: false,
+ onInsert: null,
+ onCancel: null
+ })
+
+ this.setupMentions()
+ }
+
+ setupMentions() {
+ const toolbar = this.quill.getModule('toolbar')
+ if (toolbar) {
+ toolbar.addHandler('mention', () => {
+ const range = this.quill.getSelection()
+ if (range) {
+ this.state.open = true
+ this.state.onInsert = (mentionData) => this.insertMention(mentionData, range.index)
+ this.state.onCancel = () => {
+ this.state.open = false
+ }
}
- mentionState.onCancel = () => {
- mentionState.open = false
- }
- }
+ })
+ }
+ }
+
+ insertMention(mentionData, index) {
+ if (!mentionData || typeof mentionData.field !== 'object' || mentionData.field === null) {
+ console.error("Invalid mention data for insertion:", mentionData)
+ return
+ }
+
+ this.state.open = false
+
+ // Handle selection
+ const selection = this.quill.getSelection()
+ if (selection && selection.length > 0) {
+ this.quill.deleteText(selection.index, selection.length, QuillInstance.sources.USER)
+ index = selection.index
+ }
+
+ // Prepare clean data for the blot
+ const blotData = {
+ field: {
+ id: mentionData.field.id || '',
+ name: cleanString(mentionData.field.name)
+ },
+ fallback: cleanString(mentionData.fallback)
+ }
+
+ // Insert mention as embed using the blotData
+ this.quill.insertEmbed(index, 'mention', blotData, QuillInstance.sources.USER)
+
+ // Move cursor after mention
+ nextTick(() => {
+ this.quill.focus()
+ this.quill.setSelection(index + 1, 0, QuillInstance.sources.SILENT)
})
}
}
-
- insertMention(mention, index) {
- mentionState.open = false
-
- // Insert the mention
- this.quill.insertEmbed(index, 'mention', mention, Quill.sources.USER)
-
- // Calculate the length of the inserted mention
- const mentionLength = this.quill.getLength() - index
-
- nextTick(() => {
- // Focus the editor
- this.quill.focus()
-
- // Set the selection after the mention
- this.quill.setSelection(index + mentionLength, 0, Quill.sources.SILENT)
- })
+
+ // Register the module
+ QuillInstance.register('modules/mention', MentionModule)
+ }
+
+ // Patch getSemanticHTML to handle non-breaking spaces
+ if (typeof Quill.prototype.getSemanticHTML === 'function') {
+ if (!Quill.prototype.getSemanticHTML.isPatched) {
+ const originalGetSemanticHTML = Quill.prototype.getSemanticHTML
+ Quill.prototype.getSemanticHTML = function(index = 0, length) {
+ const currentLength = this.getLength()
+ const sanitizedIndex = Math.max(0, index)
+ const sanitizedLength = Math.max(0, Math.min(length ?? (currentLength - sanitizedIndex), currentLength - sanitizedIndex))
+ if (sanitizedIndex >= currentLength && currentLength > 0) {
+ return originalGetSemanticHTML.call(this, 0, 0)
+ }
+ const html = originalGetSemanticHTML.call(this, sanitizedIndex, sanitizedLength)
+ return html.replace(/ |\u00A0/g, ' ')
+ }
+ Quill.prototype.getSemanticHTML.isPatched = true
}
}
-
- Quill.register('modules/mention', MentionModule)
-
- return mentionState
+
+ // Return reactive state for component binding
+ return reactive({
+ open: false,
+ onInsert: null,
+ onCancel: null
+ })
}
\ No newline at end of file