Personal_Website/resources/js/components/MarkdownEditor.vue

194 lines
6.7 KiB
Vue

<template>
<div class="markdown-editor">
<div class="md-editor">
<div class="controls">
<ul ref="controlsList">
<li><a href="javascript:void(0)" @click="insertTag('**', '**')"><i class="fas fa-bold"></i></a></li>
<li><a href="javascript:void(0)" @click="insertTag('*', '*')"><i class="fas fa-italic"></i></a></li>
<li><a href="javascript:void(0)" @click="insertTag('<u>', '</u>')"><i class="fas fa-underline"></i></a></li>
<li><a href="javascript:void(0)" @click="insertTag('<s>', '</s>')"><i class="fas fa-strikethrough"></i></a></li>
<li><a href="javascript:void(0)" @click="insertTag('- ', null, true)"><i class="fas fa-list-ul"></i></a></li>
<li><a href="javascript:void(0)" @click="insertTag('1. ', null, true)"><i class="fas fa-list-ol"></i></a></li>
<li><a href="javascript:void(0)" @click="imagePopupOpen = !imagePopupOpen"><i class="fas fa-image"></i></a></li>
<li><a href="javascript:void(0)" @click="linkPopupOpen = !linkPopupOpen"><i class="fas fa-link"></i></a></li>
</ul>
</div>
<textarea class="input" ref="textarea" v-model="markdown" :cols="cols" :rows="rows"></textarea>
</div>
<button class="submit" @click="save">Save</button>
<div class="render" v-html="html" v-if="render"></div>
<div class="popup" :style="{ top: popupTop + 'px', left: popupLeft + 'px' }" v-if="imagePopupOpen">
Insert Image
<form @submit.prevent="onImageInsert">
<input type="text" name="url">
or
<input type="file" name="image" accept="image/*">
<button class="submit">Insert</button>
</form>
</div>
<div class="popup" :style="{ top: popupTop + 'px', left: popupLeft + 'px' }" v-else-if="linkPopupOpen">
Insert Link
<form @submit.prevent="onLinkInsert">
<input type="text" name="url">
<button class="submit">Insert</button>
</form>
</div>
</div>
</template>
<script>
import { debounce } from "../helpers";
import { nextTick } from "vue";
export default {
data() {
return {
edited: false,
markdown: "",
html: null,
converter: null,
onChange: () => {},
imagePopupOpen: false,
linkPopupOpen: false,
};
},
props: {
render: {
type: Boolean,
default: true,
},
cols: {
type: Number,
default: 30,
},
rows: {
type: Number,
default: 10,
},
},
mounted() {
this.converter = new showdown.Converter();
this.onChange = debounce(() => {
this.onChangeHandler();
}, 500);
},
methods: {
onChangeHandler() {
this.html = this.converter.makeHtml(this.markdown);
},
save() {
this.$emit('save', this.markdown);
this.edited = false;
},
canRouteChange() {
if (!this.edited) {
return true;
}
return window.confirm('Do you really want to leave? You have unsaved changes!');
},
setContent(markdown, blockSave = true) {
this.markdown = markdown;
nextTick(() => {
this.edited = blockSave;
});
},
insertTag(tagFront, tagBack, startOfLine = false) {
const textarea = this.$refs.textarea;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.focus();
if (!startOfLine) {
textarea.selectionStart = end;
document.execCommand('insertText', false, tagBack);
textarea.selectionStart = start;
textarea.selectionEnd = start;
document.execCommand('insertText', false, tagFront);
textarea.selectionStart = start + tagFront.length;
textarea.selectionEnd = end + tagFront.length;
} else {
const contentBeforeSelection = this.markdown.slice(0, start);
const contentSelection = this.markdown.slice(start, end - 1);
let startLinePos = contentBeforeSelection.lastIndexOf('\n');
if (startLinePos < 0) {
startLinePos = -1;
}
let endLinePos = contentSelection.lastIndexOf('\n');
if (endLinePos < 0) {
endLinePos = 0;
} else {
endLinePos += contentBeforeSelection.length;
}
const allNewlines = [startLinePos];
let i = startLinePos;
while ((i = this.markdown.indexOf('\n', i+1)) >= 0) {
if (i <= endLinePos) {
allNewlines.push(i);
}
}
for (let i = 0; i < allNewlines.length; i++) {
textarea.selectionStart = textarea.selectionEnd = allNewlines[i] + tagFront.length * i + 1;
document.execCommand('insertText', false, tagFront);
}
}
},
onImageInsert(event) {
const data = new FormData(event.target);
const url = data.get('url');
const image = data.get('image');
if (image.size > 0) {
const fr = new FileReader();
fr.addEventListener('load', event => {
this.insertTag("![", `](${event.target.result})`);
});
fr.readAsDataURL(image);
} else {
if (url.length > 0) {
this.insertTag("![", `](${url})`);
}
}
},
onLinkInsert(event) {
const data = new FormData(event.target);
const url = data.get('url');
if (url.length > 0) {
this.insertTag("[", `](${url})`);
}
}
},
watch: {
markdown() {
this.edited = true;
this.$emit('update:markdown', this.markdown);
this.onChange();
},
html() {
this.$emit('update:html', this.html);
}
},
computed: {
popupTop() {
const rect = this.$refs.controlsList.getBoundingClientRect();
return rect.bottom;
},
popupLeft() {
const rect = this.$refs.controlsList.getBoundingClientRect();
return rect.left;
},
}
}
</script>