184 lines
6.2 KiB
Vue
184 lines
6.2 KiB
Vue
<script setup lang="ts">
|
|
import CKEditor from '@/ckeditor/CKEditor'
|
|
import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'
|
|
import EssentialsPlugin from '@ckeditor/ckeditor5-essentials/src/essentials'
|
|
import BoldPlugin from '@ckeditor/ckeditor5-basic-styles/src/bold'
|
|
import ItalicPlugin from '@ckeditor/ckeditor5-basic-styles/src/italic'
|
|
import UnderlinePlugin from '@ckeditor/ckeditor5-basic-styles/src/underline'
|
|
import StrikethroughPlugin from '@ckeditor/ckeditor5-basic-styles/src/strikethrough'
|
|
import LinkPlugin from '@ckeditor/ckeditor5-link/src/link'
|
|
import HeadingPlugin from '@ckeditor/ckeditor5-heading/src/heading'
|
|
import ParagraphPlugin from '@ckeditor/ckeditor5-paragraph/src/paragraph'
|
|
import ListPlugin from '@ckeditor/ckeditor5-list/src/list'
|
|
import AutoformatPlugin from '@ckeditor/ckeditor5-autoformat/src/autoformat'
|
|
import ContextedPlugin from '@/ckeditor/ContextedPlugin'
|
|
import { mdToHtml, htmlToMd } from '@/utils/markdown'
|
|
import { getNoteByTitle, setActiveNote, addNote } from '@/composables/useNotes'
|
|
import Autocomplete from '@/components/Note/Autocomplete.vue'
|
|
|
|
const props = defineProps<{ note: Note }>()
|
|
|
|
const emit = defineEmits<{
|
|
update: [mdText: string]
|
|
}>()
|
|
|
|
const html = mdToHtml(props.note.content)
|
|
|
|
const editor = BalloonEditor
|
|
const editorData = ref<string>(html)
|
|
const editorConfig = {
|
|
plugins: [
|
|
EssentialsPlugin,
|
|
BoldPlugin,
|
|
ItalicPlugin,
|
|
UnderlinePlugin,
|
|
StrikethroughPlugin,
|
|
LinkPlugin,
|
|
HeadingPlugin,
|
|
ParagraphPlugin,
|
|
ListPlugin,
|
|
AutoformatPlugin,
|
|
ContextedPlugin
|
|
],
|
|
toolbar: {
|
|
items: [
|
|
'bold',
|
|
'italic',
|
|
'underline',
|
|
'strikethrough',
|
|
'link',
|
|
'undo',
|
|
'redo',
|
|
'heading',
|
|
'bulletedList',
|
|
'numberedList'
|
|
]
|
|
},
|
|
placeholder: 'Click here to start typing...'
|
|
}
|
|
|
|
const editorElement = ref<HTMLInputElement | null>(null)
|
|
watch(editorData, () => emit('update', htmlToMd(editorData.value)))
|
|
let editorInstance: any
|
|
|
|
const handleClick = ({ data }: { data: any }) => {
|
|
if (!data.domTarget.hasAttribute('data-contexted-link')) return
|
|
const noteTitle = data.domTarget.textContent as string
|
|
let note: BaseNote | Note | undefined = getNoteByTitle(noteTitle)
|
|
if (!note) note = addNote(noteTitle, '')
|
|
setActiveNote(note.id)
|
|
}
|
|
|
|
const autocompleteRef = ref<InstanceType<typeof Autocomplete> | null>(null)
|
|
const showAutocomplete = ref<boolean>(false)
|
|
const autocompleteStyle = ref<{ [key: string]: any }>({})
|
|
const autocompleteText = ref<string>('')
|
|
const autocompleteReverse = ref<boolean>(false)
|
|
|
|
const handleAutocomplete = async (event: AutocompleteEvent) => {
|
|
const position = event.position
|
|
if (position && editorElement.value) {
|
|
const rect: any = editorElement.value?.getBoundingClientRect()
|
|
const lineHeight = parseFloat(
|
|
window.getComputedStyle(event.domElement || editorElement.value).lineHeight
|
|
)
|
|
autocompleteStyle.value = {
|
|
top: `${position.top - rect.top + lineHeight}px`,
|
|
left: `${position.left - rect.left}px`
|
|
}
|
|
}
|
|
autocompleteText.value = event.autocompleteText || ''
|
|
showAutocomplete.value = event.show
|
|
await nextTick()
|
|
const autocompleteElem = autocompleteRef.value?.$el
|
|
const autocompleteRect = autocompleteRef.value?.$el.getBoundingClientRect()
|
|
const editorRect = editorElement.value?.getBoundingClientRect()
|
|
if (
|
|
autocompleteElem &&
|
|
autocompleteRect &&
|
|
editorRect &&
|
|
autocompleteRect.bottom > editorRect.bottom
|
|
) {
|
|
const autocompleteHeight = parseFloat(window.getComputedStyle(autocompleteElem).height)
|
|
autocompleteStyle.value = {
|
|
...autocompleteStyle.value,
|
|
top: `${position.top - editorRect.top - autocompleteHeight}px`
|
|
}
|
|
autocompleteReverse.value = true
|
|
} else {
|
|
autocompleteReverse.value = false
|
|
}
|
|
}
|
|
|
|
const handleContextedKeypress = (event: any) => {
|
|
if (autocompleteRef.value) autocompleteRef.value.handleKeypress(event)
|
|
}
|
|
|
|
const createLink = (link: string) => {
|
|
if (!editor) return
|
|
const model = editorInstance.model
|
|
const getPosition = () => model.document.selection.anchor
|
|
// const getNodes = () => {
|
|
// const nodes = [
|
|
// getPosition().nodeBefore,
|
|
// getPosition().nodeAfter,
|
|
// getPosition().textNnode,
|
|
// ]
|
|
// return nodes.map((node) => ({
|
|
// data: node?.data,
|
|
// attrs: Array.from(node?.getAttributes() || []),
|
|
// }))
|
|
// }
|
|
// console.log(getNodes())
|
|
let nodeToRemove: any
|
|
if (getPosition().nodeBefore?.hasAttribute('autocomplete')) {
|
|
// Insert new link
|
|
nodeToRemove = getPosition().nodeBefore
|
|
} else if (getPosition().nodeBefore?.hasAttribute('contextedLink')) {
|
|
// Update existing link from end of existing link (backspace)
|
|
nodeToRemove = getPosition().nodeBefore
|
|
} else if (getPosition().textNode?.hasAttribute('contextedLink')) {
|
|
// Update existing link from middle of existing link
|
|
nodeToRemove = getPosition().textNode
|
|
} else if (getPosition().nodeAfter?.hasAttribute('contextedLink')) {
|
|
// Update existing link from beginning (delete)
|
|
nodeToRemove = getPosition().nodeAfter
|
|
}
|
|
|
|
model.change((writer: any) => {
|
|
if (nodeToRemove) writer.remove(nodeToRemove)
|
|
writer.insertText(link, { contextedLink: true }, getPosition(), 'after')
|
|
model.enqueueChange((writer: any) => {
|
|
const nodeAfter = getPosition().nodeAfter
|
|
if (!nodeAfter || (nodeAfter && !nodeAfter.data.startsWith(']]'))) {
|
|
writer.insertText(']]', model.document.selection.getFirstPosition())
|
|
}
|
|
})
|
|
showAutocomplete.value = false
|
|
})
|
|
}
|
|
</script>
|
|
<template>
|
|
<div class="relative overflow-x-hidden" ref="editorElement">
|
|
<CKEditor
|
|
class="w-full flex-grow text-[110%] font-light"
|
|
:editor="editor"
|
|
v-model="editorData"
|
|
:config="editorConfig"
|
|
@editor-ready="(editor) => (editorInstance = editor)"
|
|
@click="handleClick"
|
|
@contexted-link-autocomplete="handleAutocomplete"
|
|
@contexted-keypress="handleContextedKeypress"
|
|
></CKEditor>
|
|
<Autocomplete
|
|
v-if="showAutocomplete"
|
|
ref="autocompleteRef"
|
|
:autocomplete-text="autocompleteText"
|
|
:style="autocompleteStyle"
|
|
@create-link="createLink"
|
|
class="absolute w-[300px]"
|
|
:class="autocompleteReverse && 'flex-col-reverse'"
|
|
/>
|
|
</div>
|
|
</template>
|