Last active
November 5, 2024 03:53
-
-
Save westc/80bac6194dfbecd1c96bb8bda80ddb4d to your computer and use it in GitHub Desktop.
Get a <code-editor> component in Vue3 that uses Monaco.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @param {Record<"codeEditor"|"diffEditor",boolean|string>} defsToInclude | |
* @returns {Record<string,any>} | |
*/ | |
function getMonacoEditorCompDefs(defsToInclude) { | |
/** | |
* @param {(script?: HTMLScriptElement) => T} getter | |
* Function that will be called to get the result of a script running. This | |
* function will initially be called without any arguments to determine if | |
* the desired results already exist but if not the script tag will be added | |
* and then this will be run again after the script has been loaded. | |
* @param {string} scriptUrl | |
* @returns {Promise<T>} | |
* @template T | |
*/ | |
async function loadScriptValue(getter, scriptUrl) { | |
let value; | |
try { | |
value = await getter(); | |
} catch(e){} | |
return value !== undefined | |
? value | |
: new Promise((resolve, reject) => { | |
document.head.appendChild(Object.assign(document.createElement('script'), { | |
src: scriptUrl, | |
onload() { | |
const intervalID = setInterval(async () => { | |
value = await getter(this); | |
if (value !== undefined) { | |
resolve(value); | |
clearInterval(intervalID); | |
} | |
}, 50); | |
}, | |
onerror(evt) { | |
reject(evt); | |
clearInterval(intervalID); | |
} | |
})); | |
}); | |
} | |
function waitUntil(tester, interval=100) { | |
const start = +new Date(); | |
return new Promise(async resolve => { | |
while (true) { | |
const value = tester(start, interval); | |
if (value) return resolve(value); | |
await new Promise((resolveInner) => setTimeout(resolveInner, interval)); | |
} | |
}); | |
} | |
function parseRulers(rulers) { | |
if (rulers == null) return rulers; | |
if ('string' === typeof rulers) { | |
rulers = rulers.split(/\s*,\s*/); | |
} | |
return rulers.reduce((rulers, ruler) => { | |
const intRuler = parseInt(+ruler, 10); | |
if (ruler == intRuler) { | |
rulers.push(intRuler); | |
} | |
return rulers; | |
}, []); | |
} | |
async function ensureMonaco() { | |
// Add monaco editor CSS and JS if the CSS isn't already found. | |
const jsAttrKey = 'code-editor-vue3-comp'; | |
if (!document.head.querySelector(`script[${jsAttrKey}]`)) { | |
const script = document.createElement('script'); | |
script.src = 'https://unpkg.com/monaco-editor@0/min/vs/loader.js'; | |
script.setAttribute(jsAttrKey, 1); | |
document.head.appendChild(script); | |
// Load the necessary require function to then load monaco. | |
await waitUntil(() => window.require?.config); | |
require.config({ paths: { 'vs': 'https://unpkg.com/monaco-editor@0/min/vs' } }); | |
await new Promise(resolve => require(['vs/editor/editor.main'], resolve)); | |
} | |
else if (!window.monaco) { | |
await waitUntil(() => window.monaco); | |
} | |
} | |
const codeEditorDef = { | |
template: ` | |
<div ref="editor" :style="style"></div> | |
`, | |
watch: { | |
modelValue(newValue, oldValue) { | |
if (this.skipModelValueUpdate) { | |
this.skipModelValueUpdate = false; | |
} | |
else { | |
this.$.editor.setValue(newValue); | |
} | |
}, | |
readonly(newValue) { | |
this.$.editor.updateOptions({readOnly: newValue}); | |
}, | |
wrap(newValue) { | |
this.$.editor.updateOptions({wordWrap: newValue ? 'on' : 'off'}); | |
}, | |
rulers(newValue) { | |
this.$.editor.updateOptions({rulers: parseRulers(newValue)}); | |
} | |
}, | |
data() { | |
return { | |
skipModelValueUpdate: false | |
}; | |
}, | |
methods: { | |
emitModelValueUpdate(newValue) { | |
this.skipModelValueUpdate = true; | |
this.$emit('update:modelValue', newValue); | |
} | |
}, | |
props: { | |
modelValue: String, | |
mode: { | |
type: String, | |
default: 'javascript' | |
}, | |
height: { | |
type: [Number,String], | |
default: '200px' | |
}, | |
readonly: { | |
type: Boolean, | |
default: false, | |
}, | |
wrap: { | |
type: Boolean, | |
default: true, | |
}, | |
rulers: { | |
type: [Array,String], | |
validator(value) { | |
return parseRulers(value)?.length > 0; | |
}, | |
default: "80", | |
} | |
}, | |
computed: { | |
style() { | |
return { | |
height: `${this.height}`.replace(/^\d+$/, '$&px') | |
}; | |
} | |
}, | |
async mounted() { | |
// Wait until monaco is actually available. | |
await ensureMonaco(); | |
const editor = monaco.editor.create(this.$refs.editor, { | |
value: this.modelValue, | |
language: this.mode, | |
readOnly: this.readonly, | |
wordWrap: this.wrap ? 'on' : 'off', | |
rulers: parseRulers(this.rulers), | |
automaticLayout: true, | |
}); | |
editor.onDidChangeModelContent(evt => { | |
// Only emit model value update when not flush. `!evt.isFlush` | |
// seems to indicate that the value was updated from within the | |
// editor. | |
if (!evt.isFlush) this.emitModelValueUpdate(editor.getValue()); | |
}); | |
this.$.editor = editor; | |
}, | |
}; | |
const diffEditorDef = { | |
template: ` | |
<div ref="editor" :style="style"></div> | |
`, | |
watch: { | |
leftValue(newValue, oldValue) { | |
if (this.skipLeftValueUpdate) { | |
this.skipLeftValueUpdate = false; | |
} | |
else { | |
this.$.leftModel.setValue(newValue); | |
} | |
}, | |
rightValue(newValue, oldValue) { | |
if (this.skipRightValueUpdate) { | |
this.skipRightValueUpdate = false; | |
} | |
else { | |
this.$.rightModel.setValue(newValue); | |
} | |
}, | |
leftEditable(newValue) { | |
this.$.editor.updateOptions({originalEditable: newValue}); | |
}, | |
rightEditable(newValue) { | |
this.$.editor.updateOptions({readOnly: !newValue}); | |
}, | |
wrap(newValue) { | |
this.$.editor.updateOptions({wordWrap: newValue ? 'on' : 'off'}); | |
}, | |
rulers(newValue) { | |
this.$.editor.updateOptions({rulers: parseRulers(newValue)}); | |
} | |
}, | |
data() { | |
return { | |
skipLeftValueUpdate: false, | |
skipRightValueUpdate: false, | |
}; | |
}, | |
methods: { | |
emitLeftValueUpdate(newValue) { | |
this.skipLeftValueUpdate = true; | |
this.$emit('update:leftValue', newValue); | |
}, | |
emitRightValueUpdate(newValue) { | |
this.skipRightValueUpdate = true; | |
this.$emit('update:rightValue', newValue); | |
}, | |
}, | |
props: { | |
leftValue: String, | |
rightValue: String, | |
mode: { | |
type: String, | |
default: 'javascript' | |
}, | |
height: { | |
type: [Number,String], | |
default: '200px' | |
}, | |
leftEditable: { | |
type: Boolean, | |
default: false, | |
}, | |
rightEditable: { | |
type: Boolean, | |
default: false, | |
}, | |
wrap: { | |
type: Boolean, | |
default: true, | |
}, | |
rulers: { | |
type: [Array,String], | |
validator(value) { | |
return parseRulers(value)?.length > 0; | |
}, | |
default: "80", | |
} | |
}, | |
computed: { | |
style() { | |
return { | |
height: `${this.height}`.replace(/^\d+$/, '$&px') | |
}; | |
} | |
}, | |
async mounted() { | |
// Wait until monaco is actually available. | |
await ensureMonaco(); | |
this.$.leftModel = monaco.editor.createModel(this.leftValue, this.mode); | |
this.$.rightModel = monaco.editor.createModel(this.rightValue, this.mode); | |
const editor = monaco.editor.createDiffEditor(this.$refs.editor, { | |
language: this.mode, | |
readOnly: !this.rightEditable, | |
originalEditable: this.leftEditable, | |
wordWrap: this.wrap ? 'on' : 'off', | |
rulers: parseRulers(this.rulers), | |
automaticLayout: true, | |
}); | |
editor.setModel({ | |
original: this.$.leftModel, | |
modified: this.$.rightModel, | |
}); | |
editor.getOriginalEditor().onDidChangeModelContent(evt => { | |
if (!evt.isFlush) { | |
this.emitLeftValueUpdate(this.$.leftModel.getValue()); | |
} | |
}); | |
editor.getModifiedEditor().onDidChangeModelContent(evt => { | |
if (!evt.isFlush) { | |
this.emitRightValueUpdate(this.$.rightModel.getValue()); | |
} | |
}); | |
this.$.editor = editor; | |
}, | |
}; | |
const defs = {}; | |
const {codeEditor, diffEditor} = defsToInclude; | |
if (codeEditor) { | |
defs[codeEditor === true ? 'codeEditor' : codeEditor] = codeEditorDef; | |
} | |
if (diffEditor) { | |
defs[diffEditor === true ? 'diffEditor' : diffEditor] = diffEditorDef; | |
} | |
return defs; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width"> | |
<title>Monaco Editors</title> | |
<script src="https://unpkg.com/[email protected]/dist/vue.global.js"></script> | |
<script src="getMonacoEditorCompDef.js"></script> | |
<script src="page.js"></script> | |
</head> | |
<body> | |
<div id="vueApp"> | |
{{ code.length }} | |
<code-editor v-model="code" | |
mode="python" | |
height="250" | |
rulers="80,120" | |
:wrap="true" | |
:readonly="false"> | |
</code-editor> | |
<diff-editor v-model:left-value="code" | |
v-model:right-value="rightCode" | |
mode="python" | |
height="250" | |
rulers="80,120" | |
:wrap="true" | |
:left-editable="true" | |
:right-editable="true"> | |
</diff-editor> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const vueApp = window.vueApp = Vue.createApp({ | |
data() { | |
return { | |
code: 'from chc_py_transform import transform', | |
leftCode: 'from chc_py_transform import transform', | |
rightCode: 'from chc_py_transform import transform', | |
}; | |
}, | |
components: { | |
...getMonacoEditorCompDefs({codeEditor: true, diffEditor: true}) | |
} | |
}).mount('#vueApp'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment