Introduction
In this tutorial, we'll explore how to create a simple code editor using Vue.js and CodeMirror. Code editors are essential tools for developers, providing syntax highlighting, line numbering, and other features to enhance coding experience. We'll leverage Vue.js to build a user-friendly interface and integrate CodeMirror to enable code editing functionalities.
Understanding the Need
When working on projects like cloning pastebin.com, developers often encounter the challenge of providing a user-friendly code editing interface. Traditional text editors lack essential features like syntax highlighting and can be cumbersome for users. Hence, integrating a code editor within a web browser becomes crucial to enhance user experience and productivity.
Leveraging CodeMirror
CodeMirror is a powerful tool used by various companies and platforms like CodePen for syntax highlighting and code editing functionalities. We'll integrate CodeMirror with Vue.js to create a feature-rich code editor similar to popular IDEs like Sublime Text or Visual Studio Code.
Setting Up the Environment
First, we'll install the necessary package, `vue3-codemirror`, using npm. This package provides seamless integration of CodeMirror with Vue.js. Once installed, we'll import and configure it within our Vue component.
npm i vue3-codemirror
Implementing the Code Editor
We'll create a Vue component and incorporate the CodeMirror instance within its template. Configuring properties like `mode` (language), `value` (initial code), and other options will enable us to customize the editor's behavior according to our requirements.
var VCodeMirror_1;
import { __decorate, __metadata } from "tslib";
import CodeMirror from 'codemirror';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/mode/python/python';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/dracula.css';
import 'codemirror/theme/abcdef.css';
import { capitalize, h, markRaw } from 'vue';
import ResizeObserver from 'resize-observer-polyfill';
import { $theme } from 'theme-helper';
import { Component, Inreactive, Prop, VueComponentBase, Watch } from 'vue3-component-base';
const Events = [
'focus',
'blur',
'scroll',
];
let VCodeMirror = VCodeMirror_1 = class VCodeMirror extends VueComponentBase {
render() {
return h('div', { class: 'v-code-mirror' });
}
async mounted() {
const lang_mode=this.$props.mode.name
if(lang_mode!=='plaintext' && lang_mode !== null && lang_mode !== "null"){
await import(`codemirror/mode/${lang_mode}/${lang_mode}`)
}
var editor = this.editor = markRaw(CodeMirror(this.$el, {
value: this.value,
mode: this.$props.mode,
theme: $theme.get() == 'white' ? 'default' : 'dracula',
readOnly: this.readonly,
lineWrapping: this.wrap,
lineNumbers: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
...this.options,
}));
editor.on('changes', () => {
const value = editor.getValue();
this.backupValue = value;
this.$emit('update:value', editor.getValue());
});
Events.forEach(x => {
const eventName = 'on' + capitalize(x);
if (typeof this.$.vnode.props[eventName] === 'function') {
editor.on(x, this.$emit.bind(this, x));
}
});
this.cleanEvent = markRaw($theme.onchange(({ detail }) => {
this.editor.setOption('theme', detail === 'white' ? 'default' : 'dracula');
}));
this.backupValue = this.value;
this.$el._component = this;
if (!VCodeMirror_1.ro) {
VCodeMirror_1.ro = new ResizeObserver(function (entries) {
entries.forEach(entry => {
const that = entry.target._component;
if (that.autoHeight) {
that.editor.refresh();
}
else {
that.editor.setSize(entry.contentRect.width, entry.contentRect.height);
}
});
});
}
VCodeMirror_1.ro.observe(this.$el);
}
async updated(){
if(this.$props.mode.name){
const lang_mode=this.$props.mode.name
if(lang_mode!=='plaintext' && lang_mode !== null && lang_mode !== "null"){
await import(`codemirror/mode/${lang_mode}/${lang_mode}`)
}
}
this.editor.setOption('mode',this.$props.mode)
}
beforeUnmount() {
var _a, _b;
(_a = this.cleanEvent) === null || _a === void 0 ? void 0 : _a.call(this);
(_b = VCodeMirror_1.ro) === null || _b === void 0 ? void 0 : _b.unobserve(this.$el);
}
updateValue(value) {
if (value === this.backupValue)
return;
this.editor.setValue(value);
}
updateReadonly(value) {
this.editor.setOption('readOnly', value);
}
updateWrap(value) {
this.editor.setOption('lineWrapping', value);
}
focus() {
this.editor.focus();
}
};
__decorate([
Prop({ required: true }),
__metadata("design:type", String)
], VCodeMirror.prototype, "value", void 0);
__decorate([
Prop({ default: () => ({ name: 'python', json: true }) }),
__metadata("design:type", Object)
], VCodeMirror.prototype, "mode", void 0);
__decorate([
Prop(),
__metadata("design:type", Boolean)
], VCodeMirror.prototype, "readonly", void 0);
__decorate([
Prop({ default: true }),
__metadata("design:type", Boolean)
], VCodeMirror.prototype, "wrap", void 0);
__decorate([
Prop(),
__metadata("design:type", Object)
], VCodeMirror.prototype, "options", void 0);
__decorate([
Prop(),
__metadata("design:type", Boolean)
], VCodeMirror.prototype, "autoHeight", void 0);
__decorate([
Inreactive,
__metadata("design:type", Object)
], VCodeMirror.prototype, "editor", void 0);
__decorate([
Inreactive,
__metadata("design:type", String)
], VCodeMirror.prototype, "backupValue", void 0);
__decorate([
Inreactive,
__metadata("design:type", Function)
], VCodeMirror.prototype, "cleanEvent", void 0);
__decorate([
Watch('value'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", void 0)
], VCodeMirror.prototype, "updateValue", null);
__decorate([
Watch('readonly'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Boolean]),
__metadata("design:returntype", void 0)
], VCodeMirror.prototype, "updateReadonly", null);
__decorate([
Watch('wrap'),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Boolean]),
__metadata("design:returntype", void 0)
], VCodeMirror.prototype, "updateWrap", null);
VCodeMirror = VCodeMirror_1 = __decorate([
Component({
name: 'VCodeMirror',
emits: ['update:value', ...Events],
})
], VCodeMirror);
export { VCodeMirror };
//# sourceMappingURL=index.js.map
Enhancing User Experience
We'll explore additional options provided by CodeMirror, such as adjusting tab size, enabling line numbers, and more. These options allow us to tailor the editor to meet specific preferences and improve usability.
<template>
<div class="min-h-screen px-5 md:px-20 pt-10 flex md:flex-row flex-col ">
<div class=" w-full md:w-10/12">
<h2 class='font-medium text-2xl'>
New Paste
</h2>
<hr class='border-gray-500 '>
<div :class="!this.$store.getters.getUserPastePrefence.nightMode?'bg-gray-200 ':'bg-gray-700' " class=' rounded-lg p-2 my-2' v-if='error'>
<p class='text-red-400 text-sm my-1' >{{error}}</p>
</div>
<VCodeMirror @update:value='handleUpdate'
:options='{...cmOptions}'
value=''
:mode='mode'
:readonly='false'
class='w-full shadow-xl my-5 bg-gray-700 h-96 resize-none rounded-lg outline-none text-base' />
<h2 class='font-semibold text-xl'>
Optional Paste Setting
</h2>
<hr class='border-gray-500 '>
<form @submit.prevent='handleSubmit' >
<AppSelect @selectChange='changeDefaultLang' :value='defaultLang' :selectOption='languages' title='Syntax highlighting' />
<AppSelect @selectChange='changeDefaultExpire' :value='defaultExpire' :selectOption='expirations' title='Paste Expiration' />
<AppSelect @selectChange='changeDefaultPrivacy' :value='defaultPrivacy' :selectOption='["Public","Private"]' title='Paste Exposure' />
<PasteFolderTitle :value="defaultFolder" :openNewFolder='openNewFolder' @newFolder='handleNewFolder' @selectChange='handlePasteFolder' />
<AppInput title='Paste Name / Title' placeholde='Paste title' inputType='text' @InputChange='changePasteTitle' />
<CreatePastePasswordInput title='Password' @passwordChange='handlePassword' />
<div class='flex items-center my-5'>
<button class=' py-2 px-5 font-medium rounded-md mr-4'
:class="!this.$store.getters.getUserPastePrefence.nightMode?'bg-gray-200 text-gray-800':'bg-gray-700' ">
Create New Paste
</button>
<div class='flex items-center' v-if='isUser' >
<input type="checkbox" v-model='guestPost' class="p-2 mr-2 bg-gray-600 ">
<label class="text-lg">Paste as a guest</label>
</div>
</div>
</form>
</div>
<div class='md:w-2/12 w-full p-2 pb-10 md:pb-0'>
<PasteList class='ml-4' style='margin-top:0;' />
</div>
</div>
</template>
<script>
import PasteList from '../components/PasteList.vue'
import AppSelect from '../components/AppSelect.vue'
import AppInput from '../components/AppInput.vue'
import CreatePastePasswordInput from '../components/CreatePastePasswordInput.vue'
import CreatePasteTitleInput from '../components/CreatePasteTitleInput.vue'
import PasteFolderTitle from '../components/PasteFolderTitle.vue'
import { VCodeMirror } from '../components/vue3-code-mirror'
import {ref,computed, onBeforeMount,watch} from 'vue'
import handleNewPaste from '@/composibles/handleNewPaste'
import { useRouter } from 'vue-router'
import {useStore} from 'vuex'
export default {
components:{
PasteList,
VCodeMirror,
AppSelect,
AppInput,
PasteFolderTitle,
CreatePastePasswordInput,
CreatePasteTitleInput,
},
setup(){
const code= ref('')
const defaultLang=ref('PLAINTEXT')
const defaultExpire=ref('Never')
const defaultPrivacy=ref('Public')
const defaultFolder=ref('no folder selected')
const openNewFolder=ref(false)
const newFolder=ref('')
const pasteTitle=ref('')
const pastePassword=ref('')
const guestPost=ref(false)
const store=useStore()
const userPreference=computed(()=>{
return store.getters.getUserPastePrefence
})
onBeforeMount(async()=>{
// await store.dispatch('handleChangeUserPreference')
defaultLang.value=userPreference.value.defaultSyntax
defaultExpire.value=userPreference.value.defaultExpiration
defaultPrivacy.value=userPreference.value.defaultExposure?'Public':'Private'
})
const languages=computed(()=>{
return store.getters.getLanguages
})
const expirations=computed(()=>{
return store.getters.getExpirations
})
const isDark=computed(()=>{
return store.getters.getUserPastePrefence.nightMode ? "dracula" : "white"
})
const router =useRouter()
const {error,postToAPI}=handleNewPaste()
const isUser=computed(()=>{
return store.getters.getUser
})
const handleSubmit=async()=>{
await store.dispatch('handleChangeUser')
let withAuth = guestPost.value ? false : isUser
if(guestPost.value){
defaultFolder.value='no folder selected'
newFolder.value=''
}
let data
if(defaultFolder.value==='no folder selected' && newFolder.value.length){
const pasteObj={
code:code.value,
language:defaultLang.value.toUpperCase(),
expiration:defaultExpire.value.toUpperCase(),
public:defaultPrivacy.value=='Public'? true : false ,
syntax_highlighting:true,
folder:newFolder.value,
password:pastePassword.value,
title:pasteTitle.value,
}
data=await postToAPI(pasteObj,withAuth)
}
else if(defaultFolder.value!=='no folder selected'){
const pasteObj={
code:code.value,
language:defaultLang.value.toUpperCase(),
expiration:defaultExpire.value.toUpperCase(),
public: defaultPrivacy.value=='Public'? true : false ,
folder:defaultFolder.value,
password:pastePassword.value,
title:pasteTitle.value,
syntax_highlighting:true
}
data=await postToAPI(pasteObj,withAuth)
}else{
const pasteObj={
code:code.value,
language:defaultLang.value.toUpperCase(),
expiration:defaultExpire.value.toUpperCase(),
public:defaultPrivacy.value=='Public'? true : false ,
password:pastePassword.value,
title:pasteTitle.value,
syntax_highlighting:true
}
console.log(pasteObj)
data=await postToAPI(pasteObj,withAuth)
}
if(error.value===null){
router.push(`/paste/${data.uuid}`)
}
}
const cmOptions= ref({
tabSize: 4,
lineNumbers: true,
// line: true,
theme: isDark.value
})
watch(isDark,()=>{
cmOptions.value= ref({
tabSize: 4,
lineNumbers: true,
// line: true,
theme: isDark.value
})
})
const handleUpdate=(value)=>{
// console.log(value)
code.value=value
}
const mode= ref({name:defaultLang.value=='PLAINTEXT'?'null':'python',json:true})
const changeDefaultLang=(value)=>{
defaultLang.value=value
mode.value={
name: value=='PLAINTEXT'?'null':value.toLowerCase(),json:true
}
console.log(defaultLang.value)
}
const changeDefaultExpire=(value)=>{
defaultExpire.value=value
console.log(defaultExpire.value)
}
const changeDefaultPrivacy=(value)=>{
defaultPrivacy.value=value
console.log(defaultPrivacy.value)
}
const handleNewFolder=(value)=>{
newFolder.value=value
console.log(defaultFolder.value)
console.log(newFolder.value)
}
const handlePasteFolder=(value)=>{
defaultFolder.value=value
console.log(defaultFolder.value)
}
const changePasteTitle=(title)=>{
pasteTitle.value=title
console.log(pasteTitle.value)
}
const handlePassword=(value)=>{
pastePassword.value=value
console.log(pastePassword.value)
}
return {code,cmOptions,handleUpdate,mode,defaultLang,
defaultExpire,
defaultPrivacy,
defaultFolder,
changeDefaultLang,
changeDefaultExpire,
changeDefaultPrivacy,
handleNewFolder,
handlePasteFolder,
handlePassword,
openNewFolder,
changePasteTitle,
handleSubmit,
error,
languages,
expirations,
isUser,
guestPost}
}
}
</script>
<style>
.CodeMirror-vscrollbar::-webkit-scrollbar{
width:10px;
background: rgba(128, 128, 128, 0.644);
border-radius:0.5rem;
}
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb{
width:2px;
background:rgba(0, 0, 0, 0.678);
border-radius:32px
}
.CodeMirror-vscrollbar{
width:10px;
background: rgba(128, 128, 128, 0.644);
border-radius:0.5rem;
}
.CodeMirror{
border-radius: 0.5rem;
}
</style>
Handling User Input and Interaction
CodeMirror emits events whenever there's a change in the code editor. We'll utilize these events to capture user input and update the code accordingly. Additionally, we'll implement features like read-only mode to restrict user editing and ensure data integrity.
Conclusion
By integrating CodeMirror with Vue.js, we've successfully created a simple yet powerful code editor within a web browser. This tutorial demonstrates the versatility and ease of use provided by modern web development frameworks and libraries. Experiment with different configurations and functionalities to customize the code editor according to your project's needs.
Remember to subscribe to my channel for more tutorials on web development with Python, Go, Vue.js, React, and more. If you found this tutorial helpful, don't forget to like and share it. Feel free to leave comments with any questions or project requests, and we'll be glad to assist you. Happy coding!
322 views