chore: change
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
```
|
||||
<template>
|
||||
<node-view-wrapper class="code-block-wrapper" @dblclick="onContainerClick">
|
||||
<node-view-wrapper class="code-block-wrapper">
|
||||
<div class="code-block-header">
|
||||
<select v-model="selectedLanguage" class="language-select" contenteditable="false">
|
||||
<select v-model="selectedLanguage" class="language-select" contenteditable="false" :disabled="!isEditorEditable">
|
||||
<option value="null">auto</option>
|
||||
<option disabled>—</option>
|
||||
<option v-for="(language, index) in languages" :key="index" :value="language">
|
||||
@@ -15,10 +16,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-block-content" v-if="!isEditable">
|
||||
<div class="code-block-content" v-if="!isEditorEditable">
|
||||
<div v-html="highlightedCode" class="shiki-container"></div>
|
||||
</div>
|
||||
<div class="monaco-container" ref="monacoContainer" v-show="isEditable"></div>
|
||||
<div class="monaco-container" ref="monacoContainer" v-show="isEditorEditable"></div>
|
||||
</node-view-wrapper>
|
||||
</template>
|
||||
|
||||
@@ -58,7 +59,7 @@ const selectedLanguage = computed({
|
||||
|
||||
const monacoContainer = ref<HTMLElement | null>(null)
|
||||
let editor: monaco.editor.IStandaloneCodeEditor | null = null
|
||||
const isEditable = ref(false)
|
||||
const isEditorEditable = ref(props.editor.isEditable)
|
||||
const highlightedCode = ref('')
|
||||
|
||||
// Shiki highlighter instance
|
||||
@@ -93,26 +94,20 @@ const updateHighlightedCode = async () => {
|
||||
}
|
||||
|
||||
const initMonaco = async () => {
|
||||
if (!monacoContainer.value) return
|
||||
if (!monacoContainer.value || editor) return
|
||||
|
||||
// Create the highlighter, it can be reused
|
||||
const highlighter = await createHighlighter({
|
||||
themes: [
|
||||
'vitesse-dark',
|
||||
'vitesse-light',
|
||||
],
|
||||
langs: [
|
||||
'javascript',
|
||||
'typescript',
|
||||
'vue'
|
||||
],
|
||||
})
|
||||
// Register the languageIds first. Only registered languages will be highlighted.
|
||||
monaco.languages.register({ id: 'vue' })
|
||||
monaco.languages.register({ id: 'typescript' })
|
||||
monaco.languages.register({ id: 'javascript' })
|
||||
// Register the themes from Shiki, and provide syntax highlighting for Monaco.
|
||||
shikiToMonaco(highlighter, monaco)
|
||||
// Register languages and theme only once
|
||||
if (!monaco.languages.getLanguages().some(l => l.id === 'vue')) {
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ['vitesse-dark', 'vitesse-light'],
|
||||
langs: ['javascript', 'typescript', 'vue'],
|
||||
})
|
||||
|
||||
monaco.languages.register({ id: 'vue' })
|
||||
monaco.languages.register({ id: 'typescript' })
|
||||
monaco.languages.register({ id: 'javascript' })
|
||||
shikiToMonaco(highlighter, monaco)
|
||||
}
|
||||
|
||||
editor = monaco.editor.create(monacoContainer.value, {
|
||||
value: props.node.content.firstChild?.text || '',
|
||||
@@ -125,81 +120,63 @@ const initMonaco = async () => {
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
lineNumbers: 'off',
|
||||
padding: { top: 16, bottom: 16 },
|
||||
readOnly: false
|
||||
})
|
||||
|
||||
editor.onDidChangeModelContent(() => {
|
||||
if (!isEditorEditable.value) return
|
||||
|
||||
const value = editor?.getValue()
|
||||
if (value !== undefined) {
|
||||
// Update Tiptap node content
|
||||
// We need to be careful not to trigger a re-render loop
|
||||
// This is a simplified approach; for robust sync, might need more logic
|
||||
// But since we switch modes, it might be okay.
|
||||
// Actually, Tiptap expects us to update the node.
|
||||
// But since we are in a NodeView, we can't easily update the content directly without replacing the node?
|
||||
// Wait, for code blocks, usually we update the text content.
|
||||
// Let's try to update the node content using a transaction if possible, or just emit an update.
|
||||
// For now, let's just keep local state and update on blur or mode switch?
|
||||
// Better: Update on change.
|
||||
|
||||
// NOTE: Updating the node content from within the node view can be tricky.
|
||||
// A common pattern is to use `props.updateAttributes` for attributes,
|
||||
// but for content, we might need to use `props.editor.commands.command(...)`.
|
||||
// However, since we are "taking over" the rendering, we can just update the node when we are done?
|
||||
// Or we can try to keep them in sync.
|
||||
|
||||
// Let's try to just update the node content when we exit edit mode.
|
||||
const pos = props.getPos()
|
||||
if (typeof pos === 'number') {
|
||||
// We use a transaction to update the content without triggering a re-render of the node view if possible
|
||||
// But here we are just updating the text.
|
||||
// We need to be careful about infinite loops if we update the node and it updates us.
|
||||
// But since we are the source of truth in edit mode, it should be fine.
|
||||
|
||||
// However, to avoid cursor jumping or re-rendering issues, we might want to debounce or check for changes.
|
||||
// Actually, Tiptap's `insertText` might replace the node? No, it updates the text node inside.
|
||||
|
||||
// Let's check if content is different
|
||||
const currentContent = props.node.content.firstChild?.text || ''
|
||||
if (value !== currentContent) {
|
||||
props.editor.view.dispatch(
|
||||
props.editor.view.state.tr.insertText(value, pos + 1, pos + 1 + props.node.content.size)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Sync content changes from Tiptap to Monaco (if changed externally)
|
||||
// watch(() => props.node.content, ...)
|
||||
}
|
||||
|
||||
// Toggle edit mode
|
||||
// We can toggle edit mode on click
|
||||
const toggleEditMode = () => {
|
||||
isEditable.value = !isEditable.value
|
||||
if (isEditable.value) {
|
||||
setTimeout(() => {
|
||||
if (!editor) {
|
||||
initMonaco()
|
||||
} else {
|
||||
editor.setValue(props.node.content.firstChild?.text || '')
|
||||
editor.layout()
|
||||
}
|
||||
editor?.focus()
|
||||
}, 0)
|
||||
} else {
|
||||
// Save content back to Tiptap
|
||||
if (editor) {
|
||||
const content = editor.getValue()
|
||||
const pos = props.getPos()
|
||||
|
||||
if (typeof pos === 'number') {
|
||||
props.editor.view.dispatch(
|
||||
props.editor.view.state.tr.insertText(content, pos + 1, pos + 1 + props.node.content.size)
|
||||
)
|
||||
}
|
||||
}
|
||||
updateHighlightedCode()
|
||||
}
|
||||
}
|
||||
|
||||
const onContainerClick = () => {
|
||||
if (!isEditable.value) {
|
||||
toggleEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle click outside to exit edit mode
|
||||
// This is a bit complex inside a NodeView.
|
||||
// A simpler way is to use the `blur` event of Monaco editor.
|
||||
// But Monaco's blur might trigger when clicking the language select.
|
||||
// Let's try to use a "Done" button or just blur.
|
||||
|
||||
onMounted(() => {
|
||||
initShiki()
|
||||
|
||||
// Watch for editor editable state changes
|
||||
// We can listen to the transaction event
|
||||
props.editor.on('transaction', () => {
|
||||
isEditorEditable.value = props.editor.isEditable
|
||||
})
|
||||
|
||||
watch(isEditorEditable, (editable) => {
|
||||
if (editable) {
|
||||
// Initialize Monaco if not already
|
||||
if (!editor) {
|
||||
initMonaco()
|
||||
} else {
|
||||
// Update content just in case it changed while in read-only
|
||||
const currentContent = props.node.content.firstChild?.text || ''
|
||||
if (editor.getValue() !== currentContent) {
|
||||
editor.setValue(currentContent)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Update Shiki
|
||||
updateHighlightedCode()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// Watch for language changes to update syntax highlighting and Monaco model language
|
||||
watch(selectedLanguage, (newLang) => {
|
||||
if (editor) {
|
||||
@@ -213,17 +190,14 @@ onBeforeUnmount(() => {
|
||||
if (editor) {
|
||||
editor.dispose()
|
||||
}
|
||||
// Clean up event listener? Tiptap handles it usually, but good practice to remove if we could.
|
||||
// props.editor.off('transaction', ...)
|
||||
})
|
||||
|
||||
const copyCode = () => {
|
||||
const code = props.node.content.firstChild?.text || ''
|
||||
navigator.clipboard.writeText(code)
|
||||
}
|
||||
|
||||
// Expose methods if needed
|
||||
defineExpose({
|
||||
toggleEditMode
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -253,6 +227,11 @@ defineExpose({
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.language-select:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
Reference in New Issue
Block a user