200 lines
7.4 KiB
Vue
200 lines
7.4 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>
|
|
<li><a href="javascript:void(0)" @click="insertTag('> ', null, true)"><i class="fas fa-quote-left"></i></a></li>
|
|
<li><a href="javascript:void(0)" @click="insertTag('`', '`')"><i class="fas fa-code"></i></a></li>
|
|
<li><a href="javascript:void(0)" @click="insertTag('<details><summary>', '</summary>\n \n</details>')"><i class="fas fa-caret-square-down"></i></a></li>
|
|
<li><a href="javascript:void(0)" @click="insertTag('\n| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |\n', '')"><i class="fas fa-table"></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({
|
|
tables: true,
|
|
});
|
|
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("`);
|
|
});
|
|
fr.readAsDataURL(image);
|
|
} else {
|
|
if (url.length > 0) {
|
|
this.insertTag("`);
|
|
}
|
|
}
|
|
},
|
|
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>
|