show autocomplete
This commit is contained in:
@@ -224,8 +224,8 @@ export default defineComponent({
|
|||||||
this.$emit('click', { evt, data }, editor)
|
this.$emit('click', { evt, data }, editor)
|
||||||
})
|
})
|
||||||
|
|
||||||
editor.model.document.on('contextedLinkAutocomplete', (evt, data) => {
|
editor.model.document.on('contextedLinkAutocomplete', (_, data) => {
|
||||||
this.$emit('contextedLinkAutocomplete', { evt, data })
|
this.$emit('contextedLinkAutocomplete', data)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'
|
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'
|
||||||
import { TwoStepCaretMovement, inlineHighlight } from 'ckeditor5/src/typing'
|
import { TwoStepCaretMovement, inlineHighlight } from 'ckeditor5/src/typing'
|
||||||
|
import AttributeCommand from '@ckeditor/ckeditor5-basic-styles/src/attributecommand'
|
||||||
|
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect'
|
||||||
|
|
||||||
const HIGHLIGHT_CLASS = 'ck-link_selected'
|
const HIGHLIGHT_CLASS = 'ck-link_selected'
|
||||||
|
|
||||||
@@ -10,6 +12,10 @@ export default class ContextedLinkEditing extends Plugin {
|
|||||||
const twoStepCaretMovementPlugin = this.editor.plugins.get(TwoStepCaretMovement)
|
const twoStepCaretMovementPlugin = this.editor.plugins.get(TwoStepCaretMovement)
|
||||||
twoStepCaretMovementPlugin.registerAttribute('contextedLink')
|
twoStepCaretMovementPlugin.registerAttribute('contextedLink')
|
||||||
inlineHighlight(this.editor, 'contextedLink', 'a', HIGHLIGHT_CLASS)
|
inlineHighlight(this.editor, 'contextedLink', 'a', HIGHLIGHT_CLASS)
|
||||||
|
this.editor.commands.add(
|
||||||
|
'autocomplete',
|
||||||
|
new AttributeCommand(this.editor, 'autocomplete')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
afterInit() {
|
afterInit() {
|
||||||
// const editor = this.editor
|
// const editor = this.editor
|
||||||
@@ -28,7 +34,7 @@ export default class ContextedLinkEditing extends Plugin {
|
|||||||
|
|
||||||
// Extend the text node's schema to accept the abbreviation attribute.
|
// Extend the text node's schema to accept the abbreviation attribute.
|
||||||
schema.extend('$text', {
|
schema.extend('$text', {
|
||||||
allowAttributes: ['abbreviation', 'contextedLink'],
|
allowAttributes: ['contextedLink', 'autocomplete'],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_defineConverters() {
|
_defineConverters() {
|
||||||
@@ -62,10 +68,9 @@ export default class ContextedLinkEditing extends Plugin {
|
|||||||
_addAutocomplete() {
|
_addAutocomplete() {
|
||||||
// Copied from: node_modules/@ckeditor/ckeditor5-autoformat/src/inlineautoformatediting.js
|
// Copied from: node_modules/@ckeditor/ckeditor5-autoformat/src/inlineautoformatediting.js
|
||||||
const editor = this.editor
|
const editor = this.editor
|
||||||
editor.model.document.on('change:data', (_, batch) => {
|
// let autocomplete = false
|
||||||
if (batch.isUndo || !batch.isLocal) {
|
editor.model.document.on('change', (_, batch) => {
|
||||||
return
|
if (batch.isUndo || !batch.isLocal) return
|
||||||
}
|
|
||||||
const model = editor.model
|
const model = editor.model
|
||||||
const selection = model.document.selection
|
const selection = model.document.selection
|
||||||
// Do nothing if selection is not collapsed.
|
// Do nothing if selection is not collapsed.
|
||||||
@@ -75,7 +80,7 @@ export default class ContextedLinkEditing extends Plugin {
|
|||||||
// Typing is represented by only a single change.
|
// Typing is represented by only a single change.
|
||||||
if (
|
if (
|
||||||
changes.length != 1 ||
|
changes.length != 1 ||
|
||||||
entry.type !== 'insert' ||
|
(entry.type !== 'insert' && entry.type !== 'remove') ||
|
||||||
entry.name != '$text' ||
|
entry.name != '$text' ||
|
||||||
entry.length != 1
|
entry.length != 1
|
||||||
) {
|
) {
|
||||||
@@ -84,21 +89,60 @@ export default class ContextedLinkEditing extends Plugin {
|
|||||||
const focus = selection.focus
|
const focus = selection.focus
|
||||||
const block = focus?.parent
|
const block = focus?.parent
|
||||||
if (!block || !focus) return
|
if (!block || !focus) return
|
||||||
const { text, range } = getTextAfterCode(
|
const { text } = getTextAfterCode(
|
||||||
model.createRange(model.createPositionAt(block, 0), focus),
|
model.createRange(model.createPositionAt(block, 0), focus),
|
||||||
model
|
model
|
||||||
)
|
)
|
||||||
const inputText = (text as string).split(']]').at(-1)
|
const inputText = (text as string).split(']]').at(-1)
|
||||||
const autocompleteOpenMatch = (inputText as string).match(/(?<=\[\[).+/g)
|
const autocompleteText = (inputText as string).match(/(?<=\[\[).*/g)
|
||||||
const autocompleteCloseMatch = (inputText as string).match(/(?<=\[\[).+?(?=]])/g)
|
const cursorNodes = [focus.textNode, focus.nodeBefore, focus.nodeAfter]
|
||||||
const openAutocomplete = autocompleteOpenMatch && !autocompleteCloseMatch
|
const autocompleteNode: any = cursorNodes.find((node) =>
|
||||||
if (openAutocomplete) {
|
node?.hasAttribute('autocomplete')
|
||||||
editor.model.document.fire('contextedLinkAutocomplete', autocompleteOpenMatch)
|
)
|
||||||
|
interface AutocompleteEvent {
|
||||||
|
position?: any
|
||||||
|
autocompleteText?: string
|
||||||
|
show: boolean
|
||||||
|
}
|
||||||
|
if (Boolean(autocompleteText) || Boolean(autocompleteNode)) {
|
||||||
|
if (Boolean(autocompleteText) !== Boolean(autocompleteNode)) {
|
||||||
|
editor.execute('autocomplete')
|
||||||
|
if (autocompleteNode)
|
||||||
|
editor.model.document.fire('contextedLinkAutocomplete', { show: false })
|
||||||
|
} else {
|
||||||
|
const event: AutocompleteEvent = {
|
||||||
|
position: getNodePosition(
|
||||||
|
editor,
|
||||||
|
editor.model.createPositionFromPath(
|
||||||
|
autocompleteNode.root,
|
||||||
|
autocompleteNode.getPath()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
autocompleteText: autocompleteNode.data,
|
||||||
|
show: true,
|
||||||
|
}
|
||||||
|
editor.model.document.fire('contextedLinkAutocomplete', event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodePosition(editor: any, modelPosition: any) {
|
||||||
|
try {
|
||||||
|
const mapper = editor.editing.mapper
|
||||||
|
const viewPosition = mapper.toViewPosition(modelPosition)
|
||||||
|
const viewRange = editor.editing.view.createRange(viewPosition)
|
||||||
|
const domConverter = editor.editing.view.domConverter
|
||||||
|
const rangeRects = Rect.getDomRangeRects(
|
||||||
|
domConverter.viewRangeToDom(viewRange)
|
||||||
|
).pop()
|
||||||
|
return rangeRects
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// function testOutputToRanges(start: any, arrays: any[], model: any) {
|
// function testOutputToRanges(start: any, arrays: any[], model: any) {
|
||||||
// return arrays
|
// return arrays
|
||||||
// .filter((array) => array[0] !== undefined && array[1] !== undefined)
|
// .filter((array) => array[0] !== undefined && array[1] !== undefined)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import CKEditor from '@/ckeditor/CKEditor'
|
import CKEditor from '@/ckeditor/CKEditor'
|
||||||
import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'
|
import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'
|
||||||
import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'
|
import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'
|
||||||
@@ -54,18 +55,35 @@ const editorConfig = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editorElement = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const handleClick = ({ data }: { data: any }) => {
|
const handleClick = ({ data }: { data: any }) => {
|
||||||
const noteTitle = data.domTarget.textContent as string
|
const noteTitle = data.domTarget.textContent as string
|
||||||
const note = getNoteByTitle(noteTitle)
|
const note = getNoteByTitle(noteTitle)
|
||||||
if (note) activeNote.value = note
|
if (note) activeNote.value = note
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showAutocomplete = ref(false)
|
||||||
|
const autocompleteStyle = ref<any>({})
|
||||||
|
const autocompleteText = ref('')
|
||||||
const handleAutocomplete = (event: any) => {
|
const handleAutocomplete = (event: any) => {
|
||||||
console.log(event.data)
|
const position = event.position
|
||||||
|
if (position && editorElement.value) {
|
||||||
|
const rect: any = editorElement.value?.getBoundingClientRect()
|
||||||
|
const fontSize = parseFloat(
|
||||||
|
window.getComputedStyle(editorElement.value, null).getPropertyValue('font-size')
|
||||||
|
)
|
||||||
|
autocompleteStyle.value = {
|
||||||
|
top: `${position.top - rect.top + fontSize}` + 'px',
|
||||||
|
left: `${position.left - rect.left}` + 'px',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
autocompleteText.value = event.autocompleteText
|
||||||
|
showAutocomplete.value = event.show
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="relative" ref="editorElement">
|
||||||
<CKEditor
|
<CKEditor
|
||||||
class="h-full"
|
class="h-full"
|
||||||
:editor="editor"
|
:editor="editor"
|
||||||
@@ -74,6 +92,13 @@ const handleAutocomplete = (event: any) => {
|
|||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
@contexted-link-autocomplete="handleAutocomplete"
|
@contexted-link-autocomplete="handleAutocomplete"
|
||||||
></CKEditor>
|
></CKEditor>
|
||||||
|
<div
|
||||||
|
class="absolute mt-1 border-red-500 bg-primary text-white"
|
||||||
|
:style="autocompleteStyle"
|
||||||
|
v-if="showAutocomplete"
|
||||||
|
>
|
||||||
|
{{ autocompleteText }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user