chore: change
This commit is contained in:
@@ -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.
|
// However, to avoid cursor jumping or re-rendering issues, we might want to debounce or check for changes.
|
||||||
// For now, let's just keep local state and update on blur or mode switch?
|
// Actually, Tiptap's `insertText` might replace the node? No, it updates the text node inside.
|
||||||
// Better: Update on change.
|
|
||||||
|
// Let's check if content is different
|
||||||
// NOTE: Updating the node content from within the node view can be tricky.
|
const currentContent = props.node.content.firstChild?.text || ''
|
||||||
// A common pattern is to use `props.updateAttributes` for attributes,
|
if (value !== currentContent) {
|
||||||
// but for content, we might need to use `props.editor.commands.command(...)`.
|
props.editor.view.dispatch(
|
||||||
// However, since we are "taking over" the rendering, we can just update the node when we are done?
|
props.editor.view.state.tr.insertText(value, pos + 1, pos + 1 + props.node.content.size)
|
||||||
// Or we can try to keep them in sync.
|
)
|
||||||
|
}
|
||||||
// Let's try to just update the node content when we exit edit mode.
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user