chore: change

This commit is contained in:
zhuzhengjian
2025-11-21 10:12:04 +08:00
parent a46a07cb54
commit e5661226e5

View File

@@ -1,7 +1,8 @@
```
<template> <template>
<node-view-wrapper class="code-block-wrapper" @dblclick="onContainerClick"> <node-view-wrapper class="code-block-wrapper">
<div class="code-block-header"> <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 value="null">auto</option>
<option disabled>—</option> <option disabled>—</option>
<option v-for="(language, index) in languages" :key="index" :value="language"> <option v-for="(language, index) in languages" :key="index" :value="language">
@@ -15,10 +16,10 @@
</div> </div>
</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 v-html="highlightedCode" class="shiki-container"></div>
</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> </node-view-wrapper>
</template> </template>
@@ -58,7 +59,7 @@ const selectedLanguage = computed({
const monacoContainer = ref<HTMLElement | null>(null) const monacoContainer = ref<HTMLElement | null>(null)
let editor: monaco.editor.IStandaloneCodeEditor | null = null let editor: monaco.editor.IStandaloneCodeEditor | null = null
const isEditable = ref(false) const isEditorEditable = ref(props.editor.isEditable)
const highlightedCode = ref('') const highlightedCode = ref('')
// Shiki highlighter instance // Shiki highlighter instance
@@ -93,26 +94,20 @@ const updateHighlightedCode = async () => {
} }
const initMonaco = async () => { const initMonaco = async () => {
if (!monacoContainer.value) return if (!monacoContainer.value || editor) return
// Create the highlighter, it can be reused // Register languages and theme only once
const highlighter = await createHighlighter({ if (!monaco.languages.getLanguages().some(l => l.id === 'vue')) {
themes: [ const highlighter = await createHighlighter({
'vitesse-dark', themes: ['vitesse-dark', 'vitesse-light'],
'vitesse-light', langs: ['javascript', 'typescript', 'vue'],
], })
langs: [
'javascript', monaco.languages.register({ id: 'vue' })
'typescript', monaco.languages.register({ id: 'typescript' })
'vue' monaco.languages.register({ id: 'javascript' })
], shikiToMonaco(highlighter, monaco)
}) }
// 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)
editor = monaco.editor.create(monacoContainer.value, { editor = monaco.editor.create(monacoContainer.value, {
value: props.node.content.firstChild?.text || '', value: props.node.content.firstChild?.text || '',
@@ -125,81 +120,63 @@ const initMonaco = async () => {
fontFamily: 'JetBrains Mono, monospace', fontFamily: 'JetBrains Mono, monospace',
lineNumbers: 'off', lineNumbers: 'off',
padding: { top: 16, bottom: 16 }, padding: { top: 16, bottom: 16 },
readOnly: false
}) })
editor.onDidChangeModelContent(() => { editor.onDidChangeModelContent(() => {
if (!isEditorEditable.value) return
const value = editor?.getValue() const value = editor?.getValue()
if (value !== undefined) { if (value !== undefined) {
// Update Tiptap node content const pos = props.getPos()
// We need to be careful not to trigger a re-render loop if (typeof pos === 'number') {
// This is a simplified approach; for robust sync, might need more logic // We use a transaction to update the content without triggering a re-render of the node view if possible
// But since we switch modes, it might be okay. // But here we are just updating the text.
// Actually, Tiptap expects us to update the node. // We need to be careful about infinite loops if we update the node and it updates us.
// But since we are in a NodeView, we can't easily update the content directly without replacing the node? // But since we are the source of truth in edit mode, it should be fine.
// 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. // However, to avoid cursor jumping or re-rendering issues, we might want to debounce or check for changes.
// A common pattern is to use `props.updateAttributes` for attributes, // Actually, Tiptap's `insertText` might replace the node? No, it updates the text node inside.
// 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. // 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(() => { onMounted(() => {
initShiki() 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 for language changes to update syntax highlighting and Monaco model language
watch(selectedLanguage, (newLang) => { watch(selectedLanguage, (newLang) => {
if (editor) { if (editor) {
@@ -213,17 +190,14 @@ onBeforeUnmount(() => {
if (editor) { if (editor) {
editor.dispose() 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 copyCode = () => {
const code = props.node.content.firstChild?.text || '' const code = props.node.content.firstChild?.text || ''
navigator.clipboard.writeText(code) navigator.clipboard.writeText(code)
} }
// Expose methods if needed
defineExpose({
toggleEditMode
})
</script> </script>
<style scoped> <style scoped>
@@ -253,6 +227,11 @@ defineExpose({
outline: none; outline: none;
} }
.language-select:disabled {
opacity: 0.7;
cursor: default;
}
.actions { .actions {
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;