Files
contexted-v3/src/components/Note/NoteEditor.vue
2023-05-25 10:50:40 +02:00

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>